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

View File

@@ -616,6 +616,11 @@ describe('goodbye message and /resume', () => {
})
result.terminal.send('/resume')
result.terminal.send('\r')
// The loading picker owns input as soon as /resume runs, so the second
// scan starts after dismissing the first overlay, not by typing a second
// slash command over it.
result.terminal.send('\u001B')
await tick()
result.terminal.send('/resume')
result.terminal.send('\r')
await tick()
@@ -646,6 +651,37 @@ describe('goodbye message and /resume', () => {
expect(result.terminal.stopped).toBeGreaterThan(0)
})
it('opens a loading picker immediately and swaps in the scanned rows', async () => {
const target = header('late-listing', 10, '/workspace')
const listing = Promise.withResolvers<SessionRecord[]>()
const result = await setup({
cwd: '/workspace',
async configureContext(ctx) {
ctx.provide('tools', { get: () => undefined } as never)
const readSession = () => Promise.resolve({
session: target,
events: resumeEvents('Late listing'),
})
ctx.provide('sessionQuery', {
listSessions: () => listing.promise,
readSession,
projectSessions: projectViaReadSession(readSession),
} as never)
},
})
result.terminal.send('/resume')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Loading sessions…')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Sessions are still loading.')
listing.resolve([{ header: target, live: false, persisted: true }])
await tick(); await tick()
expect(result.terminal.output).toContain('Late listing')
await dispose(result)
})
it('drops loaded selector summaries when the TUI disposed during log reads', async () => {
const target = header('dispose-during-load', 10, '/workspace')
const loading = Promise.withResolvers<{ meta: SessionHeader; events: SessionEvent[] }>()