/** Shared settings context, block draft hook, form controls, Source schema field renderer, and configuration history. */ import { cloneElement, createContext, useContext, useEffect, useId, useLayoutEffect, useRef, useState, type ReactElement, type ReactNode } from 'react' import type { ApiResponse } from '../app/api.ts' import type { FieldSpec, Localized } from '../sources/kind.ts' import { usePoll, type HistoryView, type Settings, type SettingsView, type SourceView } from './api.ts' import { Button, DisclosureRow, Icon, InfoTip, Input, SegmentedControl, Switch } from './controls.tsx' import { useDraft } from './draft.tsx' import { useAction, usePanel, usePanelData } from './provider.tsx' import { settingsRoute } from './route.ts' import { errorText, localized, valueLabel, when, type T } from './text.ts' import { Hint, LoadState } from './widgets.tsx' /** Parse a numeric input string, returning `null` when empty so the server rejects blank required numbers instead of coercing to 0. */ export const numberOf = (text: string): number | null => text.trim() ? Number(text) : null interface SettingsData { settings: SettingsView; reload: () => Promise } const SettingsContext = createContext(undefined) export function useSettings(): SettingsData { const value = useContext(SettingsContext) if (!value) throw new Error('A settings block rendered outside the settings') return value } export function SettingsProvider({ children }: { children: ReactNode }) { const { call } = usePanel() const { restarts } = usePanelData() const { data, error, reload } = usePoll(() => call('GET settings'), [call, restarts]) if (!data) return return {error && } {children} } export function useBlockDraft(block: string, saved: V, revision: number, toValue: (draft: V) => unknown = draft => draft) { const { t, call, notify } = usePanel() const { state } = usePanelData() const { reload } = useSettings() return useDraft({ key: `dsh-superagent/draft/${state?.masterId ?? ''}/${block}`, saved, revision, reload, save: async (value, from) => { const result = await call('PUT settings/:block', { block, body: { value: toValue(value), revision: from } }) notify(t(result.restarting ? 'savedRestarting' : 'saved')) return result } }) } /** Fieldset wrapper that restores focus to the previously active input after a save completes and re-enables the form. */ export function Fields({ disabled, children }: { disabled: boolean; children: ReactNode }) { const set = useRef(null) const focused = useRef(null) useLayoutEffect(() => { const active = document.activeElement if (disabled) focused.current = active instanceof HTMLElement && set.current?.contains(active) ? active : null else if (focused.current && (!active || active === document.body)) { focused.current.focus({ preventScroll: true }); focused.current = null } }, [disabled]) return
{children}
} export function Field({ label, hint, note, children }: { label: string; hint?: string | undefined; note?: ReactNode; children: ReactElement<{ id?: string }> }) { const id = useId() return
{hint && }{cloneElement(children, { id })}{note && {note}}
} function SwitchRow({ label, hint, checked, onChange }: { label: string; hint?: string | undefined; checked: boolean; onChange: (next: boolean) => void }) { return
{label}{hint && }
} type RoleModel = Settings['eval']['model'] export function ModelField({ id, value, onChange }: { id: string; value: RoleModel; onChange: (next: RoleModel) => void }) { const { t, call } = usePanel() const [catalog, setCatalog] = useState>() const [error, setError] = useState() useEffect(() => { call('GET models').then(setCatalog, failure => setError(errorText(failure))) }, [call]) const current = { provider: value?.provider ?? '', model: value?.model ?? '', effort: value?.reasoningEffort ?? '' } const set = (next: Partial) => { const { provider, model, effort } = { ...current, ...next } onChange(provider.trim() || model.trim() || effort.trim() ? { provider, model, ...effort ? { reasoningEffort: effort } : {} } : null) } const models = catalog?.groups.find(group => group.id === current.provider.trim())?.models ?? [] const fallback = catalog?.default const input = (name: keyof typeof current, label: string, placeholder: string) => set({ [name]: event.target.value })} /> return
{fallback ? t('defaultModel', { model: `${fallback.provider} / ${fallback.model}${fallback.reasoningEffort ? ` · ${fallback.reasoningEffort}` : ''}` }) : t('defaultModelUnavailable')} {error &&

{t('modelCatalogError', { error })}

}
{input('provider', t('modelProvider'), t('modelInherit'))}{input('model', t('modelName'), t('modelInherit'))}{input('effort', t('modelEffort'), t('modelEffortDefault'))}
{catalog?.groups.map(group => )} {models.map(model => )} {(models.find(model => model.id === current.model.trim())?.reasoning?.efforts ?? []).map(effort => )}
} type HistoryChange = NonNullable[number] function changeText(t: T, change: HistoryChange, sources: SourceView[]): string { const source = sources.find(item => item.id === change.source) const field = (change.part ? source?.policy?.fields : source?.fields)?.find(item => item.key === change.field) const shown = (value: unknown) => value === undefined || value === null || value === '' ? t('unsetValue') : typeof value === 'boolean' ? t(value ? 'yes' : 'no') : change.code === 'mode' ? valueLabel(t, 'mode', String(value)) : String(value) return t(`change.${change.code}`, { items: (change.items ?? []).map(item => source?.labels[item] ?? item).join(t('listSeparator')), from: shown(change.from), to: shown(change.to), name: change.name ?? '', source: source ? localized(t, source.title) : change.source ?? '', field: field ? localized(t, field.label) : change.field ?? '' }) } /** * Collapsible revision history for the specified settings blocks, optionally including all Policy-changing saves. * Revisions saved before per-block tracking existed have no `block` and appear under `policy`. */ export function ConfigHistory({ blocks, versions, focus }: { blocks: string[]; versions?: boolean; focus?: string }) { const { t, call } = usePanel() const { settings } = useSettings() const { sources } = settings const [open, setOpen] = useState(!!focus) const [data, setData] = useState() const [error, setError] = useState() const revisions = JSON.stringify([Object.values(settings.blocks).map(block => block.revision), sources.map(source => source.revision)]) useEffect(() => { if (open) call('GET policy/history').then(setData, failure => setError(errorText(failure))) }, [open, call, revisions]) const listed = (block: string | undefined) => block ? blocks.some(item => item === block || (item.endsWith(':') && block.startsWith(item))) : blocks.includes('policy') const items = data?.items.filter(item => (versions && item.policyChanged) || listed(item.block)) ?? [] // Match the oldest save that produced the focused Policy version (where that version originated). const target = focus ? items.filter(item => item.policyChanged && item.policyVersion.startsWith(focus)).at(-1) : undefined const scroll = (element: HTMLLIElement | null) => { if (element && !element.dataset.scrolled) { element.dataset.scrolled = '1'; element.scrollIntoView?.({ block: 'center' }) } } return } title={t('configHistory')} open={open} onToggle={() => setOpen(!open)}>
{error &&

{error}

} {data && <> {t('historyIntro')} {focus && !target && {t('policyVersionUnrecorded', { version: focus.slice(0, 8) })}} {items.length ?
    {items.map(item =>
  • {t('historyItem', { revision: item.revision, source: valueLabel(t, 'historySource', item.source) })}
    {when(item.savedAt, t('lang'))}{item.policyChanged && <> · {t('historyPolicyChanged', { version: item.policyVersion.slice(0, 8) })}}
    {item.changes === null ? {t('historyNoBaseline')} : item.changes.length ?
      {item.changes.map((change, index) =>
    • {changeText(t, change, sources)}
    • )}
    : {t('historyNoChanges')}} {item.notified.length > 0 && {t('historyNotified', { count: item.notified.length, titles: item.notified.map(ticket => ticket.title).join(t('listSeparator')) })}}
  • )}
: {t('historyEmpty')}} }
} type Row = Record type Column = Extract['columns'][number] const ROWS_SHOWN = 6 const byName = ([, a]: [string, string], [, b]: [string, string]) => a.localeCompare(b) function RowsField({ field, value, saved, onChange, labels, refreshLabels }: { field: Extract; value: unknown; saved: unknown; onChange: (next: unknown) => void; labels: Record refreshLabels: (() => Promise) | undefined }) { const { t, notify } = usePanel() const [open, setOpen] = useState(false) const refresh = useAction(async () => { try { const added = await refreshLabels!() notify(added.length ? t('labelsAdded', { names: added.join(t('listSeparator')) }) : t('labelsCurrent')) } catch (failure) { notify(t('labelsFailed', { error: errorText(failure) })) } }) const label = localized(t, field.label), hint = localized(t, field.hint) const rows = Array.isArray(value) ? value as Row[] : [] const set = (index: number, row: Row) => onChange(rows.map((item, position) => position === index ? row : item)) const key = field.columns[0]! const savedKeys = new Set((Array.isArray(saved) ? saved as Row[] : []).map(row => row[key.key])) const choose = key.type === 'text' && key.choose && Object.keys(labels).length ? localized(t, key.choose) : '' // Exclude options already selected in another row. const choices = (id: string) => Object.entries(labels).filter(([option]) => option === id || !rows.some(other => other[key.key] === option)).sort(byName) const blank = Object.fromEntries(field.columns.map(column => [column.key, column.type === 'select' ? column.options[0]!.value : ''])) // Summarize a row by its second column only when that column is free text (e.g., a display name), not a select option. const named = field.columns[1]?.type === 'text' ? field.columns[1] : undefined const rowName = (row: Row) => (named && row[named.key]) || labels[row[key.key] ?? ''] || row[key.key] || '' const cell = (row: Row, index: number, column: Column) => { const id = row[key.key] ?? '', change = (next: string) => set(index, { ...row, [column.key]: next }) if (column === key && (savedKeys.has(id) || (!choose && labels[id]))) { return {labels[id] ?? id} } if (column === key && choose) { return } if (column.type === 'select') { return } return change(event.target.value)} /> } return
{label}{hint && }{rows.length || ''} {key.type === 'text' && key.refresh && refreshLabels && }
{rows.length > ROWS_SHOWN && } {(rows.length <= ROWS_SHOWN || open) &&
    {rows.map((row, index) =>
  • {field.columns.map(column => cell(row, index, column))}
  • )}
}
} export function SourceField({ field, value, saved, fact, onChange, labels, refreshLabels }: { field: FieldSpec; value: unknown; saved: unknown; fact: Localized | string | undefined; onChange: (next: unknown) => void labels: Record refreshLabels?: () => Promise }) { const { t, navigate } = usePanel() const label = localized(t, field.label), hint = localized(t, field.hint) switch (field.type) { case 'switch': return case 'select': return ({ value: option.value, label: localized(t, option.label) }))} onChange={onChange} /> case 'number': return onChange(event.target.value === '' ? '' : Number(event.target.value))} /> case 'text': return {field.choose && Object.keys(labels).length ? : onChange(event.target.value)} />} case 'fact': return
{label}{hint && } {localized(t, fact) || t('empty')} {field.settings && }
case 'rows': return } }