fix(plan-mode): promoted reviewed plan path into state before refine

handlePlanApproval and the ACP rejection path selected a resolved draft that could differ from PlanModeState.planFilePath but never updated the state, so a refine turn was rebuilt by #buildPlanModeMessage() from the stale path. Both paths now promote the reviewed path into plan-mode state.

Fixes #6569
This commit is contained in:
roboomp
2026-07-25 01:51:52 +00:00
parent c7ac15e974
commit 28068f53d3
3 changed files with 45 additions and 0 deletions
@@ -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: [
@@ -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);
@@ -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(