5 Commits

Author SHA1 Message Date
YoVinchen
81a6c08673 fix(skills): resolve third-party skills installation failure
- Add skills_path field to Skill struct
- Use skills_path to construct correct source path during installation
- Fix installation for repos with custom skill subdirectories
2025-11-21 12:33:12 +08:00
Jason
74969ae968 fix(dialog): prevent dialogs from closing on overlay click
Add onInteractOutside handler to DialogContent to prevent accidental
dialog closure when users click on the overlay/backdrop. This prevents
data loss in forms and improves user experience across all 11 dialog
components in the application.

Users can still close dialogs using:
- Close button (X) in the top-right corner
- Cancel/Close buttons within the dialog
- ESC key
2025-11-20 23:29:57 +08:00
Jason
1f3627add3 fix(gemini): persist settings json edits 2025-11-20 20:23:22 +08:00
YoVinchen
14ee122b27 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
2025-11-19 21:14:43 +08:00
wugeer
7aecba14fe docs: Add installation instructions for Arch Linux (#259) 2025-11-19 19:12:00 +08:00
16 changed files with 229 additions and 64 deletions

View File

@@ -103,6 +103,14 @@ Download `CC-Switch-v{version}-macOS.zip` from the [Releases](../../releases) pa
> **Note**: Since the author doesn't have an Apple Developer account, you may see an "unidentified developer" warning on first launch. Please close it first, then go to "System Settings" → "Privacy & Security" → click "Open Anyway", and you'll be able to open it normally afterwards. > **Note**: Since the author doesn't have an Apple Developer account, you may see an "unidentified developer" warning on first launch. Please close it first, then go to "System Settings" → "Privacy & Security" → click "Open Anyway", and you'll be able to open it normally afterwards.
### ArchLinux 用户
**Install via paru (Recommended)**
```bash
paru -S cc-switch-bin
```
### Linux Users ### Linux Users
Download the latest `CC-Switch-v{version}-Linux.deb` package or `CC-Switch-v{version}-Linux.AppImage` from the [Releases](../../releases) page. Download the latest `CC-Switch-v{version}-Linux.deb` package or `CC-Switch-v{version}-Linux.AppImage` from the [Releases](../../releases) page.

View File

@@ -103,6 +103,14 @@ brew upgrade --cask cc-switch
> **注意**:由于作者没有苹果开发者账号,首次打开可能出现"未知开发者"警告,请先关闭,然后前往"系统设置" → "隐私与安全性" → 点击"仍要打开",之后便可以正常打开 > **注意**:由于作者没有苹果开发者账号,首次打开可能出现"未知开发者"警告,请先关闭,然后前往"系统设置" → "隐私与安全性" → 点击"仍要打开",之后便可以正常打开
### ArchLinux 用户
**通过 paru 安装(推荐)**
```bash
paru -S cc-switch-bin
```
### Linux 用户 ### Linux 用户
从 [Releases](../../releases) 页面下载最新版本的 `CC-Switch-v{版本号}-Linux.deb` 包或者 `CC-Switch-v{版本号}-Linux.AppImage` 安装包。 从 [Releases](../../releases) 页面下载最新版本的 `CC-Switch-v{版本号}-Linux.deb` 包或者 `CC-Switch-v{版本号}-Linux.AppImage` 安装包。

View File

@@ -62,7 +62,7 @@ pub async fn install_skill(
.clone() .clone()
.unwrap_or_else(|| "main".to_string()), .unwrap_or_else(|| "main".to_string()),
enabled: true, enabled: true,
skills_path: None, // 安装时使用默认路径 skills_path: skill.skills_path.clone(), // 使用技能记录的 skills_path
}; };
service service

View File

