fix(web): bound the SGR state and follow real terminal widths
Nine findings, one critical. Terminal cases verified in a real terminal first. CRITICAL: cells held the accumulated SGR history, so every state boundary re-emitted the whole chain — output switching color without a full reset emitted O(n^2) characters. Measured: 3200 such cells produced 25 MB, and the reviewer's ~90 KB alternating-color case is well under bash's own output cap. State is now a normalized record (foreground, background, attribute set) with one canonical sequence per boundary, so the emitted text is linear in cells; the 90 KB case parses in 36 ms. That also makes the attribute closers every chalk-based tool writes actually close: `\x1b[1mbold\x1b[22mplain` leaves the following write PLAIN, which a real terminal confirms. Width follows emoji presentation, not the U+2600-U+27BF block: `A✓B` redrawn with `XY` shows `XYB`, so the check every progress line writes is ONE column. Taking the block as wide misaligned exactly the output this card exists for. Writing over either half of a wide pair blanks the other, since a terminal cannot leave one cell of a two-cell glyph standing. `line\n\x1b[0m` does not end in a newline as a string yet its last parsed line holds nothing visible, so the terminator check now reads the parsed lines — it had added a blank row and inflated the collapse count. A line with no cursor movement no longer builds a column buffer at all; only its SGR is folded, so an `ls -R` or a 5k-line log allocates nothing per character. The `.terminalDescription` rule had been inserted into an existing grouped selector, silently giving `.codeBody` description typography and changing its bottom margin from 4px to 0 — a pre-existing surface this PR does not own. Split out, `.codeBody`'s margin restored. Three comments contradicted their code: the fixture's exit-marker line (still claiming recovery from a marker deliberately removed), `bash-sample`'s header (still routing a click to the details panel, and calling the consumer's cap the block's own), and a DetailsPanel comment stacked above the wrong rule. The ui-primitives README documented only the CR/BS half of the replay, so a reader would expect `OK0%` where `100%\r\x1b[KOK` renders `OK`.
This commit is contained in:
@@ -126,9 +126,19 @@ export function TerminalBlock({
|
||||
}: TerminalBlockProps) {
|
||||
const text = output ?? ''
|
||||
// A command's output ends with a newline; that terminator is not an extra
|
||||
// blank line to draw or to count against the height cap. The copy control
|
||||
// still copies `text` untouched.
|
||||
const lines = useMemo(() => parseAnsiLines(text.endsWith('\n') ? text.slice(0, -1) : text), [text])
|
||||
// blank line to draw or to count against the height cap. The check runs on the
|
||||
// PARSED lines rather than on the raw text, because a reset after the final
|
||||
// newline (`line\n\x1b[0m`) leaves the string not ending in one while still
|
||||
// producing a last line with nothing visible in it. A genuinely blank final
|
||||
// line — the double newline — survives, since it has a real empty line before
|
||||
// the terminator. The copy control still copies `text` untouched.
|
||||
const lines = useMemo(() => {
|
||||
const parsed = parseAnsiLines(text)
|
||||
const last = parsed[parsed.length - 1]
|
||||
const terminated = parsed.length > 1 && last !== undefined
|
||||
&& last.every(span => span.text === '')
|
||||
return terminated ? parsed.slice(0, -1) : parsed
|
||||
}, [text])
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user