mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
Merge pull request #680 from AutoMaker-Org/feature/bug-improve-the-worktree-ui-79ph
fix(ui): Improve worktree panel UI with dropdown for multiple worktrees
This commit is contained in:
@@ -1,5 +1,16 @@
|
|||||||
export { BranchSwitchDropdown } from './branch-switch-dropdown';
|
export { BranchSwitchDropdown } from './branch-switch-dropdown';
|
||||||
export { DevServerLogsPanel } from './dev-server-logs-panel';
|
export { DevServerLogsPanel } from './dev-server-logs-panel';
|
||||||
export { WorktreeActionsDropdown } from './worktree-actions-dropdown';
|
export { WorktreeActionsDropdown } from './worktree-actions-dropdown';
|
||||||
|
export { WorktreeDropdown } from './worktree-dropdown';
|
||||||
|
export type { WorktreeDropdownProps } from './worktree-dropdown';
|
||||||
|
export { WorktreeDropdownItem } from './worktree-dropdown-item';
|
||||||
|
export type { WorktreeDropdownItemProps } from './worktree-dropdown-item';
|
||||||
|
export {
|
||||||
|
truncateBranchName,
|
||||||
|
getPRBadgeStyles,
|
||||||
|
getChangesBadgeStyles,
|
||||||
|
getTestStatusStyles,
|
||||||
|
} from './worktree-indicator-utils';
|
||||||
|
export type { TestStatus } from './worktree-indicator-utils';
|
||||||
export { WorktreeMobileDropdown } from './worktree-mobile-dropdown';
|
export { WorktreeMobileDropdown } from './worktree-mobile-dropdown';
|
||||||
export { WorktreeTab } from './worktree-tab';
|
export { WorktreeTab } from './worktree-tab';
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { Check, CircleDot, Globe, GitPullRequest, FlaskConical } from 'lucide-react';
|
||||||
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { WorktreeInfo, DevServerInfo, TestSessionInfo } from '../types';
|
||||||
|
import {
|
||||||
|
truncateBranchName,
|
||||||
|
getPRBadgeStyles,
|
||||||
|
getChangesBadgeStyles,
|
||||||
|
getTestStatusStyles,
|
||||||
|
} from './worktree-indicator-utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum characters for branch name before truncation in dropdown items.
|
||||||
|
* Set to 28 to accommodate longer names in the wider dropdown menu while
|
||||||
|
* still fitting comfortably with all status indicators.
|
||||||
|
*/
|
||||||
|
const MAX_ITEM_BRANCH_NAME_LENGTH = 28;
|
||||||
|
|
||||||
|
export interface WorktreeDropdownItemProps {
|
||||||
|
/** The worktree to display */
|
||||||
|
worktree: WorktreeInfo;
|
||||||
|
/** Whether this worktree is currently selected */
|
||||||
|
isSelected: boolean;
|
||||||
|
/** Whether this worktree has running features/processes */
|
||||||
|
isRunning: boolean;
|
||||||
|
/** Number of cards associated with this worktree's branch */
|
||||||
|
cardCount?: number;
|
||||||
|
/** Whether the dev server is running for this worktree */
|
||||||
|
devServerRunning?: boolean;
|
||||||
|
/** Dev server information if running */
|
||||||
|
devServerInfo?: DevServerInfo;
|
||||||
|
/** Whether auto-mode is running for this worktree */
|
||||||
|
isAutoModeRunning?: boolean;
|
||||||
|
/** Whether tests are running for this worktree */
|
||||||
|
isTestRunning?: boolean;
|
||||||
|
/** Test session info for this worktree */
|
||||||
|
testSessionInfo?: TestSessionInfo;
|
||||||
|
/** Callback when the worktree is selected */
|
||||||
|
onSelect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dropdown menu item component for displaying an individual worktree entry.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Selection indicator (checkmark when selected)
|
||||||
|
* - Running status indicator (spinner)
|
||||||
|
* - Branch name with tooltip for long names
|
||||||
|
* - Main branch badge
|
||||||
|
* - Dev server status indicator
|
||||||
|
* - Auto mode indicator
|
||||||
|
* - Test status indicator
|
||||||
|
* - Card count badge
|
||||||
|
* - Uncommitted changes indicator
|
||||||
|
* - PR status badge
|
||||||
|
*/
|
||||||
|
export function WorktreeDropdownItem({
|
||||||
|
worktree,
|
||||||
|
isSelected,
|
||||||
|
isRunning,
|
||||||
|
cardCount,
|
||||||
|
devServerRunning,
|
||||||
|
devServerInfo,
|
||||||
|
isAutoModeRunning = false,
|
||||||
|
isTestRunning = false,
|
||||||
|
testSessionInfo,
|
||||||
|
onSelect,
|
||||||
|
}: WorktreeDropdownItemProps) {
|
||||||
|
const { hasChanges, changedFilesCount, pr } = worktree;
|
||||||
|
|
||||||
|
// Truncate long branch names using shared utility
|
||||||
|
const { truncated: truncatedBranch, isTruncated: isBranchNameTruncated } = truncateBranchName(
|
||||||
|
worktree.branch,
|
||||||
|
MAX_ITEM_BRANCH_NAME_LENGTH
|
||||||
|
);
|
||||||
|
|
||||||
|
const branchNameElement = (
|
||||||
|
<span className={cn('font-mono text-xs truncate', isSelected && 'font-medium')}>
|
||||||
|
{truncatedBranch}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={onSelect}
|
||||||
|
className={cn('flex items-center gap-2 cursor-pointer pr-2', isSelected && 'bg-accent')}
|
||||||
|
aria-current={isSelected ? 'true' : undefined}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
|
{/* Selection indicator */}
|
||||||
|
{isSelected ? (
|
||||||
|
<Check className="w-3.5 h-3.5 shrink-0 text-primary" />
|
||||||
|
) : (
|
||||||
|
<div className="w-3.5 h-3.5 shrink-0" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Running indicator */}
|
||||||
|
{isRunning && <Spinner size="xs" className="shrink-0" />}
|
||||||
|
|
||||||
|
{/* Branch name with optional tooltip */}
|
||||||
|
{isBranchNameTruncated ? (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{branchNameElement}</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="font-mono text-xs">{worktree.branch}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
branchNameElement
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main badge */}
|
||||||
|
{worktree.isMain && (
|
||||||
|
<span className="text-[10px] px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">
|
||||||
|
main
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side indicators - ordered consistently with dropdown trigger */}
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
|
{/* Card count badge */}
|
||||||
|
{cardCount !== undefined && cardCount > 0 && (
|
||||||
|
<span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border">
|
||||||
|
{cardCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Uncommitted changes indicator */}
|
||||||
|
{hasChanges && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
|
||||||
|
getChangesBadgeStyles()
|
||||||
|
)}
|
||||||
|
title={`${changedFilesCount ?? 'Some'} uncommitted file${changedFilesCount !== 1 ? 's' : ''}`}
|
||||||
|
>
|
||||||
|
<CircleDot className="w-2.5 h-2.5 mr-0.5" />
|
||||||
|
{changedFilesCount ?? '!'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dev server indicator */}
|
||||||
|
{devServerRunning && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center justify-center h-4 w-4 text-green-500"
|
||||||
|
title={`Dev server running on port ${devServerInfo?.port}`}
|
||||||
|
>
|
||||||
|
<Globe className="w-3 h-3" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Test running indicator */}
|
||||||
|
{isTestRunning && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center justify-center h-4 w-4 text-blue-500"
|
||||||
|
title="Tests Running"
|
||||||
|
>
|
||||||
|
<FlaskConical className="w-3 h-3 animate-pulse" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Last test result indicator (when not running) */}
|
||||||
|
{!isTestRunning && testSessionInfo && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center h-4 w-4',
|
||||||
|
getTestStatusStyles(testSessionInfo.status)
|
||||||
|
)}
|
||||||
|
title={`Last test: ${testSessionInfo.status}`}
|
||||||
|
>
|
||||||
|
<FlaskConical className="w-3 h-3" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Auto mode indicator */}
|
||||||
|
{isAutoModeRunning && (
|
||||||
|
<span className="flex items-center justify-center h-4 px-0.5" title="Auto Mode Running">
|
||||||
|
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PR indicator */}
|
||||||
|
{pr && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center gap-0.5 h-4 px-1 text-[10px] font-medium rounded border',
|
||||||
|
getPRBadgeStyles(pr.state)
|
||||||
|
)}
|
||||||
|
title={`PR #${pr.number}: ${pr.title}`}
|
||||||
|
>
|
||||||
|
<GitPullRequest className="w-2.5 h-2.5" />#{pr.number}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import {
|
||||||
|
GitBranch,
|
||||||
|
ChevronDown,
|
||||||
|
CircleDot,
|
||||||
|
Globe,
|
||||||
|
GitPullRequest,
|
||||||
|
FlaskConical,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type {
|
||||||
|
WorktreeInfo,
|
||||||
|
BranchInfo,
|
||||||
|
DevServerInfo,
|
||||||
|
PRInfo,
|
||||||
|
GitRepoStatus,
|
||||||
|
TestSessionInfo,
|
||||||
|
} from '../types';
|
||||||
|
import { WorktreeDropdownItem } from './worktree-dropdown-item';
|
||||||
|
import { BranchSwitchDropdown } from './branch-switch-dropdown';
|
||||||
|
import { WorktreeActionsDropdown } from './worktree-actions-dropdown';
|
||||||
|
import {
|
||||||
|
truncateBranchName,
|
||||||
|
getPRBadgeStyles,
|
||||||
|
getChangesBadgeStyles,
|
||||||
|
getTestStatusStyles,
|
||||||
|
} from './worktree-indicator-utils';
|
||||||
|
|
||||||
|
export interface WorktreeDropdownProps {
|
||||||
|
/** List of all worktrees to display in the dropdown */
|
||||||
|
worktrees: WorktreeInfo[];
|
||||||
|
/** Function to check if a worktree is currently selected */
|
||||||
|
isWorktreeSelected: (worktree: WorktreeInfo) => boolean;
|
||||||
|
/** Function to check if a worktree has running features/processes */
|
||||||
|
hasRunningFeatures: (worktree: WorktreeInfo) => boolean;
|
||||||
|
/** Whether worktree activation is in progress */
|
||||||
|
isActivating: boolean;
|
||||||
|
/** Map of branch names to card counts */
|
||||||
|
branchCardCounts?: Record<string, number>;
|
||||||
|
/** Function to check if dev server is running for a worktree */
|
||||||
|
isDevServerRunning: (worktree: WorktreeInfo) => boolean;
|
||||||
|
/** Function to get dev server info for a worktree */
|
||||||
|
getDevServerInfo: (worktree: WorktreeInfo) => DevServerInfo | undefined;
|
||||||
|
/** Function to check if auto-mode is running for a worktree */
|
||||||
|
isAutoModeRunningForWorktree: (worktree: WorktreeInfo) => boolean;
|
||||||
|
/** Function to check if tests are running for a worktree */
|
||||||
|
isTestRunningForWorktree: (worktree: WorktreeInfo) => boolean;
|
||||||
|
/** Function to get test session info for a worktree */
|
||||||
|
getTestSessionInfo: (worktree: WorktreeInfo) => TestSessionInfo | undefined;
|
||||||
|
/** Callback when a worktree is selected */
|
||||||
|
onSelectWorktree: (worktree: WorktreeInfo) => void;
|
||||||
|
|
||||||
|
// Branch switching props
|
||||||
|
branches: BranchInfo[];
|
||||||
|
filteredBranches: BranchInfo[];
|
||||||
|
branchFilter: string;
|
||||||
|
isLoadingBranches: boolean;
|
||||||
|
isSwitching: boolean;
|
||||||
|
onBranchDropdownOpenChange: (worktree: WorktreeInfo) => (open: boolean) => void;
|
||||||
|
onBranchFilterChange: (value: string) => void;
|
||||||
|
onSwitchBranch: (worktree: WorktreeInfo, branchName: string) => void;
|
||||||
|
onCreateBranch: (worktree: WorktreeInfo) => void;
|
||||||
|
|
||||||
|
// Action dropdown props
|
||||||
|
isPulling: boolean;
|
||||||
|
isPushing: boolean;
|
||||||
|
isStartingDevServer: boolean;
|
||||||
|
aheadCount: number;
|
||||||
|
behindCount: number;
|
||||||
|
hasRemoteBranch: boolean;
|
||||||
|
gitRepoStatus: GitRepoStatus;
|
||||||
|
hasTestCommand: boolean;
|
||||||
|
isStartingTests: boolean;
|
||||||
|
hasInitScript: boolean;
|
||||||
|
onActionsDropdownOpenChange: (worktree: WorktreeInfo) => (open: boolean) => void;
|
||||||
|
onPull: (worktree: WorktreeInfo) => void;
|
||||||
|
onPush: (worktree: WorktreeInfo) => void;
|
||||||
|
onPushNewBranch: (worktree: WorktreeInfo) => void;
|
||||||
|
onOpenInEditor: (worktree: WorktreeInfo, editorCommand?: string) => void;
|
||||||
|
onOpenInIntegratedTerminal: (worktree: WorktreeInfo, mode?: 'tab' | 'split') => void;
|
||||||
|
onOpenInExternalTerminal: (worktree: WorktreeInfo, terminalId?: string) => void;
|
||||||
|
onViewChanges: (worktree: WorktreeInfo) => void;
|
||||||
|
onDiscardChanges: (worktree: WorktreeInfo) => void;
|
||||||
|
onCommit: (worktree: WorktreeInfo) => void;
|
||||||
|
onCreatePR: (worktree: WorktreeInfo) => void;
|
||||||
|
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||||
|
onResolveConflicts: (worktree: WorktreeInfo) => void;
|
||||||
|
onMerge: (worktree: WorktreeInfo) => void;
|
||||||
|
onDeleteWorktree: (worktree: WorktreeInfo) => void;
|
||||||
|
onStartDevServer: (worktree: WorktreeInfo) => void;
|
||||||
|
onStopDevServer: (worktree: WorktreeInfo) => void;
|
||||||
|
onOpenDevServerUrl: (worktree: WorktreeInfo) => void;
|
||||||
|
onViewDevServerLogs: (worktree: WorktreeInfo) => void;
|
||||||
|
onRunInitScript: (worktree: WorktreeInfo) => void;
|
||||||
|
onToggleAutoMode: (worktree: WorktreeInfo) => void;
|
||||||
|
onStartTests: (worktree: WorktreeInfo) => void;
|
||||||
|
onStopTests: (worktree: WorktreeInfo) => void;
|
||||||
|
onViewTestLogs: (worktree: WorktreeInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum characters for branch name before truncation in the dropdown trigger.
|
||||||
|
* Set to 24 to keep the trigger compact while showing enough context for identification.
|
||||||
|
*/
|
||||||
|
const MAX_TRIGGER_BRANCH_NAME_LENGTH = 24;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dropdown component for displaying and switching between worktrees.
|
||||||
|
* Used when there are 3+ worktrees to avoid horizontal tab wrapping.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Compact dropdown trigger showing current worktree with indicators
|
||||||
|
* - Grouped display (main branch + worktrees)
|
||||||
|
* - Full status indicators (PR, dev server, auto mode, changes)
|
||||||
|
* - Branch switch dropdown integration
|
||||||
|
* - Actions dropdown integration
|
||||||
|
* - Tooltip for truncated branch names
|
||||||
|
*/
|
||||||
|
export function WorktreeDropdown({
|
||||||
|
worktrees,
|
||||||
|
isWorktreeSelected,
|
||||||
|
hasRunningFeatures,
|
||||||
|
isActivating,
|
||||||
|
branchCardCounts,
|
||||||
|
isDevServerRunning,
|
||||||
|
getDevServerInfo,
|
||||||
|
isAutoModeRunningForWorktree,
|
||||||
|
isTestRunningForWorktree,
|
||||||
|
getTestSessionInfo,
|
||||||
|
onSelectWorktree,
|
||||||
|
// Branch switching props
|
||||||
|
branches,
|
||||||
|
filteredBranches,
|
||||||
|
branchFilter,
|
||||||
|
isLoadingBranches,
|
||||||
|
isSwitching,
|
||||||
|
onBranchDropdownOpenChange,
|
||||||
|
onBranchFilterChange,
|
||||||
|
onSwitchBranch,
|
||||||
|
onCreateBranch,
|
||||||
|
// Action dropdown props
|
||||||
|
isPulling,
|
||||||
|
isPushing,
|
||||||
|
isStartingDevServer,
|
||||||
|
aheadCount,
|
||||||
|
behindCount,
|
||||||
|
hasRemoteBranch,
|
||||||
|
gitRepoStatus,
|
||||||
|
hasTestCommand,
|
||||||
|
isStartingTests,
|
||||||
|
hasInitScript,
|
||||||
|
onActionsDropdownOpenChange,
|
||||||
|
onPull,
|
||||||
|
onPush,
|
||||||
|
onPushNewBranch,
|
||||||
|
onOpenInEditor,
|
||||||
|
onOpenInIntegratedTerminal,
|
||||||
|
onOpenInExternalTerminal,
|
||||||
|
onViewChanges,
|
||||||
|
onDiscardChanges,
|
||||||
|
onCommit,
|
||||||
|
onCreatePR,
|
||||||
|
onAddressPRComments,
|
||||||
|
onResolveConflicts,
|
||||||
|
onMerge,
|
||||||
|
onDeleteWorktree,
|
||||||
|
onStartDevServer,
|
||||||
|
onStopDevServer,
|
||||||
|
onOpenDevServerUrl,
|
||||||
|
onViewDevServerLogs,
|
||||||
|
onRunInitScript,
|
||||||
|
onToggleAutoMode,
|
||||||
|
onStartTests,
|
||||||
|
onStopTests,
|
||||||
|
onViewTestLogs,
|
||||||
|
}: WorktreeDropdownProps) {
|
||||||
|
// Find the currently selected worktree to display in the trigger
|
||||||
|
const selectedWorktree = worktrees.find((w) => isWorktreeSelected(w));
|
||||||
|
const displayBranch = selectedWorktree?.branch || 'Select worktree';
|
||||||
|
const { truncated: truncatedBranch, isTruncated: isBranchNameTruncated } = truncateBranchName(
|
||||||
|
displayBranch,
|
||||||
|
MAX_TRIGGER_BRANCH_NAME_LENGTH
|
||||||
|
);
|
||||||
|
|
||||||
|
// Separate main worktree from others for grouping
|
||||||
|
const mainWorktree = worktrees.find((w) => w.isMain);
|
||||||
|
const otherWorktrees = worktrees.filter((w) => !w.isMain);
|
||||||
|
|
||||||
|
// Get status info for selected worktree - memoized to prevent unnecessary recalculations
|
||||||
|
const selectedStatus = useMemo(() => {
|
||||||
|
if (!selectedWorktree) {
|
||||||
|
return {
|
||||||
|
devServerRunning: false,
|
||||||
|
devServerInfo: undefined,
|
||||||
|
autoModeRunning: false,
|
||||||
|
isRunning: false,
|
||||||
|
testRunning: false,
|
||||||
|
testSessionInfo: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
devServerRunning: isDevServerRunning(selectedWorktree),
|
||||||
|
devServerInfo: getDevServerInfo(selectedWorktree),
|
||||||
|
autoModeRunning: isAutoModeRunningForWorktree(selectedWorktree),
|
||||||
|
isRunning: hasRunningFeatures(selectedWorktree),
|
||||||
|
testRunning: isTestRunningForWorktree(selectedWorktree),
|
||||||
|
testSessionInfo: getTestSessionInfo(selectedWorktree),
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
selectedWorktree,
|
||||||
|
isDevServerRunning,
|
||||||
|
getDevServerInfo,
|
||||||
|
isAutoModeRunningForWorktree,
|
||||||
|
hasRunningFeatures,
|
||||||
|
isTestRunningForWorktree,
|
||||||
|
getTestSessionInfo,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Build trigger button with all indicators - memoized for performance
|
||||||
|
const triggerButton = useMemo(
|
||||||
|
() => (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'h-7 px-3 gap-1.5 font-mono text-xs bg-secondary/50 hover:bg-secondary min-w-0 border-r-0 rounded-r-none'
|
||||||
|
)}
|
||||||
|
disabled={isActivating}
|
||||||
|
>
|
||||||
|
{/* Running/Activating indicator */}
|
||||||
|
{(selectedStatus.isRunning || isActivating) && <Spinner size="xs" className="shrink-0" />}
|
||||||
|
|
||||||
|
{/* Branch icon */}
|
||||||
|
<GitBranch className="w-3.5 h-3.5 shrink-0" />
|
||||||
|
|
||||||
|
{/* Branch name with optional tooltip */}
|
||||||
|
<span className="truncate max-w-[150px]">{truncatedBranch}</span>
|
||||||
|
|
||||||
|
{/* Card count badge */}
|
||||||
|
{selectedWorktree &&
|
||||||
|
branchCardCounts?.[selectedWorktree.branch] !== undefined &&
|
||||||
|
branchCardCounts[selectedWorktree.branch] > 0 && (
|
||||||
|
<span className="inline-flex items-center justify-center h-4 min-w-4 px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border shrink-0">
|
||||||
|
{branchCardCounts[selectedWorktree.branch]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Uncommitted changes indicator */}
|
||||||
|
{selectedWorktree?.hasChanges && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center h-4 min-w-4 px-1 text-[10px] font-medium rounded border shrink-0',
|
||||||
|
getChangesBadgeStyles()
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CircleDot className="w-2.5 h-2.5 mr-0.5" />
|
||||||
|
{selectedWorktree.changedFilesCount ?? '!'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dev server indicator */}
|
||||||
|
{selectedStatus.devServerRunning && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center justify-center h-4 w-4 text-green-500 shrink-0"
|
||||||
|
title={`Dev server running on port ${selectedStatus.devServerInfo?.port}`}
|
||||||
|
>
|
||||||
|
<Globe className="w-3 h-3" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Test running indicator */}
|
||||||
|
{selectedStatus.testRunning && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center justify-center h-4 w-4 text-blue-500 shrink-0"
|
||||||
|
title="Tests Running"
|
||||||
|
>
|
||||||
|
<FlaskConical className="w-3 h-3 animate-pulse" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Last test result indicator (when not running) */}
|
||||||
|
{!selectedStatus.testRunning && selectedStatus.testSessionInfo && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center h-4 w-4 shrink-0',
|
||||||
|
getTestStatusStyles(selectedStatus.testSessionInfo.status)
|
||||||
|
)}
|
||||||
|
title={`Last test: ${selectedStatus.testSessionInfo.status}`}
|
||||||
|
>
|
||||||
|
<FlaskConical className="w-3 h-3" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Auto mode indicator */}
|
||||||
|
{selectedStatus.autoModeRunning && (
|
||||||
|
<span
|
||||||
|
className="flex items-center justify-center h-4 px-0.5 shrink-0"
|
||||||
|
title="Auto Mode Running"
|
||||||
|
>
|
||||||
|
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PR badge */}
|
||||||
|
{selectedWorktree?.pr && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center gap-0.5 h-4 px-1 text-[10px] font-medium rounded border shrink-0',
|
||||||
|
getPRBadgeStyles(selectedWorktree.pr.state)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<GitPullRequest className="w-2.5 h-2.5" />#{selectedWorktree.pr.number}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dropdown chevron */}
|
||||||
|
<ChevronDown className="w-3 h-3 shrink-0 ml-auto" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
[isActivating, selectedStatus, truncatedBranch, selectedWorktree, branchCardCounts]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wrap trigger button with dropdown trigger first to ensure ref is passed correctly
|
||||||
|
const dropdownTrigger = <DropdownMenuTrigger asChild>{triggerButton}</DropdownMenuTrigger>;
|
||||||
|
|
||||||
|
const triggerWithTooltip = isBranchNameTruncated ? (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{dropdownTrigger}</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="font-mono text-xs">{displayBranch}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
dropdownTrigger
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenu>
|
||||||
|
{triggerWithTooltip}
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className="w-80 max-h-96 overflow-y-auto"
|
||||||
|
aria-label="Worktree selection"
|
||||||
|
>
|
||||||
|
{/* Main worktree section */}
|
||||||
|
{mainWorktree && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
||||||
|
Main Branch
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<WorktreeDropdownItem
|
||||||
|
worktree={mainWorktree}
|
||||||
|
isSelected={isWorktreeSelected(mainWorktree)}
|
||||||
|
isRunning={hasRunningFeatures(mainWorktree)}
|
||||||
|
cardCount={branchCardCounts?.[mainWorktree.branch]}
|
||||||
|
devServerRunning={isDevServerRunning(mainWorktree)}
|
||||||
|
devServerInfo={getDevServerInfo(mainWorktree)}
|
||||||
|
isAutoModeRunning={isAutoModeRunningForWorktree(mainWorktree)}
|
||||||
|
isTestRunning={isTestRunningForWorktree(mainWorktree)}
|
||||||
|
testSessionInfo={getTestSessionInfo(mainWorktree)}
|
||||||
|
onSelect={() => onSelectWorktree(mainWorktree)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Other worktrees section */}
|
||||||
|
{otherWorktrees.length > 0 && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
||||||
|
Worktrees ({otherWorktrees.length})
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
{otherWorktrees.map((worktree) => (
|
||||||
|
<WorktreeDropdownItem
|
||||||
|
key={worktree.path}
|
||||||
|
worktree={worktree}
|
||||||
|
isSelected={isWorktreeSelected(worktree)}
|
||||||
|
isRunning={hasRunningFeatures(worktree)}
|
||||||
|
cardCount={branchCardCounts?.[worktree.branch]}
|
||||||
|
devServerRunning={isDevServerRunning(worktree)}
|
||||||
|
devServerInfo={getDevServerInfo(worktree)}
|
||||||
|
isAutoModeRunning={isAutoModeRunningForWorktree(worktree)}
|
||||||
|
isTestRunning={isTestRunningForWorktree(worktree)}
|
||||||
|
testSessionInfo={getTestSessionInfo(worktree)}
|
||||||
|
onSelect={() => onSelectWorktree(worktree)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
|
{worktrees.length === 0 && (
|
||||||
|
<div className="px-2 py-4 text-center text-sm text-muted-foreground">
|
||||||
|
No worktrees available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* Branch switch dropdown for main branch (only when main is selected) */}
|
||||||
|
{selectedWorktree?.isMain && (
|
||||||
|
<BranchSwitchDropdown
|
||||||
|
worktree={selectedWorktree}
|
||||||
|
isSelected={true}
|
||||||
|
branches={branches}
|
||||||
|
filteredBranches={filteredBranches}
|
||||||
|
branchFilter={branchFilter}
|
||||||
|
isLoadingBranches={isLoadingBranches}
|
||||||
|
isSwitching={isSwitching}
|
||||||
|
onOpenChange={onBranchDropdownOpenChange(selectedWorktree)}
|
||||||
|
onFilterChange={onBranchFilterChange}
|
||||||
|
onSwitchBranch={onSwitchBranch}
|
||||||
|
onCreateBranch={onCreateBranch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions dropdown for the selected worktree */}
|
||||||
|
{selectedWorktree && (
|
||||||
|
<WorktreeActionsDropdown
|
||||||
|
worktree={selectedWorktree}
|
||||||
|
isSelected={true}
|
||||||
|
aheadCount={aheadCount}
|
||||||
|
behindCount={behindCount}
|
||||||
|
hasRemoteBranch={hasRemoteBranch}
|
||||||
|
isPulling={isPulling}
|
||||||
|
isPushing={isPushing}
|
||||||
|
isStartingDevServer={isStartingDevServer}
|
||||||
|
isDevServerRunning={isDevServerRunning(selectedWorktree)}
|
||||||
|
devServerInfo={getDevServerInfo(selectedWorktree)}
|
||||||
|
gitRepoStatus={gitRepoStatus}
|
||||||
|
isAutoModeRunning={isAutoModeRunningForWorktree(selectedWorktree)}
|
||||||
|
hasTestCommand={hasTestCommand}
|
||||||
|
isStartingTests={isStartingTests}
|
||||||
|
isTestRunning={isTestRunningForWorktree(selectedWorktree)}
|
||||||
|
testSessionInfo={getTestSessionInfo(selectedWorktree)}
|
||||||
|
onOpenChange={onActionsDropdownOpenChange(selectedWorktree)}
|
||||||
|
onPull={onPull}
|
||||||
|
onPush={onPush}
|
||||||
|
onPushNewBranch={onPushNewBranch}
|
||||||
|
onOpenInEditor={onOpenInEditor}
|
||||||
|
onOpenInIntegratedTerminal={onOpenInIntegratedTerminal}
|
||||||
|
onOpenInExternalTerminal={onOpenInExternalTerminal}
|
||||||
|
onViewChanges={onViewChanges}
|
||||||
|
onDiscardChanges={onDiscardChanges}
|
||||||
|
onCommit={onCommit}
|
||||||
|
onCreatePR={onCreatePR}
|
||||||
|
onAddressPRComments={onAddressPRComments}
|
||||||
|
onResolveConflicts={onResolveConflicts}
|
||||||
|
onMerge={onMerge}
|
||||||
|
onDeleteWorktree={onDeleteWorktree}
|
||||||
|
onStartDevServer={onStartDevServer}
|
||||||
|
onStopDevServer={onStopDevServer}
|
||||||
|
onOpenDevServerUrl={onOpenDevServerUrl}
|
||||||
|
onViewDevServerLogs={onViewDevServerLogs}
|
||||||
|
onRunInitScript={onRunInitScript}
|
||||||
|
onToggleAutoMode={onToggleAutoMode}
|
||||||
|
onStartTests={onStartTests}
|
||||||
|
onStopTests={onStopTests}
|
||||||
|
onViewTestLogs={onViewTestLogs}
|
||||||
|
hasInitScript={hasInitScript}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Shared utility functions for worktree indicator styling and formatting.
|
||||||
|
* These utilities ensure consistent appearance across WorktreeTab, WorktreeDropdown,
|
||||||
|
* and WorktreeDropdownItem components.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PRInfo } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncates a branch name if it exceeds the maximum length.
|
||||||
|
* @param branchName - The full branch name
|
||||||
|
* @param maxLength - Maximum characters before truncation
|
||||||
|
* @returns Object with truncated name and whether truncation occurred
|
||||||
|
*/
|
||||||
|
export function truncateBranchName(
|
||||||
|
branchName: string,
|
||||||
|
maxLength: number
|
||||||
|
): { truncated: string; isTruncated: boolean } {
|
||||||
|
const isTruncated = branchName.length > maxLength;
|
||||||
|
const truncated = isTruncated ? `${branchName.slice(0, maxLength)}...` : branchName;
|
||||||
|
return { truncated, isTruncated };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the appropriate CSS classes for a PR badge based on PR state.
|
||||||
|
* @param state - The PR state (OPEN, MERGED, or CLOSED)
|
||||||
|
* @returns CSS class string for the badge
|
||||||
|
*/
|
||||||
|
export function getPRBadgeStyles(state: PRInfo['state']): string {
|
||||||
|
switch (state) {
|
||||||
|
case 'OPEN':
|
||||||
|
return 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/30';
|
||||||
|
case 'MERGED':
|
||||||
|
return 'bg-purple-500/15 text-purple-600 dark:text-purple-400 border-purple-500/30';
|
||||||
|
case 'CLOSED':
|
||||||
|
default:
|
||||||
|
return 'bg-rose-500/15 text-rose-600 dark:text-rose-400 border-rose-500/30';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the CSS classes for the uncommitted changes badge.
|
||||||
|
* This is a constant style used across all worktree components.
|
||||||
|
*/
|
||||||
|
export function getChangesBadgeStyles(): string {
|
||||||
|
return 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Possible test session status values */
|
||||||
|
export type TestStatus = 'pending' | 'running' | 'passed' | 'failed' | 'cancelled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the CSS classes for a test status indicator based on test result.
|
||||||
|
* @param status - The test session status
|
||||||
|
* @returns CSS class string for the indicator color
|
||||||
|
*/
|
||||||
|
export function getTestStatusStyles(status: TestStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'passed':
|
||||||
|
return 'text-green-500';
|
||||||
|
case 'failed':
|
||||||
|
return 'text-red-500';
|
||||||
|
case 'running':
|
||||||
|
return 'text-blue-500';
|
||||||
|
case 'pending':
|
||||||
|
case 'cancelled':
|
||||||
|
default:
|
||||||
|
return 'text-muted-foreground';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
WorktreeMobileDropdown,
|
WorktreeMobileDropdown,
|
||||||
WorktreeActionsDropdown,
|
WorktreeActionsDropdown,
|
||||||
BranchSwitchDropdown,
|
BranchSwitchDropdown,
|
||||||
|
WorktreeDropdown,
|
||||||
} from './components';
|
} from './components';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { ViewWorktreeChangesDialog, PushToRemoteDialog, MergeWorktreeDialog } from '../dialogs';
|
import { ViewWorktreeChangesDialog, PushToRemoteDialog, MergeWorktreeDialog } from '../dialogs';
|
||||||
@@ -36,6 +37,9 @@ import { TestLogsPanel } from '@/components/ui/test-logs-panel';
|
|||||||
import { Undo2 } from 'lucide-react';
|
import { Undo2 } from 'lucide-react';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
|
||||||
|
/** Threshold for switching from tabs to dropdown layout (number of worktrees) */
|
||||||
|
const WORKTREE_DROPDOWN_THRESHOLD = 3;
|
||||||
|
|
||||||
export function WorktreePanel({
|
export function WorktreePanel({
|
||||||
projectPath,
|
projectPath,
|
||||||
onCreateWorktree,
|
onCreateWorktree,
|
||||||
@@ -712,12 +716,112 @@ export function WorktreePanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desktop view: full tabs layout
|
// Use dropdown layout when worktree count meets or exceeds the threshold
|
||||||
|
const useDropdownLayout = worktrees.length >= WORKTREE_DROPDOWN_THRESHOLD;
|
||||||
|
|
||||||
|
// Desktop view: full tabs layout or dropdown layout depending on worktree count
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-glass/50 backdrop-blur-sm">
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-glass/50 backdrop-blur-sm">
|
||||||
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm text-muted-foreground mr-2">Branch:</span>
|
<span className="text-sm text-muted-foreground mr-2">
|
||||||
|
{useDropdownLayout ? 'Worktree:' : 'Branch:'}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Dropdown layout for 3+ worktrees */}
|
||||||
|
{useDropdownLayout ? (
|
||||||
|
<>
|
||||||
|
<WorktreeDropdown
|
||||||
|
worktrees={worktrees}
|
||||||
|
isWorktreeSelected={isWorktreeSelected}
|
||||||
|
hasRunningFeatures={hasRunningFeatures}
|
||||||
|
isActivating={isActivating}
|
||||||
|
branchCardCounts={branchCardCounts}
|
||||||
|
isDevServerRunning={isDevServerRunning}
|
||||||
|
getDevServerInfo={getDevServerInfo}
|
||||||
|
isAutoModeRunningForWorktree={isAutoModeRunningForWorktree}
|
||||||
|
isTestRunningForWorktree={isTestRunningForWorktree}
|
||||||
|
getTestSessionInfo={getTestSessionInfo}
|
||||||
|
onSelectWorktree={handleSelectWorktree}
|
||||||
|
// Branch switching props
|
||||||
|
branches={branches}
|
||||||
|
filteredBranches={filteredBranches}
|
||||||
|
branchFilter={branchFilter}
|
||||||
|
isLoadingBranches={isLoadingBranches}
|
||||||
|
isSwitching={isSwitching}
|
||||||
|
onBranchDropdownOpenChange={handleBranchDropdownOpenChange}
|
||||||
|
onBranchFilterChange={setBranchFilter}
|
||||||
|
onSwitchBranch={handleSwitchBranch}
|
||||||
|
onCreateBranch={onCreateBranch}
|
||||||
|
// Action dropdown props
|
||||||
|
isPulling={isPulling}
|
||||||
|
isPushing={isPushing}
|
||||||
|
isStartingDevServer={isStartingDevServer}
|
||||||
|
aheadCount={aheadCount}
|
||||||
|
behindCount={behindCount}
|
||||||
|
hasRemoteBranch={hasRemoteBranch}
|
||||||
|
gitRepoStatus={gitRepoStatus}
|
||||||
|
hasTestCommand={hasTestCommand}
|
||||||
|
isStartingTests={isStartingTests}
|
||||||
|
hasInitScript={hasInitScript}
|
||||||
|
onActionsDropdownOpenChange={handleActionsDropdownOpenChange}
|
||||||
|
onPull={handlePull}
|
||||||
|
onPush={handlePush}
|
||||||
|
onPushNewBranch={handlePushNewBranch}
|
||||||
|
onOpenInEditor={handleOpenInEditor}
|
||||||
|
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||||
|
onOpenInExternalTerminal={handleOpenInExternalTerminal}
|
||||||
|
onViewChanges={handleViewChanges}
|
||||||
|
onDiscardChanges={handleDiscardChanges}
|
||||||
|
onCommit={onCommit}
|
||||||
|
onCreatePR={onCreatePR}
|
||||||
|
onAddressPRComments={onAddressPRComments}
|
||||||
|
onResolveConflicts={onResolveConflicts}
|
||||||
|
onMerge={handleMerge}
|
||||||
|
onDeleteWorktree={onDeleteWorktree}
|
||||||
|
onStartDevServer={handleStartDevServer}
|
||||||
|
onStopDevServer={handleStopDevServer}
|
||||||
|
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||||
|
onViewDevServerLogs={handleViewDevServerLogs}
|
||||||
|
onRunInitScript={handleRunInitScript}
|
||||||
|
onToggleAutoMode={handleToggleAutoMode}
|
||||||
|
onStartTests={handleStartTests}
|
||||||
|
onStopTests={handleStopTests}
|
||||||
|
onViewTestLogs={handleViewTestLogs}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{useWorktreesEnabled && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={onCreateWorktree}
|
||||||
|
title="Create new worktree"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={async () => {
|
||||||
|
const removedWorktrees = await fetchWorktrees();
|
||||||
|
if (removedWorktrees && removedWorktrees.length > 0 && onRemovedWorktrees) {
|
||||||
|
onRemovedWorktrees(removedWorktrees);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
title="Refresh worktrees"
|
||||||
|
>
|
||||||
|
{isLoading ? <Spinner size="xs" /> : <RefreshCw className="w-3.5 h-3.5" />}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
/* Standard tabs layout for 1-2 worktrees */
|
||||||
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{mainWorktree && (
|
{mainWorktree && (
|
||||||
<WorktreeTab
|
<WorktreeTab
|
||||||
@@ -782,7 +886,7 @@ export function WorktreePanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Worktrees section - only show if enabled */}
|
{/* Worktrees section - only show if enabled and not using dropdown layout */}
|
||||||
{useWorktreesEnabled && (
|
{useWorktreesEnabled && (
|
||||||
<>
|
<>
|
||||||
<div className="w-px h-5 bg-border mx-2" />
|
<div className="w-px h-5 bg-border mx-2" />
|
||||||
@@ -883,6 +987,8 @@ export function WorktreePanel({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* View Changes Dialog */}
|
{/* View Changes Dialog */}
|
||||||
<ViewWorktreeChangesDialog
|
<ViewWorktreeChangesDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user