From 5e8943fd1cf8efe4c99a3f5085f55580692490bc Mon Sep 17 00:00:00 2001 From: fz Date: Thu, 23 Jul 2026 21:34:40 +0800 Subject: [PATCH] fix(plan): make direct exit discoverable --- .../multi-turn-conversation/terminal.expected.txt | 4 ++-- examples/tui-agent/tests/tui-keyless-smoke.e2e.ts | 10 ++++++---- packages/plan/plan-mode/src/index.ts | 5 ++++- packages/plan/plan-mode/tests/plan-mode.spec.ts | 10 ++++++++-- packages/ui/tui/src/index.ts | 1 + packages/ui/tui/tests/tui.spec.ts | 5 +++++ 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.expected.txt b/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.expected.txt index 39666906a8..099bf63b81 100644 --- a/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.expected.txt +++ b/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.expected.txt @@ -11,8 +11,8 @@ buffer 2| " deepseek-v4-flash • main-session" style 1-34 dim 3| -4| " Entering plan mode (applies from the next step). " - style 1-48 fg=bright-black +4| " Entering plan mode (applies from the next step). Use /plan off to leave. " + style 1-72 fg=bright-black 5| 6| "▌ " style 0-0 fg=bright-blue diff --git a/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts b/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts index 1dd6e21533..66697673ba 100644 --- a/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts +++ b/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts @@ -87,14 +87,16 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => { const output = await smoke({ label: 'tui-agent boot', actions: [ - { waitFor: 'main-session-', send: '/plan\r' }, - { waitFor: 'Entering plan mode (applies from the next step).', send: '/exit\r' }, + { waitFor: 'main-session-', send: '/plan' }, + { waitFor: '[off|message] — Enter or leave plan mode', send: '\r' }, + { waitFor: 'Entering plan mode (applies from the next step). Use /plan off to leave.', send: '/exit\r' }, ], }) expect(output).toContain('DEEPSEEK') expect(output).toContain('HARNESS') expect(output).toContain('main-session-') - expect(output).toContain('Entering plan mode (applies from the next step).') + expect(output).toContain('[off|message] — Enter or leave plan mode') + expect(output).toContain('Entering plan mode (applies from the next step). Use /plan off to leave.') // Borderless: no box-drawing frame around the banner. expect(output).not.toContain('╭') expect(output).not.toContain('╮') @@ -127,7 +129,7 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => { ], }) expect(output).toContain('I need one decision before I continue.') - expect(output).toContain('Entering plan mode (applies from the next step).') + expect(output).toContain('Entering plan mode (applies from the next step). Use /plan off to leave.') expect(output).toContain('Leaving plan mode (applies from the next step).') expect(output).toContain('Default mode confirmed.') expect(output).toContain(String.raw`\x1b]2;MODEL_CONTROLLED\x07`) diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts index 6c9d73589c..c1ece17958 100644 --- a/packages/plan/plan-mode/src/index.ts +++ b/packages/plan/plan-mode/src/index.ts @@ -228,7 +228,10 @@ export class PlanModeService extends Service { } this.set(agent, true) if (message !== '') agent.steer([{ type: 'text', text: message }]) - return { kind: 'success', text: 'Entering plan mode (applies from the next step).' } + return { + kind: 'success', + text: 'Entering plan mode (applies from the next step). Use /plan off to leave.', + } }, }) }) diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts index bfdf2b689d..f45ada4db0 100644 --- a/packages/plan/plan-mode/tests/plan-mode.spec.ts +++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts @@ -553,7 +553,10 @@ describe('/plan', () => { expect(await ctx.commands.execute(plainAgent, '/mode', signal)).toBeUndefined() expect(await ctx.commands.execute(plainAgent, '/review', signal)).toBeUndefined() const plain = await ctx.commands.execute(plainAgent, '/plan', signal) - expect(plain).toEqual({ kind: 'success', text: 'Entering plan mode (applies from the next step).' }) + expect(plain).toEqual({ + kind: 'success', + text: 'Entering plan mode (applies from the next step). Use /plan off to leave.', + }) expect(ctx.planMode.get(plainAgent)).toEqual({ active: false, pending: true }) expect(plainSteer).not.toHaveBeenCalled() @@ -561,7 +564,10 @@ describe('/plan', () => { const messageSteer = vi.fn() ;(messageAgent as unknown as { steer: typeof messageSteer }).steer = messageSteer const plan = await ctx.commands.execute(messageAgent, '/plan draft the migration ', signal) - expect(plan).toEqual({ kind: 'success', text: 'Entering plan mode (applies from the next step).' }) + expect(plan).toEqual({ + kind: 'success', + text: 'Entering plan mode (applies from the next step). Use /plan off to leave.', + }) expect(ctx.planMode.get(messageAgent)).toEqual({ active: false, pending: true }) expect(messageSteer).toHaveBeenCalledExactlyOnceWith([{ type: 'text', text: 'draft the migration' }]) }) diff --git a/packages/ui/tui/src/index.ts b/packages/ui/tui/src/index.ts index be0f309106..dfe420f1f6 100644 --- a/packages/ui/tui/src/index.ts +++ b/packages/ui/tui/src/index.ts @@ -2384,6 +2384,7 @@ export function createTuiChat( ...ctx.commands.list(agent).map(command => ({ name: command.name, description: command.description, + ...(command.input === undefined ? {} : { argumentHint: command.input.hint }), })), ...skillCommands, ], diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts index 1409758e62..dec3a7eea5 100644 --- a/packages/ui/tui/tests/tui.spec.ts +++ b/packages/ui/tui/tests/tui.spec.ts @@ -1706,6 +1706,11 @@ describe('pi-tui chat lifecycle and transcript', () => { handler: () => ({ kind: 'error' as const, text: 'plugin error result' }), }) + result.terminal.send('/plugin-ch') + await tick() + expect(result.terminal.output).toContain(' — Run a plugin command') + result.terminal.send('\x03') + result.terminal.send('/plugin-check value ') result.terminal.send('\r') await tick()