diff --git a/packages/coding-agent/src/modes/acp/acp-agent.ts b/packages/coding-agent/src/modes/acp/acp-agent.ts index aeeab0383..18e635944 100644 --- a/packages/coding-agent/src/modes/acp/acp-agent.ts +++ b/packages/coding-agent/src/modes/acp/acp-agent.ts @@ -1691,6 +1691,12 @@ export class AcpAgent implements Agent { planExists: true, }; if (!approved) { + // Rejection keeps plan mode active for another planning turn. Promote the + // reviewed path into plan-mode state so the next `#buildPlanModeMessage()` + // targets the plan just reviewed, not the stale state path. + if (state.planFilePath !== planFilePath) { + session.setPlanModeState({ ...state, planFilePath }); + } const normalizedTitle = normalizePlanTitle(resolvedTitle).title; return { content: [ diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index e58199a4f..6da2bf41d 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -3708,6 +3708,17 @@ export class InteractiveMode implements InteractiveModeContext { return; } + // resolveApprovedPlan may return a newer draft than the path recorded in + // plan-mode state. `AgentSession.#buildPlanModeMessage()` reads that state, + // so if the operator refines (or dismisses and keeps planning) the next + // planning turn must target the plan just reviewed — promote the reviewed + // path into plan-mode state now, mirroring the print-mode approval handler. + const planState = this.session.getPlanModeState(); + if (planState?.enabled && planState.planFilePath !== planFilePath) { + this.session.setPlanModeState({ ...planState, planFilePath }); + this.sessionManager.appendModeChange("plan", { planFilePath }); + } + const contextUsage = this.#getPlanApprovalContextUsage(); const keepContextLabel = this.#formatKeepContextLabel(contextUsage); const keepContextDisabled = this.#isKeepContextDisabled(contextUsage); diff --git a/packages/coding-agent/test/interactive-mode-plan-review.test.ts b/packages/coding-agent/test/interactive-mode-plan-review.test.ts index ddf831a64..a2fb1fa55 100644 --- a/packages/coding-agent/test/interactive-mode-plan-review.test.ts +++ b/packages/coding-agent/test/interactive-mode-plan-review.test.ts @@ -394,6 +394,34 @@ describe("InteractiveMode plan review rendering", () => { expect(onInput).toHaveBeenCalledTimes(1); }); + it("promotes the reviewed plan path into plan-mode state before refining", async () => { + const resolve = (url: string) => + resolveLocalUrlToPath(url, { + getArtifactsDir: () => session.sessionManager.getArtifactsDir(), + getSessionId: () => session.sessionManager.getSessionId(), + }); + const oldPlanPath = "local://old-plan.md"; + const newPlanPath = "local://new-draft-plan.md"; + await Bun.write(resolve(oldPlanPath), "# Old\n\nold body"); + await Bun.write(resolve(newPlanPath), "# New\n\nnew body"); + + mode.planModeEnabled = true; + mode.planModePlanFilePath = oldPlanPath; + // State still points at the previously reviewed (older) plan. + session.setPlanModeState({ enabled: true, planFilePath: oldPlanPath, workflow: "parallel", reentry: true }); + + const feedback = "Refinement feedback:\n- add more detail\n"; + vi.spyOn(mode, "showPlanReview").mockImplementation(async (_plan, _title, _options, dialogOptions) => { + dialogOptions?.onFeedbackChange?.(feedback); + return "Refine plan"; + }); + vi.spyOn(session, "prompt").mockResolvedValue(undefined as never); + + await mode.handlePlanApproval({ planFilePath: newPlanPath, planExists: true, title: "NEW" }); + + expect(session.getPlanModeState()?.planFilePath).toBe(newPlanPath); + }); + it("opens the annotation external editor from the real plan review overlay", async () => { const editorPath = path.join(tempDir.path(), "annotation-editor.sh"); await Bun.write(