Allow multiple merges (#510)

* Allow multiple merge for a single task attempt

Merge more than once (vibe-kanban 618829fc)

When creating a PR, new changes can be pushed after creation.
We need merge to work the same way, when changes have been made after the first merge, a second one should work.

Commit changes from coding agent for task attempt 548ff450-df77-47b2-a5ba-c88d0aa4a334

Merge more than once (vibe-kanban 618829fc)

When creating a PR, new changes can be pushed after creation.
We need merge to work the same way, when changes have been made after the first merge, a second one should work.

Remove pinned todo list (vibe-kanban cc66cda2)

Make a minimal change to remove the pinned todo list from the frontend

Remove pinned todo list (vibe-kanban cc66cda2)

Make a minimal change to remove the pinned todo list from the frontend

* Create merges table; remove task_attempt.merge_commit

Add merge model, replace ta.merge_commit with m.merge_commit

Fix frontend

* Move PR to merges table

* Refactor GitHub repository info retrieval to return structured data

* Fix frontend

* Reset task branch after PR merge

Add branch status handling to TaskDetailsProvider and related components

fmt

Add branch status handling to TaskDetailsProvider and related components

fmt

Test (vibe-kanban 1bf1a80f)

add test.txt

Show merged diff when no worktree present

Refresh branch status after PR creation

Test (vibe-kanban 1bf1a80f)

add test.txt

Test (vibe-kanban 1bf1a80f)

add test.txt

Show rebase when behind

Refactor container service to check if the container is clean before showing merged diff; remove unused BranchStatus import

Test (vibe-kanban a3c1b297)

add test.txt

Refactor branch status handling: rename BranchStatusResponse to BranchStatus and update related types and usages

Test (vibe-kanban) (#540)

* Remove test.txt

* Test (vibe-kanban aade357e)

add test.txt

* test.txt removed.

* Fix diff when merged and new commits have been made

* Remvoe logging (vibe-kanban) (#541)

* Test (vibe-kanban aade357e)

add test.txt

* Test (vibe-kanban aade357e)

add test.txt

* Perfect! I've successfully removed the "Fetching branch status" logging statement from the code. The logging has been removed from `crates/server/src/routes/task_attempts.rs:568-571`.

* Clear previous errors on successful PR creation, push, merge, and rebase actions

* Show branch in worktree dirty error message

* Add success indicators for push and merge actions in CurrentAttempt

* Refactor status display logic in CurrentAttempt for improved readability and maintainability

* Add target_branch_name to merge models and queries for direct and PR merges

* Enhance merge status display logic in CurrentAttempt for better clarity on direct merges

* Remove unnecessary condition check in attempt data fetching interval

* Clippy

* Add index for task_attempt_id in merges table to improve query performance

* Pass PR creation error

* Disable buttons (vibe-kanban 240346bf)

Instead of not showing the merge/pr buttons when theyre not available we should disable them. frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx
This commit is contained in:
Alex Netsch
2025-08-21 16:00:35 +01:00
committed by GitHub
parent 061b461397
commit ed594a3d80
34 changed files with 1348 additions and 810 deletions

View File

@@ -46,7 +46,8 @@
"react-window": "^1.8.11",
"rfc6902": "^5.1.2",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zustand": "^4.5.4"
},
"devDependencies": {
"@types/react": "^18.2.43",
@@ -8035,6 +8036,34 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View File

@@ -13,6 +13,7 @@ import type {
EditorType,
TaskAttempt,
TaskWithAttemptStatus,
BranchStatus,
} from 'shared/types';
import { attemptsApi, executionProcessesApi } from '@/lib/api.ts';
import {
@@ -52,6 +53,7 @@ const TaskDetailsProvider: FC<{
processes: [],
runningProcessDetails: {},
});
const [branchStatus, setBranchStatus] = useState<BranchStatus | null>(null);
const handleOpenInEditor = useCallback(
async (editorType?: EditorType) => {
@@ -111,6 +113,15 @@ const TaskDetailsProvider: FC<{
return newData;
});
}
// Also fetch branch status as part of attempt data
try {
const branchResult = await attemptsApi.getBranchStatus(attemptId);
setBranchStatus(branchResult);
} catch (err) {
console.error('Failed to fetch branch status:', err);
setBranchStatus(null);
}
} catch (err) {
console.error('Failed to fetch attempt data:', err);
}
@@ -165,8 +176,6 @@ const TaskDetailsProvider: FC<{
}, [attemptData.processes, selectedAttempt?.profile, profiles]);
useEffect(() => {
if (!isAttemptRunning || !task) return;
const interval = setInterval(() => {
if (selectedAttempt) {
fetchAttemptData(selectedAttempt.id);
@@ -176,6 +185,26 @@ const TaskDetailsProvider: FC<{
return () => clearInterval(interval);
}, [isAttemptRunning, task, selectedAttempt, fetchAttemptData]);
// Fetch branch status when selected attempt changes
useEffect(() => {
if (!selectedAttempt) {
setBranchStatus(null);
return;
}
const fetchBranchStatus = async () => {
try {
const result = await attemptsApi.getBranchStatus(selectedAttempt.id);
setBranchStatus(result);
} catch (err) {
console.error('Failed to fetch branch status:', err);
setBranchStatus(null);
}
};
fetchBranchStatus();
}, [selectedAttempt]);
const value = useMemo(
() => ({
task,
@@ -218,8 +247,16 @@ const TaskDetailsProvider: FC<{
fetchAttemptData,
isAttemptRunning,
defaultFollowUpVariant,
branchStatus,
setBranchStatus,
}),
[attemptData, fetchAttemptData, isAttemptRunning, defaultFollowUpVariant]
[
attemptData,
fetchAttemptData,
isAttemptRunning,
defaultFollowUpVariant,
branchStatus,
]
);
return (

View File

@@ -3,6 +3,7 @@ import type {
EditorType,
TaskAttempt,
TaskWithAttemptStatus,
BranchStatus,
} from 'shared/types';
import { AttemptData } from '@/lib/types.ts';
@@ -33,6 +34,8 @@ interface TaskAttemptDataContextValue {
fetchAttemptData: (attemptId: string) => Promise<void> | void;
isAttemptRunning: boolean;
defaultFollowUpVariant: string | null;
branchStatus: BranchStatus | null;
setBranchStatus: Dispatch<SetStateAction<BranchStatus | null>>;
}
export const TaskAttemptDataContext =

View File

@@ -37,6 +37,7 @@ export function TaskFollowUpSection() {
fetchAttemptData,
isAttemptRunning,
defaultFollowUpVariant,
branchStatus,
} = useContext(TaskAttemptDataContext);
const { profiles } = useUserSystem();
@@ -66,12 +67,24 @@ export function TaskFollowUpSection() {
) {
return false;
}
// Check if PR is merged - if so, block follow-ups
if (branchStatus?.merges) {
const mergedPR = branchStatus.merges.find(
(m) => m.type === 'pr' && m.pr_info.status === 'merged'
);
if (mergedPR) {
return false;
}
}
return true;
}, [
selectedAttempt,
attemptData.processes,
isAttemptRunning,
isSendingFollowUp,
branchStatus?.merges,
]);
const currentProfile = useMemo(() => {
if (!selectedProfile || !profiles) return null;

View File

@@ -19,6 +19,7 @@ import {
} from '@/components/ui/select';
import { useCallback, useContext, useEffect, useState } from 'react';
import {
TaskAttemptDataContext,
TaskDetailsContext,
TaskSelectedAttemptContext,
} from '@/components/context/taskDetailsContext.ts';
@@ -46,6 +47,7 @@ function CreatePrDialog({
}: Props) {
const { projectId, task } = useContext(TaskDetailsContext);
const { selectedAttempt } = useContext(TaskSelectedAttemptContext);
const { fetchAttemptData } = useContext(TaskAttemptDataContext);
const [prTitle, setPrTitle] = useState('');
const [prBody, setPrBody] = useState('');
const [prBaseBranch, setPrBaseBranch] = useState(
@@ -82,12 +84,14 @@ function CreatePrDialog({
});
if (result.success) {
setError(null); // Clear any previous errors on success
window.open(result.data, '_blank');
setShowCreatePRDialog(false);
// Reset form
setPrTitle('');
setPrBody('');
setPrBaseBranch(selectedAttempt?.base_branch || 'main');
// Refresh branch status to show the new PR
fetchAttemptData(selectedAttempt.id);
} else {
if (result.error) {
setShowCreatePRDialog(false);
@@ -112,7 +116,7 @@ function CreatePrDialog({
setError('Failed to create GitHub PR');
}
}
setShowCreatePRDialog(false);
setCreatingPR(false);
}, [
projectId,

View File

@@ -3,7 +3,6 @@ import {
GitBranch as GitBranchIcon,
GitPullRequest,
History,
Upload,
Play,
Plus,
RefreshCw,
@@ -44,7 +43,7 @@ import {
useState,
} from 'react';
import type { ExecutionProcess } from 'shared/types';
import type { BranchStatus, GitBranch, TaskAttempt } from 'shared/types';
import type { GitBranch, TaskAttempt } from 'shared/types';
import {
TaskAttemptDataContext,
TaskAttemptStoppingContext,
@@ -103,9 +102,8 @@ function CurrentAttempt({
useContext(TaskDetailsContext);
const { config } = useConfig();
const { isStopping, setIsStopping } = useContext(TaskAttemptStoppingContext);
const { attemptData, fetchAttemptData, isAttemptRunning } = useContext(
TaskAttemptDataContext
);
const { attemptData, fetchAttemptData, isAttemptRunning, branchStatus } =
useContext(TaskAttemptDataContext);
const { jumpToProcess } = useProcessSelection();
const [isStartingDevServer, setIsStartingDevServer] = useState(false);
@@ -115,12 +113,12 @@ function CurrentAttempt({
const [devServerDetails, setDevServerDetails] =
useState<ExecutionProcess | null>(null);
const [isHoveringDevServer, setIsHoveringDevServer] = useState(false);
const [branchStatus, setBranchStatus] = useState<BranchStatus | null>(null);
const [branchStatusLoading, setBranchStatusLoading] = useState(false);
const [showRebaseDialog, setShowRebaseDialog] = useState(false);
const [selectedRebaseBranch, setSelectedRebaseBranch] = useState<string>('');
const [showStopConfirmation, setShowStopConfirmation] = useState(false);
const [copied, setCopied] = useState(false);
const [mergeSuccess, setMergeSuccess] = useState(false);
const [pushSuccess, setPushSuccess] = useState(false);
const processedDevServerLogs = useMemo(() => {
if (!devServerDetails) return 'No output yet...';
@@ -263,7 +261,10 @@ function CurrentAttempt({
try {
setPushing(true);
await attemptsApi.push(selectedAttempt.id);
fetchBranchStatus();
setError(null); // Clear any previous errors on success
setPushSuccess(true);
setTimeout(() => setPushSuccess(false), 2000);
fetchAttemptData(selectedAttempt.id);
} catch (error: any) {
console.error('Failed to push changes:', error);
setError(error.message || 'Failed to push changes');
@@ -272,38 +273,16 @@ function CurrentAttempt({
}
};
const fetchBranchStatus = useCallback(async () => {
if (!selectedAttempt?.id) return;
try {
setBranchStatusLoading(true);
const result = await attemptsApi.getBranchStatus(selectedAttempt.id);
setBranchStatus((prev) => {
if (JSON.stringify(prev) === JSON.stringify(result)) return prev;
return result;
});
} catch (err) {
setError('Failed to load branch status');
} finally {
setBranchStatusLoading(false);
}
}, [projectId, selectedAttempt?.id, selectedAttempt?.task_id, setError]);
// Fetch branch status when selected attempt changes
useEffect(() => {
if (selectedAttempt) {
fetchBranchStatus();
}
}, [selectedAttempt, fetchBranchStatus]);
const performMerge = async () => {
if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) return;
try {
setMerging(true);
await attemptsApi.merge(selectedAttempt.id);
// Refetch branch status to show updated state
fetchBranchStatus();
setError(null); // Clear any previous errors on success
setMergeSuccess(true);
setTimeout(() => setMergeSuccess(false), 2000);
fetchAttemptData(selectedAttempt.id);
} catch (error) {
console.error('Failed to merge changes:', error);
// @ts-expect-error it is type ApiError
@@ -319,8 +298,8 @@ function CurrentAttempt({
try {
setRebasing(true);
await attemptsApi.rebase(selectedAttempt.id, { new_base_branch: null });
// Refresh branch status after rebase
fetchBranchStatus();
setError(null); // Clear any previous errors on success
fetchAttemptData(selectedAttempt.id);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to rebase branch');
} finally {
@@ -336,8 +315,8 @@ function CurrentAttempt({
await attemptsApi.rebase(selectedAttempt.id, {
new_base_branch: newBaseBranch,
});
// Refresh branch status after rebase
fetchBranchStatus();
setError(null); // Clear any previous errors on success
fetchAttemptData(selectedAttempt.id);
setShowRebaseDialog(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to rebase branch');
@@ -360,9 +339,9 @@ function CurrentAttempt({
const handlePRButtonClick = async () => {
if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) return;
// If PR already exists, view it in a new tab
if (selectedAttempt.pr_url) {
window.open(selectedAttempt.pr_url, '_blank');
// If PR already exists, push to it
if (mergeInfo.hasOpenPR) {
await handlePushClick();
return;
}
@@ -387,6 +366,42 @@ function CurrentAttempt({
return getEditorDisplayName(config.editor.editor_type);
}, [config?.editor?.editor_type]);
// Memoize merge status information to avoid repeated calculations
const mergeInfo = useMemo(() => {
if (!branchStatus?.merges)
return {
hasOpenPR: false,
openPR: null,
hasMergedPR: false,
mergedPR: null,
hasMerged: false,
latestMerge: null,
};
const openPR = branchStatus.merges.find(
(m) => m.type === 'pr' && m.pr_info.status === 'open'
);
const mergedPR = branchStatus.merges.find(
(m) => m.type === 'pr' && m.pr_info.status === 'merged'
);
const merges = branchStatus.merges.filter(
(m) =>
m.type === 'direct' ||
(m.type === 'pr' && m.pr_info.status === 'merged')
);
return {
hasOpenPR: !!openPR,
openPR,
hasMergedPR: !!mergedPR,
mergedPR,
hasMerged: merges.length > 0,
latestMerge: branchStatus.merges[0] || null, // Most recent merge
};
}, [branchStatus?.merges]);
const handleCopyWorktreePath = useCallback(async () => {
try {
await navigator.clipboard.writeText(selectedAttempt.container_ref || '');
@@ -397,6 +412,71 @@ function CurrentAttempt({
}
}, [selectedAttempt.container_ref]);
// Get status information for display
const getStatusInfo = useCallback(() => {
if (mergeInfo.hasMergedPR && mergeInfo.mergedPR?.type === 'pr') {
const prMerge = mergeInfo.mergedPR;
return {
dotColor: 'bg-green-500',
textColor: 'text-green-700',
text: `PR #${prMerge.pr_info.number} merged`,
isClickable: true,
onClick: () => window.open(prMerge.pr_info.url, '_blank'),
};
}
if (
mergeInfo.hasMerged &&
mergeInfo.latestMerge?.type === 'direct' &&
(branchStatus?.commits_ahead ?? 0) === 0
) {
return {
dotColor: 'bg-green-500',
textColor: 'text-green-700',
text: `Merged`,
isClickable: false,
};
}
if (mergeInfo.hasOpenPR && mergeInfo.openPR?.type === 'pr') {
const prMerge = mergeInfo.openPR;
return {
dotColor: 'bg-blue-500',
textColor: 'text-blue-700',
text: `PR #${prMerge.pr_info.number}`,
isClickable: true,
onClick: () => window.open(prMerge.pr_info.url, '_blank'),
};
}
if ((branchStatus?.commits_behind ?? 0) > 0) {
return {
dotColor: 'bg-orange-500',
textColor: 'text-orange-700',
text: `Rebase needed${branchStatus?.has_uncommitted_changes ? ' (dirty)' : ''}`,
isClickable: false,
};
}
if ((branchStatus?.commits_ahead ?? 0) > 0) {
return {
dotColor: 'bg-yellow-500',
textColor: 'text-yellow-700',
text:
branchStatus?.commits_ahead === 1
? `1 commit ahead${branchStatus?.has_uncommitted_changes ? ' (dirty)' : ''}`
: `${branchStatus?.commits_ahead} commits ahead${branchStatus?.has_uncommitted_changes ? ' (dirty)' : ''}`,
isClickable: false,
};
}
return {
dotColor: 'bg-gray-500',
textColor: 'text-gray-700',
text: `Up to date${branchStatus?.has_uncommitted_changes ? ' (dirty)' : ''}`,
isClickable: false,
};
}, [mergeInfo, branchStatus]);
return (
<div className="space-y-2">
<div className="flex gap-6 items-start">
@@ -429,9 +509,7 @@ function CurrentAttempt({
variant="ghost"
size="xs"
onClick={handleRebaseDialogOpen}
disabled={
rebasing || branchStatusLoading || isAttemptRunning
}
disabled={rebasing || isAttemptRunning}
className="h-4 w-4 p-0 hover:bg-muted"
>
<Settings className="h-3 w-3" />
@@ -456,24 +534,30 @@ function CurrentAttempt({
Status
</div>
<div className="flex items-center gap-1.5">
{selectedAttempt.merge_commit ? (
<div className="flex items-center gap-1.5 overflow-hidden">
<div className="h-2 w-2 bg-green-500 rounded-full" />
<span className="text-sm font-medium text-green-700 truncate">
Merged
</span>
<span className="text-xs font-mono text-muted-foreground truncate">
({selectedAttempt.merge_commit.slice(0, 8)})
</span>
</div>
) : (
<div className="flex items-center gap-1.5 overflow-hidden">
<div className="h-2 w-2 bg-yellow-500 rounded-full" />
<span className="text-sm font-medium text-yellow-700">
Not merged
</span>
</div>
)}
{(() => {
const statusInfo = getStatusInfo();
return (
<div className="flex items-center gap-1.5">
<div
className={`h-2 w-2 ${statusInfo.dotColor} rounded-full`}
/>
{statusInfo.isClickable ? (
<button
onClick={statusInfo.onClick}
className={`text-sm font-medium ${statusInfo.textColor} hover:underline cursor-pointer`}
>
{statusInfo.text}
</button>
) : (
<span
className={`text-sm font-medium ${statusInfo.textColor}`}
>
{statusInfo.text}
</span>
)}
</div>
);
})()}
</div>
</div>
</div>
@@ -494,7 +578,7 @@ function CurrentAttempt({
</Button>
</div>
<div
className={`text-xs font-mono px-2 py-1 rounded cursor-pointer transition-all duration-300 flex items-center gap-2 ${
className={`text-xs font-mono px-2 py-1 rounded break-all cursor-pointer transition-all duration-300 flex items-center gap-2 ${
copied
? 'bg-green-100 text-green-800 border border-green-300'
: 'text-muted-foreground bg-muted hover:bg-muted/80'
@@ -600,88 +684,73 @@ function CurrentAttempt({
<div className="flex items-center gap-2 flex-wrap">
{/* Git Operations */}
{selectedAttempt && branchStatus && (
{selectedAttempt && branchStatus && !mergeInfo.hasMergedPR && (
<>
{(branchStatus.commits_behind ?? 0) > 0 &&
!branchStatus.merged && (
<Button
onClick={handleRebaseClick}
disabled={
rebasing || branchStatusLoading || isAttemptRunning
}
variant="outline"
size="xs"
className="border-orange-300 text-orange-700 hover:bg-orange-50 gap-1"
>
<RefreshCw
className={`h-3 w-3 ${rebasing ? 'animate-spin' : ''}`}
/>
{rebasing ? 'Rebasing...' : `Rebase`}
</Button>
)}
{
// Normal merge and PR buttons for regular tasks
!branchStatus.merged && (
<>
<Button
onClick={handlePRButtonClick}
disabled={
creatingPR ||
Boolean((branchStatus.commits_behind ?? 0) > 0) ||
isAttemptRunning
}
variant="outline"
size="xs"
className="border-blue-300 text-blue-700 hover:bg-blue-50 gap-1"
>
<GitPullRequest className="h-3 w-3" />
{selectedAttempt.pr_url
? 'View PR'
: creatingPR
? 'Creating...'
: 'Create PR'}
</Button>
<Button
onClick={
selectedAttempt.pr_status === 'open'
? handlePushClick
: handleMergeClick
}
disabled={
selectedAttempt.pr_status === 'open'
? pushing ||
isAttemptRunning ||
(branchStatus.remote_up_to_date ?? true)
: merging ||
Boolean((branchStatus.commits_behind ?? 0) > 0) ||
isAttemptRunning
}
size="xs"
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 gap-1"
>
{selectedAttempt.pr_status === 'open' ? (
<>
<Upload className="h-3 w-3" />
{pushing
? 'Pushing...'
: branchStatus.remote_commits_behind === null
? 'Disconnected'
: branchStatus.remote_commits_behind === 0
? 'Push to remote'
: branchStatus.remote_commits_behind === 1
? 'Push 1 commit'
: `Push ${branchStatus.remote_commits_behind} commits`}
</>
) : (
<>
<GitBranchIcon className="h-3 w-3" />
{merging ? 'Merging...' : 'Merge'}
</>
)}
</Button>
</>
)
}
{(branchStatus.commits_behind ?? 0) > 0 && (
<Button
onClick={handleRebaseClick}
disabled={rebasing || isAttemptRunning}
variant="outline"
size="xs"
className="border-orange-300 text-orange-700 hover:bg-orange-50 gap-1"
>
<RefreshCw
className={`h-3 w-3 ${rebasing ? 'animate-spin' : ''}`}
/>
{rebasing ? 'Rebasing...' : `Rebase`}
</Button>
)}
<>
<Button
onClick={handlePRButtonClick}
disabled={
creatingPR ||
pushing ||
Boolean((branchStatus.commits_behind ?? 0) > 0) ||
isAttemptRunning ||
(mergeInfo.hasOpenPR &&
branchStatus.remote_commits_ahead === 0) ||
((branchStatus.commits_ahead ?? 0) === 0 &&
!pushSuccess &&
!mergeSuccess)
}
variant="outline"
size="xs"
className="border-blue-300 text-blue-700 hover:bg-blue-50 gap-1 min-w-[120px]"
>
<GitPullRequest className="h-3 w-3" />
{mergeInfo.hasOpenPR
? pushSuccess
? 'Pushed!'
: pushing
? 'Pushing...'
: branchStatus.remote_commits_ahead === 0
? 'Push to PR'
: branchStatus.remote_commits_ahead === 1
? 'Push 1 commit'
: `Push ${branchStatus.remote_commits_ahead || 0} commits`
: creatingPR
? 'Creating...'
: 'Create PR'}
</Button>
<Button
onClick={handleMergeClick}
disabled={
mergeInfo.hasOpenPR ||
merging ||
Boolean((branchStatus.commits_behind ?? 0) > 0) ||
isAttemptRunning ||
((branchStatus.commits_ahead ?? 0) === 0 &&
!pushSuccess &&
!mergeSuccess)
}
size="xs"
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 gap-1 min-w-[120px]"
>
<GitBranchIcon className="h-3 w-3" />
{mergeSuccess ? 'Merged!' : merging ? 'Merging...' : 'Merge'}
</Button>
</>
</>
)}