Chat detail level — implementation plan
Status: Planned
Author: Christophe
Date: 2026-07-21
Repo: north-os
Related: docs/superpowers/specs/2026-07-21-chat-detail-level-design.html
Shape
Two independent workstreams over a shared interface seam, integrated on branch chat-detail-level, merged to main. The invariant (presentation-only; the level never crosses the API boundary) is enforced by scope: no workstream touches packages/chat-runtime, packages/agent-runtime, or any prompt/configurable code. A diff touching those packages is a review-rejection by itself.
Phase 0 — seam (orchestrator, this branch)
- Commit spec + this plan; register status
planned→in-progressviabun docs/superpowers/status.ts set chat-detail-level in-progressin the implementation-start commit. - Commit the interface seam only:
export type ChatDetailLevel = 0 | 1 | 2 | 3 | 4+ optionaldetailLevelonChatEnvironment(+ export from the package index). No behavior. This keeps both worker branches independently typecheck-green.
Workstream A — storage, API, settings UI, host wiring
Branch feat/chat-detail-level-settings. Sole DB toucher — the only branch that runs bun db:generate.
- Schema: new domain file
packages/db/src/schema/user_chat_preference.tsper the spec (user_id PK → user cascade, chat_detail_level int CHECK 0–4, updated_at), exported from the schema index. Pattern:matter_hidden. - Migration:
bun db:generate, review SQL, fix schema TS and regenerate if wrong (never patch SQL). Check the journalwhenwatermark (PR #5 rebaseline) and bump metadata if the new entry sorts below the DB max. - GRANT:
SELECT, INSERT, UPDATEtoapp_roleinpackages/db/scripts/provision-roles.ts. Never in the migration. - API:
GET/PATCH /api/settings/chat, Hono +apiAuth+ Zod, in a new small preferences router mounted alongside the existing routers inapps/web/lib/api/app.ts(do not bolt onto the media-specificsettingsMediaRouter). Missing row reads as level 1; PATCH upserts. Route tests per the existing router-test pattern. - Settings UI: new "Chat detail" section on Settings → Appearance; five-stop control (shadcn primitives, semantic tokens only, DESIGN.md radius/border rules), stop labels + one-line descriptions from the spec table. The Appearance page is client-only: the control fetches
GET /api/settings/chaton mount to seed the selection (skeleton until resolved), optimistic PATCH on change. - No per-page chat wiring. Level resolution for the chat surface is Workstream B's chat-ui hook (env override →
env.fetchGET → default 1), which covers everyLiveChatmount site (deal chats, task chats, automations board, skill editor, same-origin Word pane) automatically. Workstream A owns only the API + settings control.
Workstream B — chat-ui surfacing
Branch feat/chat-detail-level-ui. Touches packages/chat-ui only.
- Level resolution hook (spec "Plumbing"): resolved level =
env.detailLevelif supplied, else oneGET /api/settings/chatviaenv.fetch/apiUrlon mount, else 1 until resolved. Serves every host with zero per-page wiring. Level 1 must be a byte-for-byte no-op against today's rendering — the existing test suite is the regression net. - Level 0 — leaf suppression, never
groupBy(groupBy-null renders parts inline — that is the visible path). In theGroupedPartsrender callback returnnullforgroup-reasoning, the activity group, internal tool-call parts, and working-note text parts. Tests assert absence from the DOM. - Level 4 — empty note membership at the source: compute
workingNoteParts/housekeepingNotePartsas empty (memoized on parts + level) so groupBy, the muted-styling text branch, the live line, and housekeeping re-homing all fall through to full answer prose. Tools stay grouped (pre-e35aae81fidelity). - Group open state: add a
defaultOpenprop toToolGroupBlock→ToolGroupRoot(none is passed today); set at levels 2–4. Individual tool cards keep their own open/closed defaults at every level. - Reasoning: gate the
group-reasoningcase inthread.tsx(notreasoning.tsx— its group impl is deprecated/unused):defaultOpen = running || level >= 3(levels 1–2 keep today's open-while-streaming);nullat level 0. - Level-0 streaming: net-new hoisted live line in the assistant-message renderer (the existing one lives inside the hidden
ToolGroupBlock): latest working note else neutral shimmer while running; makeThinkingIndicatorlevel-0-aware (no doubling, no empty message on reasoning/tool-only turns); nothing after completion. - User-facing tool cards (
isUserFacingToolPart, MCP app cards) and the inline housekeeping chip (levels 0–3, DOM position unchanged from today) stay untouched by the gate. - Tests (spec test plan items 1–4 and 6): placement-table test across all five levels on one fixture (level 0 asserts absence; level 4 asserts answer-prose slot); request-path byte-identity guard exercising
prepareSendMessagesRequestat levels 0 vs 4;convertMessagelevel-independence guard; level-0 + level-1 streaming; selector referential stability.
Integration (orchestrator)
- Fable-review each worker branch independently; findings route back to the worker. No merge with open findings.
- Merge B, then A, into
chat-detail-level; the workstreams share no files (A: db + apps/web; B: chat-ui), the only coupling is B's hook calling A's route path. - Run
bun run lint+bun run typecheckfrom root once, pluschat-uiand touched-package tests.bun run eval:gateis not required — no retrieval/routing/memory surface is touched. Invariant verification:git diff --stat main -- packages/agent-runtime packages/chat-runtimemust be empty, ANDapps/web/lib/chat+ the chat request path inlive-chat.tsxmust contain nodetailLevel/user_chat_preferencereference (the diff guard alone would not catch a leak there — the byte-identity test is the load-bearing guard). - Rebase-merge to
main; flip status toimplementedin the same push. Dev-stack restart via the deploy bot; verify on dev: settings control round-trips, one chat turn rendered at level 0 and level 4. No preprod deploy — Christophe's call.
Risks
- React #185 / selector stability: the level must come from context, never a new derived-object selector; the memoized note-membership collections must stay referentially stable per (parts, level). The existing footer-selector tests plus the new placement tests guard this.
- Level-1 regressions: the two easy ones review caught — losing reasoning's
defaultOpen={running}(gate must be additive) and relocating the housekeeping chip (it is inline today, not in the tool group). Level 1 is byte-for-byte today. - Level-4 fidelity: "like before" is defined as the pre-
e35aae81rendering — inline note text, tools still grouped; when in doubt, that commit's parent is the reference, not a new invention. - Parallel db:generate: forbidden — only Workstream A generates; journal conflict otherwise.