Over Platform's 66dbea9 key-presence seam, the five owner-locked tasks:
1. Zero-keys pill — components/setup-cta.tsx: pure hasProviderKey
(classifyKeys reuse — tool keys like Deepgram never count), useZeroKeys
(GET /api/keys, re-read on the new lh:settings-saved 'keys' signal),
ZeroKeysPill (testid setup-cta-keys) taking PRECEDENCE over the models
CTA on chat/home/new-session. SettingsSection gains 'keys'; keys-section
dispatches after save AND delete.
2. Setup stage 1 is self-contained: provider picker (MenuShell +
ProviderMenuList) + key paste + Save via PUT /api/keys/:id — the dialog
STAYS MOUNTED; its own save dispatches the keys signal, the fetch effect
re-runs on it, and the stage flips to the pickers on its own.
onOpenKeys + setup-open-keys die. SetupKeysBody exported for SSR pins.
3. parseProviderKeyRefusal (chat.tsx, envelope-verbatim twin of
parseModelsRefusal) joins the revert-draft-and-open-setup interception;
activity-dock reads 'needs a provider key'; dispatch-card shows
'Add a key' — both deep-link setup, which lands on stage 1 by itself.
4. Dictation trio: the token route's only 503 (missing Deepgram key)
becomes the client-authored DICTATION_KEYLESS short line; the errored
idle click clears the error and deep-links Settings -> Keys ->
DEEPGRAM_API_KEY via the new open(section, row) seam (dialog landOnRow
-> SettingsDialogBody initialRow -> KeysSection openRow follow-effect).
Live-verified on :3001 under LH_IGNORE_ENV_KEYS=1 (the fresh-user switch):
pill on all three surfaces + precedence over the CTA, stage-1 save ->
auto-advance with dialog mounted, real 503 send refusal reverting the
draft and opening setup, dictation 503 -> short line -> Settings lands on
the unfolded Deepgram row with the error cleared, keys-section save and
delete both flipping the pill live, and a fabricated refused research job
rendering the 'Add a key' dispatch card. 926/926, tsc/lint/build green.
- 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).