@@ 11,17 11,18 @@ Index of task specs and their state. Each row points at a `docs/tasks/<slug>.md`
| 3 | [`lethe-search-and-opencode`](tasks/lethe-search-and-opencode.md) | Designed (deferred) | Adds `GET /api/v1/search` (FTS5) and an `opencode` collector. Blocks #7. |
| 4 | [`lethe-web-ui-foundation`](tasks/lethe-web-ui-foundation.md) | **Reviewed** | Vite/React/TS SPA, embed pipeline, shell + Home + Session views, palette skeleton, 5 stub routes. Plus `/sessions` aggregate fields. |
| 5 | [`lethe-web-ui-aggregates`](tasks/lethe-web-ui-aggregates.md) | **Reviewed** | Backend `/projects` + `/stats` endpoints, Projects index + Project detail + Stats screen. Replaces 3 of #4's stubs. |
-| 6 | `lethe-web-ui-palette-savedsearch` | Not started | Full ⌘K palette (PROJECT/SESSION items), saved-searches table + UI, palette pulls from it. Small backend (1 table) + Settings → Saved searches. |
+| 6 | [`lethe-web-ui-palette-savedsearch`](tasks/lethe-web-ui-palette-savedsearch.md) | **Reviewed** | Full ⌘K palette (PROJECT/SESSION items), saved-searches table + UI, palette pulls from it. Small backend (1 table) + Settings → Saved searches. |
| 7 | `lethe-web-ui-search` | Blocked on #3 | Search route: turn-level results, FTS `<mark>` highlighting, save-search action. |
-| 8 | `lethe-web-ui-settings-display` | Not started | Settings → Display: theme toggle (light/dark/system), density toggle, "show tool calls" toggle, persisted to localStorage. |
+| 8 | [`lethe-web-ui-settings-display`](tasks/lethe-web-ui-settings-display.md) | **Reviewed** | Settings → Display: theme toggle (light/dark/system), density toggle, "show tool calls" toggle, persisted to localStorage. |
| 9 | `lethe-web-ui-health-sources` | Blocked on #2 (and ideally #3) | Settings → Sources (per-host-per-tool config table) + `/health` route (collector ingestion table, status pills, footer strip with backfill progress and last-error). |
-| 10 | `lethe-oidc-stub` | Not started | Promote the in-test `oidcTestServer` (`internal/server/auth/oidctestserver_test.go`) to an exported `internal/testutil/oidcstub/` package, optionally wrap as `cmd/oidc-stub`. Unblocks real-browser smoke for #5 (the deferred verify item) and any future SPA route work without standing up Authelia locally. JWT-based by design — see `~/data/home/second-brain/wiki/jwt-for-sessions.md`: lethe is the OIDC verifier, JWT is the only legal wire format here. |
+| 10 | [`lethe-oidc-stub`](tasks/lethe-oidc-stub.md) | **Reviewed** | Promote the in-test `oidcTestServer` (`internal/server/auth/oidctestserver_test.go`) to an exported `internal/testutil/oidcstub/` package, optionally wrap as `cmd/oidc-stub`. Unblocks real-browser smoke for #5 (the deferred verify item) and any future SPA route work without standing up Authelia locally. JWT-based by design — see `~/data/home/second-brain/wiki/jwt-for-sessions.md`: lethe is the OIDC verifier, JWT is the only legal wire format here. |
+| 11 | [`lethe-web-ui-login`](tasks/lethe-web-ui-login.md) | **Reviewed** | SPA OIDC auth-code+PKCE flow: `/login` + `/auth/callback`, in-memory token store, `apiFetch` Bearer attachment, shared `AuthGate` replacing the three "not authenticated" placeholder cards. Dev-stub upgraded to a real auth-code OP. Closes the browser-smoke gate for #5 and #6. |
## Carry-overs from `lethe-web-ui-foundation`
These were captured in `lethe-web-ui-foundation.md` Conclusion → Future work / Deferred. Pulled here so they're visible across tasks.
-- **Composite-id-in-URL** (cosmetic, 1-line fix): `SessionsTable` Link passes the full composite as `params.id`, producing URLs like `/session/T/H/T%2FH%2FID`. Fix in `web/src/features/home/SessionsTable.tsx` to pass the bare `session_id`.
+- ~~**Composite-id-in-URL**~~ — closed by #6 PH2 (`Session.sessionId` adapter field + call-site swaps in `routes/index.tsx:48` and `routes/project.$.tsx:33`).
- **Aggregates absent on `GET /sessions/{tool}/{host}/{id}`**: the new `summary`/`turn_count`/`tokens_*_total` fields appear as zero values on the Get path because the Get SQL is unchanged (Plan 1.2 said so). UI doesn't read them, but a future API consumer would. Fix is either extending the Get SQL or adding `,omitempty` to the four numeric tags.
- **Turn meta-line lacks timestamp**: the `Turn` TS interface from foundation Plan 6.1 omitted `timestamp`; meta line shows `# seq · model · tokens-in→tokens-out` only. Add `timestamp` to the interface + render.
- **`go test ./...` walks `web/node_modules/flatted/golang/pkg/flatted`**: stray Go package shipped inside an npm dep. Switch CI to `go test ./internal/...` or gitignore the path.
@@ 36,13 37,14 @@ Cross-cutting tooling polish that doesn't warrant a full task file each. Track h
- [ ] **Add `go fix ./...` to `just fmt`** — runs after `goimports`; mostly a no-op today but standardizes future API-rewrite migrations into the existing `just fmt` flow.
- [ ] **`migrate` → `go tool migrate`** — `go get -tool github.com/golang-migrate/migrate/v4/cmd/migrate@latest`; rewrite the three `migrate-*` recipes; drop the now-stale `brew install golang-migrate` comment block.
- [ ] **OIDC stub** — see task #10 (separate task file).
+- [ ] **Full web lint unblock** — `npm run lint` currently fails on pre-existing `web/src/routes/auth.callback.tsx:137:16` (`e` unused); changed files from #8 pass targeted ESLint.
## Deferred operational follow-ups
Tracked outside the task workflow because they're user-driven, not engineering work.
- **Sourcecraft.dev push** — `~/data/home/lethe` has no remote configured; per global preference all `~/data/home/*` projects mirror to sourcecraft.dev. `sc init / sc push` when you want it visible / backed up.
-- **Browser smoke for task #5** — `/projects`, `/project/<encoded cwd>`, `/stats?range=…` were verified by tsc + vitest + Go handler tests, but never walked in a real browser. Blocked on having an OIDC bearer + populated DB locally; task #10 (OIDC stub) closes both halves of that.
+- **Browser smoke for tasks #5, #6, and #8** — `/projects`, `/project/<encoded cwd>`, `/stats?range=…`, `/settings` saved-search CRUD, `/settings` Display toggles, and the widened ⌘K palette were verified by tsc + vitest + Go handler tests, but never walked in a real browser. Unblocked: #10 (OIDC stub) + #11 (login flow) ship the dev OP and SPA flow needed. Recommended walk per the affected task Verify sections.
## Notes
@@ 0,0 1,240 @@
+# lethe-web-ui-settings-display
+
+**Status:** Reviewed
+**Module:** `sourcecraft.dev/bigbes/lethe`
+**Branch:** master
+**Worktree:** none
+**Parent RFC:** Personal AI Assistant Log Aggregator (2026-04-25)
+**Sibling tasks:**
+- `lethe-web-ui-foundation.md` (#4, ✓ Reviewed) — established `/settings` route, route tree, primitives, embed pipeline; `lib/theme.ts` shipped here
+- `lethe-web-ui-palette-savedsearch.md` (#6, ✓ Reviewed) — sectioned `/settings` layout, `SectionRail`, reserved the `display` slot with the `'in #8'` muted tag — this task fills that slot
+- `lethe-web-ui-login.md` (#11, ✓ Reviewed) — `AuthGate` + OIDC flow that lets users actually reach `/settings`; smoke for this task piggybacks on the same dev-stub OP
+
+## Design
+
+### Purpose
+
+Fill the `Display` section #6 reserved at `/settings` (rendered today as a disabled row with the `"in #8"` muted tag, see `routes/settings.tsx:13`) with three persisted, app-wide visual preferences:
+
+1. **Theme** — light / dark / system (machinery shipped in `lib/theme.ts`; this task adds the UI surface and a small getter).
+2. **Density** — cozy / compact (greenfield; one CSS variable, mirrored module).
+3. **Show tool calls** — on / off (greenfield; CSS-only hide of tool-tagged turns in `Transcript`).
+
+All three persist to localStorage, drive a `data-*` attribute on `<html>`, and let CSS do the visual work. Personal-scale, single-user — no backend, no schema, no API.
+
+**In scope**: `DisplaySection.tsx` UI; two new preference modules (`lib/density.ts`, `lib/toolCalls.ts`) mirroring `lib/theme.ts`; an additive `getThemePreference()` on `lib/theme.ts`; bootstrap calls in `main.tsx`; a `--row-pad` token in `styles/tokens.css` driven by `[data-density]` for Home sessions, Projects, and Saved-search rows; a `[data-show-toolcalls="false"] ...` hide rule in `styles/session.css`; a `data-tool="1"` attribute on tool-tagged turns in `Transcript.tsx`; enabling the `display` section row in `routes/settings.tsx`.
+
+**Out of scope**: device-sync of preferences (no server-side preferences table — AS1); font-size selector or accent-color picker; splitting "show tool calls" into call vs. result toggles (UK1); applying density to `Transcript` turn padding; palette / search-results / future-route surfaces — they pick up `data-density` for free if they consume `--row-pad`, but no proactive retrofit beyond the existing row-list surfaces.
+
+### Chosen approach
+
+**Three sibling preference modules under `lib/`** — `theme.ts` (exists), `density.ts` (new), `toolCalls.ts` (new). Each exports `bootstrap*()`, `set*(value | null)`, and `get*Preference()`; sets a single `data-*` attribute on `<html>`; persists to a single localStorage key. No shared abstraction — three ~30-line files of identical shape are easier to read top-to-bottom than a generic preference layer (GPC2: one concern per unit, GPC8: rule-of-three for DRY — three near-identical modules sit at the threshold but don't merit an abstraction yet).
+
+**CSS-driven application** — preferences are body-level data-attributes; CSS does the visual work, the React tree never reads the preference state to render conditionally:
+- `[data-theme="dark"]` already drives the full token swap in `tokens.css:56`.
+- `[data-density="compact"]` flips a new `--row-pad` token from a cozy value around today's 4–5px row padding to a tighter compact value. Surfaces that consume `--row-pad`: `SessionsTable` rows, `ProjectsTable` rows, and `SavedSearchesSection` table rows. Transcript turn padding stays unchanged because transcript bodies are reading surfaces, not scan-heavy rows.
+- `[data-show-toolcalls="false"] .turn[data-tool="1"] { display: none }` — hides flagged turns in `Transcript.tsx`. The component adds `data-tool="1"` to a turn whenever `t.role === 'tool' || t.toolKind != null` (loose semantic: hides both tool invocations and tool-result turns).
+
+**UI: `DisplaySection.tsx`** — three labeled `<fieldset>` blocks, each a row of radio inputs:
+- Theme: `light` / `dark` / `system`
+- Density: `cozy` / `compact`
+- Show tool calls: `yes` / `no`
+
+Component state is `useState` seeded from the three getters on mount; `onChange` calls the matching setter. No global state, no context — the source of truth is the data-attribute on `<html>` and localStorage. The component re-reads the getter on mount only; it is not subscribed to changes (no other UI mutates these preferences).
+
+**localStorage keys (bare, not namespaced)** — `theme` (existing), `density`, `showToolCalls`. Rationale: the existing `theme` key is shipped and working in `lib/theme.ts`; renaming it to `lethe.display.theme` would silently lose any saved preference for the user. Bare keys keep the three new preferences consistent with each other. Trade-off accepted: the codebase has mixed namespacing (auth code uses `lethe.pkce.pending`, `lethe.auth.failures`; display preferences stay bare). Documented as a known asymmetry, not a defect.
+
+**Defaults** — when no preference is stored:
+- Theme follows OS (existing `prefers-color-scheme` listener in `theme.ts`).
+- Density defaults to `cozy` (matches today's row-list scale).
+- Show tool calls defaults to `yes` (matches today's transcript rendering — every shipped turn renders today).
+
+Bootstrap order in `main.tsx`: theme → density → tool-calls, before first render. Each bootstrap reads its key, applies the data-attribute, and (theme only) registers an OS-change listener.
+
+**One additive getter on `lib/theme.ts`** — `getThemePreference(): 'light' | 'dark' | 'system'` reads `localStorage.getItem('theme')` and returns the explicit value or `'system'` when null. Needed because the radio UI must seed its initial state with the user's *preference* (which could be `system`), not just the *applied* theme. The new modules each export their own getter with the same shape.
+
+### Backwards-compatibility check
+
+Greenfield modulo two additive surfaces:
+- `lib/theme.ts` gains a new exported `getThemePreference()`. Existing callers of `setTheme`/`bootstrapTheme` are unchanged.
+- `routes/settings.tsx` removes `disabled: true, tag: 'in #8'` from the `display` row of `SECTIONS` and renders `<DisplaySection />` when active. No deployed user flow today (the row was disabled — clicking did nothing).
+
+No API, schema, endpoint, or wire-format changes. No removed or renamed JSON fields. No migrations. localStorage gains two new keys (`density`, `showToolCalls`); no existing key is renamed or removed.
+
+### TDD: yes (scoped)
+
+- **Yes** for `lib/density.test.ts` and `lib/toolCalls.test.ts` — mirror `lib/theme.test.ts` shape: bootstrap reads stored value, falls back to default; `set*` writes localStorage and updates data-attribute; `set*(null)` clears localStorage and re-applies default; `get*Preference()` returns the right enum across (no-storage / explicit-value / cleared) states. Cover the assertion that density/tool-calls modules do **not** register an OS-change listener (only theme has that behavior).
+- **Yes** for `lib/theme.test.ts` extension — `getThemePreference()` returns `'system'` when no key, `'light'` / `'dark'` when stored.
+- **No** for `DisplaySection` chrome — three radio fieldsets, dominated by visual feel; vitest of radio-state-on-change is low-value coverage of `useState`. A manual smoke walk via the OIDC dev stub covers it.
+- **No** for CSS rule wiring (`--row-pad` flip on `[data-density="compact"]`, tool-turn hide on `[data-show-toolcalls="false"]`) — visual; same smoke walk, with row density checked on Home sessions, Projects, and Saved searches.
+
+## Invariants
+
+- IV1 — All three preference values are read exclusively via their module's `get*Preference()` getter; `localStorage.getItem` for the three keys (`theme`, `density`, `showToolCalls`) is called only inside `lib/theme.ts`, `lib/density.ts`, `lib/toolCalls.ts`. No other file reaches into localStorage for these keys.
+- IV2 — Each preference owns exactly one data-attribute on `<html>`: `data-theme`, `data-density`, `data-show-toolcalls`. No CSS rule keys off the localStorage value directly; all visual rules key off the data-attribute (single seam between persistence and rendering).
+- IV3 — `bootstrapDensity()` and `bootstrapToolCalls()` are side-effect-free at module-import time; effects happen only when called from `main.tsx` (mirrors the existing contract on `bootstrapTheme()`).
+- IV4 — `DisplaySection` radio state is derived on mount from the three getters and on change calls the three setters; the component holds no other source of truth and does not subscribe to storage events.
+- IV5 — Tool-call hiding is CSS-only via `[data-show-toolcalls="false"] .turn[data-tool="1"]`; the React tree always renders all turns. Reason: turns are deep-linkable by sequence id (`#turn-${t.i}`); hiding via CSS preserves anchor targets and selection state across toggles.
+- IV6 — `data-tool="1"` is set on a turn `<div>` in `Transcript.tsx` exactly when `t.role === 'tool' || t.toolKind != null` (loose semantic — hides both tool invocations and tool-result turns).
+- IV7 — Bootstrap order in `main.tsx` is theme → density → tool-calls, all before the first React render. No flash-of-wrong-density between mount and the user's preference taking effect.
+- IV8 — Density affects Home sessions, Projects, and Saved-search rows only; `Transcript` turn padding remains unchanged in this task.
+
+## Principles
+
+- PC1 — When a preference's default differs from "do nothing in CSS", the default lives in exactly one place: the bootstrap function (e.g. `bootstrapDensity()` applies `data-density="cozy"` when no key is stored). **Why:** avoids two-place defaults that drift between TS and CSS. **How to apply:** do NOT also encode the default in `:root { --row-pad: ... }` without an attribute selector — let bootstrap apply the attribute and let `[data-density="cozy"]` rule own the value. (Mild deviation from GPC1's "no hidden config" — bootstrap is implicit at component-render time, but it runs before first render so the attribute is always present.)
+
+## Assumptions
+
+- AS1 — Personal-scale UI (single user, one browser at a time) means localStorage-only persistence is sufficient; no need for a server-side preference table or device-sync. **Why this matters:** if AS1 fails, density and tool-calls toggles diverge across devices. **Verify in Conclusion:** note as a known limitation if smoke surfaces a multi-device need; otherwise mark held.
+- AS2 — The existing theme module can be called from `main.tsx` before React render without changing its applied theme semantics.
+
+## Unknowns
+
+- UK1 — Whether users will want to split "show tool calls" into separate "show tool invocations" and "show tool results" toggles in a future iteration. Initial answer (this task): single toggle, loose semantic — `t.role === 'tool' || t.toolKind != null` (IV6). Trade-off: this hides assistant turns that contain *both* prose and tool-call invocations (typical Anthropic `tool_use` shape), losing the prose with the tool indicator. Acceptable for v1; if smoke walk shows the prose loss is annoying, the natural refinement is "hide only `role === 'tool'` turns + CSS-hide the meta-line tool tags on assistant turns" (two rules instead of one). Revise during Conclusion only if smoke surfaces a need.
+
+## Plan
+
+Approach: two phases — PH1 adds the tested preference modules and exported contracts; PH2 wires those contracts into bootstrap, settings UI, and CSS-only visual behavior.
+
+### PH1 — Preference Modules
+
+- **1.1** `web/src/lib/theme.test.ts:1-172` (modify)
+ - Add RED tests for `getThemePreference(): 'light' | 'dark' | 'system'` returning `system` with no key and explicit values when stored.
+ - Respects: IV1, AS2.
+- **1.2** `web/src/lib/theme.ts:1-47` (modify)
+ - `export type ThemePreference = 'light' | 'dark' | 'system'` — shared preference type.
+ - `export function getThemePreference(): ThemePreference` — returns stored `light`/`dark`, otherwise `system`.
+ - Move the bootstrap call-site comment from `main.tsx after the React root is mounted` to `main.tsx before render`.
+ - Respects: IV1, IV2, IV3, IV7, AS2.
+- **1.3** `web/src/lib/density.test.ts` (create)
+ - Add RED tests for stored/default/cleared preferences, `data-density` writes, and no `matchMedia` listener registration.
+ - Respects: IV1, IV2, IV3, PC1.
+- **1.4** `web/src/lib/density.ts` (create)
+ - `export type DensityPreference = 'cozy' | 'compact'`.
+ - `export function getDensityPreference(): DensityPreference`.
+ - `export function bootstrapDensity(): void`.
+ - `export function setDensity(density: DensityPreference | null): void`.
+ - Respects: IV1, IV2, IV3, PC1, AS1.
+- **1.5** `web/src/lib/toolCalls.test.ts` (create)
+ - Add RED tests for stored/default/cleared preferences, `data-show-toolcalls` writes, and no `matchMedia` listener registration.
+ - Respects: IV1, IV2, IV3, PC1.
+- **1.6** `web/src/lib/toolCalls.ts` (create)
+ - `export type ToolCallsPreference = 'yes' | 'no'`.
+ - `export function getToolCallsPreference(): ToolCallsPreference`.
+ - `export function bootstrapToolCalls(): void`.
+ - `export function setToolCalls(value: ToolCallsPreference | null): void`.
+ - Respects: IV1, IV2, IV3, PC1, AS1.
+- Commit: `web: add display preference modules`
+
+### PH2 — Display UI And CSS Wiring
+
+- **2.1** `web/src/main.tsx:1-26` (modify)
+ - Import `bootstrapTheme`, `bootstrapDensity`, and `bootstrapToolCalls`.
+ - Call them in theme → density → tool-calls order before `createRoot(rootEl).render(...)`.
+ - Respects: IV2, IV3, IV7, AS2.
+- **2.2** `web/src/routes/__root.tsx:1-79` (modify)
+ - Remove `bootstrapTheme` import and the mount-effect call so bootstrap has one call site.
+ - Respects: IV3, IV7, PC1.
+- **2.3** `web/src/features/settings/DisplaySection.tsx` (create)
+ - `export function DisplaySection(): React.JSX.Element` — renders three radio fieldsets seeded from `getThemePreference`, `getDensityPreference`, and `getToolCallsPreference`.
+ - Map theme `system` to `setTheme(null)` and explicit theme values to `setTheme(value)`.
+ - Respects: IV1, IV4, AS1.
+- **2.4** `web/src/routes/settings.tsx:1-36` (modify)
+ - Import `DisplaySection`, remove `disabled: true, tag: 'in #8'`, and render `<DisplaySection />` when `active === 'display'`.
+ - Respects: IV4, AS1.
+- **2.5** `web/src/styles/settings.css:223-225` (modify)
+ - Add display-section styles for fieldsets, labels, radio rows, and preference help text using existing tokens only.
+ - Respects: IV4.
+- **2.6** `web/src/styles/tokens.css:3-56` (modify)
+ - Add `[data-density="cozy"] { --row-pad: 4px; }` and `[data-density="compact"] { --row-pad: 2px; }` outside `:root`.
+ - Respects: IV2, IV8, PC1.
+- **2.7** `web/src/styles/home.css:24-43`, `web/src/styles/projects.css:24-43`, `web/src/styles/settings.css:173-195` (modify)
+ - Replace row vertical padding with `var(--row-pad)` while preserving existing horizontal padding and cursor left-padding adjustment.
+ - Respects: IV2, IV8.
+- **2.8** `web/src/features/session/Transcript.tsx:18-61` (modify)
+ - Add `data-tool="1"` exactly when `t.role === 'tool' || t.toolKind != null`.
+ - Respects: IV5, IV6, UK1.
+- **2.9** `web/src/styles/session.css:42-51` (modify)
+ - Add `[data-show-toolcalls="false"] .turn[data-tool="1"] { display: none; }` without changing `.turn` padding.
+ - Respects: IV2, IV5, IV8, UK1.
+- Commit: `web: wire display settings UI`
+
+### Test Strategy
+
+- PH1 follows RED-GREEN-REFACTOR for `theme.test.ts`, `density.test.ts`, and `toolCalls.test.ts`; each new behavior must fail before production code lands.
+- PH2 verification runs `npm test`, `npm run lint`, and `npm run typecheck` from `web/`; visual behavior is smoke-checked on `/settings`, `/`, `/projects`, and a session transcript.
+
+### Backwards Compatibility
+
+- PH1 only adds `getThemePreference()` and new localStorage keys `density` / `showToolCalls`; existing `theme`, `setTheme`, and `bootstrapTheme` semantics stay intact.
+- PH2 only enables a previously disabled Settings section and adds CSS keyed from new `data-*` attributes; no API, schema, endpoint, config, or wire-format changes.
+
+### Risks / Rollback
+
+- RK1 — Moving `bootstrapTheme()` from `__root.tsx` to `main.tsx` changes call timing; rollback is restoring the root mount-effect call if browser smoke shows an unexpected startup regression.
+
+### Interfaces
+
+- IF1 — `getThemePreference(): ThemePreference` — reads the stored theme preference without applying it.
+- IF2 — `bootstrapDensity(): void`, `setDensity(density: DensityPreference | null): void`, `getDensityPreference(): DensityPreference` — owns the `density` key and `data-density` attribute.
+- IF3 — `bootstrapToolCalls(): void`, `setToolCalls(value: ToolCallsPreference | null): void`, `getToolCallsPreference(): ToolCallsPreference` — owns the `showToolCalls` key and `data-show-toolcalls` attribute.
+
+### Interface Graph
+
+- PH1 -> IF1, IF2, IF3 @ `web/src/lib/`
+- PH2 IF1, IF2, IF3 -> @ `web/src/main.tsx`, `web/src/routes/`, `web/src/features/`, `web/src/styles/`
+
+## Verify
+
+**Result:** passed
+
+Positive:
+- CK1 — `npm test` → 105 tests passed.
+- CK2 — `npm run typecheck` passed.
+- CK3 — `npm run build` produced the production bundle.
+- CK4 — changed TS/TSX files pass ESLint.
+
+Invariants / assumptions:
+- CK5 (IV1) — `localStorage.getItem` for `theme`, `density`, and `showToolCalls` appears only in the three preference modules.
+- CK6 (IV2, IV8) — row density CSS is limited to Home sessions, Projects, and Saved-search rows.
+- CK7 (IV5, IV6) — tool-call hiding uses `[data-show-toolcalls="false"] .turn[data-tool="1"]` and `Transcript` sets `data-tool="1"` from `role === 'tool' || toolKind != null`.
+- CK8 (IV7, AS2) — `main.tsx` calls theme → density → tool-calls bootstrap before React render.
+
+Interfaces:
+- CK9 (IF1) — `DisplaySection` calls `getThemePreference(): ThemePreference`.
+- CK10 (IF2) — `main.tsx` and `DisplaySection` call the declared density functions.
+- CK11 (IF3) — `main.tsx` and `DisplaySection` call the declared tool-call functions.
+
+Smoke: `npm run build` — bundle OK.
+
+Notes:
+- `npm run lint` still fails on pre-existing `web/src/routes/auth.callback.tsx:137:16` (`e` unused); changed TS/TSX files pass targeted ESLint.
+- Real-browser click-through smoke was not run in this tool session.
+
+## Conclusion
+
+Outcome: Display preferences shipped and review passed at `7134607`.
+
+Invariants:
+- IV1 — verified by grep: preference `localStorage.getItem` calls for the three keys are confined to `lib/theme.ts`, `lib/density.ts`, and `lib/toolCalls.ts`.
+- IV2 — verified by grep: visual rules key off `data-theme`, `data-density`, and `data-show-toolcalls`.
+- IV3 — held: density and tool-call modules are side-effect-free at import and are bootstrapped from `main.tsx`.
+- IV4 — held: `DisplaySection` seeds state from getters and mutates preferences only through setters.
+- IV5 — held after review fix: tool-call hiding is CSS-only via `[data-show-toolcalls="false"] .turn[data-tool="1"]`.
+- IV6 — held: `Transcript` marks tool turns with `data-tool="1"` using `role === 'tool' || toolKind != null`.
+- IV7 — held: `main.tsx` bootstraps theme, density, then tool-calls before React render.
+- IV8 — held: density changes Home sessions, Projects, and Saved-search rows only.
+
+### Assumptions check
+- AS1 — held for this scope: preferences are localStorage-only with no backend surface added.
+- AS2 — held: moving `bootstrapTheme()` to `main.tsx` preserved test, typecheck, and build behavior.
+
+### Unknowns outcome
+- UK1 — still-open: no browser smoke data yet on whether hiding assistant turns with tool invocations loses too much prose context.
+
+### Review findings
+- Critical: tool-call hide selector mismatched the attribute value; resolved by mapping the `yes`/`no` preference to `data-show-toolcalls="true"/"false"` in `7134607`.
+
+### Verified by
+- Full lint remains blocked by pre-existing `web/src/routes/auth.callback.tsx:137:16`; changed TS/TSX files pass targeted ESLint.
+- Real-browser click-through smoke remains deferred because this tool session has no browser automation.