@chanlerdev/scorel 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +110 -0
  2. package/dist/index.js +6675 -0
  3. package/dist/index.js.map +7 -0
  4. package/docs/CHANGELOG.md +12 -0
  5. package/docs/README.md +116 -0
  6. package/docs/ROADMAP.md +669 -0
  7. package/docs/SHIP.md +242 -0
  8. package/docs/spec/channels.md +156 -0
  9. package/docs/spec/client.md +326 -0
  10. package/docs/spec/daemon.md +408 -0
  11. package/docs/spec/events.md +423 -0
  12. package/docs/spec/extensions.md +255 -0
  13. package/docs/spec/relay.md +391 -0
  14. package/docs/spec/runtime.md +251 -0
  15. package/docs/spec/session.md +380 -0
  16. package/docs/spec/ship/S0001-docs-baseline.md +41 -0
  17. package/docs/spec/ship/S0002-package-skeleton.md +56 -0
  18. package/docs/spec/ship/S0003-protocol-contracts.md +49 -0
  19. package/docs/spec/ship/S0004-session-core.md +50 -0
  20. package/docs/spec/ship/S0005-runtime-loop.md +48 -0
  21. package/docs/spec/ship/S0006-embedded-daemon-client.md +51 -0
  22. package/docs/spec/ship/S0007-cli-alpha.md +49 -0
  23. package/docs/spec/ship/S0008-coding-tools.md +107 -0
  24. package/docs/spec/ship/S0009-code-discovery-tools.md +82 -0
  25. package/docs/spec/ship/S0010-todo-tool-and-cli.md +81 -0
  26. package/docs/spec/ship/S0011-coding-agent-alpha-smoke.md +110 -0
  27. package/docs/spec/ship/S0012-coding-tools-maturity.md +143 -0
  28. package/docs/spec/ship/S0013-local-daemon-protocol.md +57 -0
  29. package/docs/spec/ship/S0014-local-daemon-lifecycle.md +64 -0
  30. package/docs/spec/ship/S0015-local-attach-and-broadcast.md +58 -0
  31. package/docs/spec/ship/S0016-local-daemon-resync-smoke.md +60 -0
  32. package/docs/spec/ship/S0017-grep-files-output-mode.md +49 -0
  33. package/docs/spec/ship/S0018-daemon-entrypoint-smoke.md +48 -0
  34. package/docs/spec/ship/S0019-remote-transport-contract.md +59 -0
  35. package/docs/spec/ship/S0020-remote-websocket-server.md +56 -0
  36. package/docs/spec/ship/S0021-remote-websocket-client-transport.md +55 -0
  37. package/docs/spec/ship/S0022-remote-daemon-cli-lifecycle.md +60 -0
  38. package/docs/spec/ship/S0023-remote-control-e2e-validation.md +66 -0
  39. package/docs/spec/ship/S0024-remote-attach-interactive-stream.md +49 -0
  40. package/docs/spec/ship/S0025-remote-attach-session-event-view.md +57 -0
  41. package/docs/spec/ship/S0026-attach-project-cache-and-dual-seq-reconnect.md +87 -0
  42. package/docs/spec/ship/S0027-session-diagnostics-log.md +77 -0
  43. package/docs/spec/ship/S0028-client-attach-diagnostics-log.md +70 -0
  44. package/docs/spec/ship/S0029-project-index-for-session-lookup.md +119 -0
  45. package/docs/spec/ship/S0030-webui-product-intent.md +73 -0
  46. package/docs/spec/ship/S0031-daemon-projectslug-rule.md +72 -0
  47. package/docs/spec/ship/S0032-daemon-protocol-completion.md +123 -0
  48. package/docs/spec/ship/S0033-webui-skeleton-routing.md +92 -0
  49. package/docs/spec/ship/S0034-webui-device-settings.md +121 -0
  50. package/docs/spec/ship/S0035-webui-device-handshake.md +83 -0
  51. package/docs/spec/ship/S0036-webui-project-session-sync.md +70 -0
  52. package/docs/spec/ship/S0037-webui-chatbox-v1.md +97 -0
  53. package/docs/spec/ship/S0038-webui-cancel-multiclient.md +65 -0
  54. package/docs/spec/ship/S0039-webui-e2e-newchat.md +74 -0
  55. package/docs/spec/ship/S0040-webui-codex-visual-tokens.md +227 -0
  56. package/docs/spec/ship/S0041-webui-markdown-and-tool-block.md +248 -0
  57. package/docs/spec/ship/S0042-webui-streaming-ux-autoscroll.md +130 -0
  58. package/docs/spec/ship/S0043-startup-ergonomics.md +278 -0
  59. package/docs/spec/ship/S0044-webui-chatbox-rebuild.md +556 -0
  60. package/docs/spec/ship/S0045-webui-card-sidebar-and-session-fixes.md +469 -0
  61. package/docs/spec/ship/S0046-webui-empty-composer-and-lazy-session.md +428 -0
  62. package/docs/spec/ship/S0047-webui-project-hover-newchat-and-dynamic-greeting.md +176 -0
  63. package/docs/spec/ship/S0048-device-level-host-project-registry.md +253 -0
  64. package/docs/spec/ship/S0049-webui-add-project-directory-browser.md +217 -0
  65. package/docs/spec/ship/S0050-instruction-snapshot-and-agents-assembly.md +338 -0
  66. package/docs/spec/ship/S0051-harness-item-and-system-reminder.md +190 -0
  67. package/docs/spec/ship/S0052-follow-up-queue-and-dual-loop.md +195 -0
  68. package/docs/spec/ship/S0053-skill-index-and-skill-tool.md +252 -0
  69. package/docs/spec/ship/S0054-webui-running-message-behavior.md +72 -0
  70. package/docs/spec/ship/S0055-webui-composer-acceptance-and-queue-strip.md +68 -0
  71. package/docs/spec/ship/S0056-relay-and-hosted-webui-contract.md +106 -0
  72. package/docs/spec/ship/S0057-relay-service-protocol-skeleton.md +161 -0
  73. package/docs/spec/ship/S0058-host-outbound-relay-and-pair-command.md +138 -0
  74. package/docs/spec/ship/S0059-relay-transport-and-hosted-webui-connector.md +140 -0
  75. package/docs/spec/ship/S0060-relay-hosted-webui-e2e-validation.md +132 -0
  76. package/docs/spec/ship/S0060-relay-hosted-webui-e2e-validation.verification.md +90 -0
  77. package/docs/spec/ship/S0061-hosted-defaults-and-cli-command-surface.md +208 -0
  78. package/docs/spec/ship/S0062-npm-package-and-release-workflow.md +166 -0
  79. package/docs/spec/tools.md +173 -0
  80. package/package.json +51 -0
@@ -0,0 +1,65 @@
1
+ # S0038: WebUI Cancel And Multi-client Share
2
+
3
+ ## Goal
4
+
5
+ Wire WebUI composer Cancel to daemon `cancel` (restored in S0032), and validate that WebUI and CLI sharing the same remote daemon session see identical event streams in real time.
6
+
7
+ ## Scope
8
+
9
+ - Composer Cancel button:
10
+ - Visible only while a turn is in flight (`turn_start` received, `turn_end` not yet).
11
+ - Click calls `client.cancel()`. Optimistic UI: button enters "cancelling…" state until daemon echoes a `turn_end` with `stopReason: "cancelled"` (or equivalent).
12
+ - On daemon error response, surface inline error and re-enable Send.
13
+ - Cancel hotkey: `Esc` while composer is focused, mirrors button. Document behavior.
14
+ - Optimistic semantics:
15
+ - Server-side cancellation is best-effort (S0032 acceptance criteria); UI must not pretend a turn ended until daemon confirms.
16
+ - Multi-client smoke wiring:
17
+ - No code in this spec for the smoke itself; it's a manual validation. But add a `lib/diagnostics/connection-summary.ts` exposing `Device.id`, `remoteIdentity.deviceId`, currently subscribed `sessionId`, and last applied `streamLastSeq`. Render a dev-only debug panel under `/devices/:deviceId/projects/:projectSlug/sessions/:sessionId?debug=1` that shows this summary.
18
+ - Manual smoke (recorded in spec as the validation):
19
+ 1. Start daemon with real LLM provider.
20
+ 2. Open WebUI on the same session.
21
+ 3. Open `scorel attach --remote ws://... --session <id>` simultaneously.
22
+ 4. Send prompt from WebUI; verify CLI shows identical events.
23
+ 5. Send prompt from CLI; verify WebUI shows identical events.
24
+ 6. While a long tool call runs, click Cancel in WebUI; verify both clients receive `turn_end` with cancelled stop reason.
25
+ 7. Repeat (6) but click cancel in CLI (Ctrl-C); verify both clients receive cancelled.
26
+
27
+ ## Not In Scope
28
+
29
+ - `New Chat` (S0039).
30
+ - WebUI implementation of `scorel attach`-style logs panel.
31
+ - Automated end-to-end harness (Playwright); manual is fine v1.
32
+
33
+ ## Acceptance Criteria
34
+
35
+ - WebUI Cancel button appears at the right time, dispatches `cancel`, transitions UI on daemon `turn_end`.
36
+ - Esc keybind works.
37
+ - Diagnostics debug panel renders the documented summary when `?debug=1` is appended; absent otherwise.
38
+ - Manual smoke (above) passes on a real daemon + real LLM provider.
39
+ - `pnpm --filter @scorel/webui typecheck && pnpm --filter @scorel/webui test` passes.
40
+ - Repo `pnpm typecheck && pnpm test` passes.
41
+
42
+ ## Tests
43
+
44
+ - Composer cancel-button visibility tests (turn_start visible, turn_end hidden, error keeps button hidden).
45
+ - Cancel dispatch test: clicking calls `client.cancel()`; daemon error response shows inline error.
46
+ - Esc hotkey test (jsdom keyboard event).
47
+ - Manual smoke: documented above.
48
+
49
+ ## Affected Paths
50
+
51
+ - `apps/webui/components/chatbox/composer.tsx` (extend with Cancel)
52
+ - `apps/webui/components/chatbox/composer.test.tsx`
53
+ - `apps/webui/lib/connection/session.ts` (track `inFlight` state from turn events)
54
+ - `apps/webui/lib/diagnostics/connection-summary.ts` (new)
55
+ - `apps/webui/components/chatbox/debug-panel.tsx` (new — only mounted when `?debug=1`)
56
+ - `apps/webui/app/devices/[deviceId]/projects/[projectSlug]/sessions/[sessionId]/page.tsx` (mount debug panel conditionally)
57
+ - `docs/ROADMAP.md` (M5 step entry for S0038)
58
+ - `self/discussions/2026-05-30-webui-rebuild-brainstorm.md` (append manual smoke result note)
59
+
60
+ ## Risks And Boundaries
61
+
62
+ - Cancellation race: the user may click Cancel while a `turn_end` is in flight; UI must accept the late `turn_end` and clear the cancelling state cleanly.
63
+ - Multi-tab WebUI on the same session: each tab dispatches its own cancel; daemon dedup by session id; final `turn_end` is shared.
64
+ - Debug panel must be inert in production builds — gate strictly by `?debug=1`; do not ship it on by default.
65
+ - Smoke is manual; document it precisely so the next person runs it identically.
@@ -0,0 +1,74 @@
1
+ # S0039: WebUI End-to-End Validation And New Chat
2
+
3
+ ## Goal
4
+
5
+ Add `New Chat` (creates a new session under the currently selected project) and run a full real-daemon, real-LLM end-to-end pass. This spec is the gate for marking M5 WebUI Done.
6
+
7
+ ## Scope
8
+
9
+ - `New Chat` action in sidebar:
10
+ - Visible only when a project is selected (`/devices/:deviceId/projects/:projectSlug` or descendants).
11
+ - Click flow:
12
+ 1. Call `client.createSession({ meta: { projectSlug, model: <default-from-daemon-config>, title: "New chat" } })`.
13
+ 2. Daemon returns `{ sessionId }`.
14
+ 3. Optimistically prepend the new session to `DeviceProject.sessions`.
15
+ 4. Navigate to `/devices/:deviceId/projects/:projectSlug/sessions/:sessionId`.
16
+ - Failure: surface inline toast/banner; do not navigate.
17
+ - Session-creation server side already exists (`create_session` in protocol/daemon). v1 does not introduce cwd input; the daemon decides cwd by its own startup. Document this.
18
+ - End-to-end validation matrix (manual, gated on real LLM provider):
19
+ 1. Fresh state: clear `~/.scorel/`, clear browser localStorage.
20
+ 2. Start daemon: `scorel daemon serve --remote --token TOKEN --port PORT --cwd /path/to/repo`.
21
+ 3. WebUI: open root, navigate to `/settings`, add Device with the daemon's link/token. Verify sidebar dot turns green.
22
+ 4. Verify Device → Project tree shows the cwd's project; `displayName` is `basename(cwd)`.
23
+ 5. Click `New Chat`. Verify navigation to a new session route and an empty chatbox.
24
+ 6. Send "list files" prompt. Verify streaming text + tool call(s) + tool result(s) render correctly.
25
+ 7. Click Cancel mid-tool-loop on a longer prompt; verify cancel acknowledged.
26
+ 8. Open `scorel attach --remote ws://… --session <id>` in another terminal; verify both clients show identical transcript.
27
+ 9. Send prompt from CLI; verify WebUI updates live.
28
+ 10. Reload WebUI tab; chatbox restored from cache, then resyncs.
29
+ 11. Stop daemon; sidebar shows offline state. Restart daemon; click Reconnect; sidebar returns to green.
30
+ 12. Refresh `~/.scorel/sessions/`: confirm a JSONL file exists with the right `projectSlug` in header.
31
+ - WebUI README in `apps/webui/README.md` documenting the dev workflow:
32
+ - install / dev / build commands
33
+ - how to point WebUI at a real daemon
34
+ - known limitations (token cleartext in localStorage; manual reconnect on errors; no Skills/Plugins/Automations v1)
35
+
36
+ ## Not In Scope
37
+
38
+ - Optional Playwright wiring is welcome but not required; if added, it is supplementary to the manual matrix.
39
+ - Custom `cwd` input on `New Chat`.
40
+ - Project search, session search.
41
+ - Branch / fork / compact UX.
42
+
43
+ ## Acceptance Criteria
44
+
45
+ - `New Chat` button creates a session via daemon and navigates to it; failure shows banner without navigating.
46
+ - Manual end-to-end validation matrix above passes on a real daemon + real LLM provider on the engineer's local machine; results recorded in `self/discussions/2026-05-30-webui-rebuild-brainstorm.md` append section.
47
+ - README in `apps/webui` describes the user-visible dev workflow accurately.
48
+ - `pnpm --filter @scorel/webui typecheck && pnpm --filter @scorel/webui test` passes.
49
+ - Repo `pnpm typecheck && pnpm test` passes.
50
+ - `docs/ROADMAP.md` M5 status updated to **Done** in this spec's commit chain (only the final spec flips status).
51
+
52
+ ## Tests
53
+
54
+ - Unit test for `New Chat` action: stubs `createSession`, asserts optimistic prepend + navigation.
55
+ - Failure path test: error response keeps cache untouched and surfaces banner.
56
+ - Manual: full matrix as above.
57
+
58
+ ## Affected Paths
59
+
60
+ - `apps/webui/lib/sync/session-create.ts` (new)
61
+ - `apps/webui/lib/sync/session-create.test.ts` (new)
62
+ - `apps/webui/components/shell/new-chat-button.tsx` (new)
63
+ - `apps/webui/components/shell/sidebar.tsx` (mount New Chat in proper slot)
64
+ - `apps/webui/README.md` (new)
65
+ - `docs/ROADMAP.md` (M5 status → Done)
66
+ - `docs/spec/client.md` (note `createSession` is part of WebUI flow now)
67
+ - `self/discussions/2026-05-30-webui-rebuild-brainstorm.md` (append validation results)
68
+
69
+ ## Risks And Boundaries
70
+
71
+ - **`New Chat` cwd**: v1 ties new sessions to daemon cwd. If a user wants different cwd, they must run a separate daemon. Document clearly.
72
+ - **Default model**: pulled from daemon config. WebUI does not let the user pick model v1; raise as a follow-up.
73
+ - **Manual matrix repeatability**: relies on the engineer's environment; record exact commands and observed behavior in the discussion log so the next pass is reproducible.
74
+ - **Marking M5 Done**: once this spec ships, ROADMAP M5 status flips to Done; any further WebUI feature work uses new milestone or post-M5 specs.
@@ -0,0 +1,227 @@
1
+ # S0040: WebUI Codex-Style Visual Pass + Design Tokens
2
+
3
+ ## Goal
4
+
5
+ Replace the v1 zinc-grayscale Tailwind utility soup with a Codex App-style visual pass: warm-paper background, ink-blue accent, serif display + sans body + JetBrains Mono code. Introduce CSS-variable-backed design tokens so every component reads semantic classes (`bg-surface`, `text-muted`, `border-subtle`, `text-accent`) instead of literal hex / `zinc-*`. No new functionality — purely a visual + token foundation that S0041 (markdown) and S0042 (streaming UX) build on.
6
+
7
+ Locked decisions live in `self/discussions/2026-05-31-webui-polish-brainstorm.md` §5.5 and §5.5.1. This spec is the implementation contract.
8
+
9
+ ## Scope
10
+
11
+ ### Design tokens — `apps/webui/app/globals.css`
12
+
13
+ Define semantic CSS variables on `:root`. **Do not write `dark:` variants in this spec; dark mode is backlog.** Schema:
14
+
15
+ ```css
16
+ :root {
17
+ /* color */
18
+ --color-bg: #f6f1e7; /* warm paper */
19
+ --color-surface: #fbf7ee; /* card / sidebar surface */
20
+ --color-surface-raised: #ffffff;/* composer, modal */
21
+ --color-border: #e6dfd0; /* subtle separator */
22
+ --color-border-strong: #c9bfa8;
23
+ --color-text: #1f1b16; /* primary text */
24
+ --color-text-muted: #5b524a;
25
+ --color-text-faint: #8a8076;
26
+ --color-accent: #1e3a8a; /* ink-blue primary action */
27
+ --color-accent-hover: #1e40af;
28
+ --color-accent-soft: #dbe3f5; /* accent background tint */
29
+ --color-status-ok: #2f7d4f;
30
+ --color-status-warn: #b27a18;
31
+ --color-status-err: #b3261e;
32
+ --color-status-idle: #8a8076;
33
+
34
+ /* typography */
35
+ --font-display: "Newsreader", ui-serif, Georgia, serif;
36
+ --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
37
+ --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
38
+
39
+ /* sizes */
40
+ --text-xs: 12px; --leading-xs: 18px;
41
+ --text-sm: 13px; --leading-sm: 20px;
42
+ --text-base: 14px; --leading-base: 22px;
43
+ --text-md: 16px; --leading-md: 24px;
44
+ --text-lg: 18px; --leading-lg: 26px;
45
+ --text-xl: 22px; --leading-xl: 30px;
46
+
47
+ /* spacing — 4px step */
48
+ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
49
+ --space-5: 20px; --space-6: 24px; --space-8: 32px;
50
+
51
+ /* radius */
52
+ --radius-sm: 4px; --radius-md: 6px; --radius-lg: 10px;
53
+
54
+ /* shadow */
55
+ --shadow-sm: 0 1px 2px rgba(31, 27, 22, 0.05);
56
+ --shadow-md: 0 2px 8px rgba(31, 27, 22, 0.08);
57
+ --shadow-focus: 0 0 0 3px rgba(30, 58, 138, 0.25);
58
+ }
59
+ ```
60
+
61
+ ### Tailwind theme.extend — `apps/webui/tailwind.config.ts`
62
+
63
+ Map every CSS var to a Tailwind utility. After this spec, components write `bg-surface`, `text-muted`, `border-subtle`, `text-accent`, `font-display`, `text-md`, `space-y-3`, `rounded-md`, `shadow-md`, etc. Concretely:
64
+
65
+ ```ts
66
+ theme: {
67
+ extend: {
68
+ colors: {
69
+ bg: "var(--color-bg)",
70
+ surface: "var(--color-surface)",
71
+ "surface-raised": "var(--color-surface-raised)",
72
+ border: { DEFAULT: "var(--color-border)", strong: "var(--color-border-strong)" },
73
+ subtle: "var(--color-border)",
74
+ faint: "var(--color-text-faint)",
75
+ muted: "var(--color-text-muted)",
76
+ accent: { DEFAULT: "var(--color-accent)", hover: "var(--color-accent-hover)", soft: "var(--color-accent-soft)" },
77
+ status: {
78
+ ok: "var(--color-status-ok)",
79
+ warn: "var(--color-status-warn)",
80
+ err: "var(--color-status-err)",
81
+ idle: "var(--color-status-idle)",
82
+ },
83
+ },
84
+ textColor: {
85
+ DEFAULT: "var(--color-text)",
86
+ muted: "var(--color-text-muted)",
87
+ faint: "var(--color-text-faint)",
88
+ accent: "var(--color-accent)",
89
+ },
90
+ fontFamily: {
91
+ display: "var(--font-display)",
92
+ sans: "var(--font-body)",
93
+ mono: "var(--font-mono)",
94
+ },
95
+ fontSize: {
96
+ xs: ["var(--text-xs)", "var(--leading-xs)"],
97
+ sm: ["var(--text-sm)", "var(--leading-sm)"],
98
+ base: ["var(--text-base)", "var(--leading-base)"],
99
+ md: ["var(--text-md)", "var(--leading-md)"],
100
+ lg: ["var(--text-lg)", "var(--leading-lg)"],
101
+ xl: ["var(--text-xl)", "var(--leading-xl)"],
102
+ },
103
+ spacing: {
104
+ 1: "var(--space-1)", 2: "var(--space-2)", 3: "var(--space-3)",
105
+ 4: "var(--space-4)", 5: "var(--space-5)", 6: "var(--space-6)", 8: "var(--space-8)",
106
+ },
107
+ borderRadius: { sm: "var(--radius-sm)", md: "var(--radius-md)", lg: "var(--radius-lg)" },
108
+ boxShadow: {
109
+ sm: "var(--shadow-sm)",
110
+ md: "var(--shadow-md)",
111
+ focus: "var(--shadow-focus)",
112
+ },
113
+ },
114
+ },
115
+ ```
116
+
117
+ ### Fonts — self-host woff2
118
+
119
+ - Add dev dependency `@fontsource/newsreader` (serif display) and `@fontsource/jetbrains-mono`.
120
+ - Body sans uses platform `system-ui` stack (no font load → instant first paint).
121
+ - Import the two npm font packages in `apps/webui/app/layout.tsx` once at the top: `import "@fontsource/newsreader/400.css"; import "@fontsource/newsreader/600.css"; import "@fontsource/jetbrains-mono/400.css"; import "@fontsource/jetbrains-mono/500.css";`
122
+ - No Google Fonts network calls. Document in README that fonts are bundled.
123
+
124
+ ### Component refactor (utility-class swap)
125
+
126
+ Replace literal `zinc-*` / `emerald-*` / `red-*` / `amber-*` references with semantic tokens across:
127
+
128
+ - `apps/webui/app/layout.tsx` — `bg-bg text-text font-sans`; main shell flex layout untouched.
129
+ - `apps/webui/components/shell/topbar.tsx` — `bg-surface border-b border-subtle`; title in `font-display text-lg`; "disconnected" badge uses `text-faint`; Settings link uses `text-accent hover:text-accent-hover`.
130
+ - `apps/webui/components/shell/sidebar.tsx` — `bg-surface border-r border-subtle`; section headers `font-display text-sm text-muted`; tree rows `text-sm text-text`; active row `bg-accent-soft text-accent` + left border 2px accent.
131
+ - `apps/webui/components/shell/device-status.tsx` — dot colors map: idle→`bg-status-idle`, connecting/reconnecting→`bg-status-warn`, connected→`bg-status-ok`, error→`bg-status-err`. Tooltip text uses semantic colors.
132
+ - `apps/webui/components/shell/project-node.tsx` / `session-node.tsx` / `new-chat-button.tsx` — same swap.
133
+ - `apps/webui/components/settings/device-form.tsx` / `device-list.tsx` — form fields adopt `bg-surface-raised border border-subtle rounded-md`; primary submit `bg-accent text-surface-raised hover:bg-accent-hover`; destructive (Delete) `text-status-err border-status-err`.
134
+ - `apps/webui/components/chatbox/composer.tsx` — textarea on `bg-surface-raised border border-subtle rounded-md` with `focus-visible:shadow-focus`; Send button `bg-accent`; Cancel button `bg-status-err` only while in-flight.
135
+ - `apps/webui/components/chatbox/transcript.tsx` — `bg-bg`; turn separator uses `border-t border-subtle/50`.
136
+ - `apps/webui/components/chatbox/turn-user.tsx` — user bubble `bg-accent-soft border border-subtle rounded-md p-3`; text in `text-text` body sans.
137
+ - `apps/webui/components/chatbox/turn-assistant.tsx` — assistant `bg-surface border border-subtle rounded-md p-3`; serif display reserved for any future heading inside content (not used in this spec).
138
+ - `apps/webui/components/chatbox/turn-tool.tsx` — `bg-surface-raised border border-subtle rounded-md`; collapse toggle uses `text-accent`; JSON dump in `font-mono text-xs text-muted`.
139
+ - `apps/webui/components/chatbox/debug-panel.tsx` — keep fixed bottom-right; `bg-surface-raised text-text border border-subtle shadow-md font-mono text-xs`. Header `font-display text-sm`.
140
+
141
+ ### Focus ring + hover globals
142
+
143
+ - All interactive elements use `focus-visible:outline-none focus-visible:shadow-focus` (no rings on mouse focus).
144
+ - Button hover uses double axis: `hover:bg-*-hover hover:border-strong`.
145
+ - Add a `.btn-primary` / `.btn-ghost` / `.btn-danger` class set in `globals.css` for Send / Cancel / Delete reuse, OR keep utility-only — pick utility-only for v1 to avoid CSS-vs-Tailwind dual ownership. Document choice inline.
146
+
147
+ ### Layout density
148
+
149
+ - Sidebar internal padding `p-3` (12px), section gap `space-y-2`, row height ~28px.
150
+ - Chatbox transcript max-width `max-w-3xl mx-auto` to mirror Codex App reading width on wide screens.
151
+ - Composer fixed bottom of chatbox column with `p-3` and 1px top border.
152
+ - Topbar height ~44px (`h-11`), `px-4`.
153
+
154
+ ### Boundary rule update
155
+
156
+ - `apps/webui/src/package-boundaries.test.ts`: extend the allowed externals to include the two `@fontsource/*` packages. Justify each in PR description.
157
+
158
+ ## Not In Scope
159
+
160
+ - Dark mode (backlog).
161
+ - Markdown / code highlight rendering inside chatbox (S0041).
162
+ - Streaming cursor animation, autoscroll behavior, jump-to-bottom button (S0042).
163
+ - Tool block specialization (Bash/Edit/diff viewer; not in this milestone — unified rendering decided in §5.5).
164
+ - Cmd+K, keyboard shortcuts, sidebar collapse persistence (backlog).
165
+ - Base UI integration scaffolding (introduced as needed in S0041 / later spec; this spec stays utility-only).
166
+ - Tailwind plugin `@tailwindcss/typography` — not added until S0041 needs it for markdown prose.
167
+ - Animations beyond focus ring transitions.
168
+
169
+ ## Acceptance Criteria
170
+
171
+ - `apps/webui/app/globals.css` defines every CSS var listed in §Scope under `:root`.
172
+ - `apps/webui/tailwind.config.ts` `theme.extend` exposes them as semantic Tailwind classes.
173
+ - No literal `zinc-*` / `emerald-*` / `red-*` / `amber-*` colors remain in `apps/webui/{app,components}/**/*.{ts,tsx}`. Verified by an extended boundary test.
174
+ - Every interactive element (button, link, input, summary, listitem with click) has `focus-visible:shadow-focus`.
175
+ - `@fontsource/newsreader` and `@fontsource/jetbrains-mono` imported once in `app/layout.tsx`. Build emits font woff2 to static output. No Google Fonts request observed at runtime.
176
+ - Topbar, Sidebar, Chatbox, Settings page, Composer all visibly use warm-paper background, serif display for headings, ink-blue accent for primary actions.
177
+ - `apps/webui/src/package-boundaries.test.ts` extended:
178
+ 1. Allowed externals: include `@fontsource/newsreader` and `@fontsource/jetbrains-mono`.
179
+ 2. New rule: scan non-test `.ts`/`.tsx` under `app/`, `components/` and fail if any literal `(zinc|emerald|red|amber|sky|stone|slate|gray)-\d{2,3}` appears (regex match in className strings). The test allows the matched class only inside test files (already excluded).
180
+ - `pnpm --filter @scorel/app-webui typecheck && pnpm --filter @scorel/app-webui test` passes.
181
+ - `pnpm --filter @scorel/app-webui build` succeeds; bundle size delta from font packages documented in PR description.
182
+ - Repo-level `pnpm typecheck && pnpm test` passes.
183
+ - Manual visual check (browser, after `pnpm --filter @scorel/app-webui dev`): seven routes render with the new visual; engineer takes a screenshot of the chatbox route and pastes it into PR description.
184
+
185
+ ## Tests
186
+
187
+ - Extend `apps/webui/src/package-boundaries.test.ts` with:
188
+ - `it("forbids literal palette utilities outside design tokens")` — regex over allowed source files.
189
+ - `it("allows @fontsource/* packages in externals whitelist")` — import scanner already covers this; add explicit assertion.
190
+ - Update one existing component test (`device-status.test.tsx` is the cleanest) to assert the dot now uses `bg-status-ok` etc., not `bg-emerald-500`.
191
+ - All other component tests should keep passing without modification (semantic classes are interchangeable with literal palette as far as Testing Library assertions on visible text go). If any test asserts a literal palette className, update it to the new token name.
192
+ - Manual: run `pnpm --filter @scorel/app-webui dev`; click through Settings → Add Device → /devices/:id → Project → Session → New Chat. Confirm the warm paper + ink-blue + serif headings impression on each screen.
193
+
194
+ ## Affected Paths
195
+
196
+ - `apps/webui/app/globals.css`
197
+ - `apps/webui/app/layout.tsx`
198
+ - `apps/webui/tailwind.config.ts`
199
+ - `apps/webui/package.json` (+ `pnpm-lock.yaml`) — add `@fontsource/newsreader`, `@fontsource/jetbrains-mono`
200
+ - `apps/webui/components/shell/topbar.tsx`
201
+ - `apps/webui/components/shell/sidebar.tsx`
202
+ - `apps/webui/components/shell/device-status.tsx`
203
+ - `apps/webui/components/shell/device-status.test.tsx`
204
+ - `apps/webui/components/shell/project-node.tsx`
205
+ - `apps/webui/components/shell/session-node.tsx`
206
+ - `apps/webui/components/shell/new-chat-button.tsx`
207
+ - `apps/webui/components/settings/device-form.tsx`
208
+ - `apps/webui/components/settings/device-list.tsx`
209
+ - `apps/webui/components/chatbox/composer.tsx`
210
+ - `apps/webui/components/chatbox/transcript.tsx`
211
+ - `apps/webui/components/chatbox/turn-user.tsx`
212
+ - `apps/webui/components/chatbox/turn-assistant.tsx`
213
+ - `apps/webui/components/chatbox/turn-tool.tsx`
214
+ - `apps/webui/components/chatbox/debug-panel.tsx`
215
+ - `apps/webui/src/package-boundaries.test.ts`
216
+ - `docs/ROADMAP.md` — add M5.10 polish stage entry, flip S0040 row to Done
217
+ - `apps/webui/README.md` — note self-hosted fonts and design token approach
218
+
219
+ ## Risks And Boundaries
220
+
221
+ - **Color contrast**: warm paper + ink-blue passes WCAG AA on body text but the muted variant (`#5b524a` on `#f6f1e7`) sits near the threshold. If a reviewer eyeballs it as too low, raise `--color-text-muted` luminance one notch — do not ship below AA.
222
+ - **Font load FOUT**: woff2 self-hosted is fast but still flashes once. Acceptable v1; document.
223
+ - **Token churn**: changing one CSS var shifts the whole UI. That is the point. PR review must walk every screen.
224
+ - **Tailwind 4 + var-only colors**: Tailwind 4's color resolver supports `var()` natively; no `rgb(var(--x) / <alpha-value>)` trick needed. Verify in `next build`.
225
+ - **Boundary test regex**: the literal-palette ban must allow `prose-zinc` (Tailwind typography plugin variant) once S0041 adds it; regex should match standalone `\b(zinc|...)-\d` only, not as a suffix or prefix component. Implement carefully.
226
+ - **No dark mode**: every CSS var is single-value. When dark mode lands, flip to `prefers-color-scheme: dark` block in `globals.css` reusing the same var names. Components stay unchanged.
227
+ - **PR scope**: this spec touches almost every UI file. Keep one commit per `S0040: feat: …` per repo convention; final commit message: `S0040: feat: apply codex-style visual pass with design tokens`.
@@ -0,0 +1,248 @@
1
+ # S0041: WebUI Markdown Rendering, Code Highlight, Unified Tool Block
2
+
3
+ ## Goal
4
+
5
+ Replace the v1 plain `<pre>` text in `turn-user.tsx` / `turn-assistant.tsx` with a real markdown renderer (GFM + sanitize), add lazy-loaded Shiki code highlighting, render thinking blocks (default folded), and route the existing tool result JSON dump through the same markdown pipeline so every turn type shares one rendering path. Locks the stack chosen in `self/discussions/2026-05-31-webui-polish-brainstorm.md` §8.2.2.
6
+
7
+ Builds on S0040 design tokens (semantic colors, fonts, focus ring).
8
+
9
+ ## Stack
10
+
11
+ - `react-markdown@^10.1.0` — React-tree renderer, no `dangerouslySetInnerHTML`.
12
+ - `remark-gfm@^4.0.1` — tables, task lists, strikethrough, autolink.
13
+ - `rehype-sanitize@^6.0.0` — schema-based hast sanitization (XSS guard).
14
+ - `shiki@^4.1.0` + `@shikijs/rehype@^4.1.0` — VS Code-grade syntax highlighting, lazy split out of the main bundle.
15
+ - `@tailwindcss/typography@^0.5.x` (Tailwind 4 compatible) — provides `prose` defaults; tinted to design tokens.
16
+
17
+ ## Scope
18
+
19
+ ### `apps/webui/components/chatbox/markdown-view.tsx` (new)
20
+
21
+ Single source of truth for any markdown-bearing content. Exports `<MarkdownView text={...} />`.
22
+
23
+ Behavior:
24
+
25
+ - `react-markdown` configured with `remarkPlugins=[remarkGfm]`, `rehypePlugins=[[rehypeSanitize, sanitizeSchema]]`.
26
+ - **Never enable `rehype-raw`.** Document inline.
27
+ - Custom `components`:
28
+ - `code` (inline) → `<code class="rounded bg-surface-raised px-1 text-mono text-xs">`.
29
+ - `code` (block) → lazy `<ShikiCodeBlock>` (see below). Fallback `<pre><code>` shown via `<Suspense>`.
30
+ - `a` → `target="_blank" rel="noreferrer noopener"` forced; tokenized class `text-accent hover:text-accent-hover underline-offset-2 hover:underline`.
31
+ - `table` / `th` / `td` → `border border-subtle text-sm` (typography plugin handles most, this enforces token).
32
+ - `ul[data-task-list] li` (GFM task list) → unstyled bullet, checkbox stays as rendered.
33
+ - Wrapper `<div class="prose prose-sm max-w-none">` plus `prose-tweak` overrides (defined in `globals.css`) to swap heading/link/code colors to design tokens.
34
+ - Memoize the component on the `text` prop. Same prop reference → no re-parse.
35
+
36
+ ### `apps/webui/components/chatbox/shiki-code-block.tsx` (new, lazy)
37
+
38
+ Default-export only, imported via `lazy(() => import("./shiki-code-block"))` from `markdown-view.tsx`.
39
+
40
+ Behavior:
41
+
42
+ - Uses `createHighlighterCore` from Shiki with **on-demand language imports** so the initial chunk only carries the highlighter engine + theme; languages stream in.
43
+ - Single highlighter singleton at module scope.
44
+ - Theme: one light theme that matches design tokens — pick `github-light-default` (warm) and override its background via wrapper to `bg-surface-raised`.
45
+ - Props: `{ lang: string; code: string }`.
46
+ - If `lang` not yet loaded: kick off `loadLanguage(lang)` once, render `<pre>{code}</pre>` until ready, then re-render highlighted.
47
+ - "Copy" button at top-right of each code block (icon-only, `text-faint hover:text-text`); uses `navigator.clipboard.writeText`.
48
+
49
+ ### `apps/webui/components/chatbox/turn-user.tsx` (modify)
50
+
51
+ Replace `<pre class="whitespace-pre-wrap font-sans text-sm">{part.text}</pre>` with `<MarkdownView text={part.text} />`.
52
+
53
+ User markdown is rendered (matches ChatGPT / Claude norms). User input is local — no untrusted source — but still passes through the same sanitizer for consistency.
54
+
55
+ ### `apps/webui/components/chatbox/turn-assistant.tsx` (modify)
56
+
57
+ Same swap for the `text` part. Streaming text path also routes through `MarkdownView`. Streaming cursor (▋) handled in S0042; this spec keeps current `▋` rendering as a sibling element next to `MarkdownView`, not interleaved into the markdown stream.
58
+
59
+ Add a new render branch for **thinking** parts: when `part.kind === "thinking"`, render a default-collapsed `<details>` block:
60
+
61
+ ```tsx
62
+ <details class="border border-subtle rounded-md p-2 my-2 bg-surface text-muted">
63
+ <summary class="cursor-pointer select-none font-display text-sm">Thinking…</summary>
64
+ <MarkdownView text={part.text} />
65
+ </details>
66
+ ```
67
+
68
+ Thinking is folded by default per locked decision. The protocol already carries `ThinkingContentBlock`; the projector currently treats it as text — extend `lib/events/projector.ts` so an assistant content block of type `"thinking"` becomes a `TurnPart` with `kind: "thinking"`. Update the union type accordingly.
69
+
70
+ ### `apps/webui/components/chatbox/turn-tool.tsx` (modify, unify)
71
+
72
+ Replace the raw JSON dump with a unified-rendering path:
73
+
74
+ ```tsx
75
+ const fenced = "```json\n" + JSON.stringify(payload, null, 2) + "\n```";
76
+ return <MarkdownView text={fenced} />;
77
+ ```
78
+
79
+ The `tool_call` and `tool_result` parts both pass their structured payload (args / result) through this fence. Shiki highlights the JSON automatically. Keeps the "unified rendering" decision (§5.5) intact: no Bash/Edit/diff specialization; just one path.
80
+
81
+ The collapsible `<details>` outer wrapper stays — collapsed by default for `tool_call`, expanded by default for `tool_result` whose `isError === true`.
82
+
83
+ ### `apps/webui/lib/events/projector.ts` (modify)
84
+
85
+ Extend `TurnPart` union:
86
+
87
+ ```ts
88
+ export type TurnPart =
89
+ | { kind: "text"; text: string }
90
+ | { kind: "thinking"; text: string } // new
91
+ | { kind: "tool_call"; toolCallId: string; toolName: string; args: unknown }
92
+ | { kind: "tool_result"; toolCallId: string; toolName: string; result: unknown; isError?: boolean };
93
+ ```
94
+
95
+ When projecting an `assistant_message`, walk `message.content`:
96
+
97
+ - `text` → push `{kind: "text", text}`.
98
+ - `thinking` → push `{kind: "thinking", text}`.
99
+ - `tool_call` → unchanged.
100
+
101
+ For streaming, `text_delta` continues to merge into the in-flight `text` part. **Thinking blocks are not streamed today** (no transient delta for thinking); they only appear in the final `assistant_message`. Document.
102
+
103
+ ### `apps/webui/app/globals.css` (modify)
104
+
105
+ Add `prose-tweak` overrides to align Tailwind typography defaults to design tokens. Example:
106
+
107
+ ```css
108
+ .prose-tweak {
109
+ --tw-prose-body: var(--color-text);
110
+ --tw-prose-headings: var(--color-text);
111
+ --tw-prose-links: var(--color-accent);
112
+ --tw-prose-bold: var(--color-text);
113
+ --tw-prose-code: var(--color-text);
114
+ --tw-prose-pre-bg: var(--color-surface-raised);
115
+ --tw-prose-pre-code: var(--color-text);
116
+ --tw-prose-quotes: var(--color-text-muted);
117
+ --tw-prose-th-borders: var(--color-border);
118
+ --tw-prose-td-borders: var(--color-border);
119
+ }
120
+ ```
121
+
122
+ Wrapper in `markdown-view.tsx`: `<div class="prose prose-sm max-w-none prose-tweak">`.
123
+
124
+ ### `apps/webui/tailwind.config.ts` (modify)
125
+
126
+ Add `@tailwindcss/typography` to `plugins`. Ensure Tailwind 4 compat — if the plugin is not yet released for v4 at install time, fall back to a manual minimal `prose` ruleset in `globals.css` and document. Implementation note: Tailwind 4 supports v0.5.x typography in compatible mode at the time this spec is written; verify on install.
127
+
128
+ ### `apps/webui/package.json` (modify)
129
+
130
+ Add dependencies:
131
+
132
+ ```json
133
+ "react-markdown": "^10.1.0",
134
+ "remark-gfm": "^4.0.1",
135
+ "rehype-sanitize": "^6.0.0",
136
+ "shiki": "^4.1.0",
137
+ "@shikijs/rehype": "^4.1.0",
138
+ "@tailwindcss/typography": "^0.5.16"
139
+ ```
140
+
141
+ (versions floor; pnpm install will resolve latest patch.)
142
+
143
+ ### `apps/webui/src/package-boundaries.test.ts` (modify)
144
+
145
+ Extend `ALLOWED_EXTERNALS` with `react-markdown`, `remark-gfm`, `rehype-sanitize`, `shiki`, `@shikijs/rehype`. Add `@tailwindcss/typography` only if it appears in TS imports (it usually only lives in `tailwind.config.ts`, which is on the test scan list). PR description must justify each addition in one line.
146
+
147
+ ## Streaming refresh strategy
148
+
149
+ Per §8.2.6:
150
+
151
+ - `MarkdownView` is `memo` on `text` prop.
152
+ - Each `text_delta` event triggers a single `setState` in the assistant turn; only that turn's `MarkdownView` re-parses.
153
+ - A 16ms `requestAnimationFrame` batch lives in the `transcript.tsx` streaming hook (or wherever `text_delta` already integrates) so high-rate token streams cap at one parse per frame. Existing code may already throttle implicitly via React 18's automatic batching; if not, add `useEffect` + `requestAnimationFrame` flush.
154
+ - No mid-token splitting / merge-segment optimization. Accept brief flicker on unclosed `**` or open code fences. If real-world feedback is bad, S0041.1 swaps to `streamdown` (runner-up). Document this fallback in PR.
155
+
156
+ ## Sanitizer schema
157
+
158
+ Defined inline in `markdown-view.tsx`. Start from `defaultSchema` (`rehype-sanitize`):
159
+
160
+ ```ts
161
+ const sanitizeSchema: Schema = {
162
+ ...defaultSchema,
163
+ attributes: {
164
+ ...defaultSchema.attributes,
165
+ code: [...(defaultSchema.attributes?.code ?? []), ["className"]],
166
+ span: [...(defaultSchema.attributes?.span ?? []), ["className"]],
167
+ a: [...(defaultSchema.attributes?.a ?? []), ["target"], ["rel"]],
168
+ },
169
+ tagNames: (defaultSchema.tagNames ?? []).filter(t => t !== "script" && t !== "style"),
170
+ };
171
+ ```
172
+
173
+ PR review must walk this schema once.
174
+
175
+ ## Not In Scope
176
+
177
+ - Dark mode (backlog).
178
+ - Streaming cursor animation, autoscroll, jump-to-bottom (S0042).
179
+ - Tool block specialization (Bash/Edit/Read/diff viewer/TodoWrite list — not in this milestone).
180
+ - Cmd+K / shortcuts / sidebar persistence (backlog).
181
+ - Mermaid / KaTeX / Mathjax (none of these enabled in sanitizer schema).
182
+ - Image rendering: defaultSchema permits `<img>` with `src` allowlist; we leave it as default but no special UI.
183
+ - Search / find-in-transcript.
184
+ - streamdown runner-up — only switch if real-world flicker is unacceptable; not part of this spec.
185
+
186
+ ## Acceptance Criteria
187
+
188
+ - `MarkdownView` renders user, assistant, and tool turns through one component.
189
+ - GFM features observable: tables, task lists, strikethrough, autolink, code fences.
190
+ - Sanitizer drops `<script>`, `<style>`, `onerror`, `javascript:` href; verified by unit tests.
191
+ - Code blocks render via lazy Shiki: first paint shows un-highlighted `<pre>`, then highlighted version once Shiki chunk loads. Bundle analyzer (`next build`) shows Shiki in a separate chunk, not in the main route bundle.
192
+ - Thinking blocks render as `<details>` collapsed-by-default with serif `Thinking…` summary.
193
+ - Tool turns render JSON via `\`\`\`json` fence, syntactically highlighted.
194
+ - Streaming text accumulation works: while `text_delta` events flow, the assistant turn's markdown re-parses each frame; no React error boundary triggers on mid-stream unclosed markdown.
195
+ - All link `<a>` elements have `target="_blank" rel="noreferrer noopener"`.
196
+ - `apps/webui/src/package-boundaries.test.ts` allows the six new externals; PR description justifies each.
197
+ - `pnpm --filter @scorel/app-webui typecheck && pnpm --filter @scorel/app-webui test` passes.
198
+ - `pnpm --filter @scorel/app-webui build` succeeds; bundle analyzer output captured in PR description; main route `First Load JS` increase ≤ 35 KB gzip (28 KB target + headroom).
199
+ - Repo-level `pnpm typecheck && pnpm test` passes.
200
+ - Manual visual smoke: open chatbox; LLM reply containing headings, lists, a code block, an inline link, a table → all render with design-token colors and serif headings.
201
+
202
+ ## Tests
203
+
204
+ - `apps/webui/components/chatbox/markdown-view.test.tsx` (new):
205
+ - Renders headings, lists, table, link.
206
+ - Strips `<script>` and `<img onerror>`.
207
+ - Forces `rel="noreferrer noopener"` on `<a>`.
208
+ - Code block falls back to `<pre>` before Shiki resolves (mock the lazy import).
209
+ - Memoization: same `text` prop → no re-parse (assert via render counter).
210
+ - `apps/webui/components/chatbox/turn-tool.test.tsx` (new):
211
+ - Tool result with structured object → JSON fence path → renders within MarkdownView.
212
+ - Default collapsed for `tool_call`, expanded for error `tool_result`.
213
+ - `apps/webui/components/chatbox/turn-assistant.test.tsx` (extend):
214
+ - Thinking part renders as `<details>` collapsed.
215
+ - Streaming text re-renders without unmount when delta extends.
216
+ - `apps/webui/lib/events/projector.test.ts` (extend):
217
+ - Assistant message with thinking + text + tool_call content blocks projects to three TurnParts in order.
218
+ - Manual: real LLM session with markdown-rich reply; visually compare to design.
219
+
220
+ ## Affected Paths
221
+
222
+ - `apps/webui/components/chatbox/markdown-view.tsx` (new)
223
+ - `apps/webui/components/chatbox/markdown-view.test.tsx` (new)
224
+ - `apps/webui/components/chatbox/shiki-code-block.tsx` (new, lazy chunk)
225
+ - `apps/webui/components/chatbox/turn-user.tsx`
226
+ - `apps/webui/components/chatbox/turn-assistant.tsx`
227
+ - `apps/webui/components/chatbox/turn-assistant.test.tsx`
228
+ - `apps/webui/components/chatbox/turn-tool.tsx`
229
+ - `apps/webui/components/chatbox/turn-tool.test.tsx` (new)
230
+ - `apps/webui/lib/events/projector.ts`
231
+ - `apps/webui/lib/events/projector.test.ts`
232
+ - `apps/webui/app/globals.css` (prose-tweak overrides)
233
+ - `apps/webui/tailwind.config.ts` (typography plugin)
234
+ - `apps/webui/package.json` (+ `pnpm-lock.yaml`)
235
+ - `apps/webui/src/package-boundaries.test.ts`
236
+ - `docs/ROADMAP.md` — flip S0041 row to Done
237
+ - `apps/webui/README.md` — note markdown stack and security considerations (no rehype-raw, sanitize schema)
238
+
239
+ ## Risks And Boundaries
240
+
241
+ - **XSS**: hardest risk. PR review must inspect `sanitizeSchema` and confirm `rehype-raw` is absent. CI lint hint: a unit test hard-codes a malicious payload (`<img src=x onerror=alert(1)>`) and asserts it does not appear in DOM.
242
+ - **Streaming flicker**: unclosed `**` / fence → state jump on token close. Accept v1; document. Fallback to streamdown if real-world bad.
243
+ - **Bundle**: 28 KB target + Shiki lazy. Watch grammar JSON size — only load languages on demand. If a code block requests `lang="rust"`, Shiki fetches `wasm/onig` + `rust.json` (~50 KB gzip per language). Acceptable.
244
+ - **Typography plugin compat**: Tailwind 4 + `@tailwindcss/typography` 0.5 may need `important: true` or a class override. If plugin breaks build, fallback path (manual prose CSS in `globals.css`) is in scope but a deviation note in PR is mandatory.
245
+ - **Boundary test churn**: five new whitelist entries in one PR. Justify each.
246
+ - **Thinking block UX**: defaulting to folded means users may not notice the model's reasoning trail. Acceptable; future spec can flip default per project setting.
247
+ - **memo correctness**: `MarkdownView` `memo` on `text` only; if `components` prop is recreated each render, memo breaks. Define `components` as module-scope constant.
248
+ - **SSR**: `react-markdown` is ESM-only and works in server components, but `turn-*.tsx` are `"use client"`, so no SSR boundary issue. Lazy Shiki import is client-only — safe.