Skip to content

fix: reliably exit plan mode on Implement this plan#463

Open
amanthanvi wants to merge 3 commits intoDimillian:mainfrom
amanthanvi:fix/plan-mode-implement-exit
Open

fix: reliably exit plan mode on Implement this plan#463
amanthanvi wants to merge 3 commits intoDimillian:mainfrom
amanthanvi:fix/plan-mode-implement-exit

Conversation

@amanthanvi
Copy link
Contributor

Summary

  • fix plan-followup accept flow so Implement this plan never falls back to plan mode
  • keep collaboration mode UI/state and thread-scoped persisted codex params in sync when accepting/submitting plan follow-ups
  • add regression tests for default/code/non-plan fallback and plan-only race behavior

Problem

After clicking Implement this plan, some turns could start with stale plan-mode behavior (or appear to stall after the initial goal block), and UI mode state could remain on plan for the thread.

Root cause

Plan-accept mode selection could race with collaboration mode availability and fallback to an unsafe selection path, while thread-scoped mode persistence was not updated during plan follow-up actions.

What changed

src/features/app/hooks/usePlanReadyActions.ts

  • added deterministic implementation-mode resolver:
    • default -> code -> first non-plan -> none
  • for plan accept:
    • updates selected collaboration mode id to resolved non-plan id (or null)
    • persists thread codex collaborationModeId to same value
    • always sends explicit turn override (collaborationMode payload or null)
  • for plan changes submit:
    • persists plan mode id (or null) alongside selection
    • always sends explicit turn override (collaborationMode payload or null)

src/App.tsx

  • wired persistThreadCodexParams into usePlanReadyActions

src/features/app/hooks/usePlanReadyActions.test.tsx

  • new regression suite covering:
    • default-mode accept path
    • non-plan fallback when default/code absent
    • plan-only fallback to explicit neutral override
    • disconnected workspace reconnect path
    • plan changes submit persistence behavior

Linked issue

Validation

Executed before branch transfer (same code/commit content):

  • npm run lint
  • npm run test
  • npm run typecheck
  • cd src-tauri && cargo check
  • targeted:
    • npm run test -- src/features/app/hooks/usePlanReadyActions.test.tsx
    • npm run test -- src/features/messages/components/Messages.test.tsx src/features/collaboration/hooks/useCollaborationModes.test.tsx

Note: this branch was moved into an isolated worktree to avoid interfering with concurrent work on feat/followup-queue-steer-460.

(cherry picked from commit 847a31de6f5a3d4c59da9697c6ed445a8ca66310)
Copilot AI review requested due to automatic review settings February 19, 2026 23:49
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical bug where clicking "Implement this plan" could fail to exit plan mode, causing the next turn to either stall or continue with plan-mode behavior instead of switching to implementation. The root cause was a race condition in mode selection and missing synchronization between UI state and persisted thread parameters.

Changes:

  • Added deterministic implementation mode resolution that prefers defaultcode → first non-plan mode, with explicit null fallback
  • Synchronized collaboration mode state by calling both setSelectedCollaborationModeId and persistThreadCodexParams together during plan follow-up actions
  • Changed from conditional to unconditional explicit collaboration mode overrides in message options to prevent stale mode inheritance

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/features/app/hooks/usePlanReadyActions.ts Core fix: added findImplementationMode() resolver, synchronized mode state updates, and ensured explicit turn overrides
src/features/app/hooks/usePlanReadyActions.test.tsx Comprehensive regression tests covering default/code/fallback paths and plan-only edge case
src/App.tsx Wired persistThreadCodexParams into usePlanReadyActions hook

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

{ collaborationMode: null },
);
});

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test case for when the collaborationModes array is completely empty. While the current test covers the "only plan mode" scenario, an empty array is another edge case that should behave identically (returning null for implementationMode). This would provide more comprehensive coverage of the fallback logic in findImplementationMode.

