- 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 owner called the old section messy; the redesign is theirs:
- MINIMAL TABLE: provider label left, masked status right (…tail ·
chars · env), description hints GONE (they took too much space),
the 'Get a key ↗' link a tiny muted guest on curated rows. Rows
read as clean key-value pairs.
- ENV-OWNED ROWS ARE READ-ONLY (owner ruling): a real environment
variable owns the value — the row dims (graph-view graying) and no
paste field renders, because a paste box on a key the UI cannot
make live would mislead. Clicking does nothing.
- OPENROUTER UN-PINNED (owner ruling): one provider among 33 — a row
only when stored, otherwise it's an option in the Add dropdown like
everyone else. No synthesis, no lifeline description.
- ROW INTERACTION (owner choice): clicking a writable row unfolds a
paste field + Save (+ Delete for a file-sourced value) beneath it;
success folds it back to rest; Esc folds without closing the dialog
(claimed at document CAPTURE phase — the only stopPropagation that
outruns both React handlers and the dialog's window listener; the
React-level attempt leaked and closed the dialog, caught live).
- ADD-PROVIDER = the table's last row, in the app's shared dropdown
language: new ProviderMenuList (fuzzy search + arrow-walk + Enter
over the 33 pi-ai providers, label + envVar rows) in the shared
model-menu.tsx, portaled and fixed-positioned like the role menus.
Picking one unfolds a paste row; Save stores, Cancel/Esc folds.
- Tools & services (Deepgram + the Google pair) keep their own group,
same table look, same unfold interaction.
- Unknown auth.json vars stay read-only dimmed rows.
Tests: classifyKeys re-pinned (openrouter rides the normal provider
classification; addable drops stored keys), keyStatusLine minimal
('not set' for absent, ' · env' marker for environment-owned). Gates:
typecheck, lint, 759/759, build.
Live-verified: add→save→replace→delete round-trips, fold-on-save,
Esc layering both flows, env-owned rows read-only, addable count
31→30→31.
Search port + model slots wave, chunk A (owner design, 2026-09-20):
- core/search.ts: SearchHit + SearchProvider port + SearchStatus —
pure, browser-safe; adapters arrive as injected dependencies.
- server/search.ts: Tavily + Brave adapters (wire shapes VERIFIED
against docs.tavily.com's OpenAPI + Brave's dashboard docs — Tavily
auth is the Bearer header, not the legacy body api_key; Brave is
X-Subscription-Token) + engine resolution per owner decisions: a key
ALONE activates its engine (no engine choice needed), explicit
agent.search.engine pins the preference (both keys: Tavily first),
a pinned engine with a missing key is honestly unavailable — no
silent fallback, no default engine.
- Curated keys catalog: GOOGLE_SEARCH_API_KEY/GOOGLE_CSE_ID retire
(CSE closed to new customers, dies Jan 2027 — stored values become
preserved unknown vars); TAVILY_API_KEY (tvly- shape) + BRAVE_API_KEY
(length guard) join. UI keys-section TOOL_IDS now derives from the
catalog — cannot drift.
- GET /api/search: {available, engine, reason} — the honest status for
the settings gray-out; researcher-disable semantics ride the same
signal (Teacher stream rewires the tool onto the port).
- agent.search settings field (schema + sanitizer + strict parser);
web.ts's WebSearchResult is now the core SearchHit (re-exported).
Owner-approved design (2026-09-20): a Keys section right after General,
two groups over the merged auth API (GET /api/keys masked statuses,
PUT/DELETE /api/keys/:id, immediate effect, shape-gated 400s):
- Model providers: OpenRouter pinned as an always-visible row (the
app's lifeline — synthesized when absent so first boot shows it),
stored provider keys render as they appear, and an Add-provider
dropdown lists every pi-ai provider that takes an API key. The
dropdown reads GET /api/keys' optional providers array — the
Platform interlude (master-routed) derives it from pi-ai's own
env-key table; until it lands the group shows a pending note
(rolePins pattern — absent field, no drift, no guessed routes).
- Tools & services: the curated rows — Deepgram (dictation) and the
Google pair (search key + CSE ID) — with description hints and docs
links (external links route through the Electron shell to the real
browser).
- Rows: always-visible paste inputs (owner choice; no reveal —
plaintext never returns from the store), masked status line
(…tail · chars · environment|saved), the shadowedByEnv explanation
when a real env var owns the name, per-row inline 400 messages, Save
on Enter/button, Delete when present (env-owned keys honestly keep
their env status after a file delete).
- Unknown auth.json vars surface as read-only dimmed rows — preserved
by the store, not editable through the curated surface.
classifyKeys/keyStatusLine pure exports drive the two-group
projection; tests: SSR loading state, first-boot synthesis, provider
row classification, addable filtering, unknown-var read-only status
line formatting. Nav grows to six sections (Keys second).
Live-verified: nav order, structure, bad-paste 400s inline, save
round-trip with env-shadow explanation, delete round-trip, env-owned
OpenRouter status stability. Gates: typecheck, lint, 748/748, build.