Files
aiturk-hermes-ide/apps/desktop/src/app/settings/codex-link-settings.tsx
T

472 lines
16 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { useCodexLinkCopy } from '@/i18n/codex-link'
import { Cpu } from '@/lib/icons'
import { validateControlRequest } from '../../../codex-link-control'
import type {
CodexLinkContextView,
CodexLinkControlRequest,
CodexLinkControlResponse,
CodexLinkSettingsResult,
CodexLinkSettingsUpdate,
CodexLinkStatusSnapshot
} from '../../../codex-link-control.types'
import { SettingsContent, SettingsSection } from './primitives'
const PENDING_KEY = 'aiturk.codex-link.pending-control.v1'
function envelope() {
return {
contract_version: 'codex-link/1' as const,
request_id: crypto.randomUUID(),
operation_id: crypto.randomUUID()
}
}
function readPending(): CodexLinkControlRequest | null {
try {
const raw = sessionStorage.getItem(PENDING_KEY)
if (!raw) {
return null
}
const value: unknown = JSON.parse(raw)
validateControlRequest(value)
return ['settings_update', 'catalog_sync', 'context_compact_view'].includes(value.kind) ? value : null
} catch {
return null
}
}
function savedPending(value: CodexLinkControlRequest | null) {
if (value) {
sessionStorage.setItem(PENDING_KEY, JSON.stringify(value))
} else {
sessionStorage.removeItem(PENDING_KEY)
}
}
function Preference({
id,
label,
description,
checked,
disabled,
onChange
}: {
id: string
label: string
description: string
checked: boolean
disabled: boolean
onChange: (value: boolean) => void
}) {
return (
<label className="flex min-h-11 cursor-pointer items-center gap-4 py-3" htmlFor={id}>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium">{label}</span>
<span className="mt-1 block text-sm text-muted-foreground">{description}</span>
</span>
<Switch aria-label={label} checked={checked} disabled={disabled} id={id} onCheckedChange={onChange} />
</label>
)
}
export function CodexLinkSettings() {
const copy = useCodexLinkCopy()
const [settings, setSettings] = useState<CodexLinkSettingsResult | null>(null)
const [status, setStatus] = useState<CodexLinkStatusSnapshot | null>(null)
const [views, setViews] = useState<CodexLinkContextView[]>([])
const [threshold, setThreshold] = useState('')
const [busy, setBusy] = useState(false)
const [pending, setPending] = useState(readPending)
const [error, setError] = useState('')
const [notice, setNotice] = useState('')
const mounted = useRef(true)
const writeLock = useRef(false)
const readLock = useRef(false)
const generation = useRef(0)
const savedRevision = useRef(-1)
const bridge = window.hermesDesktop?.codexLink
const refresh = useCallback(async () => {
if (!bridge || readLock.current || writeLock.current) {
return
}
readLock.current = true
const current = generation.current
try {
const results = await Promise.all(
['settings_get', 'status_get', 'context_views_get'].map(kind =>
bridge.control({ ...envelope(), kind } as CodexLinkControlRequest)
)
)
if (!mounted.current || current !== generation.current) {
return
}
const [saved, live, chats] = results
if (saved.kind !== 'settings_result' || live.kind !== 'status_result' || chats.kind !== 'context_views_result') {
throw new Error('read_failed')
}
// A write in another window may fall between these three reads. Never paint a mixed revision.
if (
saved.settings.settings_revision !== live.status.settings_revision ||
saved.settings.settings_revision !== chats.settings_revision
) {
return
}
if (saved.settings.settings_revision < savedRevision.current) {
return
}
if (savedRevision.current !== saved.settings.settings_revision) {
setThreshold(String(saved.settings.threshold_tokens))
}
savedRevision.current = saved.settings.settings_revision
setSettings(saved)
setStatus(live.status)
setViews(chats.views)
setError('')
} catch {
if (mounted.current && current === generation.current) {
setError(copy.failed)
}
} finally {
readLock.current = false
}
}, [bridge, copy.failed])
// This is a component lifetime flag, not a mirror of a reactive atom/value.
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
mounted.current = true
void refresh()
const timer = setInterval(() => {
if (document.visibilityState === 'visible') {
void refresh()
}
}, 5000)
return () => {
mounted.current = false
// The current numeric generation is invalidated; it is not a captured DOM ref.
// eslint-disable-next-line react-hooks/exhaustive-deps
generation.current++
clearInterval(timer)
}
}, [refresh])
const send = async (request: CodexLinkControlRequest) => {
if (!bridge || writeLock.current) {
return
}
writeLock.current = true
generation.current++
setBusy(true)
setError('')
setNotice('')
// Persist the exact operation before dispatch: an uncertain reply must reuse this ID and body.
try {
savedPending(request)
setPending(request)
} catch {
writeLock.current = false
setBusy(false)
setError(copy.failed)
return
}
try {
const result: CodexLinkControlResponse = await bridge.control(request)
if (!mounted.current) {
return
}
if (
(result.kind === 'control_error' && result.error.recovery_action === 'reconcile_only') ||
(result.kind === 'control_accepted' && result.operation_state === 'indeterminate')
) {
setError(copy.unknown)
return
}
savedPending(null)
setPending(null)
if (result.kind === 'control_error') {
setNotice(
result.error.code.includes('conflict') || result.error.code.includes('cas')
? copy.conflict
: result.error.message
)
} else if (result.kind === 'settings_result') {
savedRevision.current = result.settings.settings_revision
setSettings(result)
setThreshold(String(result.settings.threshold_tokens))
setNotice(copy.saved)
} else if (result.kind === 'control_accepted') {
setNotice(result.error?.message ?? copy.accepted)
}
} catch {
if (mounted.current) {
setError(copy.unknown)
}
} finally {
writeLock.current = false
if (mounted.current) {
setBusy(false)
void refresh()
}
}
}
const update = (patch: CodexLinkSettingsUpdate['patch']) => {
if (settings && !pending) {
void send({
...envelope(),
kind: 'settings_update',
expected_settings_revision: settings.settings.settings_revision,
patch
})
}
}
const thresholdValue = /^[0-9]+$/.test(threshold) ? Number(threshold) : NaN
const validThreshold = Number.isSafeInteger(thresholdValue) && thresholdValue > 0
const locked = busy || pending !== null
const number = (n: number | null) => (n === null ? copy.unmeasured : n.toLocaleString())
return (
<SettingsContent>
<div className="min-w-0 py-4" data-testid="codex-link-settings">
<SettingsSection icon={Cpu} title={copy.title}>
<p className="mb-4 text-sm text-muted-foreground">{copy.intro}</p>
{!bridge ? (
<p role="alert">{copy.unavailable}</p>
) : !settings ? (
<p role="status">{error || copy.loading}</p>
) : (
<>
<Preference
checked={settings.settings.enabled}
description={copy.enabledHelp}
disabled={locked}
id="codex-link-enabled"
label={copy.enabled}
onChange={enabled => update({ enabled })}
/>
<Preference
checked={settings.settings.compression_enabled}
description={copy.compressionHelp}
disabled={locked}
id="codex-link-compression"
label={copy.compression}
onChange={compression_enabled => update({ compression_enabled })}
/>
<div className="my-4 space-y-2">
<label className="block text-sm font-medium" htmlFor="codex-link-threshold">
{copy.threshold}
</label>
<div className="flex flex-wrap gap-2">
<Input
aria-describedby="codex-link-threshold-help"
aria-invalid={!validThreshold}
className="min-h-11 w-full min-w-0 sm:w-48"
disabled={locked}
id="codex-link-threshold"
inputMode="numeric"
onChange={event => setThreshold(event.target.value)}
value={threshold}
/>
<Button
className="min-h-11"
disabled={locked || !validThreshold || thresholdValue === settings.settings.threshold_tokens}
onClick={() => update({ threshold_tokens: thresholdValue })}
>
{copy.save}
</Button>
</div>
<p className="text-sm text-muted-foreground" id="codex-link-threshold-help">
{validThreshold ? copy.thresholdHelp : copy.invalidThreshold}
</p>
</div>
<div className="my-4 space-y-2">
<label className="block text-sm font-medium" htmlFor="codex-link-source">
{copy.source}
</label>
{settings.available_sources.length ? (
<select
className="min-h-11 w-full min-w-0 rounded-md border bg-background px-3 text-sm"
disabled={locked}
id="codex-link-source"
onChange={event => update({ source_binding_handle: event.target.value })}
value={settings.settings.source_binding_handle ?? ''}
>
{!settings.settings.source_binding_handle && (
<option disabled value="">
{copy.noSource}
</option>
)}
{settings.available_sources.map(source => (
<option
disabled={!source.authorized}
key={source.source_binding_handle}
value={source.source_binding_handle}
>
{source.label}
</option>
))}
</select>
) : (
<p className="text-sm text-muted-foreground">{copy.noSource}</p>
)}
</div>
<p className="text-sm" role="status">
{copy.activation[settings.settings.activation_state]}
{status ? ` · ${copy.admission[status.external_admission]}` : ''}
</p>
{status?.needs_restart && <p className="mt-2 text-sm text-muted-foreground">{copy.restart}</p>}
{status?.error && (
<p className="mt-2 text-sm" role="alert">
{status.error.message}
</p>
)}
<div className="mt-4 flex flex-wrap gap-2">
<Button
className="min-h-11"
disabled={locked}
onClick={() => void send({ ...envelope(), kind: 'catalog_sync' })}
variant="outline"
>
{copy.sync}
</Button>
<Button className="min-h-11" disabled={busy} onClick={() => void refresh()} variant="outline">
{copy.refresh}
</Button>
</div>
</>
)}
{pending && (
<div className="mt-4 space-y-2">
<p className="text-sm" role="alert">
{copy.unknown}
</p>
<Button className="min-h-11" disabled={busy} onClick={() => void send(pending)}>
{copy.reconcile}
</Button>
</div>
)}
{error && settings && (
<p className="mt-3 text-sm" role="alert">
{error}
</p>
)}
{notice && (
<p className="mt-3 text-sm" role="status">
{notice}
</p>
)}
</SettingsSection>
{settings && (
<SettingsSection icon={Cpu} title={copy.chats}>
{!views.length && <p className="text-sm text-muted-foreground">{copy.empty}</p>}
{views.map(view => {
const target = view.target_budgets?.find(item => item.target_id === view.limiting_target_id)
const hardLimit = target
? Math.min(
target.max_input_tokens ?? Number.MAX_SAFE_INTEGER,
target.context_window_tokens - target.output_cap_tokens - target.safety_margin_tokens
)
: null
const limit =
hardLimit === null
? null
: settings.settings.compression_enabled
? Math.min(settings.settings.threshold_tokens, hardLimit)
: hardLimit
return (
<article className="min-w-0 border-b py-4 last:border-b-0" key={view.view_handle}>
<h3 className="break-words text-sm font-medium">{view.label}</h3>
<p className="break-words text-sm text-muted-foreground">{view.current_model_label}</p>
<dl className="my-3 grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-2 text-sm">
<dt>{copy.current}</dt>
<dd className="break-words">
{number(target?.input_tokens ?? null)}
{target?.count_method === 'provider_estimate' ? ` · ${copy.estimated}` : ''}
{target?.count_method === 'verified_upper_bound' ? ` · ${copy.upperBound}` : ''}
</dd>
<dt>{copy.limit}</dt>
<dd className="break-words">{number(limit)}</dd>
<dt>{copy.summaries}</dt>
<dd>
{view.active_summary_count} · {number(view.summary_tokens)}{' '}
{view.summary_tokens !== null ? copy.tokens : ''}
{view.summary_token_measurement?.count_method === 'provider_estimate' ? ` · ${copy.estimated}` : ''}
{view.summary_token_measurement?.count_method === 'verified_upper_bound' ? ` · ${copy.upperBound}` : ''}
</dd>
</dl>
{view.last_operation_state && (
<p className="my-2 text-sm" role="status">
{copy.operation[view.last_operation_state]}
</p>
)}
{view.last_operation_error && (
<p className="my-2 text-sm" role="alert">
{view.last_operation_error.message}
</p>
)}
{view.compact_attempts.map(attempt => (
<p className="break-words text-sm text-muted-foreground" key={attempt.attempt_id}>
{attempt.member_label} · {copy.operation[attempt.state]}
</p>
))}
<Button
className="mt-2 min-h-11"
disabled={locked || !view.can_compact}
onClick={() =>
void send({
...envelope(),
kind: 'context_compact_view',
view_handle: view.view_handle,
expected_checkpoint_revision: view.checkpoint_revision
})
}
variant="outline"
>
{copy.compact}
</Button>
</article>
)
})}
</SettingsSection>
)}
</div>
</SettingsContent>
)
}