feat(tui): hidden mode folds a turn's assistant steps into one message

The Ctrl+O hidden phase keeps one Assistant header per turn: the first
step with visible text/reasoning owns it, later steps render as
headerless continuations, and bodiless (tool-only) steps render
nothing. Leaving hidden restores per-step headers. Pure TUI
presentation; the session log is unchanged.
This commit is contained in:
Turtle
2026-07-29 22:38:21 +08:00
parent 9e11f438f3
commit 0c9e529f74
14 changed files with 376 additions and 23 deletions
+26
View File
@@ -44,6 +44,7 @@ const CHECKPOINTS = [
'cordis-tools-pending',
'advanced-cards-collapsed',
'advanced-cards-expanded',
'tool-cards-hidden-folded',
'untrusted-controls',
'question-dialog',
'question-dialog-single-option',
@@ -609,6 +610,31 @@ describe('TUI terminal-state snapshots', () => {
await disposeSnapshot(harness)
})
it('pins the hidden phase folding a multi-step turn into one assistant message', async () => {
const harness = await setupSnapshot({
tools: ADVANCED_CARD_TOOLS,
config: { maxToolOutputLines: 3 },
}, { columns: 100, rows: 40 })
await renderAfter(harness, () => {
appendUser(harness.session, 'Refactor the renderer.')
appendAssistant(harness.session, [{ type: 'text', text: 'Inspecting the renderer first.' }])
appendToolCalls(harness.session, [
{ id: 'fold-1', name: 'bash', arguments: { command: 'pnpm run test' } },
])
appendToolResult(harness.session, 'fold-1', [{ type: 'text', text: 'all tests pass' }])
harness.session.append('step/end', { turn: 1, step: 1 })
harness.session.append('step/start', { turn: 1, step: 2 })
appendAssistant(harness.session, [{ type: 'text', text: 'The renderer is sound; no refactor needed.' }], undefined, { turn: 1, step: 2 })
harness.session.append('step/end', { turn: 1, step: 2 })
harness.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
})
// collapsed -> expanded -> hidden: one Assistant header, no tool card.
await renderAfter(harness, () => { harness.terminal.send('\x0f') })
await renderAfter(harness, () => { harness.terminal.send('\x0f') })
await checkpoint('tool-cards-hidden-folded', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('renders terminal controls as inert text across transcripts, tools, dialogs, diagnostics, and title', async () => {
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date(2026, 6, 21, 15, 0, 0).getTime())
const tools = {