Unverified 提交 d70c8269 authored 作者: Adekunle James Adeniji's avatar Adekunle James Adeniji 提交者: GitHub

AI Conflict Resolver (#2240)

Solves part of #2207 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces an in-app merge conflict workflow with both AI-assisted and manual resolution, integrated into Git sync and branch merges. > > - New `GithubConflictResolver` dialog: previews conflicting sections, "Auto-Resolve with AI" (with optional auto-approve) and "Accept Current Changes", and progresses through multi-file conflicts > - Integrated into `GitHubConnector` and `GithubBranchManager` to surface conflicts after failed push/merge, guide resolution, and then finalize via `ipc.github.completeMerge` (rebase-continue or merge commit) > - New IPC contracts/handlers: `github:get-conflicts`, `github:resolve-conflict` (writes/stages ours), `github:complete-merge`; plus improved conflict detection by checking conflicts directly on errors > - Enhanced error/rebase-state messaging and abort/continue flows; avoids relying on IPC-serialized error names > - E2E tests for AI and manual conflict resolution; fake LLM updated to emit conflict-fixing `dyad-write` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 98b8aa07ef175a2806bbc0609b433279fbde78c8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds inline merge-conflict resolution with a chat-based flow and a cancel option. Conflict detection, streaming, and cancel behavior are more reliable; merges remain blocked until the repo is clean. - **New Features** - Inline conflict banner listing files, with “Resolve merge conflicts with AI” (opens a new chat, auto-starts streaming) and “Cancel sync”; buttons disable while resolving or cancelling. - New useResolveMergeConflictsWithAI hook that validates appId, guards rapid clicks, creates the chat, navigates, starts the stream, and refreshes chats/app on completion; integrated into GitHubConnector and GithubBranchManager and clears conflicts only after chat creation. - E2E tests for AI resolution and cancel flow; fake LLM supports new and legacy prompts; Electron IPC docs clarify stream start returns void and how to avoid duplicate streams. - **Bug Fixes** - Always fetch conflicts after failed sync/merge, not relying on serialized error names; clearer messages for rebase/merge states and when conflict checks fail. - Cancel sync only shows success when an abort actually occurs; clears conflicts and refreshes branches; buttons disable during cancellation. - Resets streaming state if stream setup or navigation fails; onError now refreshes chat list and app; e2e git commands quote filenames. <sup>Written for commit 8ec0dd605bdadbec9b64b64643c0010129c9735b. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2240"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com> Co-authored-by: 's avatarWill Chen <willchen90@gmail.com>
上级 b1c4aa28
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
import { test, Timeout } from "./helpers/test_helper"; import { test, Timeout } from "./helpers/test_helper";
import type { PageObject } from "./helpers/test_helper";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { execSync } from "child_process"; import { execSync } from "child_process";
async function createGitConflict(po: PageObject) {
await po.setUp({ disableNativeGit: false, autoApprove: true });
await po.sendPrompt("tc=basic");
await po.getTitleBarAppNameButton().click();
await po.githubConnector.connect();
const repoName = "test-git-conflict-" + Date.now();
await po.githubConnector.fillCreateRepoName(repoName);
await po.githubConnector.clickCreateRepoButton();
await expect(po.page.getByTestId("github-connected-repo")).toBeVisible({
timeout: Timeout.MEDIUM,
});
const appPath = await po.getCurrentAppPath();
if (!appPath) throw new Error("App path not found");
// Setup conflict
const conflictFile = "conflict.txt";
const conflictFilePath = path.join(appPath, conflictFile);
// Configure git user for commits
execSync("git config user.email 'test@example.com'", { cwd: appPath });
execSync("git config user.name 'Test User'", { cwd: appPath });
execSync("git config commit.gpgsign false", { cwd: appPath });
// 1. Create file on main
fs.writeFileSync(conflictFilePath, "Line 1\nLine 2\nLine 3");
execSync(`git add "${conflictFile}" && git commit -m "Add conflict file"`, {
cwd: appPath,
});
// 2. Create feature branch
const featureBranch = "feature-conflict";
execSync(`git checkout -b ${featureBranch}`, { cwd: appPath });
fs.writeFileSync(conflictFilePath, "Line 1\nLine 2 Modified Feature\nLine 3");
execSync(`git add "${conflictFile}" && git commit -m "Modify on feature"`, {
cwd: appPath,
});
// 3. Switch back to main and modify
execSync(`git checkout main`, { cwd: appPath });
fs.writeFileSync(conflictFilePath, "Line 1\nLine 2 Modified Main\nLine 3");
execSync(`git add "${conflictFile}" && git commit -m "Modify on main"`, {
cwd: appPath,
});
// 4. Try to merge feature into main via UI
await po.goToChatTab();
await po.getTitleBarAppNameButton().click(); // Open Publish Panel
// Open branches accordion
const branchesCard = po.page.getByTestId("branches-header");
await branchesCard.click();
await po.page.getByTestId(`branch-actions-${featureBranch}`).click();
await po.page.getByTestId("merge-branch-menu-item").click();
await po.page.getByTestId("merge-branch-submit-button").click();
return { conflictFile, appPath };
}
test.describe("Git Collaboration", () => { test.describe("Git Collaboration", () => {
//create git conflict helper function //create git conflict helper function
test("should create, switch, rename, merge, and delete branches", async ({ test("should create, switch, rename, merge, and delete branches", async ({
...@@ -307,4 +369,43 @@ test.describe("Git Collaboration", () => { ...@@ -307,4 +369,43 @@ test.describe("Git Collaboration", () => {
timeout: 5000, timeout: 5000,
}); });
}); });
test("should resolve merge conflicts with AI", async ({ po }) => {
const { conflictFile, appPath } = await createGitConflict(po);
// Verify inline resolve buttons appear (no modal)
const resolveButton = po.page.getByRole("button", {
name: "Resolve merge conflicts with AI",
});
await expect(resolveButton).toBeVisible({ timeout: Timeout.MEDIUM });
// Click the button to start AI resolution - this navigates to a new chat
await resolveButton.click();
// Wait for the chat to load and AI to respond (auto-approve is enabled)
const conflictFilePath = path.join(appPath, conflictFile);
await expect
.poll(() => fs.readFileSync(conflictFilePath, "utf-8"), {
timeout: Timeout.LONG,
})
.not.toMatch(/<<<<<<<|=======|>>>>>>>/);
await expect
.poll(() => fs.existsSync(path.join(appPath, ".git", "MERGE_HEAD")), {
timeout: Timeout.MEDIUM,
})
.toBe(false);
});
test("should cancel sync when merge conflicts occur", async ({ po }) => {
await createGitConflict(po);
// Verify inline resolve buttons appear
await expect(
po.page.getByRole("button", { name: "Resolve merge conflicts with AI" }),
).toBeVisible({ timeout: Timeout.MEDIUM });
// Click Cancel sync to abort the merge
await po.page.getByRole("button", { name: "Cancel sync" }).click();
// Conflict buttons should be gone (merge/rebase aborted, no toast shown)
await expect(
po.page.getByRole("button", { name: "Resolve merge conflicts with AI" }),
).not.toBeVisible({ timeout: Timeout.MEDIUM });
});
}); });
...@@ -46,6 +46,11 @@ const unsub = ipc.events.agent.onTodosUpdate((payload) => { ... }); ...@@ -46,6 +46,11 @@ const unsub = ipc.events.agent.onTodosUpdate((payload) => { ... });
ipc.chatStream.start(params, { onChunk, onEnd, onError }); ipc.chatStream.start(params, { onChunk, onEnd, onError });
``` ```
## Stream client notes
- `createStreamClient(...).start()` returns `void`, not a cleanup/unsubscribe function. You cannot capture a handle to abort or clean up an active stream from the caller side.
- To guard against duplicate streams, use a module-level `Set` (like `pendingStreamChatIds` in `useStreamChat.ts`) or a React state/ref-based lock, not the return value.
## Handler expectations ## Handler expectations
- Handlers should `throw new Error("...")` on failure instead of returning `{ success: false }` style payloads. - Handlers should `throw new Error("...")` on failure instead of returning `{ success: false }` style payloads.
......
...@@ -30,6 +30,8 @@ import { ...@@ -30,6 +30,8 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { GithubBranchManager } from "@/components/GithubBranchManager"; import { GithubBranchManager } from "@/components/GithubBranchManager";
import { useResolveMergeConflictsWithAI } from "@/hooks/useResolveMergeConflictsWithAI";
import { showSuccess, showError } from "@/lib/toast";
type SyncResult = type SyncResult =
| { error: Error; handled?: boolean } | { error: Error; handled?: boolean }
...@@ -91,8 +93,45 @@ function ConnectedGitHubConnector({ ...@@ -91,8 +93,45 @@ function ConnectedGitHubConnector({
"abort" | "continue" | "safe-push" | null "abort" | "continue" | "safe-push" | null
>(null); >(null);
const [rebaseInProgress, setRebaseInProgress] = useState(false); const [rebaseInProgress, setRebaseInProgress] = useState(false);
const [isCancellingSync, setIsCancellingSync] = useState(false);
const lastAutoSyncedAppIdRef = useRef<number | null>(null); const lastAutoSyncedAppIdRef = useRef<number | null>(null);
const { resolveWithAI, isResolving } = useResolveMergeConflictsWithAI({
appId,
conflicts,
onStartResolving: () => {
// Clear conflicts state when starting AI resolution since user will be navigated to chat
setConflicts([]);
setSyncError(null);
},
});
const handleCancelSync = async () => {
setIsCancellingSync(true);
try {
const state = await ipc.github.getGitState({ appId });
let aborted = false;
if (state.rebaseInProgress) {
await ipc.github.rebaseAbort({ appId });
setRebaseInProgress(false);
setRebaseStatusMessage("Rebase aborted.");
aborted = true;
} else if (state.mergeInProgress) {
await ipc.github.mergeAbort({ appId });
aborted = true;
}
setConflicts([]);
setSyncError(null);
if (aborted) {
showSuccess("Sync cancelled");
}
} catch (error: any) {
showError(error?.message || "Failed to cancel sync");
} finally {
setIsCancellingSync(false);
}
};
const handleDisconnectRepo = async () => { const handleDisconnectRepo = async () => {
setIsDisconnecting(true); setIsDisconnecting(true);
setDisconnectError(null); setDisconnectError(null);
...@@ -132,22 +171,41 @@ function ConnectedGitHubConnector({ ...@@ -132,22 +171,41 @@ function ConnectedGitHubConnector({
setRebaseStatusMessage(null); setRebaseStatusMessage(null);
return {}; return {};
} catch (err: any) { } catch (err: any) {
if (err?.name === "GitConflictError") { // Always check for conflicts when sync fails, regardless of error type
// IPC serialization may not preserve error.name, so we check conflicts directly
// This is important because gitPull can throw GitConflictError which might not
// be properly serialized through IPC
let conflictsDetected: string[] = [];
let conflictCheckError: unknown = null;
try { try {
const mergeConflicts = await ipc.github.getConflicts({ appId }); conflictsDetected = await ipc.github.getConflicts({ appId });
if (mergeConflicts.length > 0) { } catch (error) {
setConflicts(mergeConflicts); // If conflict check fails, keep the error to surface it with the sync failure.
conflictCheckError = error;
}
if (conflictsDetected.length > 0) {
// Conflicts were detected - show resolution buttons below
setConflicts(conflictsDetected);
setSyncError( setSyncError(
"Merge conflicts detected. Please resolve them in the editor.", "Merge conflicts detected. Use the buttons below to resolve them.",
); );
(err as Error & { handled?: boolean }).handled = true; (err as Error & { handled?: boolean }).handled = true;
return { error: err, handled: true }; return { error: err, handled: true };
} }
} catch {
// If getGithubMergeConflicts fails, fall through to handle the original GitConflictError // Check if it's a known conflict error for user messaging
// The error from getGithubMergeConflicts is intentionally not handled here // (even if conflicts check failed or returned empty)
// so the original GitConflictError can be displayed to the user const errorName = err?.name || "";
} const isConflict = errorName === "GitConflictError";
if (isConflict) {
// Conflict error detected but no conflicts found - this shouldn't happen
// but we'll show an error message
setSyncError(
"Merge conflict detected, but no conflicting files were returned. Please check git status and try again.",
);
return { error: err };
} }
// Check for structured error codes instead of parsing error messages // Check for structured error codes instead of parsing error messages
...@@ -177,8 +235,15 @@ function ConnectedGitHubConnector({ ...@@ -177,8 +235,15 @@ function ConnectedGitHubConnector({
inferredRebaseInProgress || inferredRebaseInProgress ||
messageIndicatesRebase; messageIndicatesRebase;
const errorMessage = err.message || "Failed to sync to GitHub."; const baseErrorMessage = err.message || "Failed to sync to GitHub.";
setSyncError(errorMessage); const conflictCheckMessage =
conflictCheckError instanceof Error
? ` Conflict check failed: ${conflictCheckError.message}`
: conflictCheckError
? " Conflict check failed."
: "";
const finalErrorMessage = `${baseErrorMessage}${conflictCheckMessage}`;
setSyncError(finalErrorMessage);
setRebaseInProgress(rebaseInProgressState); setRebaseInProgress(rebaseInProgressState);
setRebaseStatusMessage(null); setRebaseStatusMessage(null);
return { error: err }; return { error: err };
...@@ -460,13 +525,29 @@ function ConnectedGitHubConnector({ ...@@ -460,13 +525,29 @@ function ConnectedGitHubConnector({
)} )}
</div> </div>
)} )}
{/* Conflict Resolver */} {/* Conflict Resolution Buttons */}
{conflicts.length > 0 && ( {conflicts.length > 0 && (
//show a message that there are conflicts and to resolve them in Editor <div className="mt-3 p-3 rounded-md border border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-900/20">
<p className="text-sm text-red-600"> <p className="text-sm text-yellow-800 dark:text-yellow-200 mb-3">
There are conflicts in the repository. Please resolve them in the {conflicts.length} file{conflicts.length > 1 ? "s" : ""} with merge
editor. conflicts: {conflicts.join(", ")}
</p> </p>
<div className="flex gap-2">
<Button
onClick={resolveWithAI}
disabled={isCancellingSync || isResolving}
>
{isResolving ? "Resolving..." : "Resolve merge conflicts with AI"}
</Button>
<Button
variant="outline"
onClick={handleCancelSync}
disabled={isCancellingSync || isResolving}
>
{isCancellingSync ? "Cancelling..." : "Cancel sync"}
</Button>
</div>
</div>
)} )}
{rebaseStatusMessage && ( {rebaseStatusMessage && (
<p className="text-sm text-gray-700 dark:text-gray-300 mt-2"> <p className="text-sm text-gray-700 dark:text-gray-300 mt-2">
......
...@@ -68,6 +68,7 @@ import { ...@@ -68,6 +68,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { useResolveMergeConflictsWithAI } from "@/hooks/useResolveMergeConflictsWithAI";
interface BranchManagerProps { interface BranchManagerProps {
appId: number; appId: number;
...@@ -107,6 +108,40 @@ export function GithubBranchManager({ ...@@ -107,6 +108,40 @@ export function GithubBranchManager({
operationType: "merge" | "rebase"; operationType: "merge" | "rebase";
hasConflicts: boolean; hasConflicts: boolean;
} | null>(null); } | null>(null);
const [isCancellingSync, setIsCancellingSync] = useState(false);
const { resolveWithAI, isResolving } = useResolveMergeConflictsWithAI({
appId,
conflicts,
onStartResolving: () => {
// Clear conflicts state when starting AI resolution
setConflicts([]);
},
});
const handleCancelSync = async () => {
setIsCancellingSync(true);
try {
const state = await ipc.github.getGitState({ appId });
let aborted = false;
if (state.rebaseInProgress) {
await ipc.github.rebaseAbort({ appId });
aborted = true;
} else if (state.mergeInProgress) {
await ipc.github.mergeAbort({ appId });
aborted = true;
}
setConflicts([]);
if (aborted) {
showSuccess("Sync cancelled");
await loadBranches();
}
} catch (error: any) {
showError(error?.message || "Failed to cancel sync");
} finally {
setIsCancellingSync(false);
}
};
const loadBranches = useCallback(async () => { const loadBranches = useCallback(async () => {
setIsLoading(true); setIsLoading(true);
...@@ -332,34 +367,33 @@ export function GithubBranchManager({ ...@@ -332,34 +367,33 @@ export function GithubBranchManager({
setBranchToMerge(null); setBranchToMerge(null);
await loadBranches(); // Refresh to see any status changes if we implement them await loadBranches(); // Refresh to see any status changes if we implement them
} catch (error: any) { } catch (error: any) {
// Check if it's a merge conflict error (handler converts GitConflictError to MergeConflictError) // Always check for conflicts when merge fails, regardless of error type
const isConflict = // IPC serialization may not preserve error.name, so we check conflicts directly
error?.name === "MergeConflictError" || let conflictsDetected: string[] = [];
error?.name === "GitConflictError";
if (isConflict) {
showInfo("Merge conflict detected. Please resolve them in the editor.");
// Show conflicts dialog
try { try {
const conflicts = await ipc.github.getConflicts({ appId }); conflictsDetected = await ipc.github.getConflicts({ appId });
} catch {
// If conflict check fails, continue with original error handling below
}
if (conflicts.length > 0) { if (conflictsDetected.length > 0) {
setConflicts(conflicts); // Conflicts were detected - show the resolver
// Close the merge modal since user has been notified setConflicts(conflictsDetected);
setBranchToMerge(null); setBranchToMerge(null);
showInfo("Merge conflict detected. Please resolve them in the dialog.");
return; return;
} }
setConflicts([]);
// No conflicts found - show the original error
// Check if it's a merge conflict error for user messaging
const errorName = error?.name || "";
const isConflict =
errorName === "MergeConflictError" || errorName === "GitConflictError";
if (isConflict) {
showError( showError(
"Merge conflict detected, but no conflicting files were returned. Please check git status and try again.", "Merge conflict detected, but no conflicting files were returned. Please check git status and try again.",
); );
} catch (fetchError: any) {
setConflicts([]);
showError(
fetchError.message ||
"Merge conflict detected, but failed to fetch conflicting files. Please try again.",
);
}
} else { } else {
showError(error.message || "Failed to merge branch"); showError(error.message || "Failed to merge branch");
} }
...@@ -703,12 +737,29 @@ export function GithubBranchManager({ ...@@ -703,12 +737,29 @@ export function GithubBranchManager({
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{/* Conflict Resolver */} {/* Conflict Resolution Buttons */}
{conflicts.length > 0 && ( {conflicts.length > 0 && (
<p className="text-sm text-red-600"> <div className="mt-3 p-3 rounded-md border border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-900/20">
There are conflicts in the repository. Please resolve them in the <p className="text-sm text-yellow-800 dark:text-yellow-200 mb-3">
editor. {conflicts.length} file{conflicts.length > 1 ? "s" : ""} with merge
conflicts: {conflicts.join(", ")}
</p> </p>
<div className="flex gap-2">
<Button
onClick={resolveWithAI}
disabled={isCancellingSync || isResolving}
>
{isResolving ? "Resolving..." : "Resolve merge conflicts with AI"}
</Button>
<Button
variant="outline"
onClick={handleCancelSync}
disabled={isCancellingSync || isResolving}
>
{isCancellingSync ? "Cancelling..." : "Cancel sync"}
</Button>
</div>
</div>
)} )}
<Card className="transition-all duration-200"> <Card className="transition-all duration-200">
......
import { useCallback, useRef, useState } from "react";
import { useSetAtom } from "jotai";
import { useNavigate } from "@tanstack/react-router";
import { ipc } from "@/ipc/types";
import {
selectedChatIdAtom,
chatMessagesByIdAtom,
isStreamingByIdAtom,
chatStreamCountByIdAtom,
} from "@/atoms/chatAtoms";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { showError } from "@/lib/toast";
import { useChats } from "@/hooks/useChats";
import { useLoadApp } from "@/hooks/useLoadApp";
interface UseResolveMergeConflictsWithAIProps {
appId: number;
conflicts: string[];
onStartResolving?: () => void;
}
/**
* Hook to resolve merge conflicts with AI by creating a new chat,
* navigating to it, and automatically starting the conflict resolution stream.
*/
export function useResolveMergeConflictsWithAI({
appId,
conflicts,
onStartResolving,
}: UseResolveMergeConflictsWithAIProps) {
const setSelectedChatId = useSetAtom(selectedChatIdAtom);
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
const setMessagesById = useSetAtom(chatMessagesByIdAtom);
const setIsStreamingById = useSetAtom(isStreamingByIdAtom);
const setStreamCountById = useSetAtom(chatStreamCountByIdAtom);
const navigate = useNavigate();
const [isResolving, setIsResolving] = useState(false);
const isResolvingRef = useRef(false);
const { invalidateChats } = useChats(appId);
const { refreshApp } = useLoadApp(appId);
const resolveWithAI = useCallback(async () => {
if (!appId) {
showError("App ID is required");
return;
}
if (conflicts.length === 0) {
showError("No conflicts to resolve");
return;
}
if (isResolvingRef.current) {
return;
}
isResolvingRef.current = true;
setIsResolving(true);
let chatId: number | null = null;
try {
// Create a new chat for conflict resolution
const newChatId = await ipc.chat.createChat(appId);
chatId = newChatId;
// Clear conflicts state after successful chat creation
onStartResolving?.();
// Build the prompt for resolving all conflicts
const fileList = conflicts.map((f) => `- ${f}`).join("\n");
const prompt = `Please resolve the Git merge conflicts in the following file${conflicts.length > 1 ? "s" : ""}:
${fileList}
For each file, review the conflict markers (<<<<<<<, =======, >>>>>>>) and choose the best resolution that preserves the intended functionality from both sides. Remove all conflict markers and provide the complete resolved file content.`;
// Set up the chat state and navigate
setSelectedChatId(newChatId);
setSelectedAppId(appId);
// Mark as streaming
setIsStreamingById((prev) => {
const next = new Map(prev);
next.set(newChatId, true);
return next;
});
// Navigate to the chat page
navigate({
to: "/chat",
search: { id: newChatId },
});
// Start the stream
let hasIncrementedStreamCount = false;
ipc.chatStream.start(
{
chatId: newChatId,
prompt,
},
{
onChunk: ({ messages }) => {
if (!hasIncrementedStreamCount) {
setStreamCountById((prev) => {
const next = new Map(prev);
next.set(newChatId, (prev.get(newChatId) ?? 0) + 1);
return next;
});
hasIncrementedStreamCount = true;
}
setMessagesById((prev) => {
const next = new Map(prev);
next.set(newChatId, messages);
return next;
});
},
onEnd: () => {
setIsStreamingById((prev) => {
const next = new Map(prev);
next.set(newChatId, false);
return next;
});
isResolvingRef.current = false;
setIsResolving(false);
invalidateChats();
refreshApp();
},
onError: ({ error }) => {
showError(error || "Failed to resolve conflicts");
setIsStreamingById((prev) => {
const next = new Map(prev);
next.set(newChatId, false);
return next;
});
isResolvingRef.current = false;
setIsResolving(false);
invalidateChats();
refreshApp();
},
},
);
} catch (error: any) {
showError(error?.message || "Failed to start conflict resolution");
if (chatId !== null) {
setIsStreamingById((prev) => {
const next = new Map(prev);
next.set(chatId!, false);
return next;
});
}
isResolvingRef.current = false;
setIsResolving(false);
}
}, [
appId,
conflicts,
onStartResolving,
setSelectedChatId,
setSelectedAppId,
setMessagesById,
setIsStreamingById,
setStreamCountById,
navigate,
invalidateChats,
refreshApp,
]);
return { resolveWithAI, isResolving };
}
...@@ -90,6 +90,40 @@ DYAD_ATTACHMENT_0 ...@@ -90,6 +90,40 @@ DYAD_ATTACHMENT_0
await new Promise((resolve) => setTimeout(resolve, 10_000)); await new Promise((resolve) => setTimeout(resolve, 10_000));
} }
// Handle merge conflict resolution prompts (both old and new formats)
if (
lastMessage &&
typeof lastMessage.content === "string" &&
(lastMessage.content.includes("Resolve the Git conflict(s) in ") ||
lastMessage.content.includes(
"Please resolve the Git merge conflicts in the following file",
))
) {
// Extract conflict file path from different prompt formats
let conflictPath = "conflict.txt";
if (lastMessage.content.includes("Resolve the Git conflict(s) in ")) {
conflictPath =
lastMessage.content
.split("Resolve the Git conflict(s) in ")[1]
?.split("\n")[0]
?.replace(/\.$/, "")
.trim() || "conflict.txt";
} else {
// New format: "Please resolve the Git merge conflicts in the following file(s):\n\n- conflict.txt"
const fileListMatch = lastMessage.content.match(/^- (.+)$/m);
if (fileListMatch) {
conflictPath = fileListMatch[1].trim();
}
}
messageContent = `Resolved conflicts in ${conflictPath}.
<dyad-write path="${conflictPath}" description="Resolve merge conflicts.">
Line 1
Line 2 Modified Feature
Line 3
</dyad-write>
`;
}
// TS auto-fix prefixes // TS auto-fix prefixes
if ( if (
lastMessage && lastMessage &&
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论