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;