- settings: compaction.thresholdTokens row on the Agent tab (owner pick)
in a Context group — CommitInput mirroring the graph budget row, hint
resolved through the SAME core resolver the trigger reads; PUT rejects
below the 32k floor with the anti-churn rationale; own section
(compaction) in SETTINGS_SECTIONS + the settings-signal union
- view fold: one marker → two dividers — the minimal MOMENT divider at
the marker (retrying-message register, no rule lines) and the longer
CUT divider emitted immediately before the first kept message
(phase-divider geometry, chip carries '· Nk tok'); both carry the full
summary payload; full history stays visible to the user
- components/compaction-divider.tsx: the dividers + summary dialog
(pure exported body for SSR pins, the delete-dialog pattern; Esc +
Close dismiss, backdrop does not)
- chat.tsx: compaction-changed SSE flips the running state — composer
canSend, onSend/doSend/doSteerOrSend, and both docks' submit + hint
('compacting session…') gate on it (the server's registry mutex is the
structural gate; this is the affordance); dialog mounts beside the
abort dialog
- pins: settings-body (4), view fold (2), divider SSR (4)
Sycophancy mitigation as a structural commitment: the model writes its
full correctAnswer (plus explanation, in that order) BEFORE the learner
sees the question; the commitment rides the persisted call args, is
revealed on submit, and grades his free text in the model's next reply —
the client never grades free recall.
- tools.explain setting (segmented 'Quizzing — Multiple choice | Free
recall' in Agent settings' Tools group): free arms explain and drops
quiz for BOTH agents (prober variant carries no nodeId, same pattern
as the probe quiz); mechanics keys never pass filterEnabledTools —
quiz stays unwritable (the 2026-09-19 wedge ruling stands)
- doctrine lives pi-natively in the tool descriptions (owner ruling —
no conditional system-prompt chunks): explain carries the commitment
rule; the MC option-building craft moved out of the prompt into the
quiz description; shared/20-question-tools.md slims to the
quiz-agnostic core
- display seams: parseExplainInput (LaTeX-debris restoration included),
dock text-mode with an 'I don't know' row (kbd row 0), AnsweredCard
reveal (The answer / Why, no verdict chips), [Explain answer] format
- phases/plan seams recognize both quizzing tools' nodeId tags; explain
answers deliberately feed no ledger evidence (no client grading to
key on — a future ruling can parse the model's grade)
1049/1049 tests, typecheck/lint/build green.
Owner-confirmed semantics (verified in composeInstructions first: switch
ON = the user's override text is what the agent reads; OFF = the default
composes, the saved file stays on disk unused). Four refinements:
1. The small text under a file name states WHICH VERSION IS IN USE:
'Your version is in use' (PROMPT_IN_USE_HINT, pinned) when the
switch is on; NOTHING when the default composes — even with a saved
override sitting on disk. The old presence-based 'Your version is
saved' (shown whenever a file existed, regardless of the switch)
dies; the switch's meaning becomes legible through the line it
visibly controls.
2. Save is clickable only when there's something to save: grayed and
inert while the draft equals what's on disk (including the
untouched editor just opened), full-contrast the moment the text
differs. The always-active same-color button was the 'doesn't feel
like it does anything' half of the report.
3. Saving auto-turns the switch ON — writing an override and putting
it to work collapse into one action (a save with the switch off was
the did-something-changed-nothing trap). Fires whenever the record
genuinely has the path off, so a re-save after toggling off re-on's
too. Order is the safe one (file first, then the switch the
validation requires it).
4. A quiet inline 'Saved' / 'Deleted' confirmation rides next to the
buttons (the old status line echoed the file path at the section's
bottom — production-copy violation and easy to miss).
Plus the mirror-symmetry the auto-on needs: deleting an override while
its switch is on now composes the switch OFF in the same action — not
just redundant with Platform's 643a693 server-side clear: the dialog's
record mirror is what composes the next section write, so a stale ON
would silently defeat the auto-on after a save-following-delete (the
mirror would think the switch is already on and skip the write the
server record needs).
Live-verified on :3001: untouched editor grays Save; typing activates
it; reverting re-grays it; save-with-switch-off lands toggle ON + hint
+ 'Saved' + Save re-grayed in one click; toggle-off drops the hint
with the file retained on disk; re-save while off re-on's; delete
while on clears switch + file + hint with record left clean and a
subsequent agent-section write landing 200.
The prompts area's five owner-reported symptoms, root causes found
in code and verified live before fixing:
1. STATE ECHO — the toggle's truth (SegmentInfo.enabled) derives
from the settings record's agent switches, but the segment view
was read ONCE per section mount: the switch write landed (PUT
/api/settings/agent) yet the toggle painted its old state until
a remount re-fetched. Root-cause fix in the settings-saved
family: the section now re-reads /api/settings/prompts when the
'lh:settings-saved' signal lands for the agent section — the
reader the signal law was missing. Toggle flips verify at ~40ms,
no navigation.
2. DUPLICATION — the server contract returns the shared segments
in BOTH per-agent lists; the UI rendered both lists back to
back, so every shared chunk appeared twice (18 rows, 5 dupes).
sectionedSegments (pure, exported, pinned) dedupes: the agents'
own chunks plus the shared chunks ONCE.
3. ORDERING — Teacher first, then Prober, then Shared last
(owner-ruled), each section headed, chunk order within a section
preserved from assembly order (the server lists ARE assembly
order: base, shared sorted, own numeric-prefix sorted).
4. GRAYING — the whole-row dim died; only the TOGGLE dims now when
there is no override file (a wrapper span on the control; the
file name is always full-contrast).
5. DESCRIPTION LOGIC — the 'No changes yet' line died. No
description when there's no override (the empty state is clean:
toggle + name); 'Your version is saved' (PROMPT_SAVED_HINT,
pinned) appears ONLY when an override exists. The redundant
'Shared base — appears in both agents' editor note died too —
the Shared section header carries 'Used by both agents'.
Also: saveSegment/deleteSegment reuse the readPrompts callback
(keeps last-good on a failed re-read; the mount effect owns the
loading state).
Live-verified on :3001: sections in the ruled order with 13 unique
rows, clean-row and overridden-row states per the rulings, toggle
flip instant (view + record agree), save/delete round-trips, and a
shared-segment save + toggle through the single Shared control.
Open finding (NOT fixed here — needs a ruling): an ON switch whose
override file is later deleted orphans the record, and the
agent-section write validation 400s every subsequent write (an
ON switch must have an existing file) while the UI's disabled
toggle cannot reach the OFF escape hatch the validation
anticipates. Repro: Save + switch on, Delete the override, flip
any tool. Server validation + row semantics are Platform's /
owner's call.
Owner-approved full pass (tooltip wave, 2026-09-25). Mechanism: icon/pill
controls ride the styled Radix tooltip via a new Hint composition
(components/ui/hint.tsx) — the shared primitive adopted as-is, no
restyling; the shortcut appends as an em-dash suffix off the LIVE registry
(a rebind in Settings → Keyboard changes the suffix): "Dictate — ⌘D".
Surface-owned keys (Esc closes, Enter submits) ride a raw binding prop.
Native title= stays only for truncation-reveals (dock spans, the rename
field) and menu-interior abbreviations (MenuShell untouched). One
em-dash pattern; per-state truth on the send square (busy = "Stop the
reply — Esc", idle = "Send — ↵"), Enter on the plan verdict's accept,
"Stop dictation (Esc)" kept verbatim per the owner's flag.
Copy: the dev-speak cull — no pipeline/prober/tier/wave vocabulary
anywhere a user reads. New pins in tests/tooltip-mechanism.test.tsx
(hintText composition, asChild no-wrapper-DOM, the auto-update hint
constant); dock truncation titles pinned native; graph-chrome + settings
pins updated to the new copy.
Live-verified on :3001 — 12.6ms hover appearance (no native 1s lag);
glyphs render on every suffixed surface (⌘D, ⌘K, ⌘⇧F, ⌘,, ⌘B, F2, ↵,
Esc); per-page probes confirm Settings entry, Home logo, session search,
Start session, Close sidebar, model + thinking pills, stats summary,
rename title, plan chip, coverage ring, send square, cost button, node
search (⌘F), graph gear, settings close (Close — Esc), keyboard section
"Fixed — part of this screen", auto-update hint, light-mode "Coming
soon", the three slot rosters, graph-tools hint. Esc still closes the
settings dialog.
Part 1 — the surface:
- Settings ▸ Agent ▸ Tools gains the prober GRAPH-TOOLS row as its FIRST
row (owner ruling: in the Tools section, not the tool-toggles list —
its subject is the map, its mechanism is agent capability). Writes
agent.graphTools {enabled} (absent = on, the toggle discipline) via
the section PUT; composeGraphTools pure seam. Its own key in the agent
contract — never in the tools map.
- The row carries the search-row hint discipline: "Connects prior
sessions' knowledge into new ones — the prober reads the knowledge
map" (owner wording). The honest interplay composes both: when
graph.enabled is off the row dims + disables with a "Graph is
disabled" hint (the effective gate itself is server-side, per-turn
fresh). AgentSection reads the graph section (record.graph, passed by
the dialog) — read-only.
- Cluster labels wrap: wave 7 made labels session names and FILE PATHS,
which the old single-line fillText could not bound. wrapRegionLabel
(pure, exported): word-wrap first, char-split long paths (the
node-label discipline), capped at 3 lines, overflow folded into an
ellipsized last line (owner: wrap, cap 2–3, ellipsize). Pre-wrapped
once per geometry build at the SCREEN-fixed size (viewer-relative
labels — the wrap never goes stale mid-camera-move), drawn centered
line-by-line at the centroid. Deleted-session clusters keep their
hull-only honest silence (label null → no lines).
- The orphan sweep: StatsBar's leftSlot prop is dead (its one consumer,
the knowledge-context slider, died in c690181) — removed, and pre-chat
views now render NOTHING on the left (owner ruling this round: a
"— · —" summary over an empty popover is pure noise) — the dictation
middle just widens. Stale knowledge-context comments swept in
chat.tsx (adoption-window + activity-nudge docs), activity-dock.tsx
(useActivity ground rules), dictation.tsx (×2 adaptive-meter docs),
home.tsx (pre-chat dock comment).
Part 2 — Teacher's c690181 review: clean. Imports, KIND_ICON/KIND_LABEL
entries, render branches and event seams all excised whole; the probe-
handoff branch re-indented intact and merge-adopted; tests updated in
lockstep (merge-adopted, record-adapter, render-new-session retired).
Platform's 4b60e0f roster-string fixes (labeler dropped from both
SLOT_META blocks) reviewed — consistent across agent-section +
setup-dialog.
Gates: tsc clean, 854/854 (8 new: wrapRegionLabel cases, graphTools
SSR rows ×3; stats-bar + ladder fixtures updated to the new rulings),
lint 0, build ok. Live on :3001: graphTools row first-in-group with
hint, off→{enabled:false}→on round-trip, graph-off dimming + hint +
disabled toggle, pre-chat rows summary-free, chat view keeps its
summary, long-title cluster label wrapped without console errors
(visual read unavailable this session — pure wrap is unit-pinned).
Part 1 — the ledger + remnants interlude (master-dispatched, owner ruling
2026-09-23: nothing configures the graph beyond chat models):
- The embedding ledger surface is gone: readEmbeddings/writeEmbeddings
ports, the EmbeddingsLedger contract, EMBEDDINGS_FILE (config, store
impls, graphRevision source list), and graph-deletion's vector-pruning
step. On-disk data files stay untouched (inert history).
- The extraction embedder seam dies wholesale: ExtractionPipelineDeps
drops the embedder dep (chat-handler + backfill/import-notes stubs go
with it), the 'embedding' phase leaves ExtractionPhase, the usage
capture/persist helpers die, and stages.embedding leaves
ExtractionRunUsage (historical records keep their shape — reads are
lenient). Extraction is two stages again, like the local-embedder era.
- Consumer-less remnants: latestQuizOutcomes + QUIZ_EVENT_KINDS,
clusterContentId, matchClusterLabelings + Community + ClusterLabeling,
the readClusterLabels/appendClusterLabel port seams + store impls +
LEDGER_FILES.clusterLabels. NamingRunUsage/readNamingRuns STAY
(standing ruling — the cost pill renders historical naming spend);
clusterHue + MIN_CLUSTER_SIZE stay (live).
- The 'labeler' role dies: BACKGROUND_MODEL_ROLES, ROLE_SLOT, ModelRole,
the catalog pin projections, and the UI roster strings. RolePins keys
shrink with the union.
The notes birth fallback (owner-approved 2026-09-23, surfaced by the
interlude's live smoke: production notes imports create provenance-EMPTY
nodes (importNotes passes messages: null — the file's anchor is the
mapping line), so the provenance-only birth rule left every imported
file unclustered and invisible to the prober's session list, against the
locked file-birth ruling. A provenance-empty node now births under its
FIRST mapping line's chatId (ledger order); only notes-born nodes ever
hit the fallback (chat nodes always carry provenance; a re-encountered
node's link-append re-opens the provenance rule). Deletion-safe: mapping
lines of deleted chats are stripped alongside their provenance.
Live-verified on port 3004: a fresh notes import clusters under its
src_ id with the file path as the label, end to end with no embedder.
845/845 tests, typecheck/lint/build green.
EOF
)
- thinking-default.ts: defaultOrderLevel (the ordering's first supported,
the client twin of the server's defaultThinkingLevel) + populateLevel
(an explicit level always rides; the WRITE clamps server-side, bd68486,
and the cross-surface signal re-reads the clamped truth; only absence
populates from the ordering) + THINKING_LABEL shared.
- Setup dialog (item 5): per-slot thinking selectors pre-populated with
the default-order level per picked model, editable; disabled until the
model pick; prefillLevelFromPin keeps a stored level as-is and populates
legacy pins from the ordering.
- Composer + agent section (item 6): fresh model picks always write a
level: the stored one rides, absence populates; the unset state can
never re-enter. Item 4: records with a model but no stored level display
the RESOLVED level (ordering's first supported) instead of a dash, in
the composer pill and the settings slot triggers/menus.
- Item 7 verified live end-to-end: an explicit level rides into the PUT,
the server clamps, the signal re-reads, the pill shows the clamped truth.
- lh:settings-saved CustomEvent (the lh:chat-renamed pattern, owner
ruling): every writer (composer save, SetupDialog save, the settings
write seam — one dispatch point carrying the section) notifies after a
successful PUT; every reader re-reads its own seam: ModelControl's
record read, the Agent section's slots/catalog, and chat.tsx's
configured state (clearing slots mid-session now surfaces the setup
CTA without a reload). Self-notifications re-read idempotently.
- MenuShell now announces lh:menu-open/lh:menu-close (still placement-
only); the settings dialog's content column locks (overflow-hidden,
count-based) while any menu is open — wheel-scrolling with a dropdown
down is dead; the re-place-on-scroll fallback stays for un-lockable
surfaces.
- The settings thinking menus' 'Default' noneRow is retired (owner:
the unset state can't re-enter once chunk 2's auto-populate lands; no
unpin is needed).
- Live-caught regression from the MenuShell migration fixed: the
composer's outside-click handler never learned the portaled menu body
isn't a container descendant — menu-row pointerdowns counted as
outside and ATE THE PICK (composer picks have been dead for pointer
users since 221c99d); rows now carve out via [data-lh-menu], the
settings sections' discipline.
- Item 4 (resolved-level display for legacy records) deferred to chunk 2
per owner ruling — the payload omits the level for legacy no-level
records and DEFAULT_THINKING_ORDER was server-only (now in the core
barrel via bd68486, exactly the seam chunk 2 needs).
- existing sessions: a send that lands the 503 models_not_configured envelope
renders as the SETUP FLOW, never the error banner — the record refetch drops
the refused orphan (the gate refuses before the write-ahead persist), the
draft goes home to the composer, and Save auto-replays it (owner ruling);
parseModelsRefusal reads the envelope from the transport's verbatim error
text (exported pure, tested)
- persistent 'Set up your models' CTA above the chat composer while
slotsConfigured is false (server-read seed via app/chat/[id]/page.tsx) —
legacy per-chat overrides may still send with the pill present; the server
gate stays the truth
- SetupDialog prefill + 'needed' marks (owner ruling): pinned tiers open with
their pick (matched in the federated catalog — lapsed keys don't prefill),
unpinned tiers carry the mark; Save still requires all three;
prefillFromSlots exported pure, tested
- refused jobs read as INFO: dispatch card shows a quiet 'needs model setup'
line + 'Set up models' deep-link (ChatActionsContext.openSetup); dock blobs'
activity reads 'needs model setup' instead of 'failed'; real failures keep
the red tone; isModelsRefusal added to the envelope's home module
- agent-section ModelsBody.slots type honest to the nullable contract
Item 1 — no Default in the slot selectors (owner, fix wave): a
"default" model is not a thing in this app. Picking Default used to
delete the slot and re-arm the first-run gate — incoherent. Now the
model menus carry NO clear row at all (a slot holds a pick or reads
"—"), the thinking menus carry a "—" row (menu-none) that clears only
the thinking pin, and the hint line is the roster only — the resolved
factory pin is never named. Master's amendment confirmed: the
factory-pin hint is SUPERSEDED entirely (Platform removes the pins).
Item 2 — the composer control IS the strong slot (owner: "one and the
same"): ModelControl (model-picker.tsx) reads the raw models section
(GET /api/settings) and the federated catalog, and writes
PUT /api/settings/models via composeSlotModel — the same seam the
settings Agent section and SetupDialog use. Props retired: chatId,
settings, onSaved, fallbackModelId, noDefaultModel — the control is
self-sufficient and identical on every surface (composer, home dock,
/chat/new). The per-chat model PUT is gone from the UI; legacy
per-chat overrides keep resolving server-side. StatsBar/DockOverlay
plumbing slimmed accordingly; chat.tsx drops its per-chat settings
state and initialSettings prop.
Item 3 — the composer catalog cache no longer freezes per page load:
fetchCatalog(force) invalidates, and every model-menu open revalidates
in the background (stale-while-revalidate — cached list shows
instantly, the fresh federation swaps in). Owner's repro verified live:
a key saved mid-session widens the menu without a reload.
Item 4 — stale doc comments fixed: model-catalog.ts's "OpenRouter
catalog"/"multi-provider future" (Platform's file — mechanical,
flagged for review) and model-picker's retired "config-pinned"/
per-chat-PUT narrative.
Live-verified on 3001: unpicked slots read "—" everywhere; model
menus have no clear row; the "—" thinking row clears the pin but
keeps the model; composer pick writes strong and preserves cheap/
vision siblings; thinking pill writes strong.thinkingLevel; groq key
added mid-session appears in the open menu without reload. Caught in
live verification: the record fetch was mapping the WHOLE settings
record into the models section (picks never wrote) — fixed to read
body.settings.models.
Gates: typecheck, lint, 807/807, build.
The teacher registry drops the research tool when no search engine is
live (chat-handler's searchAvailable, 2026-09-20) — the settings row
now grays on the SAME /api/search signal, so the row and the actual
registry availability can never disagree (Teacher-stream flag,
2026-09-22, routed by master).
- researchToolUnavailable (pure): research grays exactly when the live
status says unavailable; a loading/failed fetch grays nothing; the
other tools never touch the search port.
- The row: whole-row dim (graph-view graying) + disabled toggle + the
status reason as the hint — the same line the search-engine row
under it speaks from.
Two live-caught fixes while here:
- The reason line refreshed IN the pick handler raced the section PUT
(the hint could contradict the trigger it sits under — caught with a
Brave pin: trigger 'Brave', hint still 'tavily (auto)'). The refresh
now keys on the agent prop's engine pin, so it reads post-write
state.
- The engine menu options render from a map without React keys
(console warning) — keyed.
Live-verified on 3001: no engine key → research dimmed with reason,
siblings untouched; Tavily key saved + section reopened → row alive
('search engine: tavily (auto)'); Brave pinned without its key → both
rows gray with the honest missing-key reason; Automatic restored.
Gates: typecheck, lint, 803/803, build.
Chunk B (owner-designed, model-slots wave) on the merged engine surface
(main 1d66a17):
AGENT SECTION — SLOTS ONLY (owner ruling)
- The six per-role model rows are GONE; three slot rows instead:
Strong (Prober + Teacher), Cheap (five workhorse roles), Vision
(visual maker + PDF reader), roster + resolved-default in every hint.
- Slot dropdowns share the app's menu language over the FEDERATED
catalog (/api/models = keyed providers only, fresh per call — saving
a key in Keys immediately widens the picker); the vision slot
filters to image-capable models (Model.input); the menu's highlight
now matches provider+id (ids can collide across providers).
- Slots write the new models section (PUT /api/settings/models) through
the dialog's write seam; per-role overrides stay schema-only.
- ModelMenuList rows read 'provider · id' — federation makes the
provider bookkeeping-relevant. Usage stamps render provider-qualified
(prettyModel(qualified): 'Glm 5.3 Flash · openrouter') in the stats
bar's turn lines.
SEARCH ENGINE (search wave: no default, owner)
- A row in Tools: Tavily/Brave via the shared-menu look, Automatic =
the pin removed (auto-activation). The live reason line comes from
/api/search; no engine key at all grays the whole row + disables the
trigger (a keyless choice would only block auto-activation).
FIRST-RUN SETUP FLOW (owner design A: composer CTA + send interception)
- Chats cannot start until all three slots are picked — no defaults
for 33 providers (slotsConfigured: all three slot models required).
- The graph view's new-session dock (home) shows a 'Set up your models'
CTA while unconfigured; submitting a topic opens the SetupDialog WITH
THE DRAFT PRESERVED; Save stores the models section and the queued
message sends. Same interception on /chat/new sends.
- STAGED BY DEPENDENCY (owner: keys first, models after): no keyed
provider → stage 1 deep-links the settings dialog on Keys (open now
takes a section); a saved key reopens at stage 2 on the next submit.
- Stage 2: Strong → Cheap → Vision pickers (federated, vision-filtered,
all three required). Closable without configuring — browsing stays
possible; the gate is the send, not the dialog. Layered Esc: an open
slot menu eats Esc before the dialog.
- Two closure bugs caught live: onDone→start/send captured the stale
pre-save 'configured' (the flow now forces the replayed send); menu-Esc
used to close the whole dialog.
Settings-dialog provider: open(section?) lands on a section (stage 1
deep-link). Tests: composeSlotModel/composeSearchEngine pure, slot SSR
replacing role rows, slotsConfigured/setupStage first-run gates,
provider-qualified prettyModel. Gates: typecheck, lint, 792/792, build.
- 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 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.