fix(web): close a run at the line end and count columns correctly

Eight findings, each terminal case verified in a real terminal first:

`\x1b[32mdone\rok\x1b[0m` then `plain` shows `okne` green and `plain` in the
DEFAULT color. The replay returned the last written cell's state, so a reset
landing after the final write vanished from both the text and the returned
state — and every build tool writes exactly that shape, so the color leaked
onto all later output. The replay now converges to the state the scan ended
in, which is also what it hands to the next line.

`abcd\b\x1b[1K|` shows `   |`: CSI 1K erases THROUGH the cursor column, and
the loop stopped before it. The erase mode also reads only the first
parameter now, since a terminal treats `1;2K` exactly as `1K`.

`éx\rYZ` shows `YZ`: a combining mark takes no column, so it attaches
to the cell already written instead of advancing the cursor and leaving the
`x` standing.

`中x\rA` shows `A x`: overwriting a wide character's lead cell leaves its
spacer as a blank rather than closing the gap, which would shift everything
after it one column left.

The banner lost its span when the gutter became padding — a plain block child
only reaches the content box, so the reserved column was painted in the body
color and the card's top-left radius drawn in it. Invisible in the light
theme, where banner and body share a token; visible in the dark one. The
header now pulls back across the gutter and re-insets by the same amount.

The replay trigger matches the same CSI shape the parser accepts, so a form
like `\x1b[1;2K` can no longer skip its own erase, and `replayLine`'s JSDoc
documents its new parameter and returned pair.

Docs: four places still described the dot as sitting left of the card surface,
which stopped being true when the gutter became the card's own padding, and
two fixture comments still referenced the exit marker that was deliberately
removed.
This commit is contained in:
Chinesezjc
2026-07-29 16:10:37 +08:00
parent c87a20b506
commit b588060b90
11 changed files with 127 additions and 37 deletions

View File

@@ -193,10 +193,11 @@ it('renders the keyed bash row with a resident terminal card', async () => {
const row = keyedBashRow()
const card = row.parentElement?.querySelector('[data-terminal]')
if (card === null || card === undefined) throw new Error('keyed bash row has no resident terminal card')
// The prompt shortens the nested cwd to its last segment, the exit pill
// recovers the trailing marker's code, ANSI runs land on theme tokens, and
// the chat cap (8) collapses the middle into a head/tail split with an
// expander between them.
// The prompt shortens the nested cwd to its last segment, the exit pill comes
// from the sample's authored exit status (its body deliberately carries no
// `[exit code: N]` marker, since the real presenter consumes that one), ANSI
// runs land on theme tokens, and the chat cap (8) collapses the middle into a
// head/tail split with an expander between them.
expect(readCard(card)).toMatchInlineSnapshot(`
{
"colors": [