import React, { useState, useEffect } from 'react'; import type { Language, ModelDefault, McpServer } from '../types'; import { t, setLocale as setGlobalLocale } from '../i18n'; const LANGUAGES: { value: Language; label: string }[] = [ { value: 'en', label: 'English' }, { value: 'zh-TW', label: '繁體中文 (Traditional Chinese)' }, { value: 'ja', label: '日本語 (Japanese)' } ]; const MODELS: { value: ModelDefault; label: string; description: string }[] = [ { value: 'haiku', label: 'Haiku', description: 'Fast & cheap — simple tasks' }, { value: 'sonnet', label: 'Sonnet', description: 'Balanced — complex coding (default)' }, { value: 'opus', label: 'Opus', description: 'Best quality — critical tasks (1M context)' } ]; const TOOL_COUNTS: Record = { 'trend-pulse': 11, 'claude-101': 27, 'cf-browser': 15, 'notebooklm': 13, }; const TIER_LABELS: Record = { 'trend-pulse': 'Tier 1 — Zero Auth (always available)', 'claude-101': 'Tier 1 — Zero Auth (always available)', 'cf-browser': 'Tier 2 — Requires CF_ACCOUNT_ID + CF_API_TOKEN', 'notebooklm': 'Tier 2 — Requires Google login (uvx notebooklm login)' }; function McpServerCard({ server }: { server: McpServer }) { const statusColors: Record = { connected: 'text-green-400', disconnected: 'text-gray-500', error: 'text-red-400' }; const dotColors: Record = { connected: 'bg-green-400', disconnected: 'bg-gray-600', error: 'bg-red-500' }; return (

{server.name}

{server.tools !== undefined && ( {server.tools} tools )}

{TIER_LABELS[server.name] ?? 'MCP Server'}

{server.status.charAt(0).toUpperCase() + server.status.slice(1)}
); } interface SettingsPageProps { onLocaleChange?: (locale: Language) => void; } export default function SettingsPage({ onLocaleChange }: SettingsPageProps) { const [language, setLanguage] = useState('en'); const [modelDefault, setModelDefault] = useState('sonnet'); const [isSaving, setIsSaving] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false); const [error, setError] = useState(null); const [projectPath, setProjectPath] = useState(''); const [projectStatus, setProjectStatus] = useState<{ has_claude_md: boolean; has_skills: boolean; has_memory: boolean } | null>(null); const [initPath, setInitPath] = useState(''); const [initStatus, setInitStatus] = useState(''); const [mcpServers, setMcpServers] = useState([]); // Multi-LLM Executor state const [executorType, setExecutorType] = useState('claude-cli'); const [anthropicApiKey, setAnthropicApiKey] = useState(''); const [openaiApiKey, setOpenaiApiKey] = useState(''); const [openaiModel, setOpenaiModel] = useState('gpt-4o'); const [grokApiKey, setGrokApiKey] = useState(''); const [geminiApiKey, setGeminiApiKey] = useState(''); const [ollamaBaseUrl, setOllamaBaseUrl] = useState('http://localhost:11434'); const [ollamaModel, setOllamaModel] = useState('llama3.2'); useEffect(() => { fetch('/api/settings') .then((r) => r.json()) .then((data: Record) => { if (data.language) setLanguage(data.language as Language); if (data.model_default) setModelDefault(data.model_default as ModelDefault); if (data.executor_default) setExecutorType(data.executor_default); if (data.executor_anthropic_api_key) setAnthropicApiKey(data.executor_anthropic_api_key); if (data.executor_openai_api_key) setOpenaiApiKey(data.executor_openai_api_key); if (data.executor_openai_model) setOpenaiModel(data.executor_openai_model); if (data.executor_grok_api_key) setGrokApiKey(data.executor_grok_api_key); if (data.executor_gemini_api_key) setGeminiApiKey(data.executor_gemini_api_key); if (data.executor_ollama_base_url) setOllamaBaseUrl(data.executor_ollama_base_url); if (data.executor_ollama_model) setOllamaModel(data.executor_ollama_model); }) .catch(() => {}); // Load project info fetch('/api/project') .then(r => r.json()) .then(data => { setProjectPath(data.agent_root || ''); setProjectStatus({ has_claude_md: data.has_claude_md, has_skills: data.has_skills, has_memory: data.has_memory }); }) .catch(() => {}); fetch('/api/mcp') .then((r) => r.json()) .then((data: { mcpServers?: Record }) => { if (data.mcpServers) { const servers: McpServer[] = Object.keys(data.mcpServers).map((name) => ({ name, status: 'connected' as const, tools: TOOL_COUNTS[name], })); setMcpServers(servers); } }) .catch(() => {}); }, []); const save = async () => { setIsSaving(true); setError(null); try { const res = await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ language, model_default: modelDefault, executor_default: executorType, executor_anthropic_api_key: anthropicApiKey, executor_openai_api_key: openaiApiKey, executor_openai_model: openaiModel, executor_grok_api_key: grokApiKey, executor_gemini_api_key: geminiApiKey, executor_ollama_base_url: ollamaBaseUrl, executor_ollama_model: ollamaModel, }) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 3000); } catch (err) { setError(`Save failed: ${(err as Error).message}`); } finally { setIsSaving(false); } }; return (
{/* Header */}

{t('settings.title')}

{t('settings.subtitle')}

{/* Language */}

{t('settings.language')}

{LANGUAGES.map(({ value, label }) => ( ))}
{/* Model */}

{t('settings.model')}

{MODELS.map(({ value, label, description }) => ( ))}
{/* Multi-LLM Executor */}

{t('settings.executorTitle') || 'Multi-LLM Executor'}

{t('settings.executorSubtitle') || 'Choose which AI provider powers conversations'}

{(executorType === 'claude-cli' || executorType === 'codex-cli' || executorType === 'gemini-cli' || executorType === 'opencode-cli') && (

{t('settings.executorCliNote') || 'Uses CLI tool directly — no API key needed. Ensure the CLI is installed and in PATH.'}

)} {executorType === 'anthropic-sdk' && (
setAnthropicApiKey(e.target.value)} placeholder="sk-ant-..." className="input-base w-full text-sm font-mono" />
)} {executorType === 'openai' && ( <>
setOpenaiApiKey(e.target.value)} placeholder="sk-..." className="input-base w-full text-sm font-mono" />
setOpenaiModel(e.target.value)} placeholder="gpt-4o" className="input-base w-full text-sm" />
)} {executorType === 'grok' && (
setGrokApiKey(e.target.value)} placeholder="xai-..." className="input-base w-full text-sm font-mono" />
)} {executorType === 'gemini' && (
setGeminiApiKey(e.target.value)} placeholder="AIza..." className="input-base w-full text-sm font-mono" />
)} {executorType === 'ollama' && ( <>
setOllamaBaseUrl(e.target.value)} placeholder="http://localhost:11434" className="input-base w-full text-sm" />
setOllamaModel(e.target.value)} placeholder="llama3.2" className="input-base w-full text-sm" />
)}
{/* Save */}
{saveSuccess && {t('settings.saved')}} {error && {error}}
{/* Project Directory */}

{t('settings.projectTitle') || 'Project Directory'}

{t('settings.projectDesc') || 'The directory where CLAUDE.md, skills, agents, and memory are stored.'}

{projectPath || '(not set)'} {projectStatus && (
CLAUDE.md Skills Memory
)}
setInitPath(e.target.value)} placeholder={t('settings.projectPlaceholder') || '~/claude-agent or /path/to/project'} className="input-base flex-1 text-xs" />
{initStatus &&

{initStatus}

}
{/* CLI Detection */}

{t('settings.cliTitle') || 'CLI Detection'}

{/* Updates */}

{t('settings.updatesTitle')}

{t('settings.updatesDesc')}

{/* Auto-start on Boot */}

{t('settings.autostartTitle')}

{t('settings.autostartDesc')}

{/* Webhook Secret */}

{t('settings.webhookTitle')}

{t('settings.webhookDesc')}

{/* OpenClaw Migration */}

{t('settings.migrateTitle') || 'Migrate from OpenClaw'}

{t('settings.migrateDesc') || 'Import your memory, skills, agents, and config from OpenClaw.'}

); } function CliDetector() { const [clis, setClis] = useState<{ name: string; path: string | null; version: string | null }[]>([]); const [loading, setLoading] = useState(true); const [defaultCli, setDefaultCli] = useState('claude'); const [savingDefault, setSavingDefault] = useState(null); useEffect(() => { fetch('/api/cli-detect') .then(r => r.json()) .then(data => { if (Array.isArray(data)) setClis(data); }) .catch(() => {}) .finally(() => setLoading(false)); fetch('/api/settings') .then(r => r.json()) .then((data: Record) => { if (data.default_cli) setDefaultCli(data.default_cli); }) .catch(() => {}); }, []); const setAsDefault = async (cliName: string) => { setSavingDefault(cliName); try { await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ default_cli: cliName }), }); setDefaultCli(cliName); } catch {} setSavingDefault(null); }; // Only show AI CLIs in the detection section (claude, codex, gemini, opencode) const AI_CLIS = ['claude', 'codex', 'gemini', 'opencode']; const aiClis = clis.filter(c => AI_CLIS.includes(c.name)); const otherClis = clis.filter(c => !AI_CLIS.includes(c.name)); if (loading) return

{t('settings.detecting') || 'Detecting CLIs...'}

; return (
{aiClis.length > 0 && (

AI CLIs

{aiClis.map(cli => (
{cli.name} {cli.path ? ( <> {cli.path} {cli.version && {cli.version}} {defaultCli === cli.name ? ( Default ) : ( )} ) : ( {t('settings.notInstalled') || 'Not installed'} )}
))}
)} {otherClis.length > 0 && (

Other tools

{otherClis.map(cli => (
{cli.name} {cli.path ? ( <> {cli.path} {cli.version && {cli.version}} ) : ( {t('settings.notInstalled') || 'Not installed'} )}
))}
)}
); } // ── Update checker UI ──────────────────────────────────────────────────────── function UpdateChecker() { const [info, setInfo] = useState<{ currentVersion: string; latestVersion: string | null; hasUpdate: boolean; lastChecked: string | null; releaseUrl: string | null; } | null>(null); const [desktopState, setDesktopState] = useState(null); const [checking, setChecking] = useState(false); const [checkError, setCheckError] = useState(null); useEffect(() => { fetch('/api/update-info') .then(r => r.json()) .then(setInfo) .catch(() => {}); if (!window.claudeAgentDesktop) return; window.claudeAgentDesktop.getUpdateState() .then(setDesktopState) .catch(() => {}); return window.claudeAgentDesktop.onUpdateState(setDesktopState); }, []); const checkNow = async () => { setChecking(true); setCheckError(null); let nextError: string | null = null; try { const res = await fetch('/api/check-update', { method: 'POST' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setInfo(data); } catch (err) { nextError = (err as Error).message; } if (window.claudeAgentDesktop) { try { const state = await window.claudeAgentDesktop.checkForUpdates(); setDesktopState(state); } catch (err) { nextError ||= (err as Error).message; } } setCheckError(nextError); setChecking(false); }; const installNow = async () => { if (!window.claudeAgentDesktop) return; setCheckError(null); try { const result = await window.claudeAgentDesktop.installUpdate(); if (!result.ok) setCheckError('Update is not ready to install yet.'); } catch (err) { setCheckError((err as Error).message); } }; const effectiveCurrentVersion = desktopState?.currentVersion || info?.currentVersion || '…'; const effectiveLatestVersion = desktopState?.latestVersion || info?.latestVersion || null; const hasDesktopUpdate = Boolean(desktopState?.available || desktopState?.downloaded); const hasAnyUpdate = hasDesktopUpdate || Boolean(info?.hasUpdate); const isBusy = checking || Boolean(desktopState?.checking || desktopState?.downloading); return (
{t('settings.updateCurrentVersion')} v{effectiveCurrentVersion} {desktopState?.downloaded && ( {t('settings.updateReady') || 'Ready to install'} )} {desktopState?.downloading && ( {t('settings.updateDownloading') || 'Downloading'} {desktopState.progressPercent ?? 0}% )} {!desktopState?.downloaded && !desktopState?.downloading && hasAnyUpdate && effectiveLatestVersion && ( v{effectiveLatestVersion} {t('settings.updateAvailable')} )} {info && !hasAnyUpdate && info.latestVersion && ( {t('settings.updateUpToDate')} )}
{info?.lastChecked && (

{t('settings.updateLastChecked')} {new Date(info.lastChecked).toLocaleString()}

)} {desktopState?.supported && (

{t('settings.updateAutoHint') || 'Desktop app updates download in the background and can be installed here once ready.'}

)}
{desktopState?.downloaded && ( )}
{(desktopState?.error || checkError) && (

{desktopState?.error || checkError}

)} {hasAnyUpdate && info?.releaseUrl && (

{t('settings.updateBanner', { version: `v${effectiveLatestVersion}` })}

{t('settings.updateViewRelease')}
)}
); } // ── Auto-start toggle UI ───────────────────────────────────────────────────── function AutostartToggle() { const [status, setStatus] = useState<{ enabled: boolean; platform: string; servicePath: string; error?: string; } | null>(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); useEffect(() => { fetch('/api/autostart') .then(r => r.json()) .then(setStatus) .catch(() => {}); }, []); const toggle = async () => { if (!status) return; setSaving(true); setSaveError(null); try { const res = await fetch('/api/autostart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: !status.enabled }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); setStatus(data); } catch (err) { setSaveError((err as Error).message); } setSaving(false); }; if (!status) return

Loading…

; return (
{status.enabled ? t('settings.autostartEnabled') : t('settings.autostartDisabled')} {status.platform}
{status.servicePath && (

{status.servicePath}

)}
{saveError &&

{saveError}

} {status.error && (

{status.error}

)}
); } // ── Webhook secret UI ──────────────────────────────────────────────────────── function WebhookSecretSection() { const [secret, setSecret] = useState(''); // isPlaceholder: true when the displayed value is the masked "•••" sentinel, // not a real value the user has typed or generated. Saving in this state is a no-op. const [isPlaceholder, setIsPlaceholder] = useState(false); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [saveError, setSaveError] = useState(null); const [show, setShow] = useState(false); useEffect(() => { fetch('/api/settings') .then(r => r.json()) .then((data: Record) => { if (data.webhook_secret_set === 'true') { setSecret('••••••••••••••••'); setIsPlaceholder(true); } }) .catch(() => {}); }, []); const generate = () => { // 64-char base64url alphabet avoids modulo bias with 256-byte values const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; const arr = new Uint8Array(32); crypto.getRandomValues(arr); setSecret(Array.from(arr, b => chars[b % chars.length]).join('')); setIsPlaceholder(false); setSaved(false); }; const clear = () => { setSecret(''); setIsPlaceholder(false); setSaved(false); }; const save = async () => { if (isPlaceholder) return; // nothing changed — don't overwrite with placeholder setSaving(true); setSaveError(null); try { const res = await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ webhook_secret: secret }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); setSaved(true); if (!secret) setIsPlaceholder(false); // secret was cleared setTimeout(() => setSaved(false), 3000); } catch (err) { setSaveError((err as Error).message); } setSaving(false); }; return (
{ setSecret(e.target.value); setIsPlaceholder(false); setSaved(false); }} placeholder={t('settings.webhookPlaceholder')} className="input-base flex-1 text-xs font-mono" /> {isPlaceholder ? ( ) : ( )}
{isPlaceholder && (

A secret is already set. Generate a new one or click Clear to remove it.

)} {saved &&

{t('settings.webhookSaved')}

} {saveError &&

{saveError}

}

{t('settings.webhookEndpointHint')}

); } function MigrateSection() { const [status, setStatus] = useState<'idle' | 'checking' | 'ready' | 'running' | 'done' | 'not-found'>('idle'); const [output, setOutput] = useState(''); const checkOpenclaw = async () => { setStatus('checking'); try { const res = await fetch('/api/migrate/check'); const data = await res.json(); setStatus(data.found ? 'ready' : 'not-found'); setOutput(data.summary || ''); } catch { setStatus('not-found'); } }; const runMigration = async () => { setStatus('running'); setOutput(''); try { const res = await fetch('/api/migrate/run', { method: 'POST' }); const data = await res.json(); setOutput(data.report || data.error || 'Migration completed.'); setStatus('done'); } catch (err) { setOutput(`Error: ${(err as Error).message}`); setStatus('done'); } }; useEffect(() => { checkOpenclaw(); }, []); return (
{status === 'checking' && (

{t('settings.migrateChecking') || 'Checking for OpenClaw installation...'}

)} {status === 'not-found' && (

{t('settings.migrateNotFound') || 'OpenClaw not found at ~/.openclaw/'}

{t('settings.migrateNotFoundHint') || 'If installed elsewhere, use the CLI: node scripts/migrate-openclaw.cjs --openclaw-dir /path/to/.openclaw'}

)} {status === 'ready' && (

{output}

)} {status === 'running' && (

{t('settings.migrateRunning') || 'Migration in progress...'}

)} {status === 'done' && (

{output}

)}
); }