Skip to content

feat(workspace): replace branch mismatch modal with non-blocking banner#3183

Draft
adboio wants to merge 4 commits into
mainfrom
posthog-code/branch-mismatch-banner
Draft

feat(workspace): replace branch mismatch modal with non-blocking banner#3183
adboio wants to merge 4 commits into
mainfrom
posthog-code/branch-mismatch-banner

Conversation

@adboio

@adboio adboio commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Problem

The wrong-branch AlertDialog intercepts the user's send at the worst possible moment — the Enter keypress — and blocks it behind a modal. Production analytics say the modal's premise ("you probably want to switch") is wrong most of the time:

  • ~1,250 dialogs shown in the last 60 days, ~4× per affected user per day (dismissal was session-scoped and re-armed on every branch change).
  • Users chose Continue anyway ~59%, Switch ~22%, Cancel ~19% (corrected for a bug where every Switch click also emitted a spurious cancel).

The dialog's Switch action was also broken outright — wrapped in AlertDialog.Action, its auto-close fired onCancel, wiped the pending message, and the message was never sent after checkout (errors were silent too). That bug dies here with the dialog itself (a standalone fix existed as #3179, closed unmerged in favor of this stack).

Change

The modal, its hook, and the onBeforeSubmit interception are deleted. In their place: a slim banner above the composer, rendered via a new composerNotice slot on SessionView, visible as soon as the mismatch exists — before the user has typed anything. Sending is never intercepted; the agent works on the current branch either way (which is what the majority already chose).

Banner actions:

  • Switch branch — checks out the linked branch; loading state and checkout errors render inline in the banner.
  • Use current branchnew: durably re-links the task to the current branch via workspace.linkBranch(source: "user"). This replaces the old "Continue anyway", whose session-scoped dismissal was the main source of repeat nagging.
  • Dismiss (×) — session-scoped hide, re-arms when the branch changes (same semantics as before).

Cloud tasks are unaffected (no live branchName, banner never renders). The banner hides automatically once the mismatch clears — both actions propagate through the existing BranchChanged/LinkedBranchChanged → workspace-query invalidation path.

Analytics

Event names are unchanged (Branch mismatch warning shown / Branch mismatch action) so existing dashboards keep working, with two semantic changes to note:

  • action values are now switch | relink | dismiss (continue/cancel retire with the modal).
  • shown fires once per banner appearance rather than once per intercepted send, so its volume isn't directly comparable across the change.

Tests

  • Core: branchMismatchBanner.test.ts — analytics event building for all actions, checkout/link request builders, error resolution (13 tests).
  • UI hook: useBranchMismatchBanner.test.ts — shown-once-per-appearance, switch success/failure, relink, dismiss (6 tests).
  • UI component: BranchMismatchBanner.test.tsx — branch rendering, action wiring, error display, busy state (4 tests).

Stack:

  1. feat(workspace): auto-unlink linked branches that no longer exist #3180 — auto-unlink stale linked branches
  2. → this PR — replace the modal with a non-blocking pre-send banner

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

React Doctor found 1 issue in 1 file · 1 warning.

1 warning

src/features/task-detail/components/TaskLogsPanel.tsx

Reviewed by React Doctor for commit 363bf8d.

adboio added 2 commits July 6, 2026 11:13
A task's linkedBranch is never cleaned up when its branch is deleted
(the usual end state after a PR merge), so returning to a finished task
and asking a follow-up from main nags with a wrong-branch warning on
every send — a link like that can never match the working tree again.
Production data suggests this drives the ~59% "continue anyway" rate on
the mismatch dialog.

getWorkspace now self-heals: when the linked branch mismatches the
current branch, a new anyBranchRefExists git query checks for the
branch as a local head or remote-tracking ref on any remote (no
network; tags don't count). If nothing remains, the link is dropped
with source "auto" on the existing Branch unlinked analytics event.
Being on the linked branch proves it exists, so only mismatched reads
pay the extra ref lookup, and an in-flight guard keeps concurrent
reads from stacking git calls.

Also makes the mock workspace repository's updateLinkedBranch persist
instead of being a no-op, so service tests can exercise link state.

Generated-By: PostHog Code
Task-Id: adec8438-4ca0-42f0-9d9d-948de60c3b23
Greptile review feedback: the unlink/keep cases share one shape
(seed, mock anyBranchRefExists, read, assert linkedBranch), so they
collapse into an it.each on refExists -> outcome; the unlink side
effects (event, analytics, persisted null) move to their own focused
test.

Generated-By: PostHog Code
Task-Id: adec8438-4ca0-42f0-9d9d-948de60c3b23
@adboio adboio force-pushed the posthog-code/auto-unlink-stale-branches branch from 287ea33 to ed27d0a Compare July 6, 2026 15:13
The wrong-branch AlertDialog intercepted the user's send at the worst
moment (the Enter keypress) and blocked it behind a modal. Production
data says its premise is wrong most of the time: of ~1,250 dialogs in
60 days, only ~22% chose "Switch branch" while ~59% chose "Continue
anyway" — and dismissal was session-scoped, so the same users re-hit
the dialog ~4x per day.

Replace it with a slim banner above the composer, shown as soon as the
mismatch exists (before typing) via a new composerNotice slot on
SessionView. Sending is never intercepted; the agent works on the
current branch either way, which is what the majority already chose.

Actions:
- Switch branch: checkout the linked branch, loading and errors inline.
- Use current branch: re-link the task to the current branch via
  workspace.linkBranch — a durable resolution that replaces the old
  per-session "Continue anyway" nag.
- Dismiss: hides for the session (re-arms when the branch changes),
  as before.

Analytics keep the same event names; the action union becomes
switch | relink | dismiss, and "shown" now fires once per banner
appearance instead of once per intercepted send. The old dialog,
its hook, and the beforeSubmit interception are deleted; the
banner-ineligible cloud case is unchanged (branchName is null there).

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

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "feat(workspace): replace branch mismatch..." | Re-trigger Greptile

Comment on lines +97 to +108
const { mutate: linkBranch, isPending: isRelinking } = useMutation(
trpc.workspace.linkBranch.mutationOptions({
// The LinkedBranchChanged event invalidates the workspace query, which
// clears the mismatch and hides the banner.
onSuccess: () => setActionError(null),
onError: (error) => {
log.error("Failed to re-link branch", error);
setActionError(
resolveBranchMismatchError(error, "Failed to update the task branch"),
);
},
}),

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 Missing dismissWarning on relink success

After "Use current branch" succeeds, the banner stays visible until the LinkedBranchChanged event propagates through the workspace query. The checkout path avoids this gap by calling dismissWarning() on success for immediate hiding (onSuccess at line 85), but the linkBranch path only calls setActionError(null). If the real-time event is delayed or lost, the banner is permanently stuck in a state where the mismatch has already been resolved server-side.

Comment on lines +175 to +196
it("onUseCurrentBranch re-links the task and tracks relink", () => {
const { result } = renderBanner();

mockTrack.mockClear();
act(() => {
result.current?.onUseCurrentBranch();
});

expect(mutateSpies.link).toHaveBeenCalledWith({
taskId: "task-1",
branchName: "main",
});
expect(mockTrack).toHaveBeenCalledWith(
ANALYTICS_EVENTS.BRANCH_MISMATCH_ACTION,
{
task_id: "task-1",
action: "relink",
linked_branch: "feat/foo",
current_branch: "main",
},
);
});

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 linkBranch success and error callbacks are untested

The onSwitch block has two companion tests — one for success (captured.checkout.onSuccess?.()) and one for failure (captured.checkout.onError?.(...)) — but onUseCurrentBranch only verifies the mutation is invoked and the analytics event fires. The captured.link.onSuccess and captured.link.onError callbacks are never exercised, so the error display and error-clearing logic for the relink path have no coverage. Adding the parallel success/error cases would match the pattern already established for switch.

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: "Use current branch" success now calls
dismissWarning() like the checkout path does, so the banner hides
immediately instead of waiting on the LinkedBranchChanged round-trip
(and can't stick around if that event is dropped). Adds the missing
tests for the linkBranch success and error callbacks, mirroring the
switch-path coverage.

Generated-By: PostHog Code
Task-Id: adec8438-4ca0-42f0-9d9d-948de60c3b23
Base automatically changed from posthog-code/auto-unlink-stale-branches to main July 6, 2026 21:47
@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

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