+
+ {triggerWithTooltip}
+
+ {/* Main worktree section */}
+ {mainWorktree && (
+ <>
+
+ Main Branch
+
+ onSelectWorktree(mainWorktree)}
+ />
+ >
+ )}
+
+ {/* Other worktrees section */}
+ {otherWorktrees.length > 0 && (
+ <>
+
+
+ Worktrees ({otherWorktrees.length})
+
+
+ {otherWorktrees.map((worktree) => (
+ onSelectWorktree(worktree)}
+ />
+ ))}
+
+ >
+ )}
+
+ {/* Empty state */}
+ {worktrees.length === 0 && (
+
+ No worktrees available
+
+ )}
+
+
+
+ {/* Branch switch dropdown for main branch (only when main is selected) */}
+ {selectedWorktree?.isMain && (
+
+ )}
+
+ {/* Actions dropdown for the selected worktree */}
+ {selectedWorktree && (
+
+ )}
+
+ );
+}
diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-indicator-utils.ts b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-indicator-utils.ts
new file mode 100644
index 00000000..503a8396
--- /dev/null
+++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-indicator-utils.ts
@@ -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';
+ }
+}
diff --git a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
index 40f10e85..b1e800fe 100644
--- a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
+++ b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
@@ -28,6 +28,7 @@ import {
WorktreeMobileDropdown,
WorktreeActionsDropdown,
BranchSwitchDropdown,
+ WorktreeDropdown,
} from './components';
import { useAppStore } from '@/store/app-store';
import { ViewWorktreeChangesDialog, PushToRemoteDialog, MergeWorktreeDialog } from '../dialogs';
@@ -36,6 +37,9 @@ import { TestLogsPanel } from '@/components/ui/test-logs-panel';
import { Undo2 } from 'lucide-react';
import { getElectronAPI } from '@/lib/electron';
+/** Threshold for switching from tabs to dropdown layout (number of worktrees) */
+const WORKTREE_DROPDOWN_THRESHOLD = 3;
+
export function WorktreePanel({
projectPath,
onCreateWorktree,
@@ -712,30 +716,43 @@ 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 (
-
Branch:
+
+ {useDropdownLayout ? 'Worktree:' : 'Branch:'}
+
-
- {mainWorktree && (
-
+
- )}
-
- {/* Worktrees section - only show if enabled */}
- {useWorktreesEnabled && (
+ {useWorktreesEnabled && (
+ <>
+
+
+
+ >
+ )}
+ >
+ ) : (
+ /* Standard tabs layout for 1-2 worktrees */
<>
-
-
-
Worktrees:
-
-
- {nonMainWorktrees.map((worktree) => {
- const cardCount = branchCardCounts?.[worktree.branch];
- return (
-
- );
- })}
-
-
-
-
+
+ {mainWorktree && (
+
+ )}
+
+ {/* Worktrees section - only show if enabled and not using dropdown layout */}
+ {useWorktreesEnabled && (
+ <>
+
+
+
Worktrees:
+
+
+ {nonMainWorktrees.map((worktree) => {
+ const cardCount = branchCardCounts?.[worktree.branch];
+ return (
+
+ );
+ })}
+
+
+
+
+
+ >
+ )}
>
)}