Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion docs/CHAT-TYPOGRAPHY.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,39 @@ the contribution schema only drive the settings *editor*; a hand-edited `setting
transcript — a panel with nothing left on screen to open settings with, whose only exit is finding the
JSON file again. `webviewCss.test.js` pins the clamp to the schema so the two cannot drift.

### D8 — Your turn sits right. The assistant's stays left.

Added after T4 shipped, from the reference: in the Claude Code console your messages are right-aligned
bubbles and the replies are left-aligned prose.

This is the half T4 was missing, and it closes a risk T4 opened rather than merely adding polish. T4
removed the labels and left the **bubble** carrying the speaker distinction alone — but the bubble is
`--field-bg` on a `--border` outline, which is near-invisible in some themes. On a low-contrast theme a
transcript could read as one undifferentiated voice, which is precisely the failure D6 was trying to
avoid. **Side is unmissable in every theme, at every contrast, and costs no chrome at all.**

Three things this decides, each with a way to get it wrong:

- **`align-items`, not `text-align`.** The *bubble* is what moves; the prose inside it stays
left-aligned. `text-align: right` looks identical on a one-line message and is unreadable on a
three-line one. The suite fails on it.
- **The bubble hugs its content** (`width: fit-content`), so "Yes" is a 47px bubble and a pasted stack
trace is a wide one. The shape of a turn now carries information the label used to spell out.
- **Capped at 85%, not 100%.** At 100% a long question fills the measure, reads as a full-width block
again, and the side cue disappears exactly when the transcript is densest.

Measured in headless Chrome at a 680px column, against `develop`:

| | before | after |
| --- | --- | --- |
| short bubble ("Yes") | 680px | **46.9px**, flush right |
| long user turn | 680px | **578px** — exactly the 85% cap, flush right |
| assistant | 680px, left | 680px, left (unchanged) |
| `text-align` inside the bubble | `start` | `start` |

Tint stays as the secondary cue rather than being removed: side alone would fail on any surface that
reflows the log to a single column.

---

## 3. Slices
Expand All @@ -185,7 +218,13 @@ selection, and every non-prose control still matches workbench chrome.
accessibility tree, and a turn start buys back part of the height it was occupying. **Exit:** the
transcript reads as prose with the bubble as the only visual speaker cue, a continuation is still
visibly tighter than a new turn, and `.msg.cont` still emits no label — verified by computed styles
against `develop`, not by eye.
against `develop`, not by eye. *(T6 then made side the primary cue, so "the bubble alone" describes
T4 as it shipped, not the current state.)*

**T6 — side** *(S)*. D8. **Shipped.** Your turn moves right and hugs its content; the assistant stays
left and full-measure. **Exit:** a short turn renders as a short right-flush bubble, a long one caps
below the column, the assistant is untouched, and the prose inside the bubble is still left-aligned —
all four measured, not eyeballed.

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

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

---

## 4. Risks, honestly
Expand Down
18 changes: 18 additions & 0 deletions extensions/levelcode-ai/media/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,24 @@
.msg .body > div > :last-child, .msg .body > p:last-child, .msg .body > ul:last-child, .msg .body > ol:last-child { margin-bottom: 0; }
.msg.user .body { background: var(--field-bg); border: 1px solid var(--border); border-radius: 10px; padding: 9px 11px; }
.msg.assistant .body { padding: 1px 2px; }
/* T6 (docs/CHAT-TYPOGRAPHY.md D8) — your turn sits RIGHT, the assistant's stays LEFT.
This is what T4 was missing. T4 dropped the labels and left the bubble carrying the speaker
distinction alone — but the bubble is `--field-bg`, which is near-invisible in some themes, so on
a low-contrast theme the transcript could read as one undifferentiated voice. Side is unmissable
in every theme, at every contrast, and costs no chrome at all.

The bubble HUGS its content rather than filling the column, so "Yes" is a short bubble and a
pasted stack trace is a wide one — the shape of a turn now carries information the label used to
spell out. Capped at 85%: at 100% a long question fills the column, reads as a full-width block
again, and the side cue disappears exactly when the transcript is densest.

`align-items`, NOT `text-align`: the bubble is the thing being placed, so the prose inside it
stays left-aligned — right-aligned body text is unreadable past one line. `.ckwrap` rides along
to the right edge, which is correct: the checkpoint control belongs to the turn it restores.
`width: fit-content` is what `align-items: flex-end` already implies; it is stated so that
changing the alignment later cannot silently stretch the bubble back to full width. */
.msg.user { display: flex; flex-direction: column; align-items: flex-end; }
.msg.user .body { width: fit-content; max-width: 85%; }
.err { color: var(--vscode-errorForeground); }
/* free-tier cap reached → a compelling upgrade CTA instead of raw 402 error text */
.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)); }
Expand Down
42 changes: 42 additions & 0 deletions extensions/levelcode-ai/test/webviewCss.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,48 @@ test('TRANSCRIPT: dropping the label does not collapse the gap between speakers'
'the bubble needs its border: --field-bg alone is near-invisible in some themes');
});

test('TRANSCRIPT: your turn sits right, the assistant stays left', () => {
// docs/CHAT-TYPOGRAPHY.md D8/T6. T4 removed the labels and left the bubble carrying the speaker
// distinction alone — but the bubble is `--field-bg`, near-invisible in some themes, so on a
// low-contrast theme the transcript could read as one undifferentiated voice. Side is unmissable in
// every theme, at every contrast, and costs no chrome.
assert.match(css, /\.msg\.user \{[^}]*display:\s*flex/, 'the user turn is no longer a flex container');
assert.match(css, /\.msg\.user \{[^}]*flex-direction:\s*column/,
'a row direction would put the checkpoint control beside the bubble instead of under it');
assert.match(css, /\.msg\.user \{[^}]*align-items:\s*flex-end/, 'the user turn is no longer pushed right');

// THE TRAP. `text-align: right` looks like the same change on a one-line message and is completely
// different on a three-line one: it right-aligns the PROSE, which is unreadable past one line.
// The bubble is the thing being placed; the words inside it stay left.
const userRules = [...css.matchAll(/\.msg\.user[^{}\n]*\{([^}]*)\}/g)].map((m) => m[1]).join(' ');
assert.ok(!/text-align\s*:\s*right/.test(userRules),
'right-align the BUBBLE (align-items), never the text — multi-line prose becomes unreadable');

// The asymmetry IS the cue. Give the assistant the same treatment and both sides move together,
// which restores exactly the undifferentiated column T4 was at risk of.
assert.ok(!/\.msg\.assistant[^{}\n]*\{[^}]*align-items:\s*flex-end/.test(css),
'the assistant must stay left — if both sides sit right there is no side cue at all');
});

test('TRANSCRIPT: the user bubble hugs its content, and is capped short of the column', () => {
const rule = /\.msg\.user \.body \{([^}]*width:\s*fit-content[^}]*)\}/.exec(css);
assert.ok(rule, 'the user bubble no longer hugs its content — "Yes" would be a full-width block');

// Capped BELOW the column: at 100% a long question fills the measure, reads as a full-width block
// again, and the side cue disappears exactly when the transcript is densest.
const cap = /max-width:\s*(\d+)%/.exec(rule[1]);
assert.ok(cap, 'the bubble needs a percentage cap, or a long turn spans the whole column');
const pct = Number(cap[1]);
assert.ok(pct >= 70 && pct <= 90,
'the cap is ' + pct + '%; below ~70% a normal question wraps far too early, above ~90% the '
+ 'asymmetry stops being visible');

// Measured in headless Chrome at a 680px column: "Yes" renders 46.9px wide and the long turn
// lands on 578px, exactly the cap. Both flush to the column's right edge; the assistant stays 680.
assert.match(css, /\.msg\.user \.body \{[^}]*background:\s*var\(--field-bg\)/,
'tint is the secondary cue and still earns its place — side alone would drop on a wrapped log');
});

test('TRANSCRIPT: the heading scale has steps you can actually see', () => {
// The old 1.3/1.18/1.07 put 0.11em between h2 and h3 — 1.4px at 13px, i.e. three levels of
// hierarchy that were indistinguishable without selecting the text.
Expand Down