import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { homeDir, join } from "@tauri-apps/api/path"; import { settingsApi, type AppId } from "@/lib/api"; import type { SettingsFormState } from "./useSettingsForm"; export type DirectoryAppId = Exclude; type AppDirectoryKey = | "claude" | "codex" | "gemini" | "opencode" | "openclaw" | "hermes"; type DirectoryKey = "appConfig" | AppDirectoryKey; export interface ResolvedDirectories { appConfig: string; claude: string; codex: string; gemini: string; opencode: string; openclaw: string; hermes: string; } // Single source of truth for per-app directory metadata. const APP_DIRECTORY_META: Record< DirectoryAppId, { 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(); return trimmed.length > 0 ? trimmed : undefined; }; const computeDefaultAppConfigDir = async (): Promise => { try { const home = await homeDir(); return await join(home, ".cc-switch"); } catch (error) { console.error( "[useDirectorySettings] Failed to resolve default app config dir", error, ); return undefined; } }; const computeDefaultConfigDir = async ( app: DirectoryAppId, ): Promise => { try { const home = await homeDir(); return await join(home, APP_DIRECTORY_META[app].defaultFolder); } catch (error) { console.error( "[useDirectorySettings] Failed to resolve default config dir", error, ); return undefined; } }; export interface UseDirectorySettingsProps { settings: SettingsFormState | null; onUpdateSettings: (updates: Partial) => void; } export interface UseDirectorySettingsResult { appConfigDir?: string; resolvedDirs: ResolvedDirectories; isLoading: boolean; initialAppConfigDir?: string; updateDirectory: (app: DirectoryAppId, value?: string) => void; updateAppConfigDir: (value?: string) => void; browseDirectory: (app: DirectoryAppId) => Promise; browseAppConfigDir: () => Promise; resetDirectory: (app: DirectoryAppId) => Promise; resetAppConfigDir: () => Promise; resetAllDirectories: (overrides?: ResolvedAppDirectoryOverrides) => void; } export type ResolvedAppDirectoryOverrides = Partial< Record >; /** * useDirectorySettings - 目录管理 * 负责: * - appConfigDir 状态 * - resolvedDirs 状态 * - 目录选择(browse) * - 目录重置 * - 默认值计算 */ export function useDirectorySettings({ settings, onUpdateSettings, }: UseDirectorySettingsProps): UseDirectorySettingsResult { const { t } = useTranslation(); const [appConfigDir, setAppConfigDir] = useState( undefined, ); const [resolvedDirs, setResolvedDirs] = useState({ appConfig: "", claude: "", codex: "", gemini: "", opencode: "", openclaw: "", hermes: "", }); const [isLoading, setIsLoading] = useState(true); const defaultsRef = useRef({ appConfig: "", claude: "", codex: "", gemini: "", opencode: "", openclaw: "", hermes: "", }); const initialAppConfigDirRef = useRef(undefined); // 加载目录信息 useEffect(() => { let active = true; setIsLoading(true); const load = async () => { try { const [ overrideRaw, claudeDir, codexDir, geminiDir, opencodeDir, openclawDir, hermesDir, defaultAppConfig, defaultClaudeDir, defaultCodexDir, defaultGeminiDir, defaultOpencodeDir, defaultOpenclawDir, defaultHermesDir, ] = await Promise.all([ settingsApi.getAppConfigDirOverride(), settingsApi.getConfigDir("claude"), settingsApi.getConfigDir("codex"), 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; const normalizedOverride = sanitizeDir(overrideRaw ?? undefined); defaultsRef.current = { appConfig: defaultAppConfig ?? "", claude: defaultClaudeDir ?? "", codex: defaultCodexDir ?? "", gemini: defaultGeminiDir ?? "", opencode: defaultOpencodeDir ?? "", openclaw: defaultOpenclawDir ?? "", hermes: defaultHermesDir ?? "", }; setAppConfigDir(normalizedOverride); initialAppConfigDirRef.current = normalizedOverride; setResolvedDirs({ appConfig: normalizedOverride ?? 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, hermes: hermesDir || defaultsRef.current.hermes, }); } catch (error) { console.error( "[useDirectorySettings] Failed to load directory info", error, ); } finally { if (active) { setIsLoading(false); } } }; void load(); return () => { active = false; }; }, []); const updateDirectoryState = useCallback( (key: DirectoryKey, value?: string) => { const sanitized = sanitizeDir(value); if (key === "appConfig") { setAppConfigDir(sanitized); } else { onUpdateSettings({ [DIRECTORY_KEY_TO_SETTINGS_FIELD[key]]: sanitized, }); } 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], ); const updateAppConfigDir = useCallback( (value?: string) => { updateDirectoryState("appConfig", value); }, [updateDirectoryState], ); const updateDirectory = useCallback( (app: DirectoryAppId, value?: string) => { updateDirectoryState(APP_DIRECTORY_META[app].key, value); }, [updateDirectoryState], ); const browseDirectory = useCallback( async (app: DirectoryAppId) => { const key = APP_DIRECTORY_META[app].key; const settingsField = DIRECTORY_KEY_TO_SETTINGS_FIELD[key]; const currentValue = (settings?.[settingsField] as string | undefined) ?? resolvedDirs[key]; try { const picked = await settingsApi.selectConfigDirectory(currentValue); const sanitized = sanitizeDir(picked ?? undefined); if (!sanitized) return; updateDirectoryState(key, sanitized); } catch (error) { console.error("[useDirectorySettings] Failed to pick directory", error); toast.error( t("settings.selectFileFailed", { defaultValue: "选择目录失败", }), ); } }, [settings, resolvedDirs, t, updateDirectoryState], ); const browseAppConfigDir = useCallback(async () => { const currentValue = appConfigDir ?? resolvedDirs.appConfig; try { const picked = await settingsApi.selectConfigDirectory(currentValue); const sanitized = sanitizeDir(picked ?? undefined); if (!sanitized) return; updateDirectoryState("appConfig", sanitized); } catch (error) { console.error( "[useDirectorySettings] Failed to pick app config directory", error, ); toast.error( t("settings.selectFileFailed", { defaultValue: "选择目录失败", }), ); } }, [appConfigDir, resolvedDirs.appConfig, t, updateDirectoryState]); const resetDirectory = useCallback( async (app: DirectoryAppId) => { const key = APP_DIRECTORY_META[app].key; if (!defaultsRef.current[key]) { const fallback = await computeDefaultConfigDir(app); if (fallback) { defaultsRef.current = { ...defaultsRef.current, [key]: fallback, }; } } updateDirectoryState(key, undefined); }, [updateDirectoryState], ); const resetAppConfigDir = useCallback(async () => { if (!defaultsRef.current.appConfig) { const fallback = await computeDefaultAppConfigDir(); if (fallback) { defaultsRef.current = { ...defaultsRef.current, appConfig: fallback, }; } } updateDirectoryState("appConfig", undefined); }, [updateDirectoryState]); const resetAllDirectories = useCallback( (overrides?: ResolvedAppDirectoryOverrides) => { setAppConfigDir(initialAppConfigDirRef.current); setResolvedDirs({ appConfig: initialAppConfigDirRef.current ?? defaultsRef.current.appConfig, 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, }); }, [], ); return { appConfigDir, resolvedDirs, isLoading, initialAppConfigDir: initialAppConfigDirRef.current, updateDirectory, updateAppConfigDir, browseDirectory, browseAppConfigDir, resetDirectory, resetAppConfigDir, resetAllDirectories, }; }