From b8a3534cb5209c4a5ddea5fe7ba37631b7d74fe4 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 20 Apr 2026 09:49:13 +0800 Subject: [PATCH] feat(settings): add Hermes config dir override with data-driven dispatch Adds a dedicated Hermes row to the directory-override settings so users can point CC Switch at alternate Hermes config locations (e.g. a second profile directory for work/personal split). `get_config_dir` on the Rust side already supports hermes; this just wires up the frontend row. Wiring it through `useDirectorySettings` revealed a scaling problem: every supported app required five parallel ternary chains across `computeDefaultConfigDir`, `updateDirectory`, `browseDirectory`, `resetDirectory`, and `updateDirectoryState`. Replaces those with two lookup tables (`APP_DIRECTORY_META`, `DIRECTORY_KEY_TO_SETTINGS_FIELD`) so adding the next app is two entries, not fifteen edit sites. Drive-by cleanup from the same touch: * `resetAllDirectories` takes a `ResolvedAppDirectoryOverrides` object instead of five positional optional strings. * `setResolvedDirs` returns the same reference when the sanitized value is unchanged, so no-op edits don't cascade renders. Also lands all i18n updates for this series (`hermesConfigDir` and placeholder, Memory section's enable/disable/toggleFailed copy, and the reworded `schemaMigratedV12` warning) in zh/en/ja together. --- src/components/settings/DirectorySettings.tsx | 13 ++ src/components/settings/SettingsPage.tsx | 1 + src/hooks/useDirectorySettings.ts | 155 ++++++++---------- src/hooks/useSettings.ts | 15 +- src/i18n/locales/en.json | 17 +- src/i18n/locales/ja.json | 17 +- src/i18n/locales/zh.json | 17 +- tests/hooks/useDirectorySettings.test.tsx | 18 +- tests/hooks/useSettings.test.tsx | 15 +- 9 files changed, 151 insertions(+), 117 deletions(-) diff --git a/src/components/settings/DirectorySettings.tsx b/src/components/settings/DirectorySettings.tsx index d66f09b9c..408a56901 100644 --- a/src/components/settings/DirectorySettings.tsx +++ b/src/components/settings/DirectorySettings.tsx @@ -17,6 +17,7 @@ interface DirectorySettingsProps { geminiDir?: string; opencodeDir?: string; openclawDir?: string; + hermesDir?: string; onDirectoryChange: (app: AppId, value?: string) => void; onBrowseDirectory: (app: AppId) => Promise; onResetDirectory: (app: AppId) => Promise; @@ -33,6 +34,7 @@ export function DirectorySettings({ geminiDir, opencodeDir, openclawDir, + hermesDir, onDirectoryChange, onBrowseDirectory, onResetDirectory, @@ -143,6 +145,17 @@ export function DirectorySettings({ onBrowse={() => onBrowseDirectory("openclaw")} onReset={() => onResetDirectory("openclaw")} /> + + onDirectoryChange("hermes", val)} + onBrowse={() => onBrowseDirectory("hermes")} + onReset={() => onResetDirectory("hermes")} + /> ); diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index 688e81683..ccd3e68bc 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -317,6 +317,7 @@ export function SettingsPage({ geminiDir={settings.geminiConfigDir} opencodeDir={settings.opencodeConfigDir} openclawDir={settings.openclawConfigDir} + hermesDir={settings.hermesConfigDir} onDirectoryChange={updateDirectory} onBrowseDirectory={browseDirectory} onResetDirectory={resetDirectory} diff --git a/src/hooks/useDirectorySettings.ts b/src/hooks/useDirectorySettings.ts index 51a28dc4a..d6fff53f5 100644 --- a/src/hooks/useDirectorySettings.ts +++ b/src/hooks/useDirectorySettings.ts @@ -5,13 +5,14 @@ import { homeDir, join } from "@tauri-apps/api/path"; import { settingsApi, type AppId } from "@/lib/api"; import type { SettingsFormState } from "./useSettingsForm"; -type DirectoryKey = - | "appConfig" +type AppDirectoryKey = | "claude" | "codex" | "gemini" | "opencode" - | "openclaw"; + | "openclaw" + | "hermes"; +type DirectoryKey = "appConfig" | AppDirectoryKey; export interface ResolvedDirectories { appConfig: string; @@ -20,8 +21,34 @@ export interface ResolvedDirectories { gemini: string; opencode: string; openclaw: string; + hermes: string; } +// Single source of truth for per-app directory metadata. +const APP_DIRECTORY_META: Record< + AppId, + { key: AppDirectoryKey; defaultFolder: string } +> = { + claude: { key: "claude", defaultFolder: ".claude" }, + codex: { key: "codex", defaultFolder: ".codex" }, + gemini: { key: "gemini", defaultFolder: ".gemini" }, + opencode: { key: "opencode", defaultFolder: ".config/opencode" }, + openclaw: { key: "openclaw", defaultFolder: ".openclaw" }, + hermes: { key: "hermes", defaultFolder: ".hermes" }, +}; + +const DIRECTORY_KEY_TO_SETTINGS_FIELD: Record< + AppDirectoryKey, + keyof SettingsFormState +> = { + claude: "claudeConfigDir", + codex: "codexConfigDir", + gemini: "geminiConfigDir", + opencode: "opencodeConfigDir", + openclaw: "openclawConfigDir", + hermes: "hermesConfigDir", +}; + const sanitizeDir = (value?: string | null): string | undefined => { if (!value) return undefined; const trimmed = value.trim(); @@ -46,17 +73,7 @@ const computeDefaultConfigDir = async ( ): Promise => { try { const home = await homeDir(); - const folder = - app === "claude" - ? ".claude" - : app === "codex" - ? ".codex" - : app === "gemini" - ? ".gemini" - : app === "opencode" - ? ".config/opencode" - : ".openclaw"; - return await join(home, folder); + return await join(home, APP_DIRECTORY_META[app].defaultFolder); } catch (error) { console.error( "[useDirectorySettings] Failed to resolve default config dir", @@ -82,15 +99,13 @@ export interface UseDirectorySettingsResult { browseAppConfigDir: () => Promise; resetDirectory: (app: AppId) => Promise; resetAppConfigDir: () => Promise; - resetAllDirectories: ( - claudeDir?: string, - codexDir?: string, - geminiDir?: string, - opencodeDir?: string, - openclawDir?: string, - ) => void; + resetAllDirectories: (overrides?: ResolvedAppDirectoryOverrides) => void; } +export type ResolvedAppDirectoryOverrides = Partial< + Record +>; + /** * useDirectorySettings - 目录管理 * 负责: @@ -116,6 +131,7 @@ export function useDirectorySettings({ gemini: "", opencode: "", openclaw: "", + hermes: "", }); const [isLoading, setIsLoading] = useState(true); @@ -126,6 +142,7 @@ export function useDirectorySettings({ gemini: "", opencode: "", openclaw: "", + hermes: "", }); const initialAppConfigDirRef = useRef(undefined); @@ -143,12 +160,14 @@ export function useDirectorySettings({ geminiDir, opencodeDir, openclawDir, + hermesDir, defaultAppConfig, defaultClaudeDir, defaultCodexDir, defaultGeminiDir, defaultOpencodeDir, defaultOpenclawDir, + defaultHermesDir, ] = await Promise.all([ settingsApi.getAppConfigDirOverride(), settingsApi.getConfigDir("claude"), @@ -156,12 +175,14 @@ export function useDirectorySettings({ settingsApi.getConfigDir("gemini"), settingsApi.getConfigDir("opencode"), settingsApi.getConfigDir("openclaw"), + settingsApi.getConfigDir("hermes"), computeDefaultAppConfigDir(), computeDefaultConfigDir("claude"), computeDefaultConfigDir("codex"), computeDefaultConfigDir("gemini"), computeDefaultConfigDir("opencode"), computeDefaultConfigDir("openclaw"), + computeDefaultConfigDir("hermes"), ]); if (!active) return; @@ -175,6 +196,7 @@ export function useDirectorySettings({ gemini: defaultGeminiDir ?? "", opencode: defaultOpencodeDir ?? "", openclaw: defaultOpenclawDir ?? "", + hermes: defaultHermesDir ?? "", }; setAppConfigDir(normalizedOverride); @@ -187,6 +209,7 @@ export function useDirectorySettings({ gemini: geminiDir || defaultsRef.current.gemini, opencode: opencodeDir || defaultsRef.current.opencode, openclaw: openclawDir || defaultsRef.current.openclaw, + hermes: hermesDir || defaultsRef.current.hermes, }); } catch (error) { console.error( @@ -212,23 +235,18 @@ export function useDirectorySettings({ if (key === "appConfig") { setAppConfigDir(sanitized); } else { - onUpdateSettings( - key === "claude" - ? { claudeConfigDir: sanitized } - : key === "codex" - ? { codexConfigDir: sanitized } - : key === "gemini" - ? { geminiConfigDir: sanitized } - : key === "opencode" - ? { opencodeConfigDir: sanitized } - : { openclawConfigDir: sanitized }, - ); + onUpdateSettings({ + [DIRECTORY_KEY_TO_SETTINGS_FIELD[key]]: sanitized, + }); } - setResolvedDirs((prev) => ({ - ...prev, - [key]: sanitized ?? defaultsRef.current[key], - })); + setResolvedDirs((prev) => { + const next = sanitized ?? defaultsRef.current[key]; + // Same-ref early-return: unchanged value shouldn't cascade renders + // through the settings tree. + if (prev[key] === next) return prev; + return { ...prev, [key]: next }; + }); }, [onUpdateSettings], ); @@ -242,44 +260,17 @@ export function useDirectorySettings({ const updateDirectory = useCallback( (app: AppId, value?: string) => { - updateDirectoryState( - app === "claude" - ? "claude" - : app === "codex" - ? "codex" - : app === "gemini" - ? "gemini" - : app === "opencode" - ? "opencode" - : "openclaw", - value, - ); + updateDirectoryState(APP_DIRECTORY_META[app].key, value); }, [updateDirectoryState], ); const browseDirectory = useCallback( async (app: AppId) => { - const key: DirectoryKey = - app === "claude" - ? "claude" - : app === "codex" - ? "codex" - : app === "gemini" - ? "gemini" - : app === "opencode" - ? "opencode" - : "openclaw"; + const key = APP_DIRECTORY_META[app].key; + const settingsField = DIRECTORY_KEY_TO_SETTINGS_FIELD[key]; const currentValue = - key === "claude" - ? (settings?.claudeConfigDir ?? resolvedDirs.claude) - : key === "codex" - ? (settings?.codexConfigDir ?? resolvedDirs.codex) - : key === "gemini" - ? (settings?.geminiConfigDir ?? resolvedDirs.gemini) - : key === "opencode" - ? (settings?.opencodeConfigDir ?? resolvedDirs.opencode) - : (settings?.openclawConfigDir ?? resolvedDirs.openclaw); + (settings?.[settingsField] as string | undefined) ?? resolvedDirs[key]; try { const picked = await settingsApi.selectConfigDirectory(currentValue); @@ -320,16 +311,7 @@ export function useDirectorySettings({ const resetDirectory = useCallback( async (app: AppId) => { - const key: DirectoryKey = - app === "claude" - ? "claude" - : app === "codex" - ? "codex" - : app === "gemini" - ? "gemini" - : app === "opencode" - ? "opencode" - : "openclaw"; + const key = APP_DIRECTORY_META[app].key; if (!defaultsRef.current[key]) { const fallback = await computeDefaultConfigDir(app); if (fallback) { @@ -358,22 +340,17 @@ export function useDirectorySettings({ }, [updateDirectoryState]); const resetAllDirectories = useCallback( - ( - claudeDir?: string, - codexDir?: string, - geminiDir?: string, - opencodeDir?: string, - openclawDir?: string, - ) => { + (overrides?: ResolvedAppDirectoryOverrides) => { setAppConfigDir(initialAppConfigDirRef.current); setResolvedDirs({ appConfig: initialAppConfigDirRef.current ?? defaultsRef.current.appConfig, - claude: claudeDir ?? defaultsRef.current.claude, - codex: codexDir ?? defaultsRef.current.codex, - gemini: geminiDir ?? defaultsRef.current.gemini, - opencode: opencodeDir ?? defaultsRef.current.opencode, - openclaw: openclawDir ?? defaultsRef.current.openclaw, + claude: overrides?.claude ?? defaultsRef.current.claude, + codex: overrides?.codex ?? defaultsRef.current.codex, + gemini: overrides?.gemini ?? defaultsRef.current.gemini, + opencode: overrides?.opencode ?? defaultsRef.current.opencode, + openclaw: overrides?.openclaw ?? defaultsRef.current.openclaw, + hermes: overrides?.hermes ?? defaultsRef.current.hermes, }); }, [], diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index c7c77af96..2982eb1f9 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -107,13 +107,14 @@ export function useSettings(): UseSettingsResult { const resetSettings = useCallback(() => { resetForm(data ?? null); syncLanguage(initialLanguage); - resetAllDirectories( - sanitizeDir(data?.claudeConfigDir), - sanitizeDir(data?.codexConfigDir), - sanitizeDir(data?.geminiConfigDir), - sanitizeDir(data?.opencodeConfigDir), - sanitizeDir(data?.openclawConfigDir), - ); + resetAllDirectories({ + claude: sanitizeDir(data?.claudeConfigDir), + codex: sanitizeDir(data?.codexConfigDir), + gemini: sanitizeDir(data?.geminiConfigDir), + opencode: sanitizeDir(data?.opencodeConfigDir), + openclaw: sanitizeDir(data?.openclawConfigDir), + hermes: sanitizeDir(data?.hermesConfigDir), + }); setRequiresRestart(false); }, [ data, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index dd18e1c8b..cfa420472 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -579,11 +579,14 @@ "opencodeConfigDirDescription": "Override OpenCode configuration directory (opencode.json).", "openclawConfigDir": "OpenClaw Configuration Directory", "openclawConfigDirDescription": "Override OpenClaw configuration directory (openclaw.json).", + "hermesConfigDir": "Hermes Configuration Directory", + "hermesConfigDirDescription": "Override Hermes configuration directory (config.yaml).", "browsePlaceholderClaude": "e.g., /home//.claude", "browsePlaceholderCodex": "e.g., /home//.codex", "browsePlaceholderGemini": "e.g., /home//.gemini", "browsePlaceholderOpencode": "e.g., /home//.config/opencode", "browsePlaceholderOpenclaw": "e.g., /home//.openclaw", + "browsePlaceholderHermes": "e.g., /home//.hermes", "browseDirectory": "Browse Directory", "resetDefault": "Reset to default directory (takes effect after saving)", "checkForUpdates": "Check for Updates", @@ -1674,7 +1677,14 @@ "title": "Hermes config warnings detected", "parseFailed": "config.yaml could not be parsed as valid YAML. Fix the file before editing it here.", "configNotFound": "Hermes config.yaml not found. Create it at ~/.hermes/config.yaml or configure the path in settings.", - "envParseFailed": "The .env file could not be parsed." + "envParseFailed": "The .env file could not be parsed.", + "modelNoDefault": "No default model or provider is configured in the 'model' section.", + "customProvidersNotList": "custom_providers should be a YAML list (items prefixed with '-'), not a mapping.", + "modelProviderUnknown": "model.provider references a provider that is not configured.", + "modelDefaultNotInProvider": "model.default is not in the selected provider's models list.", + "duplicateProviderName": "custom_providers contains duplicate provider names — only one entry will be used.", + "duplicateProviderBaseUrl": "custom_providers contains duplicate base_urls — possible accidental copy.", + "schemaMigratedV12": "Hermes moved some providers into the 'providers:' dict. CC Switch only manages 'custom_providers:' — edit or remove those entries via Hermes Web UI." }, "memory": { "title": "Memory", @@ -1682,7 +1692,10 @@ "userTab": "User Profile (USER.md)", "usage": "{{current}} / {{limit}} characters", "overLimit": "Over budget — Hermes will truncate on next load", - "disabled": "Memory is disabled in config.yaml. Enable it in Hermes config for edits to take effect.", + "enableOn": "Enabled", + "enableOff": "Disabled", + "disabledHint": "Hermes will skip this memory until re-enabled", + "toggleFailed": "Failed to toggle memory", "saveSuccess": "Memory saved", "saveFailed": "Failed to save memory", "loadFailed": "Failed to read memory file", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 2f9abdae4..2c00458f5 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -579,11 +579,14 @@ "opencodeConfigDirDescription": "OpenCode の設定ディレクトリ(opencode.json)を上書きします。", "openclawConfigDir": "OpenClaw 設定ディレクトリ", "openclawConfigDirDescription": "OpenClaw の設定ディレクトリ(openclaw.json)を上書きします。", + "hermesConfigDir": "Hermes 設定ディレクトリ", + "hermesConfigDirDescription": "Hermes の設定ディレクトリ(config.yaml)を上書きします。", "browsePlaceholderClaude": "例: /home//.claude", "browsePlaceholderCodex": "例: /home//.codex", "browsePlaceholderGemini": "例: /home//.gemini", "browsePlaceholderOpencode": "例: /home//.config/opencode", "browsePlaceholderOpenclaw": "例: /home//.openclaw", + "browsePlaceholderHermes": "例: /home//.hermes", "browseDirectory": "ディレクトリを選択", "resetDefault": "デフォルトに戻す(保存後に反映)", "checkForUpdates": "アップデートを確認", @@ -1674,7 +1677,14 @@ "title": "Hermes 設定の警告を検出", "parseFailed": "config.yaml を有効な YAML として解析できません。ここで編集する前にファイルを修正してください。", "configNotFound": "Hermes config.yaml が見つかりません。~/.hermes/config.yaml に作成するか、設定でパスを構成してください。", - "envParseFailed": ".env ファイルを解析できません。" + "envParseFailed": ".env ファイルを解析できません。", + "modelNoDefault": "'model' セクションに既定モデルまたはプロバイダーが設定されていません。", + "customProvidersNotList": "custom_providers はマッピングではなく、YAML リスト('-' で始まる項目)である必要があります。", + "modelProviderUnknown": "model.provider が指すプロバイダーは設定に存在しません。", + "modelDefaultNotInProvider": "model.default は選択中のプロバイダーのモデル一覧に含まれていません。", + "duplicateProviderName": "custom_providers にプロバイダー名の重複があります。1 件のみ有効になります。", + "duplicateProviderBaseUrl": "custom_providers に重複した base_url があります。誤ってコピーされた可能性があります。", + "schemaMigratedV12": "Hermes は一部のプロバイダーを 'providers:' dict に移しました。CC Switch は 'custom_providers:' のみを管理します。該当する項目は Hermes Web UI で編集・削除してください。" }, "memory": { "title": "メモリ", @@ -1682,7 +1692,10 @@ "userTab": "ユーザープロファイル (USER.md)", "usage": "{{current}} / {{limit}} 文字", "overLimit": "上限を超えました。Hermes は次回読み込み時に切り詰めます", - "disabled": "Memory が config.yaml で無効化されています。Hermes 設定で有効化すると編集内容が反映されます。", + "enableOn": "有効", + "enableOff": "無効", + "disabledHint": "再度有効化するまで Hermes はこの記憶をスキップします", + "toggleFailed": "メモリの切り替えに失敗しました", "saveSuccess": "記憶を保存しました", "saveFailed": "記憶の保存に失敗しました", "loadFailed": "記憶ファイルの読み込みに失敗しました", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index acefa6c9e..0ef934332 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -579,11 +579,14 @@ "opencodeConfigDirDescription": "覆盖 OpenCode 配置目录 (opencode.json)。", "openclawConfigDir": "OpenClaw 配置目录", "openclawConfigDirDescription": "覆盖 OpenClaw 配置目录 (openclaw.json)。", + "hermesConfigDir": "Hermes 配置目录", + "hermesConfigDirDescription": "覆盖 Hermes 配置目录 (config.yaml)。", "browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude", "browsePlaceholderCodex": "例如:/home/<你的用户名>/.codex", "browsePlaceholderGemini": "例如:/home/<你的用户名>/.gemini", "browsePlaceholderOpencode": "例如:/home/<你的用户名>/.config/opencode", "browsePlaceholderOpenclaw": "例如:/home/<你的用户名>/.openclaw", + "browsePlaceholderHermes": "例如:/home/<你的用户名>/.hermes", "browseDirectory": "浏览目录", "resetDefault": "恢复默认目录(需保存后生效)", "checkForUpdates": "检查更新", @@ -1674,7 +1677,14 @@ "title": "检测到 Hermes 配置警告", "parseFailed": "config.yaml 无法解析为有效 YAML。请先修复文件再在此编辑。", "configNotFound": "未找到 Hermes config.yaml。请在 ~/.hermes/config.yaml 创建或在设置中配置路径。", - "envParseFailed": ".env 文件无法解析。" + "envParseFailed": ".env 文件无法解析。", + "modelNoDefault": "model 段未设置默认模型或供应商。", + "customProvidersNotList": "custom_providers 应为 YAML 列表(以 `-` 开头),而不是映射。", + "modelProviderUnknown": "model.provider 指向的供应商未在配置中找到。", + "modelDefaultNotInProvider": "model.default 指向的模型不在所选供应商的模型列表中。", + "duplicateProviderName": "custom_providers 中存在重复的供应商名,只会有一条生效。", + "duplicateProviderBaseUrl": "custom_providers 中存在重复的 base_url,可能是意外复制。", + "schemaMigratedV12": "Hermes 把部分供应商移到了 'providers:' dict。CC Switch 只管理 'custom_providers:',请在 Hermes Web UI 编辑或删除这些条目。" }, "memory": { "title": "记忆管理", @@ -1682,7 +1692,10 @@ "userTab": "用户画像 (USER.md)", "usage": "已用 {{current}} / {{limit}} 字符", "overLimit": "已超过上限,Hermes 下次加载时会截断", - "disabled": "Memory 在 config.yaml 中已禁用,编辑后需在 Hermes 配置启用才会生效。", + "enableOn": "已启用", + "enableOff": "已禁用", + "disabledHint": "在重新启用前 Hermes 会跳过此记忆", + "toggleFailed": "切换记忆状态失败", "saveSuccess": "记忆已保存", "saveFailed": "保存记忆失败", "loadFailed": "读取记忆文件失败", diff --git a/tests/hooks/useDirectorySettings.test.tsx b/tests/hooks/useDirectorySettings.test.tsx index 8fa029537..d5ea101ee 100644 --- a/tests/hooks/useDirectorySettings.test.tsx +++ b/tests/hooks/useDirectorySettings.test.tsx @@ -69,7 +69,8 @@ describe("useDirectorySettings", () => { if (app === "codex") return "/remote/codex"; if (app === "gemini") return "/remote/gemini"; if (app === "opencode") return "/remote/opencode"; - return "/remote/openclaw"; + if (app === "openclaw") return "/remote/openclaw"; + return "/remote/hermes"; }); selectConfigDirectoryMock.mockReset(); }); @@ -91,6 +92,7 @@ describe("useDirectorySettings", () => { gemini: "/remote/gemini", opencode: "/remote/opencode", openclaw: "/remote/openclaw", + hermes: "/remote/hermes", }); }); @@ -243,13 +245,13 @@ describe("useDirectorySettings", () => { await waitFor(() => expect(result.current.isLoading).toBe(false)); act(() => { - result.current.resetAllDirectories( - "/server/claude", - "/server/codex", - "/server/gemini", - "/server/opencode", - "/server/openclaw", - ); + result.current.resetAllDirectories({ + claude: "/server/claude", + codex: "/server/codex", + gemini: "/server/gemini", + opencode: "/server/opencode", + openclaw: "/server/openclaw", + }); }); expect(result.current.resolvedDirs.claude).toBe("/server/claude"); diff --git a/tests/hooks/useSettings.test.tsx b/tests/hooks/useSettings.test.tsx index a4c011e9d..f16c4b90a 100644 --- a/tests/hooks/useSettings.test.tsx +++ b/tests/hooks/useSettings.test.tsx @@ -455,13 +455,14 @@ describe("useSettings hook", () => { expect(settingsFormMock.syncLanguage).toHaveBeenCalledWith( settingsFormMock.initialLanguage, ); - expect(directorySettingsMock.resetAllDirectories).toHaveBeenCalledWith( - "/server/claude", - undefined, - "/server/gemini", - "/server/opencode", - "/server/openclaw", - ); + expect(directorySettingsMock.resetAllDirectories).toHaveBeenCalledWith({ + claude: "/server/claude", + codex: undefined, + gemini: "/server/gemini", + opencode: "/server/opencode", + openclaw: "/server/openclaw", + hermes: undefined, + }); expect(metadataMock.setRequiresRestart).toHaveBeenCalledWith(false); });