feat(tui): open the /resume picker immediately with a loading state

The selector overlay opens as soon as the command dispatches: the
picker renders a loading placeholder over an undefined candidate set,
owns terminal input from its first frame, answers Enter with a
still-loading error, and cancels on Escape exactly like the loaded
list. The finished scan swaps rows in through setCandidates without
replacing the overlay; a scan failure closes it and keeps the existing
notice.
This commit is contained in:
Turtle
2026-07-31 16:39:33 +08:00
parent 3c08ca3606
commit b7ee38fef0
11 changed files with 181 additions and 44 deletions
+15 -3
View File
@@ -57,6 +57,7 @@ const CHECKPOINTS = [
'model-switching',
'errors-and-help',
'disposed-terminal',
'resume-sessions-loading',
'resume-sessions',
'resume-sessions-all-workspaces',
'status-diagnostics',
@@ -883,9 +884,13 @@ describe('TUI terminal-state snapshots', () => {
{ type: 'session/end-seed', seq: 8, time: Date.parse('2026-07-23T07:59:00.000Z'), data: {} },
],
})
const listGate = Promise.withResolvers<undefined>()
const harness = await setupSnapshot({
sessionPersistence: {
list: async () => [earlier, elsewhere],
list: async () => {
await listGate.promise
return [earlier, elsewhere]
},
load: async id => id === elsewhere.id
? log(elsewhere, 'Other workspace work', '2024-02-02')
: log(earlier, 'Resume selector design', '2024-01-01'),
@@ -893,8 +898,15 @@ describe('TUI terminal-state snapshots', () => {
}, { columns: 92, rows: 32 })
harness.terminal.send('/resume')
harness.terminal.send('\r')
// `/resume` scans persistence asynchronously, so the listing renders a tick
// after submit (the unit suite waits the same way); settle, then flush.
// The picker opens as soon as the command dispatches and owns input while
// the persistence scan is still pending, rendering a loading placeholder
// in place of rows; only the scan is gated, so this settle never lists.
await new Promise(resolve => setTimeout(resolve, 60))
await harness.terminal.flush()
await checkpoint('resume-sessions-loading', harness.terminal, { includeScrollback: true })
listGate.resolve(undefined)
// With the scan released, the listing renders a tick later (the unit suite
// waits the same way); settle, then flush.
await new Promise(resolve => setTimeout(resolve, 60))
await harness.terminal.flush()
await checkpoint('resume-sessions', harness.terminal, { includeScrollback: true })