fix(web): bind prompts to plan selection

This commit is contained in:
fz
2026-07-24 16:33:53 +08:00
parent 14524880b6
commit 76450b0c55
14 changed files with 252 additions and 21 deletions

View File

@@ -384,6 +384,100 @@ describe('prompt and cancel errors', () => {
expect(session.getSnapshot().promptError).toMatchObject({ op: 'send', error: { code: 'agent-busy' } })
})
it('waits for the current selector target and admits it with the prompt', async () => {
const { api, session } = makeSession()
api.onPlanMode = () => Promise.resolve(ok({ active: false }))
await session.open()
const selected = deferred<Awaited<ReturnType<FakeApiClient['onSetPlanMode']>>>()
api.onSetPlanMode = () => selected.promise
const selecting = session.setPlanMode(true)
const prompting = session.prompt([{ type: 'text', text: 'plan this' }], 'queue')
await Promise.resolve()
expect(api.callsOf('session.prompt')).toEqual([])
selected.resolve(ok({ active: false, pending: true }))
await selecting
expect((await prompting).ok).toBe(true)
expect(api.callsOf('session.prompt')).toEqual([{
sessionId: SID,
mode: 'queue',
content: [{ type: 'text', text: 'plan this' }],
planMode: true,
}])
})
it('does not admit a prompt when the selector request fails', async () => {
const { api, session } = makeSession()
api.onPlanMode = () => Promise.resolve(ok({ active: false }))
await session.open()
const selected = deferred<Awaited<ReturnType<FakeApiClient['onSetPlanMode']>>>()
api.onSetPlanMode = () => selected.promise
const selecting = session.setPlanMode(true)
const prompting = session.prompt([{ type: 'text', text: 'do not send' }], 'queue')
selected.resolve(err({ code: 'internal', message: 'selection failed', details: {} }))
await selecting
expect((await prompting).ok).toBe(false)
expect(api.callsOf('session.prompt')).toEqual([])
expect(session.getSnapshot().promptError).toMatchObject({
op: 'send',
error: { code: 'internal', message: 'selection failed' },
})
})
it('ignores a superseded selector failure and admits the latest successful target', async () => {
const { api, session } = makeSession()
api.onPlanMode = () => Promise.resolve(ok({ active: false }))
await session.open()
const older = deferred<Awaited<ReturnType<FakeApiClient['onSetPlanMode']>>>()
const newer = deferred<Awaited<ReturnType<FakeApiClient['onSetPlanMode']>>>()
let call = 0
api.onSetPlanMode = () => ++call === 1 ? older.promise : newer.promise
const selectPlan = session.setPlanMode(true)
const prompting = session.prompt([{ type: 'text', text: 'use latest' }], 'queue')
const selectDefault = session.setPlanMode(false)
newer.resolve(ok({ active: false }))
await selectDefault
older.resolve(err({ code: 'internal', message: 'stale failure', details: {} }))
await selectPlan
expect((await prompting).ok).toBe(true)
expect(api.callsOf('session.prompt')).toEqual([{
sessionId: SID,
mode: 'queue',
content: [{ type: 'text', text: 'use latest' }],
planMode: false,
}])
})
it('retains the latest selector failure until prompt admission observes it', async () => {
const { api, session } = makeSession()
api.onPlanMode = () => Promise.resolve(ok({ active: false }))
await session.open()
const older = deferred<Awaited<ReturnType<FakeApiClient['onSetPlanMode']>>>()
const newer = deferred<Awaited<ReturnType<FakeApiClient['onSetPlanMode']>>>()
let call = 0
api.onSetPlanMode = () => ++call === 1 ? older.promise : newer.promise
const selectPlan = session.setPlanMode(true)
const prompting = session.prompt([{ type: 'text', text: 'must stay local' }], 'queue')
const selectDefault = session.setPlanMode(false)
newer.resolve(err({ code: 'internal', message: 'latest failure', details: {} }))
await selectDefault
older.resolve(ok({ active: false, pending: true }))
await selectPlan
expect((await prompting).ok).toBe(false)
expect(api.callsOf('session.prompt')).toEqual([])
expect(session.getSnapshot().promptError).toMatchObject({
op: 'send',
error: { code: 'internal', message: 'latest failure' },
})
})
it('lands cancel failures in promptError with op=stop', async () => {
const { api, session } = makeSession()
api.onCancel = () => Promise.reject(new Error('cancel transport down'))