Suggested change
it("forces neutral mode override when no collaboration modes are available", async () => {
const {
result,
setSelectedCollaborationModeId,
persistThreadCodexParams,
sendUserMessageToThread,
} = renderPlanReadyActions({
collaborationModes: [],
});
await act(async () => {
await result.current.handlePlanAccept();
});
expect(setSelectedCollaborationModeId).toHaveBeenCalledWith(null);
expect(persistThreadCodexParams).toHaveBeenCalledWith({
collaborationModeId: null,
});
expect(sendUserMessageToThread).toHaveBeenCalledWith(
connectedWorkspace,
"thread-1",
"[[cm_plan_ready:accept]] Implement this plan.",
[],
{ collaborationMode: null },
);
});

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 9d5c8e7. Added explicit coverage for an empty collaboration mode list in handlePlanAccept, asserting neutral override (collaborationMode: null) and persisted collaborationModeId: null.

},
},
);
});
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test case for when plan mode is not available when calling handlePlanSubmitChanges. While the code handles this gracefully by setting the mode to null (line 158), having explicit test coverage would document this edge case behavior and ensure it continues to work as expected.

Suggested change
});
});
it("handles plan-change follow-up when plan mode is not available", async () => {
const {
result,
setSelectedCollaborationModeId,
persistThreadCodexParams,
sendUserMessageToThread,
} = renderPlanReadyActions({
collaborationModes: [makeMode("default")],
});
await act(async () => {
await result.current.handlePlanSubmitChanges(" Add tests ");
});
expect(setSelectedCollaborationModeId).not.toHaveBeenCalled();
expect(persistThreadCodexParams).not.toHaveBeenCalled();
expect(sendUserMessageToThread).toHaveBeenCalledWith(
connectedWorkspace,
"thread-1",
"[[cm_plan_ready:changes]] Update the plan with these changes:\n\nAdd tests",
[],
{
collaborationMode: null,
},
);
});

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 9d5c8e7 with a safety adjustment: when handlePlanSubmitChanges cannot find plan mode, we now keep selected/persisted mode untouched and still send explicit neutral override (collaborationMode: null). Added regression test coverage for this path.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1f11868cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 158 to 161
const planModeId = planMode?.id ?? null;
setSelectedCollaborationModeId(planModeId);
persistThreadCodexParams({
collaborationModeId: planModeId,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid clearing plan mode when modes are temporarily empty

When handlePlanSubmitChanges runs while collaboration modes are transiently unavailable (for example, after a disconnect/reconnect cycle), findCollaborationMode("plan") returns null, and this block immediately writes null into both UI selection and persisted thread params. Since useCollaborationModes clears modes whenever !isConnected (src/features/collaboration/hooks/useCollaborationModes.ts), this path can fire in normal reconnect flows and causes the "update plan" follow-up to be sent without plan mode while also erasing the thread’s prior plan selection.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 9d5c8e7. handlePlanSubmitChanges now updates UI + persisted mode only when plan mode exists. When modes are transiently empty, it no longer clears stored selection and sends follow-up with explicit neutral override (collaborationMode: null). Added regression test for this edge case.

@amanthanvi amanthanvi requested a review from Copilot February 20, 2026 04:33
@amanthanvi
Copy link
Contributor Author

@codex review

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d5c8e7819

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// @vitest-environment jsdom
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { CollaborationModeOption, WorkspaceInfo } from "../../../types";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use frontend import aliases in new test module

Replace this relative import with a project alias to comply with the repo contract: AGENTS.md (repo root) explicitly requires frontend code to use aliases like @/* and @app/*. Introducing new relative paths here adds avoidable path fragility and violates the documented import standard that applies to this scope.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in f8c189b. Updated the new test module to use frontend aliases per repo contract: @/types and @app/hooks/usePlanReadyActions. Validation run: npm run test -- src/features/app/hooks/usePlanReadyActions.test.tsx and npm run typecheck both passed.

@Dimillian
Copy link
Owner

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8c189b7af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const implementationMode = findImplementationMode();
const implementationModeId = implementationMode?.id ?? null;
setSelectedCollaborationModeId(implementationModeId);
persistThreadCodexParams({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist plan mode against the triggering thread

Calling persistThreadCodexParams here can write the mode to the wrong thread when the workspace is disconnected: handlePlanAccept awaits connectWorkspace, and during that await the user can switch threads, after which persistThreadCodexParams resolves the target from activeThreadIdRef.current (useThreadCodexOrchestration.ts), while sendUserMessageToThread still uses the originally captured activeThreadId. That mismatch causes thread-scoped collaborationModeId to be saved on a different thread than the one receiving the plan-followup message; the same pattern appears in handlePlanSubmitChanges.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plan mode: "Implement this plan" can stay in plan mode and stall turn startup

2 participants

Comments