Add conflict detection and resolution dialog to rebase/merge flow (Vibe Kanban) (#2001)

* I've implemented the changes to the rebase/merge flow in the new workspaces UI. Here's a summary of what was done:

## Summary of Changes

### 1. Created `ResolveConflictsDialog` component
**File:** `frontend/src/components/ui-new/dialogs/ResolveConflictsDialog.tsx`

A new dialog that:
- Shows when conflicts are detected before rebase/merge operations
- Displays the list of conflicted files (up to 5, with a count for additional)
- Asks the user whether to resolve conflicts in:
  - **Current session** (default when a session exists)
  - **New session** (toggle switch, similar to StartReviewDialog)
- When creating a new session, allows selecting agent/profile
- Sends the conflict resolution instructions to the agent automatically

### 2. Updated `GitRebase` action
**File:** `frontend/src/components/ui-new/actions/index.ts`

- Now checks for existing conflicts before opening the RebaseDialog
- If conflicts exist (`is_rebase_in_progress` or `conflicted_files.length > 0`), shows the ResolveConflictsDialog instead
- If no conflicts, proceeds with normal rebase flow

### 3. Updated `GitMerge` action
**File:** `frontend/src/components/ui-new/actions/index.ts`

- Same conflict detection logic as GitRebase
- If conflicts exist, shows the ResolveConflictsDialog
- If no conflicts, proceeds with normal merge confirmation flow

### 4. Added translations
**File:** `frontend/src/i18n/locales/en/tasks.json`

Added new translation keys under `resolveConflicts.dialog`:
- `title`: "Resolve Conflicts"
- `description`: "Conflicts were detected. Choose how you want the agent to resolve them."
- `sessionLabel`: "Session"
- `existingSession`: "Continue in current session"
- `newSession`: "New Session"
- `resolve`: "Resolve Conflicts"
- `resolving`: "Starting..."

### How it works

1. When a user clicks **Rebase** or **Merge** in the RepoCard dropdown
2. The system fetches the branch status to check for conflicts
3. If conflicts exist:
   - The `ResolveConflictsDialog` opens
   - User can choose to resolve in current session or new session
   - Clicking "Resolve Conflicts" sends pre-built conflict resolution instructions to the agent
4. If no conflicts:
   - Normal flow continues (RebaseDialog for rebase, ConfirmDialog for merge)

* The type check passes. Now the flow will be:

1. User clicks Rebase → RebaseDialog opens
2. User selects branch and clicks Rebase
3. If conflicts occur during rebase:
   - The error is caught with type `merge_conflicts` or `rebase_in_progress`
   - RebaseDialog closes
   - ResolveConflictsDialog opens with conflict details
4. If no conflicts → Rebase succeeds and dialog closes

This handles the first-time rebase scenario where conflicts are detected *after* the rebase attempt, not before.

* Cleanup script changes for workspace f54c4bcf-8b64-4260-8cae-c8d0bda2a350

* The type check passes. The key changes I made:

1. **Await query invalidation**: Changed from fire-and-forget `invalidateQueries` to awaiting them with `Promise.all()`. This ensures the sessions list is refreshed before we try to select the new session.

2. **Select session after queries refresh**: Moved the `selectSession` call to after the queries have been invalidated and refetched, so the new session exists in the list.

3. **Simplified the boolean logic**: Used a single `creatingNewSession` variable instead of computing the condition twice.

This should fix the issue where the previous session "disappears" - it was likely being overwritten by the auto-select behavior in `useWorkspaceSessions` that fires when sessions change. Now we wait for that to settle before selecting the new session.

* The type check passes. Here's a summary of the updated flow:

## Updated Flow

### Merge Action:
1. **Check for existing conflicts** → If conflicts exist, show ResolveConflictsDialog
2. **Check if branch is behind** → If `commits_behind > 0`, show "Rebase Required" dialog that offers to open RebaseDialog
3. **Otherwise** → Show normal merge confirmation dialog

### Rebase Action:
1. **Check for existing conflicts** → If conflicts exist, show ResolveConflictsDialog
2. **Otherwise** → Show RebaseDialog
3. **If rebase fails with conflicts** → RebaseDialog catches the error and shows ResolveConflictsDialog

This way:
- Clicking Merge when behind will prompt to rebase first (avoiding the "Branches diverged" error)
- Clicking Rebase when conflicts already exist will show the resolve dialog immediately
- If rebase creates new conflicts, the RebaseDialog will catch it and show the resolve dialog

* Both the type check and i18n lint pass now. The i18n issues in the ResolveConflictsDialog have been fixed by:

1. Added new translation keys in `tasks.json`:
   - `filesWithConflicts_one`: "{{count}} file has conflicts"
   - `filesWithConflicts_other`: "{{count}} files have conflicts"
   - `andMore`: "...and {{count}} more"

2. Updated `ResolveConflictsDialog.tsx` to use `t()` function with these keys instead of hardcoded strings.

* All checks pass. The i18n translations have been added to all 5 locale files (es, ja, ko, zh-Hans, zh-Hant) for the `resolveConflicts.dialog.*` keys:

- `title` - Resolve Conflicts
- `description` - Conflicts were detected...
- `sessionLabel` - Session
- `existingSession` - Continue in current session
- `newSession` - Start a new session
- `resolve` - Resolve Conflicts
- `resolving` - Starting...
- `filesWithConflicts_one` - {{count}} file has conflicts
- `filesWithConflicts_other` - {{count}} files have conflicts
- `andMore` - ...and {{count}} more

All checks now pass:
- `./scripts/check-i18n.sh`  - No missing translation keys
- `pnpm run lint`  - Both frontend and backend linting pass
- `pnpm run check`  - TypeScript and Rust type checks pass
This commit is contained in:
Anastasiia Solop
2026-01-13 12:34:28 +01:00
committed by GitHub
parent 57e802e2c9
commit a3cdca742a
9 changed files with 505 additions and 2 deletions

View File

@@ -43,6 +43,7 @@ import { workspaceSummaryKeys } from '@/components/ui-new/hooks/useWorkspaces';
import { ConfirmDialog } from '@/components/ui-new/dialogs/ConfirmDialog';
import { ChangeTargetDialog } from '@/components/ui-new/dialogs/ChangeTargetDialog';
import { RebaseDialog } from '@/components/ui-new/dialogs/RebaseDialog';
import { ResolveConflictsDialog } from '@/components/ui-new/dialogs/ResolveConflictsDialog';
import { RenameWorkspaceDialog } from '@/components/ui-new/dialogs/RenameWorkspaceDialog';
import { CreatePRDialog } from '@/components/dialogs/tasks/CreatePRDialog';
import { getIdeName } from '@/components/ide/IdeIcon';
@@ -680,6 +681,59 @@ export const Actions = {
requiresTarget: 'git',
isVisible: (ctx) => ctx.hasWorkspace && ctx.hasGitRepos,
execute: async (ctx, workspaceId, repoId) => {
// Check for existing conflicts first
const branchStatus = await attemptsApi.getBranchStatus(workspaceId);
const repoStatus = branchStatus?.find((s) => s.repo_id === repoId);
const hasConflicts =
repoStatus?.is_rebase_in_progress ||
(repoStatus?.conflicted_files?.length ?? 0) > 0;
if (hasConflicts && repoStatus) {
// Show resolve conflicts dialog
const workspace = await getWorkspace(ctx.queryClient, workspaceId);
const result = await ResolveConflictsDialog.show({
workspaceId,
conflictOp: repoStatus.conflict_op ?? 'merge',
sourceBranch: workspace.branch,
targetBranch: repoStatus.target_branch_name,
conflictedFiles: repoStatus.conflicted_files ?? [],
repoName: repoStatus.repo_name,
});
if (result.action === 'resolved') {
invalidateWorkspaceQueries(ctx.queryClient, workspaceId);
}
return;
}
// Check if branch is behind - need to rebase first
const commitsBehind = repoStatus?.commits_behind ?? 0;
if (commitsBehind > 0) {
// Prompt user to rebase first
const confirmRebase = await ConfirmDialog.show({
title: 'Rebase Required',
message: `Your branch is ${commitsBehind} commit${commitsBehind === 1 ? '' : 's'} behind the target branch. Would you like to rebase first?`,
confirmText: 'Rebase',
cancelText: 'Cancel',
});
if (confirmRebase === 'confirmed') {
// Trigger the rebase action
const repos = await attemptsApi.getRepos(workspaceId);
const repo = repos.find((r) => r.id === repoId);
if (!repo) throw new Error('Repository not found');
const branches = await repoApi.getBranches(repoId);
await RebaseDialog.show({
attemptId: workspaceId,
repoId,
branches,
initialTargetBranch: repo.target_branch,
});
}
return;
}
const confirmResult = await ConfirmDialog.show({
title: 'Merge Branch',
message:
@@ -701,7 +755,32 @@ export const Actions = {
icon: ArrowsClockwiseIcon,
requiresTarget: 'git',
isVisible: (ctx) => ctx.hasWorkspace && ctx.hasGitRepos,
execute: async (_ctx, workspaceId, repoId) => {
execute: async (ctx, workspaceId, repoId) => {
// Check for existing conflicts first
const branchStatus = await attemptsApi.getBranchStatus(workspaceId);
const repoStatus = branchStatus?.find((s) => s.repo_id === repoId);
const hasConflicts =
repoStatus?.is_rebase_in_progress ||
(repoStatus?.conflicted_files?.length ?? 0) > 0;
if (hasConflicts && repoStatus) {
// Show resolve conflicts dialog
const workspace = await getWorkspace(ctx.queryClient, workspaceId);
const result = await ResolveConflictsDialog.show({
workspaceId,
conflictOp: repoStatus.conflict_op ?? 'rebase',
sourceBranch: workspace.branch,
targetBranch: repoStatus.target_branch_name,
conflictedFiles: repoStatus.conflicted_files ?? [],
repoName: repoStatus.repo_name,
});
if (result.action === 'resolved') {
invalidateWorkspaceQueries(ctx.queryClient, workspaceId);
}
return;
}
const repos = await attemptsApi.getRepos(workspaceId);
const repo = repos.find((r) => r.id === repoId);
if (!repo) throw new Error('Repository not found');