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.
This commit is contained in:
Jason
2026-04-20 09:49:13 +08:00
parent 31fb998575
commit b8a3534cb5
9 changed files with 151 additions and 117 deletions
+10 -8
View File
@@ -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");
+8 -7
View File
@@ -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);
});