- Toggle (owner bug report): the ON disc sat flush against the pill's
right edge — translate-x-4.5 left only a 1px sliver; now translate-x-4
for a 2px inner inset both states (off: 3px left inset; on: 3px right
inset — measured live, symmetric).
- Toggle colors (owner ask): ON is a gray track (muted-foreground) with
a WHITE disc (foreground) — the full-white track was too loud; OFF
stays the muted track with the gray disc.
- Nav spacing (owner ask): gap-1 between the section nav buttons so the
hover highlights no longer touch.
- Role dropdowns (owner ask): the menus were clipped by the dialog's
overflow frame — they now portal to document.body with fixed
positioning against their trigger (AnchoredMenu), right-aligned,
repositioned on scroll/resize, so they flow over the whole screen
outside the settings dialog. Verified: a bottom-row menu extends
137px past the dialog's bottom edge; search + pick + Default-clear
work through the portal; Esc and inside-dialog clicks close the menu
but not the dialog.
- Thinking trigger (owner ask): fixed w-20 — the width the Default
state needs — so picking levels no longer shifts the row; the model
trigger takes the same width as a minimum (its values vary in length).
- Role hints (owner ask): ALWAYS visible and naming the actual default —
'Default: <model>'. The resolved per-role pins are not client-side
yet: master routed rolePins to the Platform stream (GET /api/models
extension, standalone commit); until that merge the rows read the
'config pin' fallback and light up on rebase. rolePins also lets the
thinking list use the pinned model's real supportedLevels when it's
in the catalog.
Also: staging repair commit 270db53 — the round-1 dialog rewiring
(sidebar row, chord, redirect, keyframes) was listed in 27d4f33's
message but never staged; committed as catch-up.
Live-verified on the worktree dev server. Gates: typecheck, lint,
701/701.
- Toggle knob (bug report): the knob span had no left anchor, so it sat
at its inline static position — right-edge in off, overflowing in on.
Now left-0 + the intended 2px/18px translate; verified live: 3px from
left when off, 1px right inset both states.
- Role model/thinking dropdowns (owner ask): the EXACT chat model
selector dropdown, not a lookalike — the fuzzy-search + arrow-walk +
Enter machine moved from model-picker.tsx into the shared
components/settings/model-menu.tsx (ModelMenuList/ThinkingMenuList),
ModelControl now renders the same bodies, and the agent roles open
them downward with a pinned Default row (clears the override). The
collapsed trigger keeps the thin select look (owner: 'the collapsed
look is good'). Esc claims and closes only the menu (the dialog
stays); a backdrop click is left to the dialog's own close.
- Sub-groups (owner ask): SettingsGroup — Models / Tools / System
prompts inside Agent, each with a separator + quiet label; section
headings more pronounced (text-sm, full-foreground).
- Graying (owner ask): graph-view render-panel convention — the WHOLE
row dims (opacity-40) and the control locks; the row owns the dim so
no double-opacity. Applied to the graph ladder, the mode row, and
prompt rows without an override file. The 'needs X on' prose is gone.
- Hints pruned (owner ask): mechanics-explaining lines removed — the
ladder mention, 'App-level behavior', 'Applied live as CSS
variables', per-row explanations. Only the keyboard storage note
stays, reworded 'Stored on this device' (desktop app — localStorage
is the Chromium profile in Application Support, not 'this browser').
Live-verified: knob geometry both states + PUT round-trip, menu
search/pick/clear through the record, thinking set/clear, Esc layering,
ladder dim 0.4 with siblings full, pruned phrases absent. Gates:
typecheck, lint, 701/701.
The dialog's content wrapper added px-6 py-5 ON TOP of SettingsSection's
own padding (48px horizontal, 81px top — measured live). The wrapper is
now bare; the section component owns its rhythm, and the loading/error
states carry their own padding. Section box now spans the scroll column
(61px from the dialog top = the 60px header band), text inset 24px.
Gates: typecheck, lint, 701/701.
Owner decisions this wave: a centered modal over everything (not a page
— a settings column in the main area next to the session list would read
as two sidebars), left section nav in the dialog, modal-only (the
/settings URL redirects to /chat/new), light mode deferred to its own
wave (the mode control stays honestly disabled). An actual OS window is
an Electron-round feature — the sections are surface-agnostic (API +
localStorage only), so the shell re-mounts there with near-zero churn.
- components/settings/settings-dialog.tsx: SettingsDialogProvider at
the AppShell level (portal-rendered dialog, z-50 over the dock/graph
chrome). ~920×620 centered frame, 60px header with the quiet CMU
title + ✕, w-44 text-only nav (session-list mono language, active =
bg-accent, no icons), one scrolling content column mounting one
section at a time. Escape / backdrop / ✕ all close. 150ms fade+2%
settle scale (settings-pop), reduced-motion honored. The record +
single write seam moved here from the thin page: one record instance
per dialog, section switches never refetch, appearance writes still
restyle the app LIVE behind the dialog.
- Entry points rewired: the sidebar Settings row is a button calling
open(); the mod+, chord opens the dialog instead of routing.
- app/settings/page.tsx: redirect to /chat/new (nothing links into a
dead page).
- SettingsDialogBody exported without the portal — the SSR-testable
frame (portals render nothing under renderToStaticMarkup).
Live-verified: exact centering, all three close paths, chord open,
appearance prose-size restyles the chat behind the open dialog
(16→20px), section switching retains the record, keyboard section
renders all 21 rows, no console errors. Gates: typecheck, lint,
701/701 (5 new dialog tests), build.
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.