mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 01:25:33 +08:00
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:
@@ -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<string | undefined> => {
|
||||
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<void>;
|
||||
resetDirectory: (app: AppId) => Promise<void>;
|
||||
resetAppConfigDir: () => Promise<void>;
|
||||
resetAllDirectories: (
|
||||
claudeDir?: string,
|
||||
codexDir?: string,
|
||||
geminiDir?: string,
|
||||
opencodeDir?: string,
|
||||
openclawDir?: string,
|
||||
) => void;
|
||||
resetAllDirectories: (overrides?: ResolvedAppDirectoryOverrides) => void;
|
||||
}
|
||||
|
||||
export type ResolvedAppDirectoryOverrides = Partial<
|
||||
Record<AppDirectoryKey, string | undefined>
|
||||
>;
|
||||
|
||||
/**
|
||||
* 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<string | undefined>(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,
|
||||
});
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user