Skip to content

feat(chat): prose gets its own type scale — T2 (+ T5's escape hatch) - #72

Merged
ndemianc merged 2 commits into
developfrom
feat/chat-typography-t2
Aug 15, 2026
Merged

feat(chat): prose gets its own type scale — T2 (+ T5's escape hatch)#72
ndemianc merged 2 commits into
developfrom
feat/chat-typography-t2

Conversation

@ndemianc

Copy link
Copy Markdown
Contributor

Stacked on #71 (T1) — base is feat/chat-typography-t1, so this diff shows T2 alone. Merge #71 first and the base retargets to develop.

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 .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. There's a test pinning it, and a test pinning that .msg .role keeps an absolute size.

The heading scale

before after
h1 1.3em → 16.9px 1.45em → 20.3px
h2 1.18em → 15.3px 1.25em → 17.5px
h3 1.07em → 13.9px 1.1em → 15.4px
h2→h3 step 1.4px 2.1px, against a 14px body

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:

sidebar (520px)   8px   — unchanged
reading (900px)  14px   — was 13px at 13px type

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.fontSize0 follows the workbench
  • levelcode.ai.chat.proseWidth0 uses the 680px measure

0 clears 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

bypass caught at
scope the size to .msg instead of .msg .body 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.

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).

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.
@ndemianc
ndemianc force-pushed the feat/chat-typography-t2 branch from d878165 to 8f7e883 Compare August 15, 2026 21:19
Base automatically changed from feat/chat-typography-t1 to develop August 15, 2026 21:20
@ndemianc
ndemianc requested a lite review from Copilot August 15, 2026 21:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.fontSize and levelcode.ai.chat.proseWidth, and thread both values from extension host → webview via the config message.
  • Add CSS regression tests to pin scoping (no leakage to .msg chrome), heading scale steps, and that both settings reach the stylesheet and clear correctly when set to 0.

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.

Comment thread extensions/levelcode-ai/package.json Outdated
"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."
Comment thread extensions/levelcode-ai/extension.js Outdated
Comment on lines +2093 to +2096
// 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.
Comment thread extensions/levelcode-ai/extension.js Outdated
Comment on lines +2097 to +2098
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.
@ndemianc

Copy link
Copy Markdown
Contributor Author

All three valid. Fixed in 2ac2110.

0 didn't mean what any of the four places said it meant

The description said "follows the editor's UI font size"; the code cleared the inline property and let a flat --prose-size: 14px win. Making the description true turned out to be the better fix than making it match, because the flat value was hiding a real defect.

D2 argues prose should read a step above workbench chrome. 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 this whole slice argues for, pointing the wrong way, for exactly the users who most need it not to.

--prose-size: calc(var(--vscode-font-size, 13px) + 1px);

The decision now holds at every workbench size. At 13px it still resolves to 14px, so nothing moves for anyone who hasn't changed it and T1's ~108-character figure is unaffected.

While fixing the wording I found 0 was stated four different ways across package.json, extension.js, chat.html and D7 — including "unconstrained" for the width, which was never true (its default is a 680px measure; the way to widen it is a large number). All four now say the same thing: leave the stylesheet alone.

Clamping — and why the schema wasn't already doing it

Correct, and worse than it looks. minimum/maximum in the contribution schema only drive the settings editor: it draws a squiggle and saves the value anyway, and a hand-edited settings.json, a synced profile or a bad merge never passes through that UI at all.

Both values land straight in CSS, so chat.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, so they get the narrowest readable one instead of being silently ignored.

The bound is now pinned to the schema, not to a promise

The failure mode here is the same one #71 had — two sources of truth for one number. So a test reads maximum out of package.json and asserts the host clamps to that exact value, that the floor exceeds the schema minimum (which is 0, the sentinel), and that the description tells the user the range.

Each guard bypass-verified by reverting the fix:

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.

@ndemianc
ndemianc merged commit a75414a into develop Aug 15, 2026
1 check passed
@ndemianc
ndemianc deleted the feat/chat-typography-t2 branch August 15, 2026 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants