mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 04:02:02 +08:00
feat(hermes): replace Prompts entry with Memory panel
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.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Subscribe to the presence of `class="dark"` on `<html>`. The theme-provider
|
||||
* toggles this class whenever the effective theme changes (including when the
|
||||
* OS changes theme while the app is in "system" mode), so observing the class
|
||||
* is the simplest way to drive components that need a boolean `darkMode` prop
|
||||
* (e.g. CodeMirror-based editors that don't consume the theme context).
|
||||
*/
|
||||
export function useDarkMode(): boolean {
|
||||
const [isDark, setIsDark] = useState(() =>
|
||||
typeof document !== "undefined"
|
||||
? document.documentElement.classList.contains("dark")
|
||||
: false,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver(() => {
|
||||
setIsDark(document.documentElement.classList.contains("dark"));
|
||||
});
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return isDark;
|
||||
}
|
||||
+56
-1
@@ -1,9 +1,15 @@
|
||||
import { useCallback } from "react";
|
||||
import { useQuery, type QueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type QueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { hermesApi } from "@/lib/api/hermes";
|
||||
import { providersApi } from "@/lib/api/providers";
|
||||
import type { HermesMemoryKind } from "@/types";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
|
||||
/**
|
||||
@@ -22,6 +28,8 @@ export const hermesKeys = {
|
||||
liveProviderIds: ["hermes", "liveProviderIds"] as const,
|
||||
modelConfig: ["hermes", "modelConfig"] as const,
|
||||
health: ["hermes", "health"] as const,
|
||||
memory: (kind: HermesMemoryKind) => ["hermes", "memory", kind] as const,
|
||||
memoryLimits: ["hermes", "memoryLimits"] as const,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -66,10 +74,57 @@ export function useHermesHealth(enabled: boolean) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useHermesMemory(kind: HermesMemoryKind, enabled: boolean) {
|
||||
return useQuery({
|
||||
queryKey: hermesKeys.memory(kind),
|
||||
queryFn: () => hermesApi.getMemory(kind),
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function useHermesMemoryLimits(enabled: boolean) {
|
||||
return useQuery({
|
||||
queryKey: hermesKeys.memoryLimits,
|
||||
queryFn: () => hermesApi.getMemoryLimits(),
|
||||
staleTime: 60_000,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Mutation hooks
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Save a Hermes memory file atomically and refresh the corresponding query.
|
||||
* Error toasts are emitted here so caller components don't need their own
|
||||
* try/catch; success toasts are intentionally left to the caller (to pick
|
||||
* the right localized message per tab).
|
||||
*/
|
||||
export function useSaveHermesMemory() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
kind,
|
||||
content,
|
||||
}: {
|
||||
kind: HermesMemoryKind;
|
||||
content: string;
|
||||
}) => hermesApi.setMemory(kind, content),
|
||||
onSuccess: async (_data, variables) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: hermesKeys.memory(variables.kind),
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(t("hermes.memory.saveFailed"), {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user