@@ -236,6 +236,17 @@ pub fn validate_gemini_settings(settings: &Value) -> Result<(), AppError> {
} }
} }
// 如果有 config 字段,验证它是对象或 null
if let Some(config) = settings.get("config") {
if !(config.is_object() || config.is_null()) {
return Err(AppError::localized(
"gemini.validation.invalid_config",
"Gemini 配置格式错误: config 必须是对象",
"Gemini config invalid: config must be an object",
));
}
}
Ok(()) Ok(())
} }
@@ -244,6 +255,9 @@ pub fn validate_gemini_settings(settings: &Value) -> Result<(), AppError> {
/// 此函数在切换供应商时使用,确保配置包含所有必需的字段。 /// 此函数在切换供应商时使用,确保配置包含所有必需的字段。
/// 对于需要 API Key 的供应商(如 PackyCode会验证 GEMINI_API_KEY 字段。 /// 对于需要 API Key 的供应商(如 PackyCode会验证 GEMINI_API_KEY 字段。
pub fn validate_gemini_settings_strict(settings: &Value) -> Result<(), AppError> { pub fn validate_gemini_settings_strict(settings: &Value) -> Result<(), AppError> {
// 先做基础格式验证(包含 env/config 类型)
validate_gemini_settings(settings)?;
let env_map = json_to_env(settings)?; let env_map = json_to_env(settings)?;
// 如果 env 为空,表示使用 OAuth如 Google 官方),跳过验证 // 如果 env 为空,表示使用 OAuth如 Google 官方),跳过验证

View File

@@ -229,42 +229,22 @@ impl ConfigService {
provider_id: &str, provider_id: &str,
provider: &Provider, provider: &Provider,
) -> Result<(), AppError> { ) -> Result<(), AppError> {
use crate::gemini_config::{ use crate::gemini_config::{env_to_json, read_gemini_env};
env_to_json, json_to_env, read_gemini_env, write_gemini_env_atomic,
};
let env_path = crate::gemini_config::get_gemini_env_path(); ProviderService::write_gemini_live(provider)?;
if let Some(parent) = env_path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
// 转换 JSON 配置为 .env 格式 // 读回实际写入的内容并更新到配置中(包含 settings.json
let env_map = json_to_env(&provider.settings_config)?;
// Google 官方OAuth: env 为空,写入空文件并设置安全标志后返回
if env_map.is_empty() {
write_gemini_env_atomic(&env_map)?;
ProviderService::ensure_google_oauth_security_flag(provider)?;
let live_after_env = read_gemini_env()?;
let live_after = env_to_json(&live_after_env);
if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
if let Some(target) = manager.providers.get_mut(provider_id) {
target.settings_config = live_after;
}
}
return Ok(());
}
// 非 OAuth按常规写入并在必要时设置 Packycode 安全标志
write_gemini_env_atomic(&env_map)?;
ProviderService::ensure_packycode_security_flag(provider)?;
// 读回实际写入的内容并更新到配置中
let live_after_env = read_gemini_env()?; let live_after_env = read_gemini_env()?;
let live_after = env_to_json(&live_after_env); let settings_path = crate::gemini_config::get_gemini_settings_path();
let live_after_config = if settings_path.exists() {
crate::config::read_json_file(&settings_path)?
} else {
serde_json::json!({})
};
let mut live_after = env_to_json(&live_after_env);
if let Some(obj) = live_after.as_object_mut() {
obj.insert("config".to_string(), live_after_config);
}
if let Some(manager) = config.get_manager_mut(&AppType::Gemini) { if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
if let Some(target) = manager.providers.get_mut(provider_id) { if let Some(target) = manager.providers.get_mut(provider_id) {

View File

@@ -30,6 +30,7 @@ enum LiveSnapshot {
}, },
Gemini { Gemini {
env: Option<HashMap<String, String>>, // 新增 env: Option<HashMap<String, String>>, // 新增
config: Option<Value>, // 新增settings.json 内容
}, },
} }
@@ -68,15 +69,30 @@ impl LiveSnapshot {
delete_file(&config_path)?; delete_file(&config_path)?;
} }
} }
LiveSnapshot::Gemini { env } => { LiveSnapshot::Gemini { env, .. } => {
// 新增 // 新增
use crate::gemini_config::{get_gemini_env_path, write_gemini_env_atomic}; use crate::gemini_config::{
get_gemini_env_path, get_gemini_settings_path, write_gemini_env_atomic,
};
let path = get_gemini_env_path(); let path = get_gemini_env_path();
if let Some(env_map) = env { if let Some(env_map) = env {
write_gemini_env_atomic(env_map)?; write_gemini_env_atomic(env_map)?;
} else if path.exists() { } else if path.exists() {
delete_file(&path)?; delete_file(&path)?;
} }
let settings_path = get_gemini_settings_path();
match self {
LiveSnapshot::Gemini {
config: Some(cfg), ..
} => {
write_json_file(&settings_path, cfg)?;
}
LiveSnapshot::Gemini { config: None, .. } if settings_path.exists() => {
delete_file(&settings_path)?;
}
_ => {}
}
} }
} }
Ok(()) Ok(())
@@ -612,7 +628,9 @@ impl ProviderService {
state.save()?; state.save()?;
} }
AppType::Gemini => { AppType::Gemini => {
use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env}; use crate::gemini_config::{
env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
};
let env_path = get_gemini_env_path(); let env_path = get_gemini_env_path();
if !env_path.exists() { if !env_path.exists() {
@@ -623,7 +641,18 @@ impl ProviderService {
)); ));
} }
let env_map = read_gemini_env()?; let env_map = read_gemini_env()?;
let live_after = env_to_json(&env_map); let mut live_after = env_to_json(&env_map);
let settings_path = get_gemini_settings_path();
let config_value = if settings_path.exists() {
read_json_file(&settings_path)?
} else {
json!({})
};
if let Some(obj) = live_after.as_object_mut() {
obj.insert("config".to_string(), config_value);
}
{ {
let mut guard = state.config.write().map_err(AppError::from)?; let mut guard = state.config.write().map_err(AppError::from)?;
@@ -670,14 +699,22 @@ impl ProviderService {
} }
AppType::Gemini => { AppType::Gemini => {
// 新增 // 新增
use crate::gemini_config::{get_gemini_env_path, read_gemini_env}; use crate::gemini_config::{
get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
};
let path = get_gemini_env_path(); let path = get_gemini_env_path();
let env = if path.exists() { let env = if path.exists() {
Some(read_gemini_env()?) Some(read_gemini_env()?)
} else { } else {
None None
}; };
Ok(LiveSnapshot::Gemini { env }) let settings_path = get_gemini_settings_path();
let config = if settings_path.exists() {
Some(read_json_file(&settings_path)?)
} else {
None
};
Ok(LiveSnapshot::Gemini { env, config })
} }
} }
} }
@@ -1461,7 +1498,9 @@ impl ProviderService {
config: &mut MultiAppConfig, config: &mut MultiAppConfig,
next_provider: &str, next_provider: &str,
) -> Result<(), AppError> { ) -> Result<(), AppError> {
use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env}; use crate::gemini_config::{
env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
};
let env_path = get_gemini_env_path(); let env_path = get_gemini_env_path();
if !env_path.exists() { if !env_path.exists() {
@@ -1477,7 +1516,18 @@ impl ProviderService {
} }
let env_map = read_gemini_env()?; let env_map = read_gemini_env()?;
let live = env_to_json(&env_map); let mut live = env_to_json(&env_map);
let settings_path = get_gemini_settings_path();
let config_value = if settings_path.exists() {
read_json_file(&settings_path)?
} else {
json!({})
};
if let Some(obj) = live.as_object_mut() {
obj.insert("config".to_string(), config_value);
}
if let Some(manager) = config.get_manager_mut(&AppType::Gemini) { if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
if let Some(current) = manager.providers.get_mut(&current_id) { if let Some(current) = manager.providers.get_mut(&current_id) {
current.settings_config = live; current.settings_config = live;
@@ -1495,36 +1545,71 @@ impl ProviderService {
Ok(()) Ok(())
} }
fn write_gemini_live(provider: &Provider) -> Result<(), AppError> { pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
use crate::gemini_config::{ use crate::gemini_config::{
json_to_env, validate_gemini_settings_strict, write_gemini_env_atomic, get_gemini_settings_path, json_to_env, validate_gemini_settings_strict,
write_gemini_env_atomic,
}; };
// 一次性检测认证类型,避免重复检测 // 一次性检测认证类型,避免重复检测
let auth_type = Self::detect_gemini_auth_type(provider); let auth_type = Self::detect_gemini_auth_type(provider);
let mut env_map = json_to_env(&provider.settings_config)?;
// 准备要写入 ~/.gemini/settings.json 的配置(缺省时保留现有文件内容)
let mut config_to_write = if let Some(config_value) = provider.settings_config.get("config")
{
if config_value.is_null() {
Some(json!({}))
} else if config_value.is_object() {
Some(config_value.clone())
} else {
return Err(AppError::localized(
"gemini.validation.invalid_config",
"Gemini 配置格式错误: config 必须是对象或 null",
"Gemini config invalid: config must be an object or null",
));
}
} else {
None
};
if config_to_write.is_none() {
let settings_path = get_gemini_settings_path();
if settings_path.exists() {
config_to_write = Some(read_json_file(&settings_path)?);
}
}
match auth_type { match auth_type {
GeminiAuthType::GoogleOfficial => { GeminiAuthType::GoogleOfficial => {
// Google 官方使用 OAuth清空 env // Google 官方使用 OAuth清空 env
let empty_env = std::collections::HashMap::new(); env_map.clear();
write_gemini_env_atomic(&empty_env)?; write_gemini_env_atomic(&env_map)?;
Self::ensure_google_oauth_security_flag(provider)?;
} }
GeminiAuthType::Packycode => { GeminiAuthType::Packycode => {
// PackyCode 供应商,使用 API Key切换时严格验证 // PackyCode 供应商,使用 API Key切换时严格验证
validate_gemini_settings_strict(&provider.settings_config)?; validate_gemini_settings_strict(&provider.settings_config)?;
let env_map = json_to_env(&provider.settings_config)?;
write_gemini_env_atomic(&env_map)?; write_gemini_env_atomic(&env_map)?;
Self::ensure_packycode_security_flag(provider)?;
} }
GeminiAuthType::Generic => { GeminiAuthType::Generic => {
// 通用供应商,使用 API Key切换时严格验证 // 通用供应商,使用 API Key切换时严格验证
validate_gemini_settings_strict(&provider.settings_config)?; validate_gemini_settings_strict(&provider.settings_config)?;
let env_map = json_to_env(&provider.settings_config)?;
write_gemini_env_atomic(&env_map)?; write_gemini_env_atomic(&env_map)?;
} }
} }
if let Some(config_value) = config_to_write {
let settings_path = get_gemini_settings_path();
write_json_file(&settings_path, &config_value)?;
}
match auth_type {
GeminiAuthType::GoogleOfficial => Self::ensure_google_oauth_security_flag(provider)?,
GeminiAuthType::Packycode => Self::ensure_packycode_security_flag(provider)?,
GeminiAuthType::Generic => {}
}
Ok(()) Ok(())
} }

View File

@@ -32,6 +32,9 @@ pub struct Skill {
/// 分支名称 /// 分支名称
#[serde(rename = "repoBranch")] #[serde(rename = "repoBranch")]
pub repo_branch: Option<String>, pub repo_branch: Option<String>,
/// 技能所在的子目录路径 (可选, 如 "skills")
#[serde(rename = "skillsPath")]
pub skills_path: Option<String>,
} }
/// 仓库配置 /// 仓库配置
@@ -234,6 +237,7 @@ impl SkillService {
repo_owner: Some(repo.owner.clone()), repo_owner: Some(repo.owner.clone()),
repo_name: Some(repo.name.clone()), repo_name: Some(repo.name.clone()),
repo_branch: Some(repo.branch.clone()), repo_branch: Some(repo.branch.clone()),
skills_path: repo.skills_path.clone(),
}); });
} }
Err(e) => log::warn!("解析 {} 元数据失败: {}", skill_md.display(), e), Err(e) => log::warn!("解析 {} 元数据失败: {}", skill_md.display(), e),
@@ -312,6 +316,7 @@ impl SkillService {
repo_owner: None, repo_owner: None,
repo_name: None, repo_name: None,
repo_branch: None, repo_branch: None,
skills_path: None,
}); });
} }
} }
@@ -442,12 +447,21 @@ impl SkillService {
.await .await
.map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??; .map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??;
// 复制到安装目录 // 根据 skills_path 确定源目录路径
let source = temp_dir.join(&directory); let source = if let Some(ref skills_path) = repo.skills_path {
// 如果指定了 skills_path源路径为: temp_dir/skills_path/directory
temp_dir.join(skills_path.trim_matches('/')).join(&directory)
} else {
// 否则源路径为: temp_dir/directory
temp_dir.join(&directory)
};
if !source.exists() { if !source.exists() {
let _ = fs::remove_dir_all(&temp_dir); let _ = fs::remove_dir_all(&temp_dir);
return Err(anyhow::anyhow!("技能目录不存在")); return Err(anyhow::anyhow!(
"技能目录不存在: {}",
source.display()
));
} }
// 删除旧版本 // 删除旧版本

View File

@@ -14,6 +14,7 @@ interface DirectorySettingsProps {
onResetAppConfig: () => Promise<void>; onResetAppConfig: () => Promise<void>;
claudeDir?: string; claudeDir?: string;
codexDir?: string; codexDir?: string;
geminiDir?: string;
onDirectoryChange: (app: AppId, value?: string) => void; onDirectoryChange: (app: AppId, value?: string) => void;
onBrowseDirectory: (app: AppId) => Promise<void>; onBrowseDirectory: (app: AppId) => Promise<void>;
onResetDirectory: (app: AppId) => Promise<void>; onResetDirectory: (app: AppId) => Promise<void>;
@@ -27,6 +28,7 @@ export function DirectorySettings({
onResetAppConfig, onResetAppConfig,
claudeDir, claudeDir,
codexDir, codexDir,
geminiDir,
onDirectoryChange, onDirectoryChange,
onBrowseDirectory, onBrowseDirectory,
onResetDirectory, onResetDirectory,
@@ -104,6 +106,17 @@ export function DirectorySettings({
onBrowse={() => onBrowseDirectory("codex")} onBrowse={() => onBrowseDirectory("codex")}
onReset={() => onResetDirectory("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> </section>
</> </>
); );

View File

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

View File

@@ -59,6 +59,10 @@ const DialogContent = React.forwardRef<
zIndexMap[zIndex], zIndexMap[zIndex],
className, className,
)} )}
onInteractOutside={(e) => {
// 防止点击遮罩层关闭对话框
e.preventDefault();
}}
{...props} {...props}
> >
{children} {children}

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ export interface Skill {
repoOwner?: string; repoOwner?: string;
repoName?: string; repoName?: string;
repoBranch?: string; repoBranch?: string;
skillsPath?: string; // 技能所在的子目录路径,如 "skills"
} }
export interface SkillRepo { export interface SkillRepo {

View File

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