From a0d41efbfcc5ec7adce42f9408278826b0f5925d Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Tue, 28 Apr 2026 16:51:04 +0200 Subject: [PATCH] fix(code): make plan approval primary option match user's prior mode When the agent exits plan mode, the dialog now promotes the mode the user was in before plan mode to the top of the option list and labels it "Yes, continue ..." so the natural choice is to resume the prior mode rather than picking from a flat list. Generated-By: PostHog Code Task-Id: 8845a96b-6bc0-42f8-9a9f-c9d572a7b29d --- .../agent/src/adapters/claude/claude-agent.ts | 7 +++ .../claude/permissions/permission-handlers.ts | 4 +- .../permissions/permission-options.test.ts | 51 +++++++++++++++++++ .../claude/permissions/permission-options.ts | 40 ++++++++++++--- packages/agent/src/adapters/claude/types.ts | 1 + 5 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 packages/agent/src/adapters/claude/permissions/permission-options.test.ts diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index dc704c0d2..a715cb510 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -1047,6 +1047,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent { } const previousMode = this.session.permissionMode; this.session.permissionMode = modeId as CodeExecutionMode; + if (modeId === "plan" && previousMode !== "plan") { + this.session.modeBeforePlan = previousMode; + } try { await this.session.query.setPermissionMode(modeId as CodeExecutionMode); } catch (error) { @@ -1343,7 +1346,11 @@ export class ClaudeAcpAgent extends BaseAcpAgent { private createOnModeChange() { return async (newMode: CodeExecutionMode) => { if (this.session) { + const previousMode = this.session.permissionMode; this.session.permissionMode = newMode; + if (newMode === "plan" && previousMode !== "plan") { + this.session.modeBeforePlan = previousMode; + } } await this.updateConfigOption("mode", newMode); }; diff --git a/packages/agent/src/adapters/claude/permissions/permission-handlers.ts b/packages/agent/src/adapters/claude/permissions/permission-handlers.ts index 1f515bcf3..f9e6293c7 100644 --- a/packages/agent/src/adapters/claude/permissions/permission-handlers.ts +++ b/packages/agent/src/adapters/claude/permissions/permission-handlers.ts @@ -142,7 +142,7 @@ async function requestPlanApproval( context: ToolHandlerContext, updatedInput: Record, ): Promise { - const { client, sessionId, toolUseID } = context; + const { client, sessionId, toolUseID, session } = context; const toolInfo = toolInfoFromToolUse({ name: context.toolName, @@ -150,7 +150,7 @@ async function requestPlanApproval( }); return await client.requestPermission({ - options: buildExitPlanModePermissionOptions(), + options: buildExitPlanModePermissionOptions(session.modeBeforePlan), sessionId, toolCall: { toolCallId: toolUseID, diff --git a/packages/agent/src/adapters/claude/permissions/permission-options.test.ts b/packages/agent/src/adapters/claude/permissions/permission-options.test.ts new file mode 100644 index 000000000..b41c49318 --- /dev/null +++ b/packages/agent/src/adapters/claude/permissions/permission-options.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { buildExitPlanModePermissionOptions } from "./permission-options"; + +describe("buildExitPlanModePermissionOptions", () => { + it("does not relabel any option when no previous mode is provided", () => { + const options = buildExitPlanModePermissionOptions(); + for (const opt of options) { + expect(opt.name).not.toMatch(/^Yes, continue/); + } + expect(options[options.length - 1].optionId).toBe("reject_with_feedback"); + }); + + it("promotes the previous mode to the first position with a continue label", () => { + const options = buildExitPlanModePermissionOptions("default"); + expect(options[0]).toMatchObject({ + optionId: "default", + name: "Yes, continue manually approving edits", + }); + expect(options[options.length - 1].optionId).toBe("reject_with_feedback"); + }); + + it("relabels the auto option when it is the previous mode", () => { + const options = buildExitPlanModePermissionOptions("auto"); + expect(options[0]).toMatchObject({ + optionId: "auto", + name: 'Yes, continue in "auto" mode', + }); + }); + + it("relabels the acceptEdits option when it is the previous mode", () => { + const options = buildExitPlanModePermissionOptions("acceptEdits"); + expect(options[0]).toMatchObject({ + optionId: "acceptEdits", + name: "Yes, continue auto-accepting edits", + }); + }); + + it("ignores an unknown previous mode", () => { + const options = buildExitPlanModePermissionOptions("plan"); + expect(options[0].name).toMatch(/^Yes, /); + expect(options[0].name).not.toMatch(/^Yes, continue/); + expect(options[options.length - 1].optionId).toBe("reject_with_feedback"); + }); + + it("always keeps the reject option last", () => { + for (const previousMode of ["auto", "acceptEdits", "default", undefined]) { + const options = buildExitPlanModePermissionOptions(previousMode); + expect(options[options.length - 1].optionId).toBe("reject_with_feedback"); + } + }); +}); diff --git a/packages/agent/src/adapters/claude/permissions/permission-options.ts b/packages/agent/src/adapters/claude/permissions/permission-options.ts index fdaa8b530..ff658b536 100644 --- a/packages/agent/src/adapters/claude/permissions/permission-options.ts +++ b/packages/agent/src/adapters/claude/permissions/permission-options.ts @@ -92,7 +92,16 @@ export function buildPermissionOptions( return permissionOptions("Yes, always allow"); } -export function buildExitPlanModePermissionOptions(): PermissionOption[] { +const CONTINUE_LABELS: Record = { + auto: 'Yes, continue in "auto" mode', + acceptEdits: "Yes, continue auto-accepting edits", + default: "Yes, continue manually approving edits", + bypassPermissions: "Yes, continue bypassing all permissions", +}; + +export function buildExitPlanModePermissionOptions( + previousMode?: string, +): PermissionOption[] { const options: PermissionOption[] = []; if (ALLOW_BYPASS) { @@ -119,13 +128,30 @@ export function buildExitPlanModePermissionOptions(): PermissionOption[] { name: "Yes, and manually approve edits", optionId: "default", }, - { - kind: "reject_once", - name: "No, and tell the agent what to do differently", - optionId: "reject_with_feedback", - _meta: { customInput: true }, - }, ); + const previousIndex = previousMode + ? options.findIndex((opt) => opt.optionId === previousMode) + : -1; + if (previousIndex > 0) { + const [previous] = options.splice(previousIndex, 1); + const continueLabel = CONTINUE_LABELS[previous.optionId]; + options.unshift( + continueLabel ? { ...previous, name: continueLabel } : previous, + ); + } else if (previousIndex === 0) { + const continueLabel = CONTINUE_LABELS[options[0].optionId]; + if (continueLabel) { + options[0] = { ...options[0], name: continueLabel }; + } + } + + options.push({ + kind: "reject_once", + name: "No, and tell the agent what to do differently", + optionId: "reject_with_feedback", + _meta: { customInput: true }, + }); + return options; } diff --git a/packages/agent/src/adapters/claude/types.ts b/packages/agent/src/adapters/claude/types.ts index 91c106ff5..1efd77258 100644 --- a/packages/agent/src/adapters/claude/types.ts +++ b/packages/agent/src/adapters/claude/types.ts @@ -46,6 +46,7 @@ export type Session = BaseSession & { input: Pushable; settingsManager: SettingsManager; permissionMode: CodeExecutionMode; + modeBeforePlan?: CodeExecutionMode; modelId?: string; cwd: string; taskRunId?: string;