/** * Settings Page * Makelore application configuration. */ import { useEffect, useMemo, useState, type FormEvent } from 'react'; import { useNavigate } from 'react-router-dom'; import { ChevronDown, ExternalLink, FileText, FolderPlus, KeyRound, RefreshCw, RotateCw, } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { DisclosureContent } from '@/components/ui/disclosure'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { UpdateSettings } from '@/components/settings/UpdateSettings'; import { SUPPORTED_LANGUAGES } from '@/i18n'; import { invokeIpc, toUserMessage } from '@/lib/api-client'; import { clearUiTelemetry, getUiTelemetrySnapshot, subscribeUiTelemetry, type UiTelemetryEntry, } from '@/lib/telemetry'; import { hostApiFetch } from '@/lib/host-api'; import { cn } from '@/lib/utils'; import { useOpencodeStore } from '@/stores/opencode'; import { useProjectConfigStore } from '@/stores/project-config'; import { useSettingsStore } from '@/stores/settings'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; export function Settings() { const { t } = useTranslation('settings'); const navigate = useNavigate(); const { setLanguage, launchAtStartup, setLaunchAtStartup, proxyEnabled, proxyServer, proxyHttpServer, proxyHttpsServer, proxyAllServer, proxyBypassRules, setProxyEnabled, setProxyServer, setProxyHttpServer, setProxyHttpsServer, setProxyAllServer, setProxyBypassRules, devModeUnlocked, unlockDevMode, lockDevMode, telemetryEnabled, setTelemetryEnabled, } = useSettingsStore(); const opencodeStatus = useOpencodeStore((state) => state.status); const opencodeRuntimeConfigSummary = useOpencodeStore((state) => state.runtimeConfigSummary); const opencodeProjects = useOpencodeStore((state) => state.projects); const activeOpencodeProject = useOpencodeStore((state) => state.activeProject); const opencodeLoading = useOpencodeStore((state) => state.loading); const opencodeError = useOpencodeStore((state) => state.error); const refreshOpencodeStatus = useOpencodeStore((state) => state.refreshStatus); const restartOpencodeRuntime = useOpencodeStore((state) => state.restart); const loadOpencodeRuntimeConfigSummary = useOpencodeStore( (state) => state.loadRuntimeConfigSummary ); const loadOpencodeProjects = useOpencodeStore((state) => state.loadProjects); const pickAndOpenOpencodeProject = useOpencodeStore((state) => state.pickAndOpenProject); const setActiveOpencodeProject = useOpencodeStore((state) => state.setActiveProject); const loadProjectConfig = useProjectConfigStore((state) => state.load); 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 [showLogs, setShowLogs] = useState(false); const [logContent, setLogContent] = useState(''); const [showTelemetryViewer, setShowTelemetryViewer] = useState(false); const [telemetryEntries, setTelemetryEntries] = useState([]); const [showAdditionalSettings, setShowAdditionalSettings] = useState(false); const [adminDialogOpen, setAdminDialogOpen] = useState(false); const [adminPassword, setAdminPassword] = useState(''); const [adminPasswordError, setAdminPasswordError] = useState(null); const [adminUnlocking, setAdminUnlocking] = useState(false); const [workspaceProjectError, setWorkspaceProjectError] = useState(null); useEffect(() => { setProxyEnabledDraft(proxyEnabled); }, [proxyEnabled]); useEffect(() => { setProxyServerDraft(proxyServer); }, [proxyServer]); useEffect(() => { setProxyHttpServerDraft(proxyHttpServer); }, [proxyHttpServer]); useEffect(() => { setProxyHttpsServerDraft(proxyHttpsServer); }, [proxyHttpsServer]); useEffect(() => { setProxyAllServerDraft(proxyAllServer); }, [proxyAllServer]); useEffect(() => { setProxyBypassRulesDraft(proxyBypassRules); }, [proxyBypassRules]); 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(() => { void Promise.allSettled([ refreshOpencodeStatus(), ]); }, [refreshOpencodeStatus]); useEffect(() => { if (!devModeUnlocked) return; void Promise.allSettled([ loadOpencodeRuntimeConfigSummary(), loadOpencodeProjects(), ]); }, [devModeUnlocked, loadOpencodeProjects, loadOpencodeRuntimeConfigSummary]); const proxySettingsDirty = useMemo(() => { return ( proxyEnabledDraft !== proxyEnabled || proxyServerDraft.trim() !== proxyServer || proxyHttpServerDraft.trim() !== proxyHttpServer || proxyHttpsServerDraft.trim() !== proxyHttpsServer || proxyAllServerDraft.trim() !== proxyAllServer || proxyBypassRulesDraft.trim() !== proxyBypassRules ); }, [ proxyAllServer, proxyAllServerDraft, proxyBypassRules, proxyBypassRulesDraft, proxyEnabled, proxyEnabledDraft, proxyHttpServer, proxyHttpServerDraft, proxyHttpsServer, proxyHttpsServerDraft, proxyServer, proxyServerDraft, ]); 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, }); setProxyEnabled(proxyEnabledDraft); setProxyServer(normalizedProxyServer); setProxyHttpServer(normalizedHttpServer); setProxyHttpsServer(normalizedHttpsServer); setProxyAllServer(normalizedAllServer); setProxyBypassRules(normalizedBypassRules); toast.success('Proxy settings saved'); } catch (error) { toast.error(`Failed to save proxy settings: ${toUserMessage(error)}`); } finally { setSavingProxy(false); } }; const handleRefreshOpencodeSettings = () => { void Promise.allSettled([ refreshOpencodeStatus(), loadOpencodeRuntimeConfigSummary(), loadOpencodeProjects(), ]); }; const handleRevealAdditionalSettings = () => { if (showAdditionalSettings) { setShowAdditionalSettings(false); return; } if (devModeUnlocked) { setShowAdditionalSettings(true); return; } setAdminPassword(''); setAdminPasswordError(null); setAdminDialogOpen(true); }; const handleUnlockDevMode = async (event: FormEvent) => { event.preventDefault(); setAdminUnlocking(true); setAdminPasswordError(null); try { const unlocked = await unlockDevMode(adminPassword); if (!unlocked) { setAdminPasswordError(t('admin.invalidPassword', 'The administrator password is incorrect.')); return; } setAdminDialogOpen(false); setAdminPassword(''); setShowAdditionalSettings(true); } finally { setAdminUnlocking(false); } }; const handleLockDevMode = async () => { await lockDevMode(); setShowAdditionalSettings(false); }; const handleAddOpencodeProject = async () => { const project = await pickAndOpenOpencodeProject(); if (!project) return; await activateVerifiedProject(project.id); }; const activateVerifiedProject = async (projectId: string) => { try { setWorkspaceProjectError(null); const result = await loadProjectConfig(projectId); if (result.status === 'valid') { await setActiveOpencodeProject(projectId); return; } if (result.status === 'missing') { setWorkspaceProjectError('Project configuration is missing. Create the project from the sidebar first.'); return; } setWorkspaceProjectError(result.error ?? 'Project configuration is invalid.'); } catch (error) { setWorkspaceProjectError(error instanceof Error ? error.message : String(error)); } }; 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 } }; return (

{t('title')}

{t('appearance.title')}

{t('appearance.launchAtStartupDesc')}

Runtime

{opencodeStatus.state}
Status
{opencodeStatus.state}
Port
{opencodeStatus.port}
{(opencodeStatus.error || opencodeError) && (
{opencodeStatus.error ?? opencodeError}
)}

{t('updates.title')}

{ setAdminDialogOpen(open); if (!open) { setAdminPassword(''); setAdminPasswordError(null); } }} > {t('admin.title', '管理员验证')} {t('admin.description', '请输入管理员密码以管理开发者设置。')}
setAdminPassword(event.target.value)} autoFocus autoComplete="current-password" data-testid="settings-admin-password-input" aria-invalid={Boolean(adminPasswordError)} /> {adminPasswordError && (

{adminPasswordError}

)}
{showAdditionalSettings && devModeUnlocked && (

Workspace

Configure the local runtime and project workspace.

Resolved from the runtime configuration.

Default model
{opencodeRuntimeConfigSummary?.model ?? 'No configured model'}
Small model
{opencodeRuntimeConfigSummary?.smallModel ?? 'No configured small model'}
Providers {`${opencodeRuntimeConfigSummary?.providerCount ?? 0} providers`}

Folders opened as project workspaces.

Active project
{activeOpencodeProject?.name ?? 'No project selected'}
{activeOpencodeProject?.path ?? 'Add a project folder to start a session.'}
{workspaceProjectError && (
{workspaceProjectError}
)} {opencodeProjects.length > 0 && (
{opencodeProjects.map((project) => { const active = activeOpencodeProject?.id === project.id; return (
{project.name}
{project.path}
); })}
)}

Network

Configure proxy settings used by Makelore runtime.

Route local runtime traffic through a proxy.

setProxyServerDraft(event.target.value)} placeholder="http://127.0.0.1:7890" />
setProxyHttpServerDraft(event.target.value)} placeholder="http://127.0.0.1:7890" />
setProxyHttpsServerDraft(event.target.value)} placeholder="http://127.0.0.1:7890" />
setProxyAllServerDraft(event.target.value)} placeholder="socks5://127.0.0.1:7890" />
setProxyBypassRulesDraft(event.target.value)} placeholder="localhost,127.0.0.1" />

{t('advanced.title')}

{t('advanced.description')}

{t('advanced.sessionUnlocked', '管理员权限已在本次应用运行期间解锁。')}

{t('admin.unlocked', '已解锁')}

{t('advanced.telemetryDesc')}

Inspect recent local application logs.

{logContent || '(No logs available yet)'}

Local-only UI and performance events.

{telemetryEntries.length > 0 ? JSON.stringify(telemetryEntries, null, 2) : '(No telemetry entries)'}
)}
); } export default Settings;