repo_description: A cross-platform desktop All-in-One assistant tool for Claude Code, Codex, OpenCode, openclaw & Gemini CLI.
repo_topics: ai-tools claude-code codex desktop-app mcp minimax omo open-source openclaw openclaw-ui opencode provider-management rust skills skills-management tauri typescript wsl-support
code: 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";
  
  type DirectoryKey = "appConfig" | "claude" | "codex" | "gemini" | "opencode";
  
  export interface ResolvedDirectories {
    appConfig: string;
    claude: string;
    codex: string;
    gemini: string;
    opencode: string;
  }
  
  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<string | undefined> => {
    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: AppId,
  ): Promise<string | undefined> => {
    try {
      const home = await homeDir();
      const folder =
        app === "claude"
          ? ".claude"
          : app === "codex"
            ? ".codex"
            : app === "gemini"
              ? ".gemini"
              : ".config/opencode";
      return await join(home, folder);
    } catch (error) {
      console.error(
        "[useDirectorySettings] Failed to resolve default config dir",
        error,
      );
      return undefined;
    }
  };
  
  export interface UseDirectorySettingsProps {
    settings: SettingsFormState | null;
    onUpdateSettings: (updates: Partial<SettingsFormState>) => void;
  }
  
  export interface UseDirectorySettingsResult {
    appConfigDir?: string;
    resolvedDirs: ResolvedDirectories;
    isLoading: boolean;
    initialAppConfigDir?: string;
    updateDirectory: (app: AppId, value?: string) => void;
    updateAppConfigDir: (value?: string) => void;
    browseDirectory: (app: AppId) => Promise<void>;
    browseAppConfigDir: () => Promise<void>;
    resetDirectory: (app: AppId) => Promise<void>;
    resetAppConfigDir: () => Promise<void>;
    resetAllDirectories: (
      claudeDir?: string,
      codexDir?: string,
      geminiDir?: string,
      opencodeDir?: string,
    ) => void;
  }
  
  /**
   * useDirectorySettings - 目录管理
   * 负责：
   * - appConfigDir 状态
   * - resolvedDirs 状态
   * - 目录选择（browse）
   * - 目录重置
   * - 默认值计算
   */
  export function useDirectorySettings({
    settings,
    onUpdateSettings,
  }: UseDirectorySettingsProps): UseDirectorySettingsResult {
    const { t } = useTranslation();
  
    const [appConfigDir, setAppConfigDir] = useState<string | undefined>(
      undefined,
    );
    const [resolvedDirs, setResolvedDirs] = useState<ResolvedDirectories>({
      appConfig: "",
      claude: "",
      codex: "",
      gemini: "",
      opencode: "",
    });
    const [isLoading, setIsLoading] = useState(true);
  
    const defaultsRef = useRef<ResolvedDirectories>({
      appConfig: "",
      claude: "",
      codex: "",
      gemini: "",
      opencode: "",
    });
    const initialAppConfigDirRef = useRef<string | undefined>(undefined);
  
    // 加载目录信息
    useEffect(() => {
      let active = true;
      setIsLoading(true);
  
      const load = async () => {
        try {
          const [
            overrideRaw,
            claudeDir,
            codexDir,
            geminiDir,
            opencodeDir,
            defaultAppConfig,
            defaultClaudeDir,
            defaultCodexDir,
            defaultGeminiDir,
            defaultOpencodeDir,
          ] = await Promise.all([
            settingsApi.getAppConfigDirOverride(),
            settingsApi.getConfigDir("claude"),
            settingsApi.getConfigDir("codex"),
            settingsApi.getConfigDir("gemini"),
            settingsApi.getConfigDir("opencode"),
            computeDefaultAppConfigDir(),
            computeDefaultConfigDir("claude"),
            computeDefaultConfigDir("codex"),
            computeDefaultConfigDir("gemini"),
            computeDefaultConfigDir("opencode"),
          ]);
  
          if (!active) return;
  
          const normalizedOverride = sanitizeDir(overrideRaw ?? undefined);
  
          defaultsRef.current = {
            appConfig: defaultAppConfig ?? "",
            claude: defaultClaudeDir ?? "",
            codex: defaultCodexDir ?? "",
            gemini: defaultGeminiDir ?? "",
            opencode: defaultOpencodeDir ?? "",
          };
  
          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,
          });
        } 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(
            key === "claude"
              ? { claudeConfigDir: sanitized }
              : key === "codex"
                ? { codexConfigDir: sanitized }
                : key === "gemini"
                  ? { geminiConfigDir: sanitized }
                  : { opencodeConfigDir: sanitized },
          );
        }
  
        setResolvedDirs((prev) => ({
          ...prev,
          [key]: sanitized ?? defaultsRef.current[key],
        }));
      },
      [onUpdateSettings],
    );
  
    const updateAppConfigDir = useCallback(
      (value?: string) => {
        updateDirectoryState("appConfig", value);
      },
      [updateDirectoryState],
    );
  
    const updateDirectory = useCallback(
      (app: AppId, value?: string) => {
        updateDirectoryState(
          app === "claude"
            ? "claude"
            : app === "codex"
              ? "codex"
              : app === "gemini"
                ? "gemini"
                : "opencode",
          value,
        );
      },
      [updateDirectoryState],
    );
  
    const browseDirectory = useCallback(
      async (app: AppId) => {
        const key: DirectoryKey =
          app === "claude"
            ? "claude"
            : app === "codex"
              ? "codex"
              : app === "gemini"
                ? "gemini"
                : "opencode";
        const currentValue =
          key === "claude"
            ? (settings?.claudeConfigDir ?? resolvedDirs.claude)
            : key === "codex"
              ? (settings?.codexConfigDir ?? resolvedDirs.codex)
              : key === "gemini"
                ? (settings?.geminiConfigDir ?? resolvedDirs.gemini)
                : (settings?.opencodeConfigDir ?? resolvedDirs.opencode);
  
        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: AppId) => {
        const key: DirectoryKey =
          app === "claude"
            ? "claude"
            : app === "codex"
              ? "codex"
              : app === "gemini"
                ? "gemini"
                : "opencode";
        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(
      (
        claudeDir?: string,
        codexDir?: string,
        geminiDir?: string,
        opencodeDir?: string,
      ) => {
        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,
        });
      },
      [],
    );
  
    return {
      appConfigDir,
      resolvedDirs,
      isLoading,
      initialAppConfigDir: initialAppConfigDirRef.current,
      updateDirectory,
      updateAppConfigDir,
      browseDirectory,
      browseAppConfigDir,
      resetDirectory,
      resetAppConfigDir,
      resetAllDirectories,
    };
  }
  