feat(chat): prose gets its own type scale — T2 (+ T5's escape hatch) - #72
Conversation
The chat inherited `--vscode-font-size`: the size of menu labels and tree rows. Right for a sidebar widget, wrong for reading three paragraphs, and the reason the panel felt cramped beside the reference. Message bodies now have their own size and leading (14px / 1.65) behind custom properties. Scoped to `.msg .body`, NOT `.msg` — on `.msg` it would drag the role label, the copy button and the checkpoint control up with it and the panel would stop matching the editor. A test pins that scoping, because it is the whole safety property of this slice. The heading scale widens from 1.3/1.18/1.07 to 1.45/1.25/1.1. The old scale put 0.11em between h2 and h3 — 1.4px at 13px — so three levels of hierarchy were indistinguishable without selecting the text. Measured after: 20.3 / 17.5 / 15.4 against 14px body, ~3px steps instead of 1.4px. T1's em-based rhythm now scales with it, which was the point of making it em in the first place: the paragraph gap resolves against the prose size, so it reads 8px in the sidebar and 14px at reading width without a second rule. The measure holds at ~107 characters (was ~115 at 13px, 154 before T1) — the cap tightening as the type grows, as predicted. **T5 is folded in rather than sequenced after.** Ordering it last was a mistake: T2 is the one slice that changes what every existing user sees, and shipping a divisive change with no way back is worse than not shipping it. `levelcode.ai.chat.fontSize` and `chat.proseWidth`, both defaulting to 0 meaning "follow the default" — and 0 CLEARS the custom property so the stylesheet wins again, rather than pinning whatever today's default happens to be. The doc records the reordering and why. Four guards, verified non-vacuous: scope the size to .msg (16/20), revert the heading scale (17/20), make the role label relative (16/20), hard-code the cleared fallback (18/20), drop one of the two config payloads (18/20). 33 suites green.
d878165 to
8f7e883
Compare
There was a problem hiding this comment.
Pull request overview
This PR implements T2 of the chat typography plan for extensions/levelcode-ai by giving message bodies their own prose type scale (size + leading) via CSS custom properties, while keeping surrounding “chrome” (role label, buttons, controls) at the workbench size. It also includes the T5 escape hatch plumbing by adding settings that can override (or clear) the prose size and transcript measure.
Changes:
- Add prose typography CSS variables (
--prose-size,--prose-leading) scoped to.msg .body, plus a more distinct h1/h2/h3 scale. - Add settings
levelcode.ai.chat.fontSizeandlevelcode.ai.chat.proseWidth, and thread both values from extension host → webview via theconfigmessage. - Add CSS regression tests to pin scoping (no leakage to
.msgchrome), heading scale steps, and that both settings reach the stylesheet and clear correctly when set to0.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| extensions/levelcode-ai/test/webviewCss.test.js | Adds assertions guarding prose scoping, heading scale, and settings plumbing/clearing behavior. |
| extensions/levelcode-ai/package.json | Introduces the new user-facing settings (chat.fontSize, chat.proseWidth). |
| extensions/levelcode-ai/media/chat.html | Adds prose CSS variables + .msg .body scoping, updates heading scale, and applies config-driven overrides via setProperty. |
| extensions/levelcode-ai/extension.js | Reads the new settings and includes them in both config payload paths (gateway and BYOK). |
| docs/CHAT-TYPOGRAPHY.md | Updates sequencing notes to fold the escape hatch (T5) into T2. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| "default": 0, | ||
| "minimum": 0, | ||
| "maximum": 24, | ||
| "markdownDescription": "Font size for chat **prose** (message bodies), in pixels. `0` follows the editor's UI font size.\n\nThe workbench size is tuned for menu labels and tree rows; message bodies default to a slightly larger size so a long answer reads comfortably. Controls, cards and the composer always follow the workbench." |
| // T2/T5 (docs/CHAT-TYPOGRAPHY.md D2, D7). Prose gets its own size because `--vscode-font-size` is the | ||
| // size of menu labels and tree rows — right for chrome, wrong for reading three paragraphs. Both are | ||
| // escape hatches by design: 0 means "follow the workbench" / "do not constrain", so anyone who | ||
| // preferred the old density has a one-setting way back rather than an argument. |
| const proseSize = Number(cfg.get('chat.fontSize', 0)) || 0; | ||
| const proseWidth = Number(cfg.get('chat.proseWidth', 0)) || 0; |
…e CSS Review on #72 found `0` documented as "follows the editor's UI font size" while the code cleared the inline property and let a flat `--prose-size: 14px` win. Making the description true was the better fix than making it match, because the flat value hid a real defect: D2 argues prose should read a step ABOVE workbench chrome, and a flat 14px only satisfies that at the default 13px. Raise the editor's UI font for accessibility and the relationship inverts — 14px prose inside 18px buttons, the divergence pointing the wrong way, for the users who most need it not to. `--prose-size` is now `calc(var(--vscode-font-size, 13px) + 1px)`, so the decision holds at every workbench size. At 13px it still resolves to 14px: nothing moves for anyone who has not changed it, and T1's ~108-character figure is unaffected. Both settings are now clamped at the host boundary. `minimum`/`maximum` in the contribution schema only drive the settings editor — it squiggles and saves anyway, and a hand-edited settings.json, a synced profile or a bad merge never passes through that UI. The values land directly in CSS, where `proseWidth: 1` is a one-pixel transcript: a panel with nothing left on screen to open settings with, whose only way out is finding the JSON file again. Out-of-range is pulled into range rather than rejected — someone asking for a 200px measure wants it narrow. `0` is now stated the same way in all four places it appears: it means "leave the stylesheet alone", not "unconstrained" (the width's default is a 680px measure) and not "follow the workbench" (the size's default tracks it, but by the stylesheet's offset, not by the sentinel). Guards, each bypass-verified by reverting the fix: - the size must be workbench-relative, with a px fallback, offset 1-3px, and must still resolve to 14px at the default — 4 separate failures - the clamp's ceiling must equal the schema's `maximum`, its floor must exceed the schema `minimum` (0, the sentinel), and the description must name both - 0/undefined/NaN/negative/Infinity fall back; below-floor snaps up; absurd snaps down 32 suites green.
|
All three valid. Fixed in 2ac2110.
|
| bypass | caught |
|---|---|
flat 14px instead of the offset |
✓ |
offset with no px fallback (void outside the webview) |
✓ |
| offset of 5px — too large, and no longer 14px at default | ✓ |
fontSize read without the clamp |
✓ |
clamp ceiling drifts from the schema maximum |
✓ |
sentinel 0 clamped up to the floor |
✓ |
| description stops naming the range | ✓ |
Plus behavioral coverage of the helper extracted from the shipped source: 0/undefined/NaN/negative/Infinity fall back, below-floor snaps up, absurd snaps down, in-range passes through untouched.
23 tests in webviewCss, 32 suites green.
Stacked on #71 (T1) — base is
feat/chat-typography-t1, so this diff shows T2 alone. Merge #71 first and the base retargets todevelop.The change
The chat inherited
--vscode-font-size— the size of menu labels and tree rows. Right for a sidebar widget, wrong for reading three paragraphs, and the reason the panel felt cramped beside the reference.Message bodies now have their own size and leading (14px / 1.65) behind custom properties.
Scoped to
.msg .body, not.msg— that scoping is the entire safety property of this slice. On.msgit would drag the role label, the copy button and the checkpoint control up with it, and the panel would stop matching the editor. There's a test pinning it, and a test pinning that.msg .rolekeeps an absolute size.The heading scale
The old scale put 0.11em between h2 and h3 — three levels of hierarchy that were indistinguishable without selecting the text.
T1's rhythm scales with it, for free
That was the point of making it
em. The paragraph gap resolves against the prose size, so with no second rule:And the measure tightens as the type grows: ~107 characters, from ~115 after T1 and 154 before it.
T5 is folded in, not sequenced after
Ordering the escape hatch last was a mistake in my own plan, and the doc now says so. T2 is the one slice that changes what every existing user sees — shipping that with no way back is worse than not shipping it. The plumbing is shared anyway: once one custom property reaches the webview from settings, the second is one line.
levelcode.ai.chat.fontSize—0follows the workbenchlevelcode.ai.chat.proseWidth—0uses the 680px measure0clears the custom property so the stylesheet wins again, rather than pinning whatever today's default happens to be. That's tested — a hard-coded fallback would quietly freeze the default at the value it had when someone last touched this code.Guards
.msginstead of.msg .body33 suites green.
Worth judging by eye
This is the slice with a real trade-off — the chat no longer matches workbench chrome exactly. That was D2's stated cost, and it's much easier to judge from a screenshot than from a diff. Remaining after this: T3 (code surfaces) and T4 (speaker treatment).