feat(hermes): memory enable switch + clearer migration warning copy

Replaces the greyed-out "Memory is disabled" banner with a real Switch
at the top of each memory tab. Users can now toggle Hermes' memory/user
blobs without leaving CC Switch; the underlying write goes through the
merge-aware `set_memory_enabled`, so budgets and external-provider
settings survive toggle operations. The new `useToggleHermesMemoryEnabled`
mutation invalidates the limits query so the Switch state and the
amber disabled-hint update in lockstep.

Reworks the `schema_migrated_v12` health banner copy to match the
simplified "CC Switch only manages custom_providers" posture — it now
tells users to reconcile migrated dict entries via Hermes Web UI,
instead of the earlier (and now inaccurate) "CC Switch reads both".
This commit is contained in:
Jason
2026-04-20 09:49:23 +08:00
parent b8a3534cb5
commit 185ac2be9b
4 changed files with 104 additions and 4 deletions
@@ -30,6 +30,41 @@ function getWarningText(
return t("hermes.health.envParseFailed", {
defaultValue: "The .env file could not be parsed.",
});
case "model_no_default":
return t("hermes.health.modelNoDefault", {
defaultValue:
"No default model or provider is configured in the 'model' section.",
});
case "custom_providers_not_list":
return t("hermes.health.customProvidersNotList", {
defaultValue:
"custom_providers should be a YAML list (items prefixed with '-'), not a mapping.",
});
case "model_provider_unknown":
return t("hermes.health.modelProviderUnknown", {
defaultValue:
"model.provider references a provider that is not configured.",
});
case "model_default_not_in_provider":
return t("hermes.health.modelDefaultNotInProvider", {
defaultValue:
"model.default is not in the selected provider's models list.",
});
case "duplicate_provider_name":
return t("hermes.health.duplicateProviderName", {
defaultValue:
"custom_providers contains duplicate provider names — only one entry will be used.",
});
case "duplicate_provider_base_url":
return t("hermes.health.duplicateProviderBaseUrl", {
defaultValue:
"custom_providers contains duplicate base_urls — possible accidental copy.",
});
case "schema_migrated_v12":
return t("hermes.health.schemaMigratedV12", {
defaultValue:
"Hermes moved some providers into the 'providers:' dict. CC Switch only manages 'custom_providers:' — edit or remove those entries via Hermes Web UI.",
});
default:
return fallback;
}
+30 -4
View File
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { ExternalLink } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import MarkdownEditor from "@/components/MarkdownEditor";
import {
@@ -10,6 +11,7 @@ import {
useHermesMemoryLimits,
useOpenHermesWebUI,
useSaveHermesMemory,
useToggleHermesMemoryEnabled,
} from "@/hooks/useHermes";
import { useDarkMode } from "@/hooks/useDarkMode";
import type { HermesMemoryKind } from "@/types";
@@ -30,6 +32,7 @@ const MemoryTabPane: React.FC<MemoryTabPaneProps> = ({
const darkMode = useDarkMode();
const { data, isLoading } = useHermesMemory(kind, true);
const saveMutation = useSaveHermesMemory();
const toggleMutation = useToggleHermesMemoryEnabled();
const [content, setContent] = useState("");
const [loaded, setLoaded] = useState(false);
@@ -57,11 +60,34 @@ const MemoryTabPane: React.FC<MemoryTabPaneProps> = ({
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
className={cn(
"flex items-center justify-between px-3 py-2 rounded-md border",
enabled
? "bg-muted/30"
: "bg-amber-500/10 border-amber-500/30",
)}
>
<div className="flex items-center gap-2">
<Switch
checked={enabled}
disabled={toggleMutation.isPending}
onCheckedChange={(next) =>
toggleMutation.mutate({ kind, enabled: next })
}
/>
<span className="text-sm">
{enabled
? t("hermes.memory.enableOn")
: t("hermes.memory.enableOff")}
</span>
</div>
)}
{!enabled && (
<span className="text-xs text-amber-700 dark:text-amber-400">
{t("hermes.memory.disabledHint")}
</span>
)}
</div>
{isLoading && !loaded ? (
<div className="flex items-center justify-center h-64 text-muted-foreground">
+28
View File
@@ -125,6 +125,34 @@ export function useSaveHermesMemory() {
});
}
/**
* Toggle one memory blob's on/off flag in Hermes' `config.yaml`. Invalidates
* the limits query so the switch UI and disabled banner update immediately.
*/
export function useToggleHermesMemoryEnabled() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: ({
kind,
enabled,
}: {
kind: HermesMemoryKind;
enabled: boolean;
}) => hermesApi.setMemoryEnabled(kind, enabled),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: hermesKeys.memoryLimits,
});
},
onError: (error) => {
toast.error(t("hermes.memory.toggleFailed"), {
description: extractErrorMessage(error) || undefined,
});
},
});
}
/**
* Returns a handler that probes the local Hermes Web UI, opens it in the
* system browser, and surfaces a localized toast on failure. Callers only
+11
View File
@@ -53,4 +53,15 @@ export const hermesApi = {
async getMemoryLimits(): Promise<HermesMemoryLimits> {
return await invoke("get_hermes_memory_limits");
},
/**
* Toggle the on/off flag for one memory blob. Other fields in the `memory:`
* section (budgets, external provider config) are preserved.
*/
async setMemoryEnabled(
kind: HermesMemoryKind,
enabled: boolean,
): Promise<void> {
await invoke("set_hermes_memory_enabled", { kind, enabled });
},
};