import { useCallback, useMemo, useState, type Dispatch, type SetStateAction } from "react"; import { ActivityIndicator, Platform, Pressable, ScrollView, Text, View, type LayoutChangeEvent, } from "react-native"; import { CopyX, ArrowLeftToLine, ArrowRightToLine, Columns2, Copy, Plus, Rows2, SquarePen, SquareTerminal, X, } from "lucide-react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { SortableInlineList } from "@/components/sortable-inline-list"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { Shortcut } from "@/components/ui/shortcut"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout"; import { useWorkspaceTabLayout } from "@/screens/workspace/use-workspace-tab-layout"; import { WorkspaceTabPresentationResolver, WorkspaceTabIcon, type WorkspaceTabPresentation, } from "@/screens/workspace/workspace-tab-presentation"; import { buildWorkspaceDesktopTabActions, type WorkspaceDesktopTabActions, } from "@/screens/workspace/workspace-tab-menu"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; const DROPDOWN_WIDTH = 220; const LOADING_TAB_LABEL_SKELETON_WIDTH = 80; export interface WorkspaceDesktopTabRowItem { tab: WorkspaceTabDescriptor; isActive: boolean; isCloseHovered: boolean; isClosingTab: boolean; } type WorkspaceDesktopTabsRowProps = { paneId?: string; isFocused?: boolean; tabs: WorkspaceDesktopTabRowItem[]; normalizedServerId: string; normalizedWorkspaceId: string; setHoveredTabKey: Dispatch>; setHoveredCloseTabKey: Dispatch>; onNavigateTab: (tabId: string) => void; onCloseTab: (tabId: string) => Promise | void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; onCloseTabsToLeft: (tabId: string) => Promise | void; onCloseTabsToRight: (tabId: string) => Promise | void; onCloseOtherTabs: (tabId: string) => Promise | void; onCreateLauncherTab: (input: { paneId?: string }) => void; onReorderTabs: (nextTabs: WorkspaceTabDescriptor[]) => void; onSplitRight: () => void; onSplitDown: () => void; externalDndContext?: boolean; activeDragTabId?: string | null; tabDropPreviewIndex?: number | null; showPaneSplitActions?: boolean; }; function getFallbackTabLabel(tab: WorkspaceTabDescriptor): string { if (tab.target.kind === "launcher") { return "New Tab"; } if (tab.target.kind === "draft") { return "New Agent"; } if (tab.target.kind === "terminal") { return "Terminal"; } if (tab.target.kind === "file") { return tab.target.path.split("/").filter(Boolean).pop() ?? tab.target.path; } return "Agent"; } function TabChip({ tab, isActive, isDragging, isFocused, resolvedTabWidth, showLabel, showCloseButton, isCloseHovered, isClosingTab, presentation, tooltipLabel, resolvedTab, setHoveredTabKey, setHoveredCloseTabKey, onNavigateTab, onCloseTab, dragHandleProps, }: { tab: WorkspaceTabDescriptor; isActive: boolean; isDragging: boolean; isFocused: boolean; resolvedTabWidth: number; showLabel: boolean; showCloseButton: boolean; isCloseHovered: boolean; isClosingTab: boolean; presentation: WorkspaceTabPresentation; tooltipLabel: string; resolvedTab: WorkspaceDesktopTabActions; setHoveredTabKey: Dispatch>; setHoveredCloseTabKey: Dispatch>; onNavigateTab: (tabId: string) => void; onCloseTab: (tabId: string) => Promise | void; dragHandleProps: any; }) { const { theme } = useUnistyles(); const { closeButtonTestId, contextMenuTestId, menuEntries } = resolvedTab; const [hovered, setHovered] = useState(false); const isHighlighted = isActive || hovered || isCloseHovered; const closeButtonDragBlockers = Platform.OS === "web" ? ({ onPointerDown: (event: { stopPropagation?: () => void }) => { event.stopPropagation?.(); }, onMouseDown: (event: { stopPropagation?: () => void }) => { event.stopPropagation?.(); }, } as const) : undefined; return ( [ styles.tab, Platform.OS === "web" && isDragging && ({ cursor: "grabbing" } as const), { minWidth: resolvedTabWidth, width: resolvedTabWidth, maxWidth: resolvedTabWidth, }, ]} onHoverIn={() => { setHovered(true); setHoveredTabKey(tab.key); }} onHoverOut={() => { setHovered(false); setHoveredTabKey((current) => (current === tab.key ? null : current)); }} onPressIn={() => { onNavigateTab(tab.tabId); }} onPress={() => { onNavigateTab(tab.tabId); }} accessibilityLabel={tooltipLabel} > {isActive && ( )} {showLabel ? ( presentation.titleState === "loading" ? ( ) : ( {presentation.label} ) ) : null} {showCloseButton ? ( { event.stopPropagation?.(); }} onHoverIn={() => { setHoveredTabKey(tab.key); setHoveredCloseTabKey(tab.key); }} onHoverOut={() => { setHoveredTabKey((current) => (current === tab.key ? null : current)); setHoveredCloseTabKey((current) => (current === tab.key ? null : current)); }} onPress={(event) => { event.stopPropagation?.(); void onCloseTab(tab.tabId); }} style={({ hovered, pressed }) => [ styles.tabCloseButton, styles.tabCloseButtonShown, (hovered || pressed) && styles.tabCloseButtonActive, ]} > {({ hovered, pressed }) => isClosingTab ? ( ) : ( ) } ) : null} {tab.target.kind === "agent" ? ( {tooltipLabel} {tab.target.agentId.slice(0, 7)} ) : ( {tooltipLabel} )} {menuEntries.map((entry) => entry.kind === "separator" ? ( ) : ( { const iconColor = theme.colors.foregroundMuted; switch (entry.icon) { case "copy": return ; case "arrow-left-to-line": return ; case "arrow-right-to-line": return ; case "copy-x": return ; case "x": return ; default: return undefined; } })()} trailing={ entry.hint ? ( {entry.hint} ) : undefined } > {entry.label} ), )} ); } export function WorkspaceDesktopTabsRow({ paneId, isFocused = false, tabs, normalizedServerId, normalizedWorkspaceId, setHoveredTabKey, setHoveredCloseTabKey, onNavigateTab, onCloseTab, onCopyResumeCommand, onCopyAgentId, onCloseTabsToLeft, onCloseTabsToRight, onCloseOtherTabs, onCreateLauncherTab, onReorderTabs, onSplitRight, onSplitDown, externalDndContext = false, activeDragTabId = null, tabDropPreviewIndex = null, showPaneSplitActions = true, }: WorkspaceDesktopTabsRowProps) { const { theme } = useUnistyles(); const newTabKeys = useShortcutKeys("workspace-tab-new"); const splitRightKeys = useShortcutKeys("workspace-pane-split-right"); const splitDownKeys = useShortcutKeys("workspace-pane-split-down"); const [tabsContainerWidth, setTabsContainerWidth] = useState(0); const [tabsActionsWidth, setTabsActionsWidth] = useState(0); const handleTabsContainerLayout = useCallback((event: LayoutChangeEvent) => { const nextWidth = Math.round(event.nativeEvent.layout.width); setTabsContainerWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current)); }, []); const handleTabsActionsLayout = useCallback((event: LayoutChangeEvent) => { const nextWidth = Math.round(event.nativeEvent.layout.width); setTabsActionsWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current)); }, []); const layoutMetrics = useMemo( () => ({ rowHorizontalInset: 0, actionsReservedWidth: Math.max(0, tabsActionsWidth), rowPaddingHorizontal: 0, tabGap: 0, maxTabWidth: 200, tabIconWidth: 14, tabHorizontalPadding: theme.spacing[3], estimatedCharWidth: 7, closeButtonWidth: 22, }), [tabsActionsWidth, theme.spacing], ); const tabLabelLengths = useMemo( () => tabs.map((tab) => { const label = getFallbackTabLabel(tab.tab); return label.length; }), [tabs], ); const { layout } = useWorkspaceTabLayout({ tabLabelLengths, viewportWidthOverride: tabsContainerWidth > 0 ? tabsContainerWidth : null, metrics: layoutMetrics, }); return ( `${tab.tab.key}:${tab.tab.kind}`} useDragHandle disabled={!externalDndContext && tabs.length < 2} onDragEnd={(nextTabs) => onReorderTabs(nextTabs.map((tab) => tab.tab))} externalDndContext={externalDndContext} activeId={activeDragTabId} getItemData={ paneId ? (tab) => ({ kind: "workspace-tab", paneId, tabId: tab.tab.tabId, }) : undefined } renderItem={({ item, index, dragHandleProps, isActive }) => { const shouldShowCloseButton = layout.closeButtonPolicy === "all"; const layoutItem = layout.items[index] ?? null; const resolvedTabWidth = layoutItem?.width ?? 150; const showLabel = layoutItem?.showLabel ?? true; const showDropIndicatorBefore = activeDragTabId !== null && tabDropPreviewIndex === index; const showDropIndicatorAfter = activeDragTabId !== null && tabDropPreviewIndex === tabs.length && index === tabs.length - 1; return ( ); }} /> onCreateLauncherTab({ paneId })} accessibilityRole="button" accessibilityLabel="New tab" style={({ hovered, pressed }) => [ styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered, ]} > New tab {newTabKeys ? ( ) : null} {showPaneSplitActions ? ( <> [ styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered, ]} > Split pane right {splitRightKeys ? ( ) : null} [ styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered, ]} > Split pane down {splitDownKeys ? ( ) : null} ) : null} ); } function ResolvedDesktopTabChip({ item, isFocused, isDragging, index, tabCount, normalizedServerId, normalizedWorkspaceId, onCopyResumeCommand, onCopyAgentId, onCloseTabsToLeft, onCloseTabsToRight, onCloseOtherTabs, resolvedTabWidth, showLabel, showCloseButton, setHoveredTabKey, setHoveredCloseTabKey, onNavigateTab, onCloseTab, dragHandleProps, showDropIndicatorBefore, showDropIndicatorAfter, }: { item: WorkspaceDesktopTabRowItem; isFocused: boolean; isDragging: boolean; index: number; tabCount: number; normalizedServerId: string; normalizedWorkspaceId: string; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; onCloseTabsToLeft: (tabId: string) => Promise | void; onCloseTabsToRight: (tabId: string) => Promise | void; onCloseOtherTabs: (tabId: string) => Promise | void; resolvedTabWidth: number; showLabel: boolean; showCloseButton: boolean; setHoveredTabKey: Dispatch>; setHoveredCloseTabKey: Dispatch>; onNavigateTab: (tabId: string) => void; onCloseTab: (tabId: string) => Promise | void; dragHandleProps: any; showDropIndicatorBefore: boolean; showDropIndicatorAfter: boolean; }) { const resolvedTab = useMemo( () => buildWorkspaceDesktopTabActions({ tab: item.tab, index, tabCount, onCopyResumeCommand, onCopyAgentId, onCloseTab, onCloseTabsToLeft, onCloseTabsToRight, onCloseOtherTabs, }), [ index, item.tab, onCloseOtherTabs, onCloseTab, onCloseTabsToLeft, onCloseTabsToRight, onCopyAgentId, onCopyResumeCommand, tabCount, ], ); return ( {(presentation) => { const tooltipLabel = presentation.titleState === "loading" ? "Loading agent title" : presentation.label; return ( {showDropIndicatorBefore ? ( ) : null} {showDropIndicatorAfter ? ( ) : null} ); }} ); } const styles = StyleSheet.create((theme) => ({ tabsContainer: { minWidth: 0, height: WORKSPACE_SECONDARY_HEADER_HEIGHT, borderBottomWidth: 1, borderBottomColor: theme.colors.border, backgroundColor: theme.colors.surface0, flexDirection: "row", alignItems: "center", overflow: "visible", }, tabsScroll: { minWidth: 0, }, tabsScrollFitContent: { flex: 1, }, tabsScrollOverflow: { flex: 1, }, tabsContent: { flexDirection: "row", alignItems: "stretch", }, tabsActions: { flexDirection: "row", alignItems: "center", paddingHorizontal: theme.spacing[2], }, tab: { paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], borderRightWidth: 1, borderRightColor: theme.colors.border, flexDirection: "row", alignItems: "center", gap: theme.spacing[1], userSelect: "none", }, tabSlot: { position: "relative", overflow: "visible", }, tabHandle: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], flex: 1, minWidth: 0, userSelect: "none", }, tabIcon: { flexShrink: 0, }, tabFocusIndicator: { position: "absolute", top: 0, left: 0, right: 0, height: 2, backgroundColor: theme.colors.accent, }, tabFocusIndicatorUnfocused: { backgroundColor: theme.colors.borderAccent, }, tabDropIndicator: { position: "absolute", top: theme.spacing[2], bottom: theme.spacing[2], width: 5, borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.accent, zIndex: 10, pointerEvents: "none", }, tabDropIndicatorBefore: { left: -3, }, tabDropIndicatorAfter: { right: -3, }, tabLabel: { flexShrink: 1, minWidth: 0, color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, userSelect: "none", }, tabLabelSkeleton: { width: 96, maxWidth: "100%", flexShrink: 1, minWidth: 0, height: 10, borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.surface3, opacity: 0.9, }, tabLabelSkeletonWithCloseButton: { width: LOADING_TAB_LABEL_SKELETON_WIDTH, }, tabLabelWithCloseButton: { paddingRight: 0, }, tabLabelActive: { color: theme.colors.foreground, }, tabCloseButton: { width: 18, height: 18, marginLeft: 0, borderRadius: theme.borderRadius.sm, alignItems: "center", justifyContent: "center", }, tabCloseButtonShown: { opacity: 1, }, tabCloseButtonActive: { backgroundColor: theme.colors.surface3, }, newTabActionButton: { width: 22, height: 22, borderRadius: theme.borderRadius.md, alignItems: "center", justifyContent: "center", }, newTabActionButtonHovered: { backgroundColor: theme.colors.surface2, }, newTabTooltipText: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, }, newTabTooltipRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, newTabTooltipShortcut: { backgroundColor: theme.colors.surface3, borderColor: theme.colors.borderAccent, }, tooltipAgentRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, tooltipAgentId: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, }, menuItemHint: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, }, }));