Skip to content

fix(workspace): make branch mismatch "Switch branch" actually send the message#3179

Closed
adboio wants to merge 2 commits into
mainfrom
posthog-code/fix-branch-switch-send
Closed

fix(workspace): make branch mismatch "Switch branch" actually send the message#3179
adboio wants to merge 2 commits into
mainfrom
posthog-code/fix-branch-switch-send

Conversation

@adboio

@adboio adboio commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Problem

In the branch-mismatch dialog, the Switch branch button was wrapped in AlertDialog.Action, which auto-closes the dialog on click. Closing fires onOpenChange(false)onCancel(), which:

  • discarded the pending message + clear-editor refs while the checkout mutation was still in flight, so the user's message was never sent after a successful switch — it silently stayed in the composer;
  • meant the loading state on the button was never visible (dialog already gone);
  • rendered checkout errors into an already-closed dialog — failed switches were completely silent;
  • double-fired analytics: every switch also emitted a cancel.

The double-fire is visible in production: over the last 60 days, Branch mismatch warning shown fired 1,249× while actions total 1,513 (continue 737 / cancel 505 / switch 271). The 264-event excess matches the 271 switches almost exactly — corrected, the real distribution is ~59% continue / ~22% switch / ~19% cancel.

Fix

  • Unwrap the Switch button from AlertDialog.Action so clicking it no longer closes the dialog; it now stays open through the mutation (spinner visible, errors surface inline) and closes via state when the checkout succeeds.
  • Ignore onOpenChange close requests while isSwitching, so Escape can't wipe the pending message mid-checkout.
  • Clear a stale switchError when a retry succeeds.

Tests

New BranchMismatchDialog.test.tsx component test clicks the real Radix buttons and asserts onSwitch doesn't co-fire onCancel (the regression), plus coverage for Cancel/Continue/Escape/error states. Existing hook tests unchanged and passing.

Part of a stack reworking the branch-mismatch UX:

  1. → this PR — fix the broken Switch action
  2. Auto-unlink stale linked branches
  3. Replace the modal with a non-blocking pre-send banner
  4. Staff-gated auto-switch for clean worktree tasks

🤖 Generated with Claude Code

…e message

The Switch button was wrapped in AlertDialog.Action, which closes the
dialog on click. Closing fires onOpenChange(false) -> onCancel, which
discarded the pending message and clear-editor refs while the checkout
mutation was still in flight, so on success the user's message was never
sent (and checkout errors rendered into an already-closed dialog,
failing silently). It also double-fired the "Branch mismatch action"
analytics event: every "switch" also emitted a "cancel", which is
visible in production data (60d: 1,249 showns vs 1,513 actions; the
264-event excess matches the 271 switches).

Unwrap the button so the dialog stays open through the mutation
(loading spinner now actually visible, errors surface inline), ignore
close requests while switching so Escape can't wipe the pending
message mid-checkout, and clear a stale switch error on success.

Generated-By: PostHog Code
Task-Id: adec8438-4ca0-42f0-9d9d-948de60c3b23
@trunk-io

trunk-io Bot commented Jul 6, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 9d16443.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff (1)

  1. packages/ui/src/features/workspace/useBranchMismatchDialog.ts, line 73-77 (link)

    P2 setSwitchError(null) here is redundant. handleSwitch already calls setSwitchError(null) synchronously before the mutation starts, so by the time onSuccess fires the error is already cleared — no stale value can exist at this point.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "fix(workspace): make branch mismatch "Sw..." | Re-trigger Greptile

Comment on lines +46 to +75
const user = userEvent.setup();
const { onSwitch, onContinue, onCancel } = renderDialog();

await user.click(screen.getByRole("button", { name: "Cancel" }));

expect(onCancel).toHaveBeenCalledTimes(1);
expect(onSwitch).not.toHaveBeenCalled();
expect(onContinue).not.toHaveBeenCalled();
});

it("clicking Continue anyway fires onContinue only", async () => {
const user = userEvent.setup();
const { onSwitch, onContinue, onCancel } = renderDialog();

await user.click(screen.getByRole("button", { name: "Continue anyway" }));

expect(onContinue).toHaveBeenCalledTimes(1);
expect(onSwitch).not.toHaveBeenCalled();
expect(onCancel).not.toHaveBeenCalled();
});

it("Escape closes and fires onCancel when idle", async () => {
const user = userEvent.setup();
const { onCancel } = renderDialog();

await user.keyboard("{Escape}");

expect(onCancel).toHaveBeenCalledTimes(1);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Prefer parameterised tests for similar button-action cases. The "clicking Cancel fires onCancel only" and "clicking Continue anyway fires onContinue only" tests have an identical shape — click a button, assert one callback fired once, assert the other two did not. The team prefers it.each for this pattern; consolidating them removes the duplicated assertion structure without losing any coverage.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Greptile review feedback: the three button-click tests shared one
shape (click a button, exactly one handler fires), so they collapse
into an it.each — the Switch case still asserts onCancel never fires,
which is the regression this PR guards. Also removes the
setSwitchError(null) in the checkout onSuccess: handleSwitch clears
the error synchronously before every mutate and onError is the only
other setter, so success can never observe a stale error.

Generated-By: PostHog Code
Task-Id: adec8438-4ca0-42f0-9d9d-948de60c3b23

adboio commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Closing unmerged: the branch-mismatch dialog this fixes is deleted wholesale by #3183 (the modal → banner replacement), so merging this would only add review load for code that dies two PRs up the stack. The stack is now #3180#3183, rebased so this PR's commits are no longer part of it. If the banner direction gets dropped, this fix stands alone and can be reopened.

@adboio adboio closed this Jul 6, 2026
@adboio adboio deleted the posthog-code/fix-branch-switch-send branch July 6, 2026 15:14
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.

1 participant