Add Restore Default Claude CLI button to settings page

Co-authored-by: simon-ami <102378134+simon-ami@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-11 11:54:13 +00:00
parent 66c80452e3
commit 8eba68e47a
5 changed files with 104 additions and 2 deletions

View File

@@ -9,6 +9,7 @@ window.Components.claudeConfig = () => ({
configPath: '', // Dynamic path from backend configPath: '', // Dynamic path from backend
models: [], models: [],
loading: false, loading: false,
restoring: false,
gemini1mSuffix: false, gemini1mSuffix: false,
// Model fields that may contain Gemini model names // Model fields that may contain Gemini model names
@@ -141,5 +142,27 @@ window.Components.claudeConfig = () => ({
} finally { } finally {
this.loading = false; this.loading = false;
} }
},
async restoreDefaultClaudeConfig() {
this.restoring = true;
const password = Alpine.store('global').webuiPassword;
try {
const { response, newPassword } = await window.utils.request('/api/claude/config/restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, password);
if (newPassword) Alpine.store('global').webuiPassword = newPassword;
if (!response.ok) throw new Error(`HTTP ${response.status}`);
Alpine.store('global').showToast(Alpine.store('global').t('claudeConfigRestored'), 'success');
// Reload the config to reflect the changes
await this.fetchConfig();
} catch (e) {
Alpine.store('global').showToast(Alpine.store('global').t('restoreConfigFailed') + ': ' + e.message, 'error');
} finally {
this.restoring = false;
}
} }
}); });

View File

@@ -157,7 +157,10 @@ document.addEventListener('alpine:init', () => {
accountsReloaded: "Accounts reloaded", accountsReloaded: "Accounts reloaded",
reloadFailed: "Reload failed", reloadFailed: "Reload failed",
claudeConfigSaved: "Claude configuration saved", claudeConfigSaved: "Claude configuration saved",
claudeConfigRestored: "Claude CLI restored to defaults",
saveConfigFailed: "Failed to save configuration", saveConfigFailed: "Failed to save configuration",
restoreConfigFailed: "Failed to restore configuration",
restoreDefault: "Restore Default",
claudeActive: "Claude Active", claudeActive: "Claude Active",
claudeEmpty: "Claude Empty", claudeEmpty: "Claude Empty",
geminiActive: "Gemini Active", geminiActive: "Gemini Active",
@@ -406,7 +409,10 @@ document.addEventListener('alpine:init', () => {
accountsReloaded: "账号配置已重载", accountsReloaded: "账号配置已重载",
reloadFailed: "重载失败", reloadFailed: "重载失败",
claudeConfigSaved: "Claude 配置已保存", claudeConfigSaved: "Claude 配置已保存",
claudeConfigRestored: "Claude CLI 已恢复默认设置",
saveConfigFailed: "保存配置失败", saveConfigFailed: "保存配置失败",
restoreConfigFailed: "恢复配置失败",
restoreDefault: "恢复默认",
claudeActive: "Claude 活跃", claudeActive: "Claude 活跃",
claudeEmpty: "Claude 耗尽", claudeEmpty: "Claude 耗尽",
geminiActive: "Gemini 活跃", geminiActive: "Gemini 活跃",

View File

@@ -523,7 +523,16 @@
</div> </div>
</div> </div>
<div class="flex justify-end pt-2"> <div class="flex justify-end pt-2 gap-3">
<button class="btn btn-sm btn-ghost border border-space-border/50 hover:border-red-500/30 hover:bg-red-500/5 text-gray-400 hover:text-red-400 px-6 gap-2"
@click="restoreDefaultClaudeConfig" :disabled="restoring">
<svg x-show="!restoring" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span x-show="!restoring" x-text="$store.global.t('restoreDefault')">Restore Default</span>
<span x-show="restoring" class="loading loading-spinner loading-xs"></span>
</button>
<button class="btn btn-sm bg-neon-purple hover:bg-purple-600 border-none text-white px-6 gap-2" <button class="btn btn-sm bg-neon-purple hover:bg-purple-600 border-none text-white px-6 gap-2"
@click="saveClaudeConfig" :disabled="loading"> @click="saveClaudeConfig" :disabled="loading">
<svg x-show="!loading" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg x-show="!loading" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@@ -83,6 +83,35 @@ export async function updateClaudeConfig(updates) {
} }
} }
/**
* Replace the global Claude CLI configuration entirely
* Unlike updateClaudeConfig, this replaces the config instead of merging.
*
* @param {Object} config - The new configuration to write
* @returns {Promise<Object>} The written configuration
*/
export async function replaceClaudeConfig(config) {
const configPath = getClaudeConfigPath();
// 1. Ensure .claude directory exists
const configDir = path.dirname(configPath);
try {
await fs.mkdir(configDir, { recursive: true });
} catch (error) {
// Ignore if exists
}
// 2. Write config directly (no merge)
try {
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
logger.info(`[ClaudeConfig] Replaced config at ${configPath}`);
return config;
} catch (error) {
logger.error(`[ClaudeConfig] Failed to write config:`, error.message);
throw error;
}
}
/** /**
* Simple deep merge for objects * Simple deep merge for objects
*/ */

View File

@@ -18,7 +18,7 @@ import { fileURLToPath } from 'url';
import express from 'express'; import express from 'express';
import { getPublicConfig, saveConfig, config } from '../config.js'; import { getPublicConfig, saveConfig, config } from '../config.js';
import { DEFAULT_PORT, ACCOUNT_CONFIG_PATH } from '../constants.js'; import { DEFAULT_PORT, ACCOUNT_CONFIG_PATH } from '../constants.js';
import { readClaudeConfig, updateClaudeConfig, getClaudeConfigPath } from '../utils/claude-config.js'; import { readClaudeConfig, updateClaudeConfig, replaceClaudeConfig, getClaudeConfigPath } from '../utils/claude-config.js';
import { logger } from '../utils/logger.js'; import { logger } from '../utils/logger.js';
import { getAuthorizationUrl, completeOAuthFlow, startCallbackServer } from '../auth/oauth.js'; import { getAuthorizationUrl, completeOAuthFlow, startCallbackServer } from '../auth/oauth.js';
import { loadAccounts, saveAccounts } from '../account-manager/storage.js'; import { loadAccounts, saveAccounts } from '../account-manager/storage.js';
@@ -438,6 +438,41 @@ export function mountWebUI(app, dirname, accountManager) {
} }
}); });
/**
* POST /api/claude/config/restore - Restore Claude CLI to default (remove proxy settings)
*/
app.post('/api/claude/config/restore', async (req, res) => {
try {
const claudeConfig = await readClaudeConfig();
// Remove proxy-related environment variables to restore defaults
if (claudeConfig.env) {
delete claudeConfig.env.ANTHROPIC_BASE_URL;
delete claudeConfig.env.ANTHROPIC_AUTH_TOKEN;
delete claudeConfig.env.ANTHROPIC_MODEL;
delete claudeConfig.env.CLAUDE_CODE_SUBAGENT_MODEL;
delete claudeConfig.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
delete claudeConfig.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
delete claudeConfig.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
delete claudeConfig.env.ENABLE_EXPERIMENTAL_MCP_CLI;
}
// Use replaceClaudeConfig to completely overwrite the config (not merge)
const newConfig = await replaceClaudeConfig(claudeConfig);
logger.info(`[WebUI] Restored Claude CLI config to defaults at ${getClaudeConfigPath()}`);
res.json({
status: 'ok',
config: newConfig,
message: 'Claude CLI configuration restored to defaults'
});
} catch (error) {
logger.error('[WebUI] Error restoring Claude config:', error);
res.status(500).json({ status: 'error', error: error.message });
}
});
/** /**
* POST /api/models/config - Update model configuration (hidden/pinned/alias) * POST /api/models/config - Update model configuration (hidden/pinned/alias)
*/ */