/** * Settings Page * Application configuration */ import { useEffect, useMemo, useState } from 'react'; import { Sun, Moon, Monitor, RefreshCw, ExternalLink, Copy, FileText, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Separator } from '@/components/ui/separator'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { toast } from 'sonner'; import { useSettingsStore } from '@/stores/settings'; import { useGatewayStore } from '@/stores/gateway'; import { useUpdateStore } from '@/stores/update'; import { UpdateSettings } from '@/components/settings/UpdateSettings'; import { getGatewayWsDiagnosticEnabled, invokeIpc, setGatewayWsDiagnosticEnabled, toUserMessage, } from '@/lib/api-client'; import { clearUiTelemetry, getUiTelemetrySnapshot, subscribeUiTelemetry, trackUiEvent, type UiTelemetryEntry, } from '@/lib/telemetry'; import { useTranslation } from 'react-i18next'; import { SUPPORTED_LANGUAGES } from '@/i18n'; import { hostApiFetch } from '@/lib/host-api'; import { cn } from '@/lib/utils'; type ControlUiInfo = { url: string; token: string; port: number; }; export function Settings() { const { t } = useTranslation('settings'); const { theme, setTheme, language, setLanguage, launchAtStartup, setLaunchAtStartup, gatewayAutoStart, setGatewayAutoStart, proxyEnabled, proxyServer, proxyHttpServer, proxyHttpsServer, proxyAllServer, proxyBypassRules, setProxyEnabled, setProxyServer, setProxyHttpServer, setProxyHttpsServer, setProxyAllServer, setProxyBypassRules, autoCheckUpdate, setAutoCheckUpdate, autoDownloadUpdate, setAutoDownloadUpdate, devModeUnlocked, setDevModeUnlocked, telemetryEnabled, setTelemetryEnabled, } = useSettingsStore(); const { status: gatewayStatus, restart: restartGateway } = useGatewayStore(); const currentVersion = useUpdateStore((state) => state.currentVersion); const updateSetAutoDownload = useUpdateStore((state) => state.setAutoDownload); const [controlUiInfo, setControlUiInfo] = useState(null); const [openclawCliCommand, setOpenclawCliCommand] = useState(''); const [openclawCliError, setOpenclawCliError] = useState(null); const [proxyServerDraft, setProxyServerDraft] = useState(''); const [proxyHttpServerDraft, setProxyHttpServerDraft] = useState(''); const [proxyHttpsServerDraft, setProxyHttpsServerDraft] = useState(''); const [proxyAllServerDraft, setProxyAllServerDraft] = useState(''); const [proxyBypassRulesDraft, setProxyBypassRulesDraft] = useState(''); const [proxyEnabledDraft, setProxyEnabledDraft] = useState(false); const [savingProxy, setSavingProxy] = useState(false); const [wsDiagnosticEnabled, setWsDiagnosticEnabled] = useState(false); const [showTelemetryViewer, setShowTelemetryViewer] = useState(false); const [telemetryEntries, setTelemetryEntries] = useState([]); const isWindows = window.electron.platform === 'win32'; const showCliTools = true; const [showLogs, setShowLogs] = useState(false); const [logContent, setLogContent] = useState(''); const [doctorRunningMode, setDoctorRunningMode] = useState<'diagnose' | 'fix' | null>(null); const [doctorResult, setDoctorResult] = useState<{ mode: 'diagnose' | 'fix'; success: boolean; exitCode: number | null; stdout: string; stderr: string; command: string; cwd: string; durationMs: number; timedOut?: boolean; error?: string; } | null>(null); const handleShowLogs = async () => { try { const logs = await hostApiFetch<{ content: string }>('/api/logs?tailLines=100'); setLogContent(logs.content); setShowLogs(true); } catch { setLogContent('(Failed to load logs)'); setShowLogs(true); } }; const handleOpenLogDir = async () => { try { const { dir: logDir } = await hostApiFetch<{ dir: string | null }>('/api/logs/dir'); if (logDir) { await invokeIpc('shell:showItemInFolder', logDir); } } catch { // ignore } }; const handleRunOpenClawDoctor = async (mode: 'diagnose' | 'fix') => { setDoctorRunningMode(mode); try { const result = await hostApiFetch<{ mode: 'diagnose' | 'fix'; success: boolean; exitCode: number | null; stdout: string; stderr: string; command: string; cwd: string; durationMs: number; timedOut?: boolean; error?: string; }>('/api/app/openclaw-doctor', { method: 'POST', body: JSON.stringify({ mode }), }); setDoctorResult(result); if (result.success) { toast.success(mode === 'fix' ? t('developer.doctorFixSucceeded') : t('developer.doctorSucceeded')); } else { toast.error(result.error || (mode === 'fix' ? t('developer.doctorFixFailed') : t('developer.doctorFailed'))); } } catch (error) { const message = toUserMessage(error) || (mode === 'fix' ? t('developer.doctorFixRunFailed') : t('developer.doctorRunFailed')); toast.error(message); setDoctorResult({ mode, success: false, exitCode: null, stdout: '', stderr: '', command: 'openclaw doctor', cwd: '', durationMs: 0, error: message, }); } finally { setDoctorRunningMode(null); } }; const handleCopyDoctorOutput = async () => { if (!doctorResult) return; const payload = [ `command: ${doctorResult.command}`, `cwd: ${doctorResult.cwd}`, `exitCode: ${doctorResult.exitCode ?? 'null'}`, `durationMs: ${doctorResult.durationMs}`, '', '[stdout]', doctorResult.stdout.trim() || '(empty)', '', '[stderr]', doctorResult.stderr.trim() || '(empty)', ].join('\n'); try { await navigator.clipboard.writeText(payload); toast.success(t('developer.doctorCopied')); } catch (error) { toast.error(`Failed to copy doctor output: ${String(error)}`); } }; const refreshControlUiInfo = async () => { try { const result = await hostApiFetch<{ success: boolean; url?: string; token?: string; port?: number; }>('/api/gateway/control-ui'); if (result.success && result.url && result.token && typeof result.port === 'number') { setControlUiInfo({ url: result.url, token: result.token, port: result.port }); } } catch { // Ignore refresh errors } }; const handleCopyGatewayToken = async () => { if (!controlUiInfo?.token) return; try { await navigator.clipboard.writeText(controlUiInfo.token); toast.success(t('developer.tokenCopied')); } catch (error) { toast.error(`Failed to copy token: ${String(error)}`); } }; useEffect(() => { if (!showCliTools) return; let cancelled = false; (async () => { try { const result = await invokeIpc<{ success: boolean; command?: string; error?: string; }>('openclaw:getCliCommand'); if (cancelled) return; if (result.success && result.command) { setOpenclawCliCommand(result.command); setOpenclawCliError(null); } else { setOpenclawCliCommand(''); setOpenclawCliError(result.error || 'OpenClaw CLI unavailable'); } } catch (error) { if (cancelled) return; setOpenclawCliCommand(''); setOpenclawCliError(String(error)); } })(); return () => { cancelled = true; }; }, [devModeUnlocked, showCliTools]); const handleCopyCliCommand = async () => { if (!openclawCliCommand) return; try { await navigator.clipboard.writeText(openclawCliCommand); toast.success(t('developer.cmdCopied')); } catch (error) { toast.error(`Failed to copy command: ${String(error)}`); } }; useEffect(() => { const unsubscribe = window.electron.ipcRenderer.on( 'openclaw:cli-installed', (...args: unknown[]) => { const installedPath = typeof args[0] === 'string' ? args[0] : ''; toast.success(`openclaw CLI installed at ${installedPath}`); }, ); return () => { unsubscribe?.(); }; }, []); useEffect(() => { setWsDiagnosticEnabled(getGatewayWsDiagnosticEnabled()); }, []); useEffect(() => { if (!devModeUnlocked) return; setTelemetryEntries(getUiTelemetrySnapshot(200)); const unsubscribe = subscribeUiTelemetry((entry) => { setTelemetryEntries((prev) => { const next = [...prev, entry]; if (next.length > 200) { next.splice(0, next.length - 200); } return next; }); }); return unsubscribe; }, [devModeUnlocked]); useEffect(() => { setProxyEnabledDraft(proxyEnabled); }, [proxyEnabled]); useEffect(() => { setProxyServerDraft(proxyServer); }, [proxyServer]); useEffect(() => { setProxyHttpServerDraft(proxyHttpServer); }, [proxyHttpServer]); useEffect(() => { setProxyHttpsServerDraft(proxyHttpsServer); }, [proxyHttpsServer]); useEffect(() => { setProxyAllServerDraft(proxyAllServer); }, [proxyAllServer]); useEffect(() => { setProxyBypassRulesDraft(proxyBypassRules); }, [proxyBypassRules]); const handleSaveProxySettings = async () => { setSavingProxy(true); try { const normalizedProxyServer = proxyServerDraft.trim(); const normalizedHttpServer = proxyHttpServerDraft.trim(); const normalizedHttpsServer = proxyHttpsServerDraft.trim(); const normalizedAllServer = proxyAllServerDraft.trim(); const normalizedBypassRules = proxyBypassRulesDraft.trim(); await invokeIpc('settings:setMany', { proxyEnabled: proxyEnabledDraft, proxyServer: normalizedProxyServer, proxyHttpServer: normalizedHttpServer, proxyHttpsServer: normalizedHttpsServer, proxyAllServer: normalizedAllServer, proxyBypassRules: normalizedBypassRules, }); setProxyServer(normalizedProxyServer); setProxyHttpServer(normalizedHttpServer); setProxyHttpsServer(normalizedHttpsServer); setProxyAllServer(normalizedAllServer); setProxyBypassRules(normalizedBypassRules); setProxyEnabled(proxyEnabledDraft); toast.success(t('gateway.proxySaved')); trackUiEvent('settings.proxy_saved', { enabled: proxyEnabledDraft }); } catch (error) { toast.error(`${t('gateway.proxySaveFailed')}: ${toUserMessage(error)}`); } finally { setSavingProxy(false); } }; const telemetryStats = useMemo(() => { let errorCount = 0; let slowCount = 0; for (const entry of telemetryEntries) { if (entry.event.endsWith('_error') || entry.event.includes('request_error')) { errorCount += 1; } const durationMs = typeof entry.payload.durationMs === 'number' ? entry.payload.durationMs : Number.NaN; if (Number.isFinite(durationMs) && durationMs >= 800) { slowCount += 1; } } return { total: telemetryEntries.length, errorCount, slowCount }; }, [telemetryEntries]); const telemetryByEvent = useMemo(() => { const map = new Map(); for (const entry of telemetryEntries) { const current = map.get(entry.event) ?? { event: entry.event, count: 0, errorCount: 0, slowCount: 0, totalDuration: 0, timedCount: 0, lastTs: entry.ts, }; current.count += 1; current.lastTs = entry.ts; if (entry.event.endsWith('_error') || entry.event.includes('request_error')) { current.errorCount += 1; } const durationMs = typeof entry.payload.durationMs === 'number' ? entry.payload.durationMs : Number.NaN; if (Number.isFinite(durationMs)) { current.totalDuration += durationMs; current.timedCount += 1; if (durationMs >= 800) { current.slowCount += 1; } } map.set(entry.event, current); } return [...map.values()] .sort((a, b) => b.count - a.count) .slice(0, 12); }, [telemetryEntries]); const handleCopyTelemetry = async () => { try { const serialized = telemetryEntries.map((entry) => JSON.stringify(entry)).join('\n'); await navigator.clipboard.writeText(serialized); toast.success(t('developer.telemetryCopied')); } catch (error) { toast.error(`${t('common:status.error')}: ${String(error)}`); } }; const handleClearTelemetry = () => { clearUiTelemetry(); setTelemetryEntries([]); toast.success(t('developer.telemetryCleared')); }; const handleWsDiagnosticToggle = (enabled: boolean) => { setGatewayWsDiagnosticEnabled(enabled); setWsDiagnosticEnabled(enabled); toast.success( enabled ? t('developer.wsDiagnosticEnabled') : t('developer.wsDiagnosticDisabled'), ); }; return (
{/* Header */}

{t('title')}

{t('subtitle')}

{/* Content Area */}
{/* Appearance */}

{t('appearance.title')}

{SUPPORTED_LANGUAGES.map((lang) => ( ))}

{t('appearance.launchAtStartupDesc')}

{/* Gateway */}

{t('gateway.title')}

{t('gateway.port')}: {gatewayStatus.port}

{gatewayStatus.state}
{showLogs && (

{t('gateway.appLogs')}

                    {logContent || t('chat:noLogs')}
                  
)}

{t('gateway.autoStartDesc')}

{t('advanced.devModeDesc')}

{t('advanced.telemetryDesc')}

{/* Developer */} {devModeUnlocked && ( <>

{t('developer.title')}

{/* Gateway Proxy */}

{t('gateway.proxyDesc')}

{proxyEnabledDraft && (
setProxyServerDraft(event.target.value)} placeholder="http://127.0.0.1:7890" className="h-10 rounded-xl bg-black/5 dark:bg-white/5 border-transparent font-mono text-[13px]" />

{t('gateway.proxyServerHelp')}

setProxyHttpServerDraft(event.target.value)} placeholder={proxyServerDraft || 'http://127.0.0.1:7890'} className="h-10 rounded-xl bg-black/5 dark:bg-white/5 border-transparent font-mono text-[13px]" />

{t('gateway.proxyHttpServerHelp')}

setProxyHttpsServerDraft(event.target.value)} placeholder={proxyServerDraft || 'http://127.0.0.1:7890'} className="h-10 rounded-xl bg-black/5 dark:bg-white/5 border-transparent font-mono text-[13px]" />

{t('gateway.proxyHttpsServerHelp')}

setProxyAllServerDraft(event.target.value)} placeholder={proxyServerDraft || 'socks5://127.0.0.1:7891'} className="h-10 rounded-xl bg-black/5 dark:bg-white/5 border-transparent font-mono text-[13px]" />

{t('gateway.proxyAllServerHelp')}

setProxyBypassRulesDraft(event.target.value)} placeholder=";localhost;127.0.0.1;::1" className="h-10 rounded-xl bg-black/5 dark:bg-white/5 border-transparent font-mono text-[13px]" />

{t('gateway.proxyBypassHelp')}

{t('gateway.proxyRestartNote')}

)}

{t('developer.gatewayTokenDesc')}

{showCliTools && (

{t('developer.cliDesc')}

{isWindows && (

{t('developer.cliPowershell')}

)}
)}

{t('developer.doctorDesc')}

{doctorResult && (
{doctorResult.mode === 'fix' ? (doctorResult.success ? t('developer.doctorFixOk') : t('developer.doctorFixIssue')) : (doctorResult.success ? t('developer.doctorOk') : t('developer.doctorIssue'))} {t('developer.doctorExitCode')}: {doctorResult.exitCode ?? 'null'} {t('developer.doctorDuration')}: {Math.round(doctorResult.durationMs)}ms

{t('developer.doctorCommand')}: {doctorResult.command}

{t('developer.doctorWorkingDir')}: {doctorResult.cwd || '-'}

{doctorResult.error &&

{t('developer.doctorError')}: {doctorResult.error}

}

{t('developer.doctorStdout')}

                              {doctorResult.stdout.trim() || t('developer.doctorOutputEmpty')}
                            

{t('developer.doctorStderr')}

                              {doctorResult.stderr.trim() || t('developer.doctorOutputEmpty')}
                            
)}

{t('developer.wsDiagnosticDesc')}

{t('developer.telemetryViewerDesc')}

{showTelemetryViewer && (
{t('developer.telemetryTotal')}: {telemetryStats.total} 0 ? 'destructive' : 'secondary'} className={cn("rounded-full px-3 py-1", telemetryStats.errorCount === 0 && "bg-white dark:bg-card border border-black/5 dark:border-white/5")}> {t('developer.telemetryErrors')}: {telemetryStats.errorCount} 0 ? 'secondary' : 'outline'} className={cn("rounded-full px-3 py-1", telemetryStats.slowCount === 0 && "bg-white dark:bg-card border border-black/5 dark:border-white/5")}> {t('developer.telemetrySlow')}: {telemetryStats.slowCount}
{telemetryByEvent.length > 0 && (

{t('developer.telemetryAggregated')}

{telemetryByEvent.map((item) => (
{item.event} n={item.count} avg={item.timedCount > 0 ? Math.round(item.totalDuration / item.timedCount) : 0}ms slow={item.slowCount} err={item.errorCount}
))}
)}
{telemetryEntries.length === 0 ? (
{t('developer.telemetryEmpty')}
) : ( telemetryEntries .slice() .reverse() .map((entry) => (
{entry.event} {entry.ts}
                                      {JSON.stringify({ count: entry.count, ...entry.payload }, null, 2)}
                                    
)) )}
)}
)} {/* Updates */}

{t('updates.title')}

{t('updates.autoCheckDesc')}

{t('updates.autoDownloadDesc')}

{ setAutoDownloadUpdate(value); updateSetAutoDownload(value); }} />
{/* About */}

{t('about.title')}

{t('about.appName')} - {t('about.tagline')}

{t('about.basedOn')}

{t('about.version', { version: currentVersion })}

); } export default Settings;