feat(tui): /details Tab toggles apply immediately

Drop the pending state and confirm step: Tab cycles the highlighted
entry and applies at once, so the transcript behind the dialog is the
live preview; Enter/Esc/Ctrl+C just close.
This commit is contained in:
Turtle
2026-07-30 23:55:33 +08:00
parent f13003df75
commit 5b144238a2
11 changed files with 98 additions and 111 deletions

View File

@@ -20,41 +20,53 @@ buffer
8| "You "
style 0-2 fg=bright-magenta bold underline
9| "Inspect the renderer. "
10| "Model wait 0.0s · Completed 2026-07-30 18:00:00 "
style 0-46 dim
11| <blank>
12| "Reasoning blocks hidden. "
10| <blank>
11| "Assistant "
style 0-8 fg=bright-magenta bold underline
12| <blank>
13| "● Tool / bash / Run the coverage gate"
style 0-36 fg=green
14| "$ pnpm run test:coverage "
style 0-23 dim
13| <blank>
14| "Tool cards hidden. "
15| "/workspace/project "
style 0-17 dim
15| <blank>
16| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
style 0-17 fg=bright-magenta bold
style 18-31 dim
style 34-50 dim
style 53-57 dim
style 60-69 dim
17| " dsh > ╭ Transcript details ──────────────────────────────────────────────────╮ "
style 1-3 fg=bright-magenta bold
style 5-6 dim
style 7-7 inverse
16| "… +4 lines (Ctrl+O to expand) "
style 0-28 dim
17| "[exit 0] ╭ Transcript details ──────────────────────────────────────────────────╮ "
style 0-7 dim
style 14-85 fg=bright-magenta
18| " │ → Tool cards hidden → collapsed │ "
18| "Model wait 0.0│ → Tool cards collapsed │ "
style 0-13 dim
style 14-14 fg=bright-magenta
style 16-67 fg=bright-magenta inverse
style 16-58 fg=bright-magenta inverse
style 85-85 fg=bright-magenta
19| " │ Reasoning hidden │ "
style 14-14 fg=bright-magenta
style 27-55 dim
style 85-85 fg=bright-magenta
20| " │ │ "
20| "Reasoning bloc│ │ "
style 0-13 dim
style 14-14 fg=bright-magenta
style 85-85 fg=bright-magenta
21| " │ ↑/↓ move • Tab cycle • Enter apply • Esc cancel │ "
21| " │ ↑/↓ move • Tab toggle • Enter/Esc close │ "
style 14-14 fg=bright-magenta
style 16-62 dim
style 16-54 dim
style 85-85 fg=bright-magenta
22| " ╰──────────────────────────────────────────────────────────────────────╯ "
22| "Tool cards hid╰──────────────────────────────────────────────────────────────────────╯ "
style 0-13 dim
style 14-85 fg=bright-magenta
23-39| <blank>
23| <blank>
24| "Tool and context cards collapsed. "
style 0-32 dim
25| <blank>
26| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
style 0-17 fg=bright-magenta bold
style 18-31 dim
style 34-50 dim
style 53-57 dim
style 60-69 dim
27| " dsh > "
style 1-3 fg=bright-magenta bold
style 5-6 dim
style 7-7 inverse
28-39| <blank>

View File

@@ -663,9 +663,10 @@ describe('TUI terminal-state snapshots', () => {
harness.terminal.send('\r')
})
await checkpoint('details-command', harness.terminal, { includeScrollback: true })
// Bare /details opens the two-entry selector seeded with the current
// hidden/reasoning-off state; one Tab renders the tool-card entry's
// pending cycle as `hidden → collapsed`.
// Bare /details opens the two-entry toggle seeded with the current
// hidden/reasoning-off state; one Tab immediately cycles tool cards
// hidden -> collapsed, so the frame pins the applied notice, the restored
// tool card behind the dialog, and the updated entry value together.
await renderAfter(harness, () => {
harness.terminal.send('/details')
harness.terminal.send('\r')

View File

@@ -2544,7 +2544,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
await dispose(result)
})
it('bare /details opens the transcript-details selector and applies the confirmed state', async () => {
it('bare /details opens the transcript-details toggle and Tab applies immediately', async () => {
const result = await setup()
const open = async (): Promise<number> => {
const from = result.terminal.output.length
@@ -2563,50 +2563,40 @@ describe('pi-tui chat lifecycle and transcript', () => {
await result.ctx.commands.execute(result.agent, '/details', new AbortController().signal)
await tick()
// Esc cancels without touching the state.
const cancelOutput = result.terminal.output.length
result.terminal.send('\x1b')
await tick()
expect(result.terminal.output.slice(cancelOutput)).not.toContain('Tool and context cards')
// Tab cycles the highlighted entry's pending value; Enter applies it.
const cycled = await open()
// Each Tab applies one step immediately while the dialog stays open:
// collapsed -> expanded -> hidden -> collapsed (wraparound).
result.terminal.send('\t')
await tick()
expect(result.terminal.output.slice(cycled)).toContain('collapsed → expanded')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Tool and context cards expanded.')
// Both entries apply in one confirm: cycle tool cards through the
// wraparound back to collapsed and toggle reasoning off.
const reopened = await open()
result.terminal.send('\t')
result.terminal.send('\t')
await tick()
expect(result.terminal.output.slice(reopened)).toContain('expanded → collapsed')
expect(result.terminal.output).toContain('Tool cards hidden.')
result.terminal.send('\t')
await tick()
expect(result.terminal.output).toContain('Tool and context cards collapsed.')
// The reasoning entry toggles the same way.
result.terminal.send('\x1b[B')
result.terminal.send('\t')
await tick()
expect(result.terminal.output.slice(reopened)).toContain('shown → hidden')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Reasoning blocks hidden.')
expect(result.terminal.output).toContain('Tool and context cards collapsed.')
// Enter with no pending change closes without a notice.
const unchanged = await open()
// Enter closes without further changes.
const entered = result.terminal.output.length
result.terminal.send('\r')
await tick()
expect(result.terminal.output.slice(unchanged)).not.toContain('Tool and context cards')
expect(result.terminal.output.slice(unchanged)).not.toContain('Reasoning blocks')
expect(result.terminal.output.slice(entered)).not.toContain('Reasoning blocks')
// Ctrl+C also cancels.
const ctrlCOutput = result.terminal.output.length
await open()
// Esc and Ctrl+C also close; the reopened dialog shows the live values.
const reopened = await open()
expect(result.terminal.output.slice(reopened)).toContain('collapsed')
expect(result.terminal.output.slice(reopened)).toContain('hidden')
result.terminal.send('\x1b')
await tick()
const ctrlCOutput = await open()
result.terminal.send('\x03')
await tick()
expect(result.terminal.output.slice(ctrlCOutput)).not.toContain('Reasoning blocks shown.')
expect(result.terminal.output.slice(ctrlCOutput)).not.toContain('Reasoning blocks')
await dispose(result)
})