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:
Jason
2026-04-19 21:49:27 +08:00
parent 088b47b08a
commit acc6d795e4
12 changed files with 552 additions and 6 deletions
+29
View File
@@ -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
View File
@@ -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