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 { 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');
|
||||
|
||||
@@ -11,11 +11,14 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { defineModal } from '@/lib/modals';
|
||||
import { GitOperationsProvider } from '@/contexts/GitOperationsContext';
|
||||
import { useGitOperations } from '@/hooks/useGitOperations';
|
||||
import { useAttempt } from '@/hooks/useAttempt';
|
||||
import { attemptsApi, type Result } from '@/lib/api';
|
||||
import { ResolveConflictsDialog } from './ResolveConflictsDialog';
|
||||
|
||||
export interface RebaseDialogProps {
|
||||
attemptId: string;
|
||||
@@ -49,6 +52,7 @@ function RebaseDialogContent({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const git = useGitOperations(attemptId, repoId);
|
||||
const { data: workspace } = useAttempt(attemptId);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialTargetBranch) {
|
||||
@@ -69,6 +73,36 @@ function RebaseDialogContent({
|
||||
});
|
||||
modal.hide();
|
||||
} 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';
|
||||
if (err && typeof err === 'object') {
|
||||
// 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",
|
||||
"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": {
|
||||
"title": "Stop Sharing Task",
|
||||
"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.",
|
||||
"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": {
|
||||
"title": "Detener uso compartido de la tarea",
|
||||
"description": "¿Detener el uso compartido de \"{{title}}\" con tu organización?",
|
||||
|
||||
@@ -89,6 +89,20 @@
|
||||
"setupHelpText": "{{agent}}が正しく設定されていません。「セットアップを実行」をクリックしてインストールとログインを行ってください。",
|
||||
"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": {
|
||||
"title": "タスクの共有を停止",
|
||||
"description": "「{{title}}」の共有を組織向けに停止しますか?",
|
||||
|
||||
@@ -89,6 +89,20 @@
|
||||
"setupHelpText": "{{agent}}이(가) 올바르게 설정되지 않았습니다. '설정 실행'을 클릭하여 설치하고 로그인하세요.",
|
||||
"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": {
|
||||
"title": "작업 공유 중지",
|
||||
"description": "\"{{title}}\" 작업의 공유를 조직에서 중지하시겠습니까?",
|
||||
|
||||
@@ -425,6 +425,20 @@
|
||||
"includeGitContextDescription": "告诉代理如何查看此分支上的所有更改",
|
||||
"newSession": "新会话"
|
||||
},
|
||||
"resolveConflicts": {
|
||||
"dialog": {
|
||||
"title": "解决冲突",
|
||||
"description": "检测到冲突。选择您希望代理如何解决它们。",
|
||||
"sessionLabel": "会话",
|
||||
"existingSession": "在当前会话中继续",
|
||||
"newSession": "新会话",
|
||||
"resolve": "解决冲突",
|
||||
"resolving": "开始中...",
|
||||
"filesWithConflicts_one": "{{count}} 个文件有冲突",
|
||||
"filesWithConflicts_other": "{{count}} 个文件有冲突",
|
||||
"andMore": "...还有 {{count}} 个"
|
||||
}
|
||||
},
|
||||
"stopShareDialog": {
|
||||
"title": "停止共享任务",
|
||||
"description": "停止与您的组织共享 {{title}} ?",
|
||||
|
||||
@@ -425,6 +425,20 @@
|
||||
"includeGitContextDescription": "告訴代理如何查看此分支上的所有變更",
|
||||
"newSession": "新工作階段"
|
||||
},
|
||||
"resolveConflicts": {
|
||||
"dialog": {
|
||||
"title": "解決衝突",
|
||||
"description": "偵測到衝突。選擇您希望代理如何解決它們。",
|
||||
"sessionLabel": "工作階段",
|
||||
"existingSession": "在目前工作階段中繼續",
|
||||
"newSession": "新工作階段",
|
||||
"resolve": "解決衝突",
|
||||
"resolving": "開始中...",
|
||||
"filesWithConflicts_one": "{{count}} 個檔案有衝突",
|
||||
"filesWithConflicts_other": "{{count}} 個檔案有衝突",
|
||||
"andMore": "...還有 {{count}} 個"
|
||||
}
|
||||
},
|
||||
"stopShareDialog": {
|
||||
"title": "停止分享任務",
|
||||
"description": "停止與您的組織分享 {{title}}?",
|
||||
|
||||
Reference in New Issue
Block a user