Fix concurrency limits and remote branch fetching issues (#788)

* Changes from fix/bug-fixes

* feat: Refactor worktree iteration and improve error logging across services

* feat: Extract URL/port patterns to module level and fix abort condition

* fix: Improve IPv6 loopback handling, select component layout, and terminal UI

* feat: Add thinking level defaults and adjust list row padding

* Update apps/ui/src/store/app-store.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* feat: Add worktree-aware terminal creation and split options, fix npm security issues from audit

* feat: Add tracked remote detection to pull dialog flow

* feat: Add merge state tracking to git operations

* feat: Improve merge detection and add post-merge action preferences

* Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: Pass merge detection info to stash reapplication and handle merge state consistently

* fix: Call onPulled callback in merge handlers and add validation checks

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
gsxdsm
2026-02-20 13:48:22 -08:00
committed by GitHub
parent 7df2182818
commit 0a5540c9a2
70 changed files with 4525 additions and 857 deletions

View File

@@ -13,6 +13,9 @@ import {
X,
SquarePlus,
Settings,
GitBranch,
ChevronDown,
FolderGit,
} from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { getServerUrlSync } from '@/lib/http-api-client';
@@ -28,6 +31,17 @@ import { Label } from '@/components/ui/label';
import { Slider } from '@/components/ui/slider';
import { Switch } from '@/components/ui/switch';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Select,
SelectContent,
@@ -255,6 +269,8 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
setTerminalScrollbackLines,
setTerminalScreenReaderMode,
updateTerminalPanelSizes,
currentWorktreeByProject,
worktreesByProject,
} = useAppStore();
const navigate = useNavigate();
@@ -946,13 +962,50 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
}
};
// Helper: find the branchName of the given session ID within a layout tree
const findSessionBranchName = (
layout: TerminalPanelContent | null,
sessionId: string
): string | undefined => {
if (!layout) return undefined;
if (layout.type === 'terminal') {
return layout.sessionId === sessionId ? layout.branchName : undefined;
}
if (layout.type === 'split') {
for (const panel of layout.panels) {
const found = findSessionBranchName(panel, sessionId);
if (found !== undefined) return found;
}
}
return undefined;
};
// Helper: resolve the worktree cwd and branchName for the currently active terminal session.
// Returns { cwd, branchName } if the active terminal was opened in a worktree, or {} otherwise.
const getActiveSessionWorktreeInfo = (): { cwd?: string; branchName?: string } => {
const activeSessionId = terminalState.activeSessionId;
if (!activeSessionId || !activeTab?.layout || !currentProject) return {};
const branchName = findSessionBranchName(activeTab.layout, activeSessionId);
if (!branchName) return {};
// Look up the worktree path for this branch in the project's worktree list
const projectWorktrees = worktreesByProject[currentProject.path] ?? [];
const worktree = projectWorktrees.find((wt) => wt.branch === branchName);
if (!worktree) return { branchName };
return { cwd: worktree.path, branchName };
};
// Create a new terminal session
// targetSessionId: the terminal to split (if splitting an existing terminal)
// customCwd: optional working directory to use instead of the current project path
// branchName: optional branch name to display in the terminal panel header
const createTerminal = async (
direction?: 'horizontal' | 'vertical',
targetSessionId?: string,
customCwd?: string
customCwd?: string,
branchName?: string
) => {
if (!canCreateTerminal('[Terminal] Debounced terminal creation')) {
return;
@@ -971,7 +1024,7 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
const data = await response.json();
if (data.success) {
addTerminalToLayout(data.data.id, direction, targetSessionId);
addTerminalToLayout(data.data.id, direction, targetSessionId, branchName);
// Mark this session as new for running initial command
if (defaultRunScript) {
setNewSessionIds((prev) => new Set(prev).add(data.data.id));
@@ -1004,11 +1057,18 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
};
// Create terminal in new tab
const createTerminalInNewTab = async () => {
// customCwd: optional working directory (e.g., a specific worktree path)
// branchName: optional branch name to display in the terminal panel header
const createTerminalInNewTab = async (customCwd?: string, branchName?: string) => {
if (!canCreateTerminal('[Terminal] Debounced terminal tab creation')) {
return;
}
// Use provided cwd/branch, or inherit from active session's worktree
const { cwd: worktreeCwd, branchName: worktreeBranch } = customCwd
? { cwd: customCwd, branchName: branchName }
: getActiveSessionWorktreeInfo();
const tabId = addTerminalTab();
try {
const headers: Record<string, string> = {};
@@ -1018,14 +1078,14 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
const response = await apiFetch('/api/terminal/sessions', 'POST', {
headers,
body: { cwd: currentProject?.path || undefined, cols: 80, rows: 24 },
body: { cwd: worktreeCwd || currentProject?.path || undefined, cols: 80, rows: 24 },
});
const data = await response.json();
if (data.success) {
// Add to the newly created tab
// Add to the newly created tab (passing branchName so the panel header shows the branch badge)
const { addTerminalToTab } = useAppStore.getState();
addTerminalToTab(data.data.id, tabId);
addTerminalToTab(data.data.id, tabId, undefined, worktreeBranch);
// Mark this session as new for running initial command
if (defaultRunScript) {
setNewSessionIds((prev) => new Set(prev).add(data.data.id));
@@ -1344,8 +1404,14 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
isActive={terminalState.activeSessionId === content.sessionId}
onFocus={() => setActiveTerminalSession(content.sessionId)}
onClose={() => killTerminal(content.sessionId)}
onSplitHorizontal={() => createTerminal('horizontal', content.sessionId)}
onSplitVertical={() => createTerminal('vertical', content.sessionId)}
onSplitHorizontal={() => {
const { cwd, branchName } = getActiveSessionWorktreeInfo();
createTerminal('horizontal', content.sessionId, cwd, branchName);
}}
onSplitVertical={() => {
const { cwd, branchName } = getActiveSessionWorktreeInfo();
createTerminal('vertical', content.sessionId, cwd, branchName);
}}
onNewTab={createTerminalInNewTab}
onNavigateUp={() => navigateToTerminal('up')}
onNavigateDown={() => navigateToTerminal('down')}
@@ -1502,6 +1568,15 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
// No terminals yet - show welcome screen
if (terminalState.tabs.length === 0) {
// Get the current worktree for this project (if any)
const currentWorktreeInfo = currentProject
? (currentWorktreeByProject[currentProject.path] ?? null)
: null;
// Only show worktree button when the current worktree has a specific path set
// (non-null path means a worktree is selected, as opposed to the main project)
const currentWorktreePath = currentWorktreeInfo?.path ?? null;
const currentWorktreeBranch = currentWorktreeInfo?.branch ?? null;
return (
<div className="flex-1 flex flex-col items-center justify-center text-center p-6">
<div className="p-4 rounded-full bg-brand-500/10 mb-4">
@@ -1518,10 +1593,40 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
)}
</p>
<Button onClick={() => createTerminal()}>
<Plus className="h-4 w-4 mr-2" />
New Terminal
</Button>
<div className="flex flex-col items-center gap-3 w-full max-w-xs">
{currentWorktreePath && (
<Button
className="w-full flex-col h-auto py-2"
onClick={() =>
createTerminal(
undefined,
undefined,
currentWorktreePath,
currentWorktreeBranch ?? undefined
)
}
>
<span className="flex items-center">
<GitBranch className="h-4 w-4 mr-2 shrink-0" />
Open Terminal in Worktree
</span>
{currentWorktreeBranch && (
<span className="text-xs opacity-70 truncate max-w-full px-2">
{currentWorktreeBranch}
</span>
)}
</Button>
)}
<Button
className="w-full"
variant={currentWorktreePath ? 'outline' : 'default'}
onClick={() => createTerminal()}
>
<Plus className="h-4 w-4 mr-2" />
New Terminal
</Button>
</div>
{status?.platform && (
<p className="text-xs text-muted-foreground mt-6">
@@ -1564,14 +1669,94 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
{(activeDragId || activeDragTabId) && <NewTabDropZone isDropTarget={true} />}
{/* New tab button */}
<button
className="flex items-center justify-center p-1.5 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
onClick={createTerminalInNewTab}
title="New Tab"
>
<Plus className="h-4 w-4" />
</button>
{/* New tab split button */}
<div className="flex items-center">
<button
className="flex items-center justify-center p-1.5 rounded-l hover:bg-accent text-muted-foreground hover:text-foreground"
onClick={() => createTerminalInNewTab()}
title="New Tab"
>
<Plus className="h-4 w-4" />
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="flex items-center justify-center px-0.5 py-1.5 rounded-r hover:bg-accent text-muted-foreground hover:text-foreground border-l border-border"
title="New Terminal Options"
>
<ChevronDown className="h-3 w-3" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="bottom" className="w-56">
<DropdownMenuItem onClick={() => createTerminalInNewTab()} className="gap-2">
<Plus className="h-4 w-4" />
New Tab
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
const { cwd, branchName } = getActiveSessionWorktreeInfo();
createTerminal('horizontal', undefined, cwd, branchName);
}}
className="gap-2"
>
<SplitSquareHorizontal className="h-4 w-4" />
Split Right
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const { cwd, branchName } = getActiveSessionWorktreeInfo();
createTerminal('vertical', undefined, cwd, branchName);
}}
className="gap-2"
>
<SplitSquareVertical className="h-4 w-4" />
Split Down
</DropdownMenuItem>
{/* Worktree options - show when project has worktrees */}
{(() => {
const projectWorktrees = currentProject
? (worktreesByProject[currentProject.path] ?? [])
: [];
if (projectWorktrees.length === 0) return null;
const mainWorktree = projectWorktrees.find((wt) => wt.isMain);
const featureWorktrees = projectWorktrees.filter((wt) => !wt.isMain);
return (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground">
Open in Worktree
</DropdownMenuLabel>
{mainWorktree && (
<DropdownMenuItem
onClick={() =>
createTerminalInNewTab(mainWorktree.path, mainWorktree.branch)
}
className="gap-2"
>
<FolderGit className="h-4 w-4" />
<span className="truncate">{mainWorktree.branch}</span>
<span className="ml-auto text-[10px] text-muted-foreground shrink-0">
main
</span>
</DropdownMenuItem>
)}
{featureWorktrees.map((wt) => (
<DropdownMenuItem
key={wt.path}
onClick={() => createTerminalInNewTab(wt.path, wt.branch)}
className="gap-2"
>
<GitBranch className="h-4 w-4" />
<span className="truncate">{wt.branch}</span>
</DropdownMenuItem>
))}
</>
);
})()}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Toolbar buttons */}
@@ -1580,7 +1765,10 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-foreground"
onClick={() => createTerminal('horizontal')}
onClick={() => {
const { cwd, branchName } = getActiveSessionWorktreeInfo();
createTerminal('horizontal', undefined, cwd, branchName);
}}
title="Split Right"
>
<SplitSquareHorizontal className="h-4 w-4" />
@@ -1589,7 +1777,10 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-foreground"
onClick={() => createTerminal('vertical')}
onClick={() => {
const { cwd, branchName } = getActiveSessionWorktreeInfo();
createTerminal('vertical', undefined, cwd, branchName);
}}
title="Split Down"
>
<SplitSquareVertical className="h-4 w-4" />
@@ -1771,12 +1962,14 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
isActive={true}
onFocus={() => setActiveTerminalSession(terminalState.maximizedSessionId!)}
onClose={() => killTerminal(terminalState.maximizedSessionId!)}
onSplitHorizontal={() =>
createTerminal('horizontal', terminalState.maximizedSessionId!)
}
onSplitVertical={() =>
createTerminal('vertical', terminalState.maximizedSessionId!)
}
onSplitHorizontal={() => {
const { cwd, branchName } = getActiveSessionWorktreeInfo();
createTerminal('horizontal', terminalState.maximizedSessionId!, cwd, branchName);
}}
onSplitVertical={() => {
const { cwd, branchName } = getActiveSessionWorktreeInfo();
createTerminal('vertical', terminalState.maximizedSessionId!, cwd, branchName);
}}
onNewTab={createTerminalInNewTab}
onSessionInvalid={() => {
const sessionId = terminalState.maximizedSessionId!;