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:
@@ -43,6 +43,7 @@ import { workspaceSummaryKeys } from '@/components/ui-new/hooks/useWorkspaces';
|
|||||||
import { ConfirmDialog } from '@/components/ui-new/dialogs/ConfirmDialog';
|
import { ConfirmDialog } from '@/components/ui-new/dialogs/ConfirmDialog';
|
||||||
import { ChangeTargetDialog } from '@/components/ui-new/dialogs/ChangeTargetDialog';
|
import { ChangeTargetDialog } from '@/components/ui-new/dialogs/ChangeTargetDialog';
|
||||||
import { RebaseDialog } from '@/components/ui-new/dialogs/RebaseDialog';
|
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 { RenameWorkspaceDialog } from '@/components/ui-new/dialogs/RenameWorkspaceDialog';
|
||||||
import { CreatePRDialog } from '@/components/dialogs/tasks/CreatePRDialog';
|
import { CreatePRDialog } from '@/components/dialogs/tasks/CreatePRDialog';
|
||||||
import { getIdeName } from '@/components/ide/IdeIcon';
|
import { getIdeName } from '@/components/ide/IdeIcon';
|
||||||
@@ -680,6 +681,59 @@ export const Actions = {
|
|||||||
requiresTarget: 'git',
|
requiresTarget: 'git',
|
||||||
isVisible: (ctx) => ctx.hasWorkspace && ctx.hasGitRepos,
|
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 ?? '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({
|
const confirmResult = await ConfirmDialog.show({
|
||||||
title: 'Merge Branch',
|
title: 'Merge Branch',
|
||||||
message:
|
message:
|
||||||
@@ -701,7 +755,32 @@ export const Actions = {
|
|||||||
icon: ArrowsClockwiseIcon,
|
icon: ArrowsClockwiseIcon,
|
||||||
requiresTarget: 'git',
|
requiresTarget: 'git',
|
||||||
isVisible: (ctx) => ctx.hasWorkspace && ctx.hasGitRepos,
|
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 repos = await attemptsApi.getRepos(workspaceId);
|
||||||
const repo = repos.find((r) => r.id === repoId);
|
const repo = repos.find((r) => r.id === repoId);
|
||||||
if (!repo) throw new Error('Repository not found');
|
if (!repo) throw new Error('Repository not found');
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import BranchSelector from '@/components/tasks/BranchSelector';
|
import BranchSelector from '@/components/tasks/BranchSelector';
|
||||||
import type { GitBranch } from 'shared/types';
|
import type { GitBranch, GitOperationError } from 'shared/types';
|
||||||
import NiceModal, { useModal } from '@ebay/nice-modal-react';
|
import NiceModal, { useModal } from '@ebay/nice-modal-react';
|
||||||
import { defineModal } from '@/lib/modals';
|
import { defineModal } from '@/lib/modals';
|
||||||
import { GitOperationsProvider } from '@/contexts/GitOperationsContext';
|
import { GitOperationsProvider } from '@/contexts/GitOperationsContext';
|
||||||
import { useGitOperations } from '@/hooks/useGitOperations';
|
import { useGitOperations } from '@/hooks/useGitOperations';
|
||||||
|
import { useAttempt } from '@/hooks/useAttempt';
|
||||||
|
import { attemptsApi, type Result } from '@/lib/api';
|
||||||
|
import { ResolveConflictsDialog } from './ResolveConflictsDialog';
|
||||||
|
|
||||||
export interface RebaseDialogProps {
|
export interface RebaseDialogProps {
|
||||||
attemptId: string;
|
attemptId: string;
|
||||||
@@ -49,6 +52,7 @@ function RebaseDialogContent({
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const git = useGitOperations(attemptId, repoId);
|
const git = useGitOperations(attemptId, repoId);
|
||||||
|
const { data: workspace } = useAttempt(attemptId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialTargetBranch) {
|
if (initialTargetBranch) {
|
||||||
@@ -69,6 +73,36 @@ function RebaseDialogContent({
|
|||||||
});
|
});
|
||||||
modal.hide();
|
modal.hide();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Check if this is a conflict error (Result type with success=false)
|
||||||
|
const resultErr = err as Result<void, GitOperationError> | undefined;
|
||||||
|
const errorType =
|
||||||
|
resultErr && !resultErr.success ? resultErr.error?.type : undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
errorType === 'merge_conflicts' ||
|
||||||
|
errorType === 'rebase_in_progress'
|
||||||
|
) {
|
||||||
|
// Hide this dialog and show the resolve conflicts dialog
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
// Fetch fresh branch status to get conflict details
|
||||||
|
const branchStatus = await attemptsApi.getBranchStatus(attemptId);
|
||||||
|
const repoStatus = branchStatus?.find((s) => s.repo_id === repoId);
|
||||||
|
|
||||||
|
if (repoStatus) {
|
||||||
|
await ResolveConflictsDialog.show({
|
||||||
|
workspaceId: attemptId,
|
||||||
|
conflictOp: repoStatus.conflict_op ?? 'rebase',
|
||||||
|
sourceBranch: workspace?.branch ?? null,
|
||||||
|
targetBranch: repoStatus.target_branch_name,
|
||||||
|
conflictedFiles: repoStatus.conflicted_files ?? [],
|
||||||
|
repoName: repoStatus.repo_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle other errors
|
||||||
let message = 'Failed to rebase';
|
let message = 'Failed to rebase';
|
||||||
if (err && typeof err === 'object') {
|
if (err && typeof err === 'object') {
|
||||||
// Handle Result<void, GitOperationError> structure
|
// Handle Result<void, GitOperationError> structure
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { AgentSelector } from '@/components/tasks/AgentSelector';
|
||||||
|
import { ConfigSelector } from '@/components/tasks/ConfigSelector';
|
||||||
|
import { useUserSystem } from '@/components/ConfigProvider';
|
||||||
|
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||||
|
import { sessionsApi } from '@/lib/api';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import NiceModal, { useModal } from '@ebay/nice-modal-react';
|
||||||
|
import { defineModal } from '@/lib/modals';
|
||||||
|
import { buildResolveConflictsInstructions } from '@/lib/conflicts';
|
||||||
|
import type {
|
||||||
|
BaseCodingAgent,
|
||||||
|
ExecutorProfileId,
|
||||||
|
ConflictOp,
|
||||||
|
} from 'shared/types';
|
||||||
|
|
||||||
|
export interface ResolveConflictsDialogProps {
|
||||||
|
workspaceId: string;
|
||||||
|
conflictOp: ConflictOp;
|
||||||
|
sourceBranch: string | null;
|
||||||
|
targetBranch: string;
|
||||||
|
conflictedFiles: string[];
|
||||||
|
repoName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResolveConflictsDialogResult =
|
||||||
|
| { action: 'resolved'; sessionId?: string }
|
||||||
|
| { action: 'cancelled' };
|
||||||
|
|
||||||
|
const ResolveConflictsDialogImpl =
|
||||||
|
NiceModal.create<ResolveConflictsDialogProps>(
|
||||||
|
({
|
||||||
|
workspaceId,
|
||||||
|
conflictOp,
|
||||||
|
sourceBranch,
|
||||||
|
targetBranch,
|
||||||
|
conflictedFiles,
|
||||||
|
repoName,
|
||||||
|
}) => {
|
||||||
|
const modal = useModal();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { profiles, config } = useUserSystem();
|
||||||
|
const { sessions, selectedSession, selectedSessionId, selectSession } =
|
||||||
|
useWorkspaceContext();
|
||||||
|
const { t } = useTranslation(['tasks', 'common']);
|
||||||
|
|
||||||
|
const resolvedSession = useMemo(() => {
|
||||||
|
if (!selectedSessionId) return selectedSession ?? null;
|
||||||
|
return (
|
||||||
|
sessions.find((session) => session.id === selectedSessionId) ??
|
||||||
|
selectedSession ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [sessions, selectedSessionId, selectedSession]);
|
||||||
|
const sessionExecutor =
|
||||||
|
resolvedSession?.executor as BaseCodingAgent | null;
|
||||||
|
|
||||||
|
const resolvedDefaultProfile = useMemo(() => {
|
||||||
|
if (sessionExecutor) {
|
||||||
|
const variant =
|
||||||
|
config?.executor_profile?.executor === sessionExecutor
|
||||||
|
? config.executor_profile.variant
|
||||||
|
: null;
|
||||||
|
return { executor: sessionExecutor, variant };
|
||||||
|
}
|
||||||
|
return config?.executor_profile ?? null;
|
||||||
|
}, [sessionExecutor, config?.executor_profile]);
|
||||||
|
|
||||||
|
// Default to creating a new session if no existing session
|
||||||
|
const [createNewSession, setCreateNewSession] =
|
||||||
|
useState(!selectedSessionId);
|
||||||
|
const [userSelectedProfile, setUserSelectedProfile] =
|
||||||
|
useState<ExecutorProfileId | null>(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const effectiveProfile = userSelectedProfile ?? resolvedDefaultProfile;
|
||||||
|
const canSubmit = Boolean(effectiveProfile && !isSubmitting);
|
||||||
|
|
||||||
|
// Build the conflict resolution instructions
|
||||||
|
const conflictInstructions = useMemo(
|
||||||
|
() =>
|
||||||
|
buildResolveConflictsInstructions(
|
||||||
|
sourceBranch,
|
||||||
|
targetBranch,
|
||||||
|
conflictedFiles,
|
||||||
|
conflictOp,
|
||||||
|
repoName
|
||||||
|
),
|
||||||
|
[sourceBranch, targetBranch, conflictedFiles, conflictOp, repoName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(async () => {
|
||||||
|
if (!effectiveProfile) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let targetSessionId = selectedSessionId;
|
||||||
|
const creatingNewSession = createNewSession || !selectedSessionId;
|
||||||
|
|
||||||
|
// Create new session if user selected that option or no existing session
|
||||||
|
if (creatingNewSession) {
|
||||||
|
const session = await sessionsApi.create({
|
||||||
|
workspace_id: workspaceId,
|
||||||
|
executor: effectiveProfile.executor,
|
||||||
|
});
|
||||||
|
targetSessionId = session.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetSessionId) {
|
||||||
|
setError('Failed to create session');
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send follow-up with conflict resolution instructions
|
||||||
|
await sessionsApi.followUp(targetSessionId, {
|
||||||
|
prompt: conflictInstructions,
|
||||||
|
variant: effectiveProfile.variant,
|
||||||
|
retry_process_id: null,
|
||||||
|
force_when_dirty: null,
|
||||||
|
perform_git_reset: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate queries and wait for them to complete
|
||||||
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['workspaceSessions', workspaceId],
|
||||||
|
}),
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['processes', workspaceId],
|
||||||
|
}),
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['branchStatus', workspaceId],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Navigate to the new session if one was created
|
||||||
|
// Do this after queries are refreshed so the session exists in the list
|
||||||
|
if (creatingNewSession && targetSessionId) {
|
||||||
|
selectSession(targetSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.resolve({
|
||||||
|
action: 'resolved',
|
||||||
|
sessionId: creatingNewSession ? targetSessionId : undefined,
|
||||||
|
} as ResolveConflictsDialogResult);
|
||||||
|
modal.hide();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to resolve conflicts:', err);
|
||||||
|
setError('Failed to start conflict resolution. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
effectiveProfile,
|
||||||
|
selectedSessionId,
|
||||||
|
createNewSession,
|
||||||
|
workspaceId,
|
||||||
|
conflictInstructions,
|
||||||
|
queryClient,
|
||||||
|
selectSession,
|
||||||
|
modal,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
modal.resolve({ action: 'cancelled' } as ResolveConflictsDialogResult);
|
||||||
|
modal.hide();
|
||||||
|
}, [modal]);
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (!open) handleCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewSessionChange = (checked: boolean) => {
|
||||||
|
setCreateNewSession(checked);
|
||||||
|
// Reset to default profile when toggling back to existing session
|
||||||
|
if (!checked && resolvedDefaultProfile) {
|
||||||
|
setUserSelectedProfile(resolvedDefaultProfile);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasExistingSession = Boolean(selectedSessionId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={modal.visible} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[500px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t('resolveConflicts.dialog.title', 'Resolve Conflicts')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t(
|
||||||
|
'resolveConflicts.dialog.description',
|
||||||
|
'Conflicts were detected. Choose how you want the agent to resolve them.'
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Conflict summary */}
|
||||||
|
<div className="rounded-md border border-warning/40 bg-warning/10 p-3 text-sm">
|
||||||
|
<p className="font-medium text-warning-foreground dark:text-warning">
|
||||||
|
{t('resolveConflicts.dialog.filesWithConflicts', {
|
||||||
|
count: conflictedFiles.length,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
{conflictedFiles.length > 0 && (
|
||||||
|
<ul className="mt-2 space-y-1 text-xs text-warning-foreground/80 dark:text-warning/80">
|
||||||
|
{conflictedFiles.slice(0, 5).map((file) => (
|
||||||
|
<li key={file} className="truncate">
|
||||||
|
{file}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{conflictedFiles.length > 5 && (
|
||||||
|
<li className="text-warning-foreground/60 dark:text-warning/60">
|
||||||
|
{t('resolveConflicts.dialog.andMore', {
|
||||||
|
count: conflictedFiles.length - 5,
|
||||||
|
})}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="text-sm text-destructive">{error}</div>}
|
||||||
|
|
||||||
|
{/* Agent/profile selector - only show when creating new session */}
|
||||||
|
{profiles && createNewSession && (
|
||||||
|
<div className="flex gap-3 flex-col sm:flex-row">
|
||||||
|
<AgentSelector
|
||||||
|
profiles={profiles}
|
||||||
|
selectedExecutorProfile={effectiveProfile}
|
||||||
|
onChange={setUserSelectedProfile}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
<ConfigSelector
|
||||||
|
profiles={profiles}
|
||||||
|
selectedExecutorProfile={effectiveProfile}
|
||||||
|
onChange={setUserSelectedProfile}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="sm:!justify-between">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{t('common:buttons.cancel')}
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{hasExistingSession && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="new-session-switch"
|
||||||
|
checked={createNewSession}
|
||||||
|
onCheckedChange={handleNewSessionChange}
|
||||||
|
className="!bg-border data-[state=checked]:!bg-foreground disabled:opacity-50"
|
||||||
|
aria-label={t(
|
||||||
|
'resolveConflicts.dialog.newSession',
|
||||||
|
'New Session'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="new-session-switch"
|
||||||
|
className="text-sm cursor-pointer"
|
||||||
|
>
|
||||||
|
{t('resolveConflicts.dialog.newSession', 'New Session')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button onClick={handleSubmit} disabled={!canSubmit}>
|
||||||
|
{isSubmitting
|
||||||
|
? t('resolveConflicts.dialog.resolving', 'Starting...')
|
||||||
|
: t('resolveConflicts.dialog.resolve', 'Resolve Conflicts')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ResolveConflictsDialog = defineModal<
|
||||||
|
ResolveConflictsDialogProps,
|
||||||
|
ResolveConflictsDialogResult
|
||||||
|
>(ResolveConflictsDialogImpl);
|
||||||
@@ -492,6 +492,20 @@
|
|||||||
"includeGitContextDescription": "Tells the agent how to view all changes made on this branch",
|
"includeGitContextDescription": "Tells the agent how to view all changes made on this branch",
|
||||||
"newSession": "New Session"
|
"newSession": "New Session"
|
||||||
},
|
},
|
||||||
|
"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": "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"
|
||||||
|
}
|
||||||
|
},
|
||||||
"stopShareDialog": {
|
"stopShareDialog": {
|
||||||
"title": "Stop Sharing Task",
|
"title": "Stop Sharing Task",
|
||||||
"description": "Stop sharing \"{{title}}\" with your organization?",
|
"description": "Stop sharing \"{{title}}\" with your organization?",
|
||||||
|
|||||||
@@ -89,6 +89,20 @@
|
|||||||
"setupHelpText": "{{agent}} no está configurado correctamente. Haz clic en 'Ejecutar Configuración' para instalarlo e iniciar sesión.",
|
"setupHelpText": "{{agent}} no está configurado correctamente. Haz clic en 'Ejecutar Configuración' para instalarlo e iniciar sesión.",
|
||||||
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
|
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
|
||||||
},
|
},
|
||||||
|
"resolveConflicts": {
|
||||||
|
"dialog": {
|
||||||
|
"title": "Resolver Conflictos",
|
||||||
|
"description": "Se detectaron conflictos. Elige cómo quieres que el agente los resuelva.",
|
||||||
|
"sessionLabel": "Sesión",
|
||||||
|
"existingSession": "Continuar en la sesión actual",
|
||||||
|
"newSession": "Nueva sesión",
|
||||||
|
"resolve": "Resolver Conflictos",
|
||||||
|
"resolving": "Iniciando...",
|
||||||
|
"filesWithConflicts_one": "{{count}} archivo tiene conflictos",
|
||||||
|
"filesWithConflicts_other": "{{count}} archivos tienen conflictos",
|
||||||
|
"andMore": "...y {{count}} más"
|
||||||
|
}
|
||||||
|
},
|
||||||
"stopShareDialog": {
|
"stopShareDialog": {
|
||||||
"title": "Detener uso compartido de la tarea",
|
"title": "Detener uso compartido de la tarea",
|
||||||
"description": "¿Detener el uso compartido de \"{{title}}\" con tu organización?",
|
"description": "¿Detener el uso compartido de \"{{title}}\" con tu organización?",
|
||||||
|
|||||||
@@ -89,6 +89,20 @@
|
|||||||
"setupHelpText": "{{agent}}が正しく設定されていません。「セットアップを実行」をクリックしてインストールとログインを行ってください。",
|
"setupHelpText": "{{agent}}が正しく設定されていません。「セットアップを実行」をクリックしてインストールとログインを行ってください。",
|
||||||
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
|
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
|
||||||
},
|
},
|
||||||
|
"resolveConflicts": {
|
||||||
|
"dialog": {
|
||||||
|
"title": "競合を解決",
|
||||||
|
"description": "競合が検出されました。エージェントにどのように解決させるか選択してください。",
|
||||||
|
"sessionLabel": "セッション",
|
||||||
|
"existingSession": "現在のセッションで続行",
|
||||||
|
"newSession": "新規セッション",
|
||||||
|
"resolve": "競合を解決",
|
||||||
|
"resolving": "開始中...",
|
||||||
|
"filesWithConflicts_one": "{{count}}件のファイルに競合があります",
|
||||||
|
"filesWithConflicts_other": "{{count}}件のファイルに競合があります",
|
||||||
|
"andMore": "...他{{count}}件"
|
||||||
|
}
|
||||||
|
},
|
||||||
"stopShareDialog": {
|
"stopShareDialog": {
|
||||||
"title": "タスクの共有を停止",
|
"title": "タスクの共有を停止",
|
||||||
"description": "「{{title}}」の共有を組織向けに停止しますか?",
|
"description": "「{{title}}」の共有を組織向けに停止しますか?",
|
||||||
|
|||||||
@@ -89,6 +89,20 @@
|
|||||||
"setupHelpText": "{{agent}}이(가) 올바르게 설정되지 않았습니다. '설정 실행'을 클릭하여 설치하고 로그인하세요.",
|
"setupHelpText": "{{agent}}이(가) 올바르게 설정되지 않았습니다. '설정 실행'을 클릭하여 설치하고 로그인하세요.",
|
||||||
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
|
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
|
||||||
},
|
},
|
||||||
|
"resolveConflicts": {
|
||||||
|
"dialog": {
|
||||||
|
"title": "충돌 해결",
|
||||||
|
"description": "충돌이 감지되었습니다. 에이전트가 어떻게 해결하기를 원하는지 선택하세요.",
|
||||||
|
"sessionLabel": "세션",
|
||||||
|
"existingSession": "현재 세션에서 계속",
|
||||||
|
"newSession": "새 세션",
|
||||||
|
"resolve": "충돌 해결",
|
||||||
|
"resolving": "시작 중...",
|
||||||
|
"filesWithConflicts_one": "{{count}}개 파일에 충돌이 있습니다",
|
||||||
|
"filesWithConflicts_other": "{{count}}개 파일에 충돌이 있습니다",
|
||||||
|
"andMore": "...외 {{count}}개"
|
||||||
|
}
|
||||||
|
},
|
||||||
"stopShareDialog": {
|
"stopShareDialog": {
|
||||||
"title": "작업 공유 중지",
|
"title": "작업 공유 중지",
|
||||||
"description": "\"{{title}}\" 작업의 공유를 조직에서 중지하시겠습니까?",
|
"description": "\"{{title}}\" 작업의 공유를 조직에서 중지하시겠습니까?",
|
||||||
|
|||||||
@@ -425,6 +425,20 @@
|
|||||||
"includeGitContextDescription": "告诉代理如何查看此分支上的所有更改",
|
"includeGitContextDescription": "告诉代理如何查看此分支上的所有更改",
|
||||||
"newSession": "新会话"
|
"newSession": "新会话"
|
||||||
},
|
},
|
||||||
|
"resolveConflicts": {
|
||||||
|
"dialog": {
|
||||||
|
"title": "解决冲突",
|
||||||
|
"description": "检测到冲突。选择您希望代理如何解决它们。",
|
||||||
|
"sessionLabel": "会话",
|
||||||
|
"existingSession": "在当前会话中继续",
|
||||||
|
"newSession": "新会话",
|
||||||
|
"resolve": "解决冲突",
|
||||||
|
"resolving": "开始中...",
|
||||||
|
"filesWithConflicts_one": "{{count}} 个文件有冲突",
|
||||||
|
"filesWithConflicts_other": "{{count}} 个文件有冲突",
|
||||||
|
"andMore": "...还有 {{count}} 个"
|
||||||
|
}
|
||||||
|
},
|
||||||
"stopShareDialog": {
|
"stopShareDialog": {
|
||||||
"title": "停止共享任务",
|
"title": "停止共享任务",
|
||||||
"description": "停止与您的组织共享 {{title}} ?",
|
"description": "停止与您的组织共享 {{title}} ?",
|
||||||
|
|||||||
@@ -425,6 +425,20 @@
|
|||||||
"includeGitContextDescription": "告訴代理如何查看此分支上的所有變更",
|
"includeGitContextDescription": "告訴代理如何查看此分支上的所有變更",
|
||||||
"newSession": "新工作階段"
|
"newSession": "新工作階段"
|
||||||
},
|
},
|
||||||
|
"resolveConflicts": {
|
||||||
|
"dialog": {
|
||||||
|
"title": "解決衝突",
|
||||||
|
"description": "偵測到衝突。選擇您希望代理如何解決它們。",
|
||||||
|
"sessionLabel": "工作階段",
|
||||||
|
"existingSession": "在目前工作階段中繼續",
|
||||||
|
"newSession": "新工作階段",
|
||||||
|
"resolve": "解決衝突",
|
||||||
|
"resolving": "開始中...",
|
||||||
|
"filesWithConflicts_one": "{{count}} 個檔案有衝突",
|
||||||
|
"filesWithConflicts_other": "{{count}} 個檔案有衝突",
|
||||||
|
"andMore": "...還有 {{count}} 個"
|
||||||
|
}
|
||||||
|
},
|
||||||
"stopShareDialog": {
|
"stopShareDialog": {
|
||||||
"title": "停止分享任務",
|
"title": "停止分享任務",
|
||||||
"description": "停止與您的組織分享 {{title}}?",
|
"description": "停止與您的組織分享 {{title}}?",
|
||||||
|
|||||||
Reference in New Issue
Block a user