Skip to content

Commit ef504f2

Browse files
authored
Merge pull request #74 from levelcodeai/feat/chat-typography-t6
feat(chat): your turn sits right, the assistant's stays left — T6
2 parents c13ec4d + f9c8f12 commit ef504f2

3 files changed

Lines changed: 105 additions & 1 deletion

File tree

docs/CHAT-TYPOGRAPHY.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,39 @@ the contribution schema only drive the settings *editor*; a hand-edited `setting
163163
transcript — a panel with nothing left on screen to open settings with, whose only exit is finding the
164164
JSON file again. `webviewCss.test.js` pins the clamp to the schema so the two cannot drift.
165165

166+
### D8 — Your turn sits right. The assistant's stays left.
167+
168+
Added after T4 shipped, from the reference: in the Claude Code console your messages are right-aligned
169+
bubbles and the replies are left-aligned prose.
170+
171+
This is the half T4 was missing, and it closes a risk T4 opened rather than merely adding polish. T4
172+
removed the labels and left the **bubble** carrying the speaker distinction alone — but the bubble is
173+
`--field-bg` on a `--border` outline, which is near-invisible in some themes. On a low-contrast theme a
174+
transcript could read as one undifferentiated voice, which is precisely the failure D6 was trying to
175+
avoid. **Side is unmissable in every theme, at every contrast, and costs no chrome at all.**
176+
177+
Three things this decides, each with a way to get it wrong:
178+
179+
- **`align-items`, not `text-align`.** The *bubble* is what moves; the prose inside it stays
180+
left-aligned. `text-align: right` looks identical on a one-line message and is unreadable on a
181+
three-line one. The suite fails on it.
182+
- **The bubble hugs its content** (`width: fit-content`), so "Yes" is a 47px bubble and a pasted stack
183+
trace is a wide one. The shape of a turn now carries information the label used to spell out.
184+
- **Capped at 85%, not 100%.** At 100% a long question fills the measure, reads as a full-width block
185+
again, and the side cue disappears exactly when the transcript is densest.
186+
187+
Measured in headless Chrome at a 680px column, against `develop`:
188+
189+
| | before | after |
190+
| --- | --- | --- |
191+
| short bubble ("Yes") | 680px | **46.9px**, flush right |
192+
| long user turn | 680px | **578px** — exactly the 85% cap, flush right |
193+
| assistant | 680px, left | 680px, left (unchanged) |
194+
| `text-align` inside the bubble | `start` | `start` |
195+
196+
Tint stays as the secondary cue rather than being removed: side alone would fail on any surface that
197+
reflows the log to a single column.
198+
166199
---
167200

168201
## 3. Slices
@@ -185,7 +218,13 @@ selection, and every non-prose control still matches workbench chrome.
185218
accessibility tree, and a turn start buys back part of the height it was occupying. **Exit:** the
186219
transcript reads as prose with the bubble as the only visual speaker cue, a continuation is still
187220
visibly tighter than a new turn, and `.msg.cont` still emits no label — verified by computed styles
188-
against `develop`, not by eye.
221+
against `develop`, not by eye. *(T6 then made side the primary cue, so "the bubble alone" describes
222+
T4 as it shipped, not the current state.)*
223+
224+
**T6 — side** *(S)*. D8. **Shipped.** Your turn moves right and hugs its content; the assistant stays
225+
left and full-measure. **Exit:** a short turn renders as a short right-flush bubble, a long one caps
226+
below the column, the assistant is untouched, and the prose inside the bubble is still left-aligned —
227+
all four measured, not eyeballed.
189228

190229
**T5 — the escape hatch** *(S)*. D7. **Folded into T2 and shipped with it.** Sequencing it last was a
191230
mistake: T2 is the one slice that changes what every existing user sees, and shipping a divisive
@@ -195,6 +234,11 @@ property reaches the webview from settings, the second is a line — so splittin
195234
Sequencing: T1 first and alone — it may turn out to be most of the perceived fix, and shipping it
196235
by itself is the cheapest way to find out before spending effort on T2–T4.
197236

237+
That held up: T1–T2, T4 and T6 have shipped in that order, each visible on its own. **T3 is the only
238+
slice of this plan still outstanding.** D8/T6 was not in the original decomposition — it came from
239+
looking at the reference again after T4, which is the argument for shipping slices small enough to
240+
look at.
241+
198242
---
199243

200244
## 4. Risks, honestly

extensions/levelcode-ai/media/chat.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,24 @@
215215
.msg .body > div > :last-child, .msg .body > p:last-child, .msg .body > ul:last-child, .msg .body > ol:last-child { margin-bottom: 0; }
216216
.msg.user .body { background: var(--field-bg); border: 1px solid var(--border); border-radius: 10px; padding: 9px 11px; }
217217
.msg.assistant .body { padding: 1px 2px; }
218+
/* T6 (docs/CHAT-TYPOGRAPHY.md D8) — your turn sits RIGHT, the assistant's stays LEFT.
219+
This is what T4 was missing. T4 dropped the labels and left the bubble carrying the speaker
220+
distinction alone — but the bubble is `--field-bg`, which is near-invisible in some themes, so on
221+
a low-contrast theme the transcript could read as one undifferentiated voice. Side is unmissable
222+
in every theme, at every contrast, and costs no chrome at all.
223+
224+
The bubble HUGS its content rather than filling the column, so "Yes" is a short bubble and a
225+
pasted stack trace is a wide one — the shape of a turn now carries information the label used to
226+
spell out. Capped at 85%: at 100% a long question fills the column, reads as a full-width block
227+
again, and the side cue disappears exactly when the transcript is densest.
228+
229+
`align-items`, NOT `text-align`: the bubble is the thing being placed, so the prose inside it
230+
stays left-aligned — right-aligned body text is unreadable past one line. `.ckwrap` rides along
231+
to the right edge, which is correct: the checkpoint control belongs to the turn it restores.
232+
`width: fit-content` is what `align-items: flex-end` already implies; it is stated so that
233+
changing the alignment later cannot silently stretch the bubble back to full width. */
234+
.msg.user { display: flex; flex-direction: column; align-items: flex-end; }
235+
.msg.user .body { width: fit-content; max-width: 85%; }
218236
.err { color: var(--vscode-errorForeground); }
219237
/* free-tier cap reached → a compelling upgrade CTA instead of raw 402 error text */
220238
.upgradecard { border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border)); border-radius: 12px; margin: 8px 2px; padding: 13px 15px 14px; background: color-mix(in srgb, var(--accent) 9%, var(--field-bg)); }

extensions/levelcode-ai/test/webviewCss.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,48 @@ test('TRANSCRIPT: dropping the label does not collapse the gap between speakers'
437437
'the bubble needs its border: --field-bg alone is near-invisible in some themes');
438438
});
439439

440+
test('TRANSCRIPT: your turn sits right, the assistant stays left', () => {
441+
// docs/CHAT-TYPOGRAPHY.md D8/T6. T4 removed the labels and left the bubble carrying the speaker
442+
// distinction alone — but the bubble is `--field-bg`, near-invisible in some themes, so on a
443+
// low-contrast theme the transcript could read as one undifferentiated voice. Side is unmissable in
444+
// every theme, at every contrast, and costs no chrome.
445+
assert.match(css, /\.msg\.user \{[^}]*display:\s*flex/, 'the user turn is no longer a flex container');
446+
assert.match(css, /\.msg\.user \{[^}]*flex-direction:\s*column/,
447+
'a row direction would put the checkpoint control beside the bubble instead of under it');
448+
assert.match(css, /\.msg\.user \{[^}]*align-items:\s*flex-end/, 'the user turn is no longer pushed right');
449+
450+
// THE TRAP. `text-align: right` looks like the same change on a one-line message and is completely
451+
// different on a three-line one: it right-aligns the PROSE, which is unreadable past one line.
452+
// The bubble is the thing being placed; the words inside it stay left.
453+
const userRules = [...css.matchAll(/\.msg\.user[^{}\n]*\{([^}]*)\}/g)].map((m) => m[1]).join(' ');
454+
assert.ok(!/text-align\s*:\s*right/.test(userRules),
455+
'right-align the BUBBLE (align-items), never the text — multi-line prose becomes unreadable');
456+
457+
// The asymmetry IS the cue. Give the assistant the same treatment and both sides move together,
458+
// which restores exactly the undifferentiated column T4 was at risk of.
459+
assert.ok(!/\.msg\.assistant[^{}\n]*\{[^}]*align-items:\s*flex-end/.test(css),
460+
'the assistant must stay left — if both sides sit right there is no side cue at all');
461+
});
462+
463+
test('TRANSCRIPT: the user bubble hugs its content, and is capped short of the column', () => {
464+
const rule = /\.msg\.user \.body \{([^}]*width:\s*fit-content[^}]*)\}/.exec(css);
465+
assert.ok(rule, 'the user bubble no longer hugs its content — "Yes" would be a full-width block');
466+
467+
// Capped BELOW the column: at 100% a long question fills the measure, reads as a full-width block
468+
// again, and the side cue disappears exactly when the transcript is densest.
469+
const cap = /max-width:\s*(\d+)%/.exec(rule[1]);
470+
assert.ok(cap, 'the bubble needs a percentage cap, or a long turn spans the whole column');
471+
const pct = Number(cap[1]);
472+
assert.ok(pct >= 70 && pct <= 90,
473+
'the cap is ' + pct + '%; below ~70% a normal question wraps far too early, above ~90% the '
474+
+ 'asymmetry stops being visible');
475+
476+
// Measured in headless Chrome at a 680px column: "Yes" renders 46.9px wide and the long turn
477+
// lands on 578px, exactly the cap. Both flush to the column's right edge; the assistant stays 680.
478+
assert.match(css, /\.msg\.user \.body \{[^}]*background:\s*var\(--field-bg\)/,
479+
'tint is the secondary cue and still earns its place — side alone would drop on a wrapped log');
480+
});
481+
440482
test('TRANSCRIPT: the heading scale has steps you can actually see', () => {
441483
// The old 1.3/1.18/1.07 put 0.11em between h2 and h3 — 1.4px at 13px, i.e. three levels of
442484
// hierarchy that were indistinguishable without selecting the text.

0 commit comments

Comments
 (0)