From e6a4c8a75562b9a80d1e1b025d6e63ceff4e9dcb Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Sun, 26 Jul 2026 12:10:22 +0800 Subject: [PATCH] feat: add Codex configuration cleanup action --- src-tauri/src/main.rs | 69 ++++++++++++++++++++++++++++++++++++ src/i18n/ja.ts | 5 +++ src/i18n/resources.ts | 10 ++++++ src/pages/AgentsPage.tsx | 76 ++++++++++++++++++++++++++++++++++++++-- src/styles.css | 8 +++++ 5 files changed, 166 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3bd2669..2fc1d51 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1653,6 +1653,18 @@ async fn reset_agent_config_to_default( ) } +#[tauri::command] +fn clear_codex_config(app: tauri::AppHandle) -> Result, String> { + let home = app + .path() + .home_dir() + .map_err(|error| format!("无法获取用户目录: {error}"))?; + let _guard = AGENT_CONFIG_FILE_LOCK + .lock() + .map_err(|_| "智能体配置文件锁已损坏".to_string())?; + clear_codex_config_files(&home) +} + #[tauri::command] async fn set_agent_config_enabled( app: tauri::AppHandle, @@ -2137,6 +2149,37 @@ fn agent_config_paths(client: AgentClient, home: &Path) -> Vec { } } +fn remove_codex_config_file(path: &Path) -> Result { + match fs::remove_file(path) { + Ok(()) => Ok(true), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(error) => Err(format!( + "删除 Codex 配置文件失败 {}: {error}", + path_to_string(path) + )), + } +} + +fn clear_codex_config_files(home: &Path) -> Result, String> { + let codex_dir = home.join(".codex"); + let config_path = codex_dir.join("config.toml"); + let targets = [codex_dir.join("auth.json"), config_path.clone()]; + let mut deleted = Vec::new(); + + for path in targets { + if remove_codex_config_file(&path)? { + deleted.push(path_to_string(&path)); + } + } + + // The state file belongs to this app. Remove it after the Codex files are + // cleared so the UI does not report the deliberate deletion as an external edit. + let state_path = agent_state_path(std::slice::from_ref(&config_path))?; + remove_codex_config_file(&state_path)?; + + Ok(deleted) +} + fn codex_model_catalog_path(home: &Path) -> PathBuf { home.join(".codex").join(CODEX_MODEL_CATALOG_FILE) } @@ -12782,6 +12825,7 @@ fn main() { delete_thinking_alias, apply_agent_config, reset_agent_config_to_default, + clear_codex_config, set_agent_config_enabled, update_agent_config, launch_agent, @@ -12993,6 +13037,31 @@ mod tests { fs::remove_dir_all(home).unwrap(); } + #[test] + fn clear_codex_config_removes_only_requested_config_files_and_application_state() { + let home = agent_test_home("clear-codex-config"); + let codex_dir = home.join(".codex"); + fs::create_dir_all(&codex_dir).unwrap(); + let auth_path = codex_dir.join("auth.json"); + let config_path = codex_dir.join("config.toml"); + let state_path = agent_state_path(std::slice::from_ref(&config_path)).unwrap(); + let preserved_path = codex_dir.join("history.jsonl"); + fs::write(&auth_path, "{}").unwrap(); + fs::write(&config_path, "model = \"gpt-test\"\n").unwrap(); + fs::write(&state_path, "{}").unwrap(); + fs::write(&preserved_path, "keep").unwrap(); + + let deleted = clear_codex_config_files(&home).unwrap(); + + assert_eq!(deleted.len(), 2); + assert!(!auth_path.exists()); + assert!(!config_path.exists()); + assert!(!state_path.exists()); + assert_eq!(fs::read_to_string(&preserved_path).unwrap(), "keep"); + assert!(clear_codex_config_files(&home).unwrap().is_empty()); + fs::remove_dir_all(home).unwrap(); + } + #[test] fn direct_agent_apply_rebuilds_an_unparseable_file_without_backup() { let home = agent_test_home("invalid-apply"); diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 76538f3..7d19e23 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -462,6 +462,7 @@ export const jaOverrides = { 'agents.modify.title': '設定変更', 'agents.modify.apply': '設定変更を適用', 'agents.modify.default': 'デフォルト設定', + 'agents.modify.clear': '設定を消去', 'agents.modify.externalChanged': '外部の設定変更を検出し、その内容を保持しています。「設定変更を適用」をクリックするまで管理対象フィールドは書き換えません。', 'agents.modify.invalid': '現在の設定を解析できません。「設定変更を適用」は最小の有効な設定を再構築し、「デフォルト設定」はファイル全体をリセットします。', 'agents.modify.applied': 'アプリが管理するフィールドは適用済みで、それ以外のユーザー設定は保持されています。', @@ -484,6 +485,10 @@ export const jaOverrides = { 'agents.default.title': 'デフォルト設定にリセット', 'agents.default.description': '選択中のモデルを使って {name} の関連設定ファイルをすべて書き直し、その他のユーザー内容を削除します。バックアップは作成されず、元に戻せません。', 'agents.default.confirm': 'デフォルト設定を書き込む', + 'agents.clear.title': 'Codex 設定を消去', + 'agents.clear.description': 'Codex 設定ディレクトリの auth.json と config.toml を完全に削除し、このアプリが記録した適用状態をリセットします。バックアップは作成されず、元に戻せません。', + 'agents.clear.confirm': '設定を消去', + 'agents.clear.success': 'Codex 設定を消去しました。', 'apiAccess.compatibleName': 'OpenAI 互換 {number}', 'apiAccess.error.headerMissingColon': 'カスタムヘッダーの {number} 行目にコロンがありません', 'apiAccess.error.headerInvalid': 'カスタムヘッダーの {number} 行目の形式が無効です', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 4b97b44..194334f 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -465,6 +465,7 @@ export const zhCN = { 'agents.modify.title': '配置修改', 'agents.modify.apply': '应用配置修改', 'agents.modify.default': '默认配置', + 'agents.modify.clear': '清空配置', 'agents.modify.externalChanged': '检测到外部配置变化。软件会保留这些内容;点击“应用配置修改”后才会重新写入受管字段。', 'agents.modify.invalid': '当前配置无法解析。点击“应用配置修改”会重建最小合法配置;“默认配置”会重置整个文件。', 'agents.modify.applied': '软件负责的配置字段已应用,其他用户内容保持不变。', @@ -487,6 +488,10 @@ export const zhCN = { 'agents.default.title': '重置为默认配置', 'agents.default.description': '这会使用当前选中模型重写 {name} 的全部相关配置文件,并删除其中其他用户内容。该操作没有备份且无法恢复。', 'agents.default.confirm': '确认写入默认配置', + 'agents.clear.title': '清空 Codex 配置', + 'agents.clear.description': '这会永久删除 Codex 配置目录中的 auth.json 和 config.toml,并重置软件记录的应用状态。该操作没有备份且无法恢复。', + 'agents.clear.confirm': '确认清空配置', + 'agents.clear.success': 'Codex 配置已清空。', 'apiAccess.compatibleName': 'OpenAI 兼容 {number}', 'apiAccess.error.headerMissingColon': '自定义请求头第 {number} 行缺少冒号', 'apiAccess.error.headerInvalid': '自定义请求头第 {number} 行格式无效', @@ -1244,6 +1249,7 @@ export const en: Record = { 'agents.modify.title': 'Configuration Changes', 'agents.modify.apply': 'Apply Configuration Changes', 'agents.modify.default': 'Default Configuration', + 'agents.modify.clear': 'Clear Configuration', 'agents.modify.externalChanged': 'External configuration changes were detected and preserved. Managed fields are rewritten only after you click Apply Configuration Changes.', 'agents.modify.invalid': 'The current configuration cannot be parsed. Apply Configuration Changes rebuilds a minimal valid configuration; Default Configuration resets the whole file.', 'agents.modify.applied': 'The fields managed by the app are applied; all other user content remains unchanged.', @@ -1266,6 +1272,10 @@ export const en: Record = { 'agents.default.title': 'Reset to Default Configuration', 'agents.default.description': 'This rewrites every related {name} configuration file using the selected model and deletes all other user content in those files. No backup is created and the operation cannot be undone.', 'agents.default.confirm': 'Write Default Configuration', + 'agents.clear.title': 'Clear Codex Configuration', + 'agents.clear.description': 'This permanently deletes auth.json and config.toml from the Codex configuration directory and resets the application state tracked by this app. No backup is created and the operation cannot be undone.', + 'agents.clear.confirm': 'Clear Configuration', + 'agents.clear.success': 'The Codex configuration has been cleared.', 'apiAccess.compatibleName': 'OpenAI Compatible {number}', 'apiAccess.error.headerMissingColon': 'Custom header line {number} is missing a colon', 'apiAccess.error.headerInvalid': 'Custom header line {number} is invalid', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index a99ae05..1344e11 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -21,6 +21,7 @@ import { RefreshCw, Search, Sparkles, + Trash2, X, } from 'lucide-react'; import claudeIcon from '../assets/icons/claude.svg'; @@ -488,7 +489,7 @@ export function AgentsPage() { const [launchTargetByClient, setLaunchTargetByClient] = useState>>({}); const [loading, setLoading] = useState(true); const [modelLoading, setModelLoading] = useState(false); - const [busyAction, setBusyAction] = useState<'apply' | 'default' | 'launch' | null>(null); + const [busyAction, setBusyAction] = useState<'apply' | 'default' | 'clear' | 'launch' | null>(null); const busy = busyAction !== null; const [detectionError, setDetectionError] = useState(''); const [modelError, setModelError] = useState(''); @@ -497,6 +498,9 @@ export function AgentsPage() { const [launchError, setLaunchError] = useState(''); const [defaultError, setDefaultError] = useState(''); const [defaultConfirmOpen, setDefaultConfirmOpen] = useState(false); + const [clearError, setClearError] = useState(''); + const [clearNotice, setClearNotice] = useState(''); + const [clearConfirmOpen, setClearConfirmOpen] = useState(false); const loadStatuses = useCallback(async (forceRefresh = false) => { const command = forceRefresh @@ -575,6 +579,9 @@ export function AgentsPage() { setLaunchError(''); setDefaultError(''); setDefaultConfirmOpen(false); + setClearError(''); + setClearNotice(''); + setClearConfirmOpen(false); }, [selected]); useEffect(() => { @@ -701,6 +708,22 @@ export function AgentsPage() { } }; + const clearCodexConfiguration = async () => { + setBusyAction('clear'); + setClearError(''); + setClearNotice(''); + try { + await invoke('clear_codex_config'); + setClearConfirmOpen(false); + setClearNotice(t('agents.clear.success')); + await reloadStatusesAfterAction(); + } catch (requestError) { + setClearError(String(requestError)); + } finally { + setBusyAction(null); + } + }; + const launchAgent = async () => { setBusyAction('launch'); setLaunchError(''); @@ -726,6 +749,16 @@ export function AgentsPage() { setDefaultConfirmOpen(false); }; + const openClearConfirmation = () => { + setClearError(''); + setClearConfirmOpen(true); + }; + + const closeClearConfirmation = () => { + setClearError(''); + setClearConfirmOpen(false); + }; + const availableSubpages = agentSubpages.filter( (subpage) => !subpage.clients || subpage.clients.includes(selected), ); @@ -874,7 +907,7 @@ export function AgentsPage() {
-
+
+ {selected === 'codex' ? ( + + ) : null}
{configurationError ? ( {configurationError} ) : null} + {clearNotice ? ( + + {clearNotice} + + ) : null}
@@ -1013,6 +1062,29 @@ export function AgentsPage() {
) : null} + + {clearConfirmOpen ? ( +
+
+
+

{t('agents.clear.title')}

+
+

{t('agents.clear.description')}

+ {clearError ? ( + + {clearError} + + ) : null} +
+ + +
+
+
+ ) : null} ); } diff --git a/src/styles.css b/src/styles.css index 3689a25..f7425d9 100644 --- a/src/styles.css +++ b/src/styles.css @@ -4851,6 +4851,10 @@ input:focus-visible { width: 100%; } +.agent-modification-buttons.codex { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + .agent-modification-buttons button { width: 100%; min-width: 0; @@ -6192,6 +6196,10 @@ input:focus-visible { } @media (max-width: 620px) { + .agent-modification-buttons.codex { + grid-template-columns: 1fr; + } + .config-side-stack { grid-template-columns: 1fr; grid-template-rows: repeat(2, 206px);