mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-19 22:53:08 +00:00
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:
@@ -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!;
|
||||
|
||||
Reference in New Issue
Block a user