feat(plan): add direct plan exit command

This commit is contained in:
fz
2026-07-23 21:06:03 +08:00
parent 8833fc2b58
commit 8e4f666238
19 changed files with 143 additions and 51 deletions
@@ -546,7 +546,7 @@ describe('/plan', () => {
const plainSteer = vi.fn()
;(plainAgent as unknown as { steer: typeof plainSteer }).steer = plainSteer
expect(ctx.commands.list(plainAgent)).toEqual([
{ name: 'plan', description: 'Enter plan mode', input: { hint: '[message]' } },
{ name: 'plan', description: 'Enter or leave plan mode', input: { hint: '[off|message]' } },
])
const signal = new AbortController().signal
@@ -566,6 +566,42 @@ describe('/plan', () => {
expect(messageSteer).toHaveBeenCalledExactlyOnceWith([{ type: 'text', text: 'draft the migration' }])
})
it('leaves active plan mode, cancels a pending entry, and treats inactive exit as idempotent', async () => {
const ctx = await setup()
await ctx.plugin(CommandService)
await new Promise(resolve => setImmediate(resolve))
const signal = new AbortController().signal
const inactive = await agentWithSession(ctx, 'inactive-plan-command')
expect(await ctx.commands.execute(inactive, '/plan off', signal))
.toEqual({ kind: 'success', text: 'Plan mode is already inactive.' })
expect(ctx.planMode.get(inactive)).toEqual({ active: false })
const entering = await agentWithSession(ctx, 'entering-plan-command')
const enteringSteer = vi.fn()
;(entering as unknown as { steer: typeof enteringSteer }).steer = enteringSteer
await ctx.commands.execute(entering, '/plan', signal)
expect(await ctx.commands.execute(entering, '/plan off', signal))
.toEqual({ kind: 'success', text: 'Plan mode entry cancelled.' })
expect(ctx.planMode.get(entering)).toEqual({ active: false, pending: false })
expect(enteringSteer).not.toHaveBeenCalled()
await boundary(ctx, entering, 'turn/start')
expect(ctx.planMode.get(entering)).toEqual({ active: false })
expect(entering.session.events.some(event => event.type === 'plan/mode')).toBe(false)
const active = await agentWithSession(ctx, 'active-plan-command', { active: true })
const activeSteer = vi.fn()
;(active as unknown as { steer: typeof activeSteer }).steer = activeSteer
expect(await ctx.commands.execute(active, '/plan off', signal))
.toEqual({ kind: 'success', text: 'Leaving plan mode (applies from the next step).' })
expect(ctx.planMode.get(active)).toEqual({ active: true, pending: false })
expect(await ctx.commands.execute(active, '/plan off', signal))
.toEqual({ kind: 'success', text: 'Leaving plan mode (applies from the next step).' })
expect(activeSteer).not.toHaveBeenCalled()
await boundary(ctx, active, 'turn/start')
expect(ctx.planMode.get(active)).toEqual({ active: false })
})
it('removes the contributed command when the plan-mode plugin is disposed', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)