import { useCallback, useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { Switch } from '@/components/ui/switch' import { type CodexLinkCopy, useCodexLinkCopy } from '@/i18n/codex-link' import { triggerHaptic } from '@/lib/haptics' import { AlertTriangle, Check, CircleIcon, Link, RefreshCw } from '@/lib/icons' import { cn } from '@/lib/utils' import { notifyError } from '@/store/notifications' import type { CodexLinkPatch, CodexLinkStatus } from '../../../electron/codex-link-types' import { EmptyState, ListRow, ListRowSkeleton, SettingsContent, SettingsSection } from './primitives' type Tone = 'muted' | 'ok' | 'warn' interface StatusLine { id: string text: string tone: Tone } /** * What the user needs to know, top-down, stopping at the first fact that makes * the rest moot (no Codex → nothing else to say; another bridge → ours is not * involved; setup pending → the bridge cannot be running yet). */ export function codexLinkStatusLines(status: CodexLinkStatus, copy: CodexLinkCopy): StatusLine[] { const lines: StatusLine[] = [] const error = status.lastError ? [{ id: 'error', text: copy.errors[status.lastError], tone: 'warn' as const }] : [] if (!status.codexFound) { return [{ id: 'codex', text: copy.codexMissing, tone: 'muted' }, ...error] } lines.push({ id: 'codex', text: copy.codexFound, tone: 'ok' }) if (status.foreign) { return [...lines, { id: 'foreign', text: copy.foreign, tone: 'warn' }, ...error] } if (!status.enabled) { return [...lines, { id: 'off', text: copy.off, tone: 'muted' }, ...error] } if (!status.venvReady) { return [...lines, { id: 'venv', text: copy.venvPending, tone: 'muted' }, ...error] } if (!status.bridgeRunning) { return [...lines, { id: 'bridge', text: copy.bridgeStopped, tone: 'warn' }, ...error] } lines.push({ id: 'bridge', text: copy.bridgeRunning, tone: 'ok' }) if (status.ideDurum === 'anahtar-yok') { lines.push({ id: 'ide', text: copy.signIn, tone: 'warn' }) } else if (status.ideDurum === 'tamam' && status.ideModel !== null) { lines.push({ id: 'ide', text: copy.models(status.ideModel), tone: 'ok' }) } return [...lines, ...error] } const TONE_ICON = { muted: CircleIcon, ok: Check, warn: AlertTriangle } as const const TONE_CLASS: Record = { muted: 'text-(--ui-text-tertiary)', ok: 'text-(--ui-text-secondary)', warn: 'text-amber-500/90' } // Phone-sized hit area around the compact switch: the visible control keeps the // design-system size, the label that wraps it is the 44px target. const TOUCH_TARGET = 'flex min-h-11 min-w-11 cursor-pointer items-center @2xl:justify-end' /** * Settings → Codex Link. Main owns everything real (the preference file, the * bridge, Codex's config, the login item); this page reads the state it reports * and asks for changes, adopting whatever comes back as the truth. */ export function CodexLinkSettings() { const copy = useCodexLinkCopy() const bridge = typeof window === 'undefined' ? undefined : window.hermesDesktop?.codexLink const [status, setStatus] = useState(null) const [loadFailed, setLoadFailed] = useState(false) const [busy, setBusy] = useState<'sync' | 'toggle' | null>(null) // A slow initial read must not overwrite the answer to a newer change. const generation = useRef(0) useEffect(() => { if (!bridge) { return } const ticket = generation.current bridge.get().then( next => { if (generation.current === ticket) { setStatus(next) } }, () => { if (generation.current === ticket) { setLoadFailed(true) } } ) }, [bridge]) const change = useCallback( async (patch: CodexLinkPatch, kind: 'sync' | 'toggle') => { if (!bridge || busy) { return } const before = status generation.current += 1 setBusy(kind) setLoadFailed(false) // The switch moves at once; main's reply (or the rollback) has the last word. if (typeof patch.enabled === 'boolean' && before) { setStatus({ ...before, enabled: patch.enabled }) } try { setStatus(await bridge.set(patch)) } catch (error) { setStatus(before) notifyError(error, copy.saveFailed) } finally { setBusy(null) } }, [bridge, busy, copy.saveFailed, status] ) if (!bridge) { return ( ) } const lines = status ? codexLinkStatusLines(status, copy) : [] return ( { triggerHaptic('selection') void change({ enabled }, 'toggle') }} /> } description={copy.description} title={copy.title} /> {!status && !loadFailed && } {loadFailed && !status && (

{copy.loadFailed}

)} {status && ( {lines.map(line => { const Icon = TONE_ICON[line.tone] return (
  • {line.text}
  • ) })} } title={copy.statusTitle} wide /> )} { triggerHaptic('open') void change({ sync: true }, 'sync') }} type="button" variant="secondary" > {busy === 'sync' ? copy.syncing : copy.sync} } description={copy.syncDesc} title={copy.syncTitle} />

    {copy.restartNote}

    ) }