feat(settings): add Gemini configuration directory support (#255)

* style: apply code formatting across backend and frontend

Apply cargo fmt and prettier formatting to improve code readability.
No functional changes.

Changes:
- Rust: multi-line assertion formatting (gemini_config, env_checker)
- Rust: simplify chained method calls (provider)
- TypeScript: add trailing commas to function parameters (codexProviderPresets)

* feat(settings): add Gemini configuration directory support

Add custom configuration directory support for Gemini:
- Add geminiConfigDir field to Settings type
- Extend DirectorySettings component with Gemini input
- Update useDirectorySettings hook for Gemini directory management
- Add i18n translations for Gemini directory settings
This commit is contained in:
YoVinchen
2025-11-19 21:14:43 +08:00
committed by GitHub
parent 7aecba14fe
commit 14ee122b27
7 changed files with 62 additions and 11 deletions

View File

@@ -14,6 +14,7 @@ interface DirectorySettingsProps {
onResetAppConfig: () => Promise<void>;
claudeDir?: string;
codexDir?: string;
geminiDir?: string;
onDirectoryChange: (app: AppId, value?: string) => void;
onBrowseDirectory: (app: AppId) => Promise<void>;
onResetDirectory: (app: AppId) => Promise<void>;
@@ -27,6 +28,7 @@ export function DirectorySettings({
onResetAppConfig,
claudeDir,
codexDir,
geminiDir,
onDirectoryChange,
onBrowseDirectory,
onResetDirectory,
@@ -104,6 +106,17 @@ export function DirectorySettings({
onBrowse={() => onBrowseDirectory("codex")}
onReset={() => onResetDirectory("codex")}
/>
<DirectoryInput
label={t("settings.geminiConfigDir")}
description={undefined}
value={geminiDir}
resolvedValue={resolvedDirs.gemini}
placeholder={t("settings.browsePlaceholderGemini")}
onChange={(val) => onDirectoryChange("gemini", val)}
onBrowse={() => onBrowseDirectory("gemini")}
onReset={() => onResetDirectory("gemini")}
/>
</section>
</>
);

View File

@@ -220,6 +220,7 @@ export function SettingsDialog({
onResetAppConfig={resetAppConfigDir}
claudeDir={settings.claudeConfigDir}
codexDir={settings.codexConfigDir}
geminiDir={settings.geminiConfigDir}
onDirectoryChange={updateDirectory}
onBrowseDirectory={browseDirectory}
onResetDirectory={resetDirectory}

View File

@@ -5,12 +5,13 @@ 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";
type DirectoryKey = "appConfig" | "claude" | "codex" | "gemini";
export interface ResolvedDirectories {
appConfig: string;
claude: string;
codex: string;
gemini: string;
}
const sanitizeDir = (value?: string | null): string | undefined => {
@@ -37,7 +38,8 @@ const computeDefaultConfigDir = async (
): Promise<string | undefined> => {
try {
const home = await homeDir();
const folder = app === "claude" ? ".claude" : ".codex";
const folder =
app === "claude" ? ".claude" : app === "codex" ? ".codex" : ".gemini";
return await join(home, folder);
} catch (error) {
console.error(
@@ -64,7 +66,11 @@ export interface UseDirectorySettingsResult {
browseAppConfigDir: () => Promise<void>;
resetDirectory: (app: AppId) => Promise<void>;
resetAppConfigDir: () => Promise<void>;
resetAllDirectories: (claudeDir?: string, codexDir?: string) => void;
resetAllDirectories: (
claudeDir?: string,
codexDir?: string,
geminiDir?: string,
) => void;
}
/**
@@ -89,6 +95,7 @@ export function useDirectorySettings({
appConfig: "",
claude: "",
codex: "",
gemini: "",
});
const [isLoading, setIsLoading] = useState(true);
@@ -96,6 +103,7 @@ export function useDirectorySettings({
appConfig: "",
claude: "",
codex: "",
gemini: "",
});
const initialAppConfigDirRef = useRef<string | undefined>(undefined);
@@ -110,16 +118,20 @@ export function useDirectorySettings({
overrideRaw,
claudeDir,
codexDir,
geminiDir,
defaultAppConfig,
defaultClaudeDir,
defaultCodexDir,
defaultGeminiDir,
] = await Promise.all([
settingsApi.getAppConfigDirOverride(),
settingsApi.getConfigDir("claude"),
settingsApi.getConfigDir("codex"),
settingsApi.getConfigDir("gemini"),
computeDefaultAppConfigDir(),
computeDefaultConfigDir("claude"),
computeDefaultConfigDir("codex"),
computeDefaultConfigDir("gemini"),
]);
if (!active) return;
@@ -130,6 +142,7 @@ export function useDirectorySettings({
appConfig: defaultAppConfig ?? "",
claude: defaultClaudeDir ?? "",
codex: defaultCodexDir ?? "",
gemini: defaultGeminiDir ?? "",
};
setAppConfigDir(normalizedOverride);
@@ -139,6 +152,7 @@ export function useDirectorySettings({
appConfig: normalizedOverride ?? defaultsRef.current.appConfig,
claude: claudeDir || defaultsRef.current.claude,
codex: codexDir || defaultsRef.current.codex,
gemini: geminiDir || defaultsRef.current.gemini,
});
} catch (error) {
console.error(
@@ -167,7 +181,9 @@ export function useDirectorySettings({
onUpdateSettings(
key === "claude"
? { claudeConfigDir: sanitized }
: { codexConfigDir: sanitized },
: key === "codex"
? { codexConfigDir: sanitized }
: { geminiConfigDir: sanitized },
);
}
@@ -188,18 +204,24 @@ export function useDirectorySettings({
const updateDirectory = useCallback(
(app: AppId, value?: string) => {
updateDirectoryState(app === "claude" ? "claude" : "codex", value);
updateDirectoryState(
app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini",
value,
);
},
[updateDirectoryState],
);
const browseDirectory = useCallback(
async (app: AppId) => {
const key: DirectoryKey = app === "claude" ? "claude" : "codex";
const key: DirectoryKey =
app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini";
const currentValue =
key === "claude"
? (settings?.claudeConfigDir ?? resolvedDirs.claude)
: (settings?.codexConfigDir ?? resolvedDirs.codex);
: key === "codex"
? (settings?.codexConfigDir ?? resolvedDirs.codex)
: (settings?.geminiConfigDir ?? resolvedDirs.gemini);
try {
const picked = await settingsApi.selectConfigDirectory(currentValue);
@@ -240,7 +262,8 @@ export function useDirectorySettings({
const resetDirectory = useCallback(
async (app: AppId) => {
const key: DirectoryKey = app === "claude" ? "claude" : "codex";
const key: DirectoryKey =
app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini";
if (!defaultsRef.current[key]) {
const fallback = await computeDefaultConfigDir(app);
if (fallback) {
@@ -269,13 +292,14 @@ export function useDirectorySettings({
}, [updateDirectoryState]);
const resetAllDirectories = useCallback(
(claudeDir?: string, codexDir?: string) => {
(claudeDir?: string, codexDir?: string, geminiDir?: 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,
});
},
[],

View File

@@ -102,6 +102,7 @@ export function useSettings(): UseSettingsResult {
resetAllDirectories(
sanitizeDir(data?.claudeConfigDir),
sanitizeDir(data?.codexConfigDir),
sanitizeDir(data?.geminiConfigDir),
);
setRequiresRestart(false);
}, [
@@ -120,14 +121,17 @@ export function useSettings(): UseSettingsResult {
const sanitizedAppDir = sanitizeDir(appConfigDir);
const sanitizedClaudeDir = sanitizeDir(settings.claudeConfigDir);
const sanitizedCodexDir = sanitizeDir(settings.codexConfigDir);
const sanitizedGeminiDir = sanitizeDir(settings.geminiConfigDir);
const previousAppDir = initialAppConfigDir;
const previousClaudeDir = sanitizeDir(data?.claudeConfigDir);
const previousCodexDir = sanitizeDir(data?.codexConfigDir);
const previousGeminiDir = sanitizeDir(data?.geminiConfigDir);
const payload: Settings = {
...settings,
claudeConfigDir: sanitizedClaudeDir,
codexConfigDir: sanitizedCodexDir,
geminiConfigDir: sanitizedGeminiDir,
language: settings.language,
};
@@ -170,10 +174,11 @@ export function useSettings(): UseSettingsResult {
console.warn("[useSettings] Failed to refresh tray menu", error);
}
// 如果 Claude/Codex 的目录覆盖发生变化,则立即将“当前使用的供应商”写回对应应用的 live 配置
// 如果 Claude/Codex/Gemini 的目录覆盖发生变化,则立即将“当前使用的供应商”写回对应应用的 live 配置
const claudeDirChanged = sanitizedClaudeDir !== previousClaudeDir;
const codexDirChanged = sanitizedCodexDir !== previousCodexDir;
if (claudeDirChanged || codexDirChanged) {
const geminiDirChanged = sanitizedGeminiDir !== previousGeminiDir;
if (claudeDirChanged || codexDirChanged || geminiDirChanged) {
const syncResult = await syncCurrentProvidersLiveSafe();
if (!syncResult.ok) {
console.warn(

View File

@@ -179,8 +179,11 @@
"claudeConfigDirDescription": "Override Claude configuration directory (settings.json) and keep claude.json (MCP) alongside it.",
"codexConfigDir": "Codex Configuration Directory",
"codexConfigDirDescription": "Override Codex configuration directory.",
"geminiConfigDir": "Gemini Configuration Directory",
"geminiConfigDirDescription": "Override Gemini configuration directory (.env).",
"browsePlaceholderClaude": "e.g., /home/<your-username>/.claude",
"browsePlaceholderCodex": "e.g., /home/<your-username>/.codex",
"browsePlaceholderGemini": "e.g., /home/<your-username>/.gemini",
"browseDirectory": "Browse Directory",
"resetDefault": "Reset to default directory (takes effect after saving)",
"checkForUpdates": "Check for Updates",

View File

@@ -179,8 +179,11 @@
"claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json),同时会在同级存放 Claude MCP 的 claude.json。",
"codexConfigDir": "Codex 配置目录",
"codexConfigDirDescription": "覆盖 Codex 配置目录。",
"geminiConfigDir": "Gemini 配置目录",
"geminiConfigDirDescription": "覆盖 Gemini 配置目录 (.env)。",
"browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude",
"browsePlaceholderCodex": "例如:/home/<你的用户名>/.codex",
"browsePlaceholderGemini": "例如:/home/<你的用户名>/.gemini",
"browseDirectory": "浏览目录",
"resetDefault": "恢复默认目录(需保存后生效)",
"checkForUpdates": "检查更新",

View File

@@ -97,6 +97,8 @@ export interface Settings {
claudeConfigDir?: string;
// 覆盖 Codex 配置目录(可选)
codexConfigDir?: string;
// 覆盖 Gemini 配置目录(可选)
geminiConfigDir?: string;
// 首选语言(可选,默认中文)
language?: "en" | "zh";
// Claude 自定义端点列表