diff --git a/docs/CHAT-TYPOGRAPHY.md b/docs/CHAT-TYPOGRAPHY.md index dd0d4d1..0f66a1f 100644 --- a/docs/CHAT-TYPOGRAPHY.md +++ b/docs/CHAT-TYPOGRAPHY.md @@ -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 @@ -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 @@ -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 diff --git a/extensions/levelcode-ai/media/chat.html b/extensions/levelcode-ai/media/chat.html index 9337a34..54de311 100644 --- a/extensions/levelcode-ai/media/chat.html +++ b/extensions/levelcode-ai/media/chat.html @@ -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)); } diff --git a/extensions/levelcode-ai/test/webviewCss.test.js b/extensions/levelcode-ai/test/webviewCss.test.js index 318f86d..2a7f615 100644 --- a/extensions/levelcode-ai/test/webviewCss.test.js +++ b/extensions/levelcode-ai/test/webviewCss.test.js @@ -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.