fix(web): keep escapes, UNC roots, and truncated cwd honest

Four review findings. Two are defects the previous two rounds introduced,
which the existing tests did not catch:

A backspace erased raw bytes, so one landing after an SGR reset ate part of
the escape: `\x1b[31mabc\x1b[0m\b\bXY` left `\x1b[` and repainted the rest of
the line with whatever the remainder parsed as. Backspaces now resolve over
VISIBLE characters — a CSI sequence is one indivisible unit a backspace steps
over on its way to the last printed character, so the surviving text keeps
the color its run authored.

The cwd normalizer popped a UNC share root: `\\server\share` with a `..`
became `/server`, losing the separators too. A UNC path's server and share
are its root, and Windows cannot climb above a share, so they are split off
and the remainder collapses against that root.

The other two are gaps the earlier fixes left:

The render-site fallback row still passed the args-derived summary, so any
terminal-declaring tool without its own keyed row (`terminal_send`) lost the
contract's above-card description. It now prefers the description exactly as
BashRow does.

A settled call read `call?.cwd`, which cannot tell "the call omitted a cwd"
from "the paging window dropped the call head". The second case has no cwd
anywhere and the original call may have used an explicit workdir, so it now
draws a bare `$` instead of naming the session workspace.
This commit is contained in:
Chinesezjc
2026-07-29 13:10:44 +08:00
parent 0f70886e0c
commit bbe1481a9e
8 changed files with 150 additions and 25 deletions
@@ -184,6 +184,24 @@ describe('parseAnsiLines: backspaces', () => {
])
})
it('steps over an SGR sequence instead of erasing its bytes', () => {
// `abc` reset then two backspaces then `XY`: erasing the reset's bytes would
// corrupt it and repaint the rest of the line with whatever the remainder
// parses as. The visible result is `aXY`, still red, with the reset intact.
expect(parseAnsiLines(`${sgr('31', 'abc')}${BS}${BS}XY`)).toEqual([[
{ text: 'a', style: { color: 'var(--dsw-alias-state-error-primary)' } },
{ text: 'XY', style: undefined },
]])
})
it('erases across a style boundary without dropping the styles between', () => {
// The backspace reaches back past the reset to the last printed character.
expect(parseAnsiLines(`${sgr('32', 'ok')}${ESC}[31m${BS}bad`)).toEqual([[
{ text: 'o', style: { color: 'var(--dsw-alias-state-success-primary)' } },
{ text: 'bad', style: { color: 'var(--dsw-alias-state-error-primary)' } },
]])
})
it('applies the overwrite after a carriage-return redraw, not before', () => {
// The redraw wins first; the backspace then erases inside what survived.
expect(onlySpan(`old\rnew${BS}`)).toEqual({ text: 'ne', style: undefined })