From 325d711a6bc2dfe24ae1399f9e91798c0e3dad2c Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Sun, 5 Apr 2026 01:56:35 -0500 Subject: [PATCH] Handle stale merge-conflict errors gracefully - Add recoverable error toasts for stale conflict candidates and refresh failures - Keep users in the prepared workspace with clearer manual-resolution guidance - Update conflict shell tests and copy for the no-safe-patch fallback --- apps/web/src/components/GitActionsControl.tsx | 1 + .../MergeConflictShell.logic.test.ts | 8 ++ .../MergeConflictShell.logic.ts | 14 ++- .../merge-conflicts/MergeConflictShell.tsx | 87 ++++++++++++++++--- 4 files changed, 99 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 491bd07ac..a3c877526 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -56,6 +56,7 @@ import { import { Group, GroupSeparator } from "~/components/ui/group"; import { Menu, + MenuGroup, MenuGroupLabel, MenuItem, MenuPopup, diff --git a/apps/web/src/components/merge-conflicts/MergeConflictShell.logic.test.ts b/apps/web/src/components/merge-conflicts/MergeConflictShell.logic.test.ts index 4cc64bdda..e48662ec3 100644 --- a/apps/web/src/components/merge-conflicts/MergeConflictShell.logic.test.ts +++ b/apps/web/src/components/merge-conflicts/MergeConflictShell.logic.test.ts @@ -156,6 +156,14 @@ describe("humanizeConflictError", () => { expect(result.summary).toBe("Not a git repository"); }); + it("recognises when a conflict candidate has gone stale", () => { + const result = humanizeConflictError( + "PrReview failed in applyConflictResolution: Conflict candidate not found: src/auth.ts:ours", + ); + expect(result.summary).toBe("Conflict candidate is out of date"); + expect(result.detail).toContain("Refresh the conflict analysis"); + }); + it("falls back to stripping the prefix and using the first sentence", () => { const result = humanizeConflictError( "Git manager failed in preparePullRequestThread: Something unexpected happened. More details here.", diff --git a/apps/web/src/components/merge-conflicts/MergeConflictShell.logic.ts b/apps/web/src/components/merge-conflicts/MergeConflictShell.logic.ts index 23575dbd2..e4245b33b 100644 --- a/apps/web/src/components/merge-conflicts/MergeConflictShell.logic.ts +++ b/apps/web/src/components/merge-conflicts/MergeConflictShell.logic.ts @@ -122,7 +122,7 @@ export function buildConflictRecommendation(input: { tone: "warning", title: "Manual merge work is still required.", detail: - "The workspace is prepared, but no candidate patch was safe to generate. Resolve the markers manually and keep the handoff note explicit.", + "The workspace is prepared, but no candidate patch was safe to generate. Stay in this conflict workspace, resolve the markers manually, and keep the handoff note explicit before you commit.", }; } @@ -195,6 +195,18 @@ const KNOWN_ERROR_PATTERNS: ReadonlyArray<{ detail: "The selected project directory is not a valid git repository. Check the project path in the intake panel.", }, + { + pattern: "conflict candidate not found", + summary: "Conflict candidate is out of date", + detail: + "The selected candidate no longer matches the current workspace. Refresh the conflict analysis, then pick a new option or continue the merge manually.", + }, + { + pattern: "failed to inspect merge conflicts", + summary: "Conflict analysis is temporarily unavailable", + detail: + "OK Code could not refresh the local conflict analysis, but your workspace is still open. Refresh the panel, reopen the prepared worktree, or continue the merge manually.", + }, ]; const ERROR_PREFIX_RE = /^[A-Za-z ]+(?:failed|error) in [A-Za-z]+:\s*/i; diff --git a/apps/web/src/components/merge-conflicts/MergeConflictShell.tsx b/apps/web/src/components/merge-conflicts/MergeConflictShell.tsx index 52df6048e..0cd524166 100644 --- a/apps/web/src/components/merge-conflicts/MergeConflictShell.tsx +++ b/apps/web/src/components/merge-conflicts/MergeConflictShell.tsx @@ -44,7 +44,7 @@ import { parsePullRequestReference } from "~/pullRequestReference"; import { findProjectMatchingPullRequestReference } from "~/pullRequestProjectMatch"; import { useStore } from "~/store"; import type { Project } from "~/types"; -import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; +import { Alert, AlertAction, AlertDescription, AlertTitle } from "~/components/ui/alert"; import { toastManager } from "~/components/ui/toast"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; @@ -139,6 +139,15 @@ async function openPathInEditor(targetPath: string) { await openInPreferredEditor(ensureNativeApi(), targetPath); } +function notifyRecoverableConflictError(rawMessage: string) { + const { summary, detail } = humanizeConflictError(rawMessage); + toastManager.add({ + type: "error", + title: summary, + description: detail, + }); +} + function ConflictCandidateButton({ candidate, isRecommended, @@ -700,10 +709,18 @@ export function MergeConflictShell({ const handlePrepareWorkspace = async (mode: "local" | "worktree") => { if (!resolvedPullRequest || !parsedReference) return; - const result = await preparePullRequestThreadMutation.mutateAsync({ - reference: parsedReference, - mode, - }); + let result: Awaited>; + try { + result = await preparePullRequestThreadMutation.mutateAsync({ + reference: parsedReference, + mode, + }); + } catch (error) { + notifyRecoverableConflictError( + error instanceof Error ? error.message : "Failed to prepare merge-conflict workspace.", + ); + return; + } const nextWorkspace: PreparedWorkspace = { branch: result.branch, cwd: result.worktreePath ?? project.cwd, @@ -725,6 +742,22 @@ export function MergeConflictShell({ }); }; + const handleApplyCandidate = async () => { + if (!selectedCandidate || !resolvedPullRequest) return; + + try { + await applyConflictResolutionMutation.mutateAsync({ + candidateId: selectedCandidate.id, + cwd: activeWorkspaceCwd, + prNumber: resolvedPullRequest.number, + }); + } catch (error) { + notifyRecoverableConflictError( + error instanceof Error ? error.message : "Failed to apply conflict candidate.", + ); + } + }; + return ( <>
@@ -1093,6 +1126,44 @@ export function MergeConflictShell({ )) ) : preparedWorkspace && conflictedFiles.length > 0 ? (
+ + + Manual resolution required + +

+ OK Code prepared the workspace and found the conflicted files, but + it could not generate a patch that was safe to apply + automatically. +

+

+ Keep working here: open a file, resolve the markers, and capture + the handoff note before you commit. +

+
+ + + + +

Conflicted files @@ -1218,11 +1289,7 @@ export function MergeConflictShell({