feat: add Codex configuration cleanup action
This commit is contained in:
1 parent
4e8125b94a
commit
e6a4c8a755
5 files changed
+166
-2
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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} 行目の形式が無効です',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user