- MenuShell: after placement, focus lands in the search when the menu has
one, else the body (tabIndex -1) — the lists' mount-time focus ran during
the hidden measuring pass and silently failed, so no dropdown ever opened
focused. The body owns the arrow machine for searchless menus:
ArrowDown/Up cycle options with wrap (first/last entry from the body,
native-select convention), Home/End, Enter activates, preventDefault so
arrows never scroll the page.
- Appearance: the CSS-var applier only ever setProperty'd — a later write
omitting a var left the stale inline value forever (empty background
never reset, serif toggle-back looked dead). appliedNamesRef tracks what
was set; staleVarNames (pure, exported) returns what to removeProperty.
- CommitInput: controlled — after a commit the field snaps to the stored
record, so the clamp on a -5 prose size is visible (12), Esc reverts.
- Keys: Add-provider dropdown closes on pick; the paste row is inline
(label | input | Save | Cancel); existing-row editors go inline with an
explicit Cancel beside Save/Delete — folding is visible, not Esc-only.
- Graph context: readout is content-sized (tabular-nums) — no fixed-width
dead gap when "off"; the dictation strip fills whatever space remains.
Gates: tsc clean, 866/866 tests (new staleVarNames suite), lint 0, build ok.
Live-verified via Playwright: dropdown focus+arrow cycles+wrap+Enter pick,
background set→empty reset, serif round-trip, visible clamp to 12, provider
pick closes menu, inline editor + Cancel + Esc fold, slider width 270→277px.
The validation surface for the settings foundation — deliberately thin,
real read/write against the delta record, polish is a later wave.
Mount (owner decisions): dedicated page, single scroll; entry = sidebar
bottom row + ⌘/Ctrl+, (open-settings in the registry); chat defaults NOT
shown (they live on /chat/new); mode dark-only (light palette = design
wave); final tool list = research/demo/visual/pdf (master-adjudicated).
Sections (PUT /api/settings/[section], replace-whole per section):
- General: auto-update toggle.
- Agent: six background-role model+thinking selects (Default = config
pin; a stored id stays VISIBLE even when the catalog hasn't loaded or
dropped it — a select showing Default over a live override would lie),
the four tool toggles, prompt override switches (file-gated) with
source/override text editors over the segment routes.
- Appearance: applied LIVE as runtime CSS variables by an
AppearanceProvider at the root layout — proseFontSize → the one
reading-size var, fontFamily → --font-prose, background → --background
+ the derived raised/accent/edge palette (dark curve, today's exact
spacing: +8/+22/+47 per channel, clamped), sessionNameSerif →
--font-title (titles flip with ZERO chat.tsx edits), headingWeight →
new --lh-heading-weight token, bold/italic colors → the lh-ink vars.
Mode renders honestly DISABLED.
- Graph: the ladder grays a rung when any ANCESTOR is effectively off
(stored values persist, resolve inert — switch shows stored, graying
shows effective).
- Keyboard: view/rebind/reset over the registry; overrides persist in
lh:keymap and apply INSTANTLY (dispatcher reads per keydown); bare
modifier presses are ignored while armed; displayOnly actions list but
never rebind.
First-boot fix: {settings: null} is no-record, not fetch-failure.
CROSS-LANE (flagged for master): src/core/keyboard.ts gained the
open-settings action/binding and exports saveOverrides/loadOverrides
(one-liners; no existing pins); app/layout.tsx mounts the provider.
Live-verified: structure, record writes (general/tools/graph/appearance),
prose-size applying live (16→20px), rebind + chord navigation from
another page, ladder graying, no console errors. Gates: typecheck,
lint, 689/689 (28 new), build.