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
@@ -153,6 +153,32 @@ describe('terminalCardModel', () => {
}))?.card.cwd).toBe('../elsewhere')
})
it('keeps a UNC server and share as an unpoppable root', () => {
// Windows cannot climb above a share, so `..` from the share root stays put.
expect(terminalCardModel(settled({
callView: callTerminal({ cwd: '..' }),
}), '\\\\server\\share')?.card.cwd).toBe('\\\\server\\share')
// Below the share it pops normally, keeping the UNC separators.
expect(terminalCardModel(settled({
callView: callTerminal({ cwd: '..' }),
}), '\\\\server\\share\\app')?.card.cwd).toBe('\\\\server\\share')
// Several `..` cannot escape the root either.
expect(terminalCardModel(settled({
callView: callTerminal({ cwd: '../../..' }),
}), '\\\\server\\share\\app')?.card.cwd).toBe('\\\\server\\share')
})
it('draws a bare $ when the window dropped the call head, rather than guessing', () => {
// A truncated call carries no cwd anywhere: the result view has none, and
// the original call may have used an explicit workdir. Falling back to the
// session workspace here would name a directory the card cannot know.
expect(terminalCardModel(settled({
call: null, callView: null, resultView: resultTerminal({ title: 'ls -la' }),
}), '/w/app')?.card.cwd).toBeUndefined()
// A present call view that omits its cwd still means the workspace.
expect(terminalCardModel(settled(), '/w/app')?.card.cwd).toBe('/w/app')
})
it('carries the call view\'s description, which the contract renders above the card', () => {
expect(terminalCardModel(settled())?.description).toBe('List files')
expect(terminalCardModel(running())?.description).toBe('List files')
@@ -231,6 +257,16 @@ describe('chat row terminal body', () => {
expect(view.container.querySelectorAll('[data-terminal] [data-state]')).toHaveLength(1)
})
it('the fallback row shows the presenter description, not the args summary', () => {
// Any terminal-declaring tool without its own keyed row lands here, so the
// contract's above-card description has to win at this render site as well.
const view = render(<GenericToolCard {...ownerProps(settled({
callView: callTerminal({ description: 'Terminal 3' }),
}))} />)
expect(view.getByText('Terminal 3')).toBeTruthy()
expect(view.queryByText('List files')).toBeNull()
})
it('a running terminal call expands to the prompt line with no output yet', () => {
const view = render(<GenericToolCard {...ownerProps(running())} />)
fireEvent.click(view.container.querySelector('button')!)