mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-23 06:04:43 +08:00
acc6d795e4
Hermes has no slash-prompt concept (templates live as Skills), so the
Prompts tab for the Hermes app was always empty. Swap the toolbar Book
button for a Brain button that opens a new Memory panel editing
~/.hermes/memories/{MEMORY,USER}.md — Hermes' first-class memory store
which its Web UI exposes only as on/off toggles, never as an editor.
The panel shows each file in its own tab with a character-budget bar
read from config.yaml's nested memory.* section (memory_char_limit /
user_char_limit, default 2200 / 1375). Edits are written atomically;
Hermes picks them up on the next session start per MemoryStore.
Also extract useDarkMode to src/hooks/useDarkMode.ts — the codebase
already repeats the same MutationObserver pattern in 12+ places; this
PR introduces the shared hook and uses it once, leaving the migration
of the other copies to a follow-up.
156 lines
4.8 KiB
TypeScript
156 lines
4.8 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner";
|
|
import { ExternalLink } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import MarkdownEditor from "@/components/MarkdownEditor";
|
|
import {
|
|
useHermesMemory,
|
|
useHermesMemoryLimits,
|
|
useOpenHermesWebUI,
|
|
useSaveHermesMemory,
|
|
} from "@/hooks/useHermes";
|
|
import { useDarkMode } from "@/hooks/useDarkMode";
|
|
import type { HermesMemoryKind } from "@/types";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface MemoryTabPaneProps {
|
|
kind: HermesMemoryKind;
|
|
limit: number;
|
|
enabled: boolean;
|
|
}
|
|
|
|
const MemoryTabPane: React.FC<MemoryTabPaneProps> = ({
|
|
kind,
|
|
limit,
|
|
enabled,
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const darkMode = useDarkMode();
|
|
const { data, isLoading } = useHermesMemory(kind, true);
|
|
const saveMutation = useSaveHermesMemory();
|
|
const [content, setContent] = useState("");
|
|
const [loaded, setLoaded] = useState(false);
|
|
|
|
// Hydrate local dirty buffer from query data only on first load. Later
|
|
// refetches (e.g. after a successful save) must not clobber in-flight user
|
|
// edits — the caller owns `content` until they click Save again.
|
|
useEffect(() => {
|
|
if (!loaded && data !== undefined) {
|
|
setContent(data);
|
|
setLoaded(true);
|
|
}
|
|
}, [data, loaded]);
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
await saveMutation.mutateAsync({ kind, content });
|
|
toast.success(t("hermes.memory.saveSuccess"));
|
|
} catch {
|
|
// useSaveHermesMemory already surfaces a localized error toast.
|
|
}
|
|
};
|
|
|
|
const charCount = content.length;
|
|
const isOver = charCount > limit;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
{!enabled && (
|
|
<div className="text-sm text-amber-700 dark:text-amber-400 px-3 py-2 rounded-md bg-amber-500/10 border border-amber-500/30">
|
|
{t("hermes.memory.disabled")}
|
|
</div>
|
|
)}
|
|
|
|
{isLoading && !loaded ? (
|
|
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
|
{t("prompts.loading")}
|
|
</div>
|
|
) : (
|
|
<MarkdownEditor
|
|
value={content}
|
|
onChange={setContent}
|
|
darkMode={darkMode}
|
|
minHeight="calc(100vh - 320px)"
|
|
/>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between gap-3 text-sm">
|
|
<span
|
|
className={cn(
|
|
"text-muted-foreground",
|
|
isOver && "text-red-600 dark:text-red-400 font-medium",
|
|
)}
|
|
>
|
|
{t("hermes.memory.usage", { current: charCount, limit })}
|
|
{isOver ? ` — ${t("hermes.memory.overLimit")}` : ""}
|
|
</span>
|
|
<div className="flex items-center gap-3">
|
|
<span className="hidden md:inline text-xs text-muted-foreground">
|
|
{t("hermes.memory.runtimeNote")}
|
|
</span>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={saveMutation.isPending || !loaded}
|
|
>
|
|
{saveMutation.isPending ? t("common.saving") : t("common.save")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const HermesMemoryPanel: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
const [activeTab, setActiveTab] = useState<HermesMemoryKind>("memory");
|
|
const openHermesWebUI = useOpenHermesWebUI();
|
|
const { data: limits } = useHermesMemoryLimits(true);
|
|
|
|
const memoryLimit = limits?.memory ?? 2200;
|
|
const userLimit = limits?.user ?? 1375;
|
|
const memoryEnabled = limits?.memoryEnabled ?? true;
|
|
const userEnabled = limits?.userEnabled ?? true;
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<Tabs
|
|
value={activeTab}
|
|
onValueChange={(v) => setActiveTab(v as HermesMemoryKind)}
|
|
className="flex-1 flex flex-col"
|
|
>
|
|
<div className="px-6 pt-4 flex items-center justify-between gap-3 flex-wrap">
|
|
<TabsList>
|
|
<TabsTrigger value="memory">
|
|
{t("hermes.memory.agentTab")}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="user">{t("hermes.memory.userTab")}</TabsTrigger>
|
|
</TabsList>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => void openHermesWebUI("/config")}
|
|
>
|
|
<ExternalLink className="w-3.5 h-3.5 mr-1" />
|
|
{t("hermes.memory.openConfig")}
|
|
</Button>
|
|
</div>
|
|
|
|
<TabsContent value="memory" className="flex-1 px-6 pb-4 mt-4">
|
|
<MemoryTabPane
|
|
kind="memory"
|
|
limit={memoryLimit}
|
|
enabled={memoryEnabled}
|
|
/>
|
|
</TabsContent>
|
|
<TabsContent value="user" className="flex-1 px-6 pb-4 mt-4">
|
|
<MemoryTabPane kind="user" limit={userLimit} enabled={userEnabled} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default HermesMemoryPanel;
|