feat: add Codex configuration cleanup action

This commit is contained in:
lzt404 committed 2026-07-26 12:10:22 +08:00
1 parent 4e8125b94a
commit e6a4c8a755
5 files changed
+166 -2

No files matched your search

+69
View File
@@ -1653,6 +1653,18 @@ async fn reset_agent_config_to_default(
)
}
#[tauri::command]
fn clear_codex_config(app: tauri::AppHandle) -> Result<Vec<String>, 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<PathBuf> {
}
}
fn remove_codex_config_file(path: &Path) -> Result<bool, String> {
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<Vec<String>, 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");
+5
View File
@@ -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} 行目の形式が無効です',
+10
View File
@@ -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<MessageKey, string> = {
'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<MessageKey, string> = {
'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',
+74 -2
View File
@@ -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<Partial<Record<AgentClientId, string>>>({});
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<string[]>('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() {
</div>
</div>
<div className="agent-modification-control">
<div className="agent-modification-buttons">
<div className={`agent-modification-buttons ${selected === 'codex' ? 'codex' : ''}`}>
<button
type="button"
className="primary-button"
@@ -896,12 +929,28 @@ export function AgentsPage() {
{busyAction === 'default' ? <LoaderCircle size={16} className="spin" /> : <RefreshCw size={16} />}
{t('agents.modify.default')}
</button>
{selected === 'codex' ? (
<button
type="button"
className="danger-button"
onClick={openClearConfirmation}
disabled={busy}
>
{busyAction === 'clear' ? <LoaderCircle size={16} className="spin" /> : <Trash2 size={16} />}
{t('agents.modify.clear')}
</button>
) : null}
</div>
{configurationError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{configurationError}
</span>
) : null}
{clearNotice ? (
<span className="agent-inline-message" role="status" aria-live="polite">
{clearNotice}
</span>
) : null}
</div>
</section>
@@ -1013,6 +1062,29 @@ export function AgentsPage() {
</section>
</div>
) : null}
{clearConfirmOpen ? (
<div className="config-dialog-backdrop">
<section className="config-dialog agent-restore-dialog" role="alertdialog" aria-modal="true" aria-labelledby="agent-clear-title">
<div className="config-dialog-heading">
<div><AlertTriangle size={19} /><h2 id="agent-clear-title">{t('agents.clear.title')}</h2></div>
</div>
<p>{t('agents.clear.description')}</p>
{clearError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{clearError}
</span>
) : null}
<div className="config-dialog-actions two-actions">
<button type="button" className="secondary-button" onClick={closeClearConfirmation} disabled={busy}>{t('common.cancel')}</button>
<button type="button" className="danger-button" onClick={() => void clearCodexConfiguration()} disabled={busy}>
{busyAction === 'clear' ? <LoaderCircle size={16} className="spin" /> : <Trash2 size={16} />}
{t('agents.clear.confirm')}
</button>
</div>
</section>
</div>
) : null}
</section>
);
}
+8
View File
@@ -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);