fix(web): replay cursor movements the way a terminal paints them
Carriage return and backspace only MOVE the cursor; neither erases. Both of my earlier approximations were wrong, and I checked each case against a real terminal rather than reasoning about it: `100%\rOK` shows `OK0%`, not `OK` — the redraw is shorter than the frame beneath it, so the tail stands. `abc\b` still shows `abc`, not `ab` — a trailing backspace has nothing to overwrite. `\x1b[31mgone\rkept` paints `kept` RED, because a carriage return does not reset the graphic state, which one of my own tests had asserted the opposite of. Both now replay into a per-line column buffer with SGR state stamped per column, as a terminal stores it per cell. That gives the partial-overwrite case its real result too: red `bad`, three backspaces, then `ok` shows `okd` with the `d` still red, since `ok` reached only two of the three cells. The presenter description now also renders at every site. An expanded row draws it itself — the collapsed summary is hidden while open, so otherwise the description was visible only collapsed, the opposite of "above the card" — and the details panel draws it above the card as well. Three of my own tests encoded the wrong semantics and were corrected with their behavior, and the emit loop's gap-filling arm was removed as unreachable: `\r` and backspace only move left, so no column can be unwritten.
This commit is contained in:
@@ -267,6 +267,19 @@ describe('chat row terminal body', () => {
|
||||
expect(view.queryByText('List files')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the presenter description visible once the terminal card is expanded', () => {
|
||||
// The contract puts the description ABOVE the card. The collapsed summary is
|
||||
// hidden while a row is open, so an expanded terminal row has to draw it
|
||||
// itself or the description would only ever be visible collapsed.
|
||||
const view = render(<GenericToolCard {...ownerProps(settled({
|
||||
callView: callTerminal({ description: 'Terminal 3' }),
|
||||
}))} />)
|
||||
expect(view.getByText('Terminal 3')).toBeTruthy()
|
||||
fireEvent.click(view.container.querySelector('button')!)
|
||||
expect(view.container.querySelector('[data-terminal]')).not.toBeNull()
|
||||
expect(view.getByText('Terminal 3')).toBeTruthy()
|
||||
})
|
||||
|
||||
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')!)
|
||||
@@ -421,6 +434,17 @@ describe('DetailsPanel Output section', () => {
|
||||
expect(second.getByRole('button', { name: '展开其余 4 行输出' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders the presenter description above the card', () => {
|
||||
const view = mount(snapshot({
|
||||
nodes: [settled({ callView: callTerminal({ description: 'Terminal 3' }) })],
|
||||
}), target)
|
||||
const description = view.getByText('Terminal 3')
|
||||
const card = view.container.querySelector('[data-terminal]')
|
||||
expect(card).not.toBeNull()
|
||||
// Above, not below: document order is what places it as the card's heading.
|
||||
expect(description.compareDocumentPosition(card!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
})
|
||||
|
||||
it('resolves the prompt cwd against the session workspace', () => {
|
||||
const view = mount(snapshot({ nodes: [settled()] }), target, '/w/app')
|
||||
// No workdir in the call view: the prompt label is the workspace basename.
|
||||
|
||||
Reference in New Issue
Block a user