import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useStoreWithEqualityFn } from "zustand/traditional"; import { useIsFocused } from "@react-navigation/native"; import { ActivityIndicator, BackHandler, Platform, Pressable, Text, useColorScheme, View, } from "react-native"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import * as Clipboard from "expo-clipboard"; import { ChevronDown, Copy, Ellipsis, EllipsisVertical, PanelRight, SquarePen, SquareTerminal, } from "lucide-react-native"; import { GestureDetector } from "react-native-gesture-handler"; import Animated from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import invariant from "tiny-invariant"; import { SidebarMenuToggle } from "@/components/headers/menu-header"; import { HeaderToggleButton } from "@/components/headers/header-toggle-button"; import { ScreenHeader } from "@/components/headers/screen-header"; import { Combobox, type ComboboxOption } from "@/components/ui/combobox"; import { Shortcut } from "@/components/ui/shortcut"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { ExplorerSidebar } from "@/components/explorer-sidebar"; import { SplitContainer } from "@/components/split-container"; import { SourceControlPanelIcon } from "@/components/icons/source-control-panel-icon"; import { WorkspaceGitActions } from "@/screens/workspace/workspace-git-actions"; import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context"; import { useToast } from "@/contexts/toast-context"; import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture"; import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store"; import { useSessionStore, } from "@/stores/session-store"; import { buildWorkspaceTabPersistenceKey, collectAllTabs, useWorkspaceLayoutStore, } from "@/stores/workspace-layout-store"; import type { WorkspaceTab, WorkspaceTabTarget } from "@/stores/workspace-tabs-store"; import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher"; import { useCreateFlowStore } from "@/stores/create-flow-store"; import { decodeWorkspaceIdFromPathSegment, } from "@/utils/host-routes"; import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity"; import { normalizeWorkspaceTabTarget, workspaceTabTargetsEqual, } from "@/utils/workspace-tab-identity"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useWorkspaceTerminalSessionRetention } from "@/terminal/hooks/use-workspace-terminal-session-retention"; import { checkoutStatusQueryKey, type CheckoutStatusPayload, } from "@/hooks/use-checkout-status-query"; import type { ListTerminalsResponse } from "@server/shared/messages"; import { upsertTerminalListEntry } from "@/utils/terminal-list"; import { confirmDialog } from "@/utils/confirm-dialog"; import { useArchiveAgent } from "@/hooks/use-archive-agent"; import { buildProviderCommand } from "@/utils/provider-command-templates"; import { generateDraftId } from "@/stores/draft-keys"; import { WorkspaceTabPresentationResolver, WorkspaceTabIcon, WorkspaceTabOptionRow, } from "@/screens/workspace/workspace-tab-presentation"; import { buildWorkspaceTabMenuEntries } from "@/screens/workspace/workspace-tab-menu"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; import { resolveWorkspaceHeader, shouldRenderMissingWorkspaceDescriptor, } from "@/screens/workspace/workspace-header-source"; import { deriveWorkspaceAgentVisibility, shouldPruneWorkspaceAgentTab, workspaceAgentVisibilityEqual, } from "@/screens/workspace/workspace-agent-visibility"; import { deriveWorkspacePaneState, } from "@/screens/workspace/workspace-pane-state"; import { buildWorkspacePaneContentModel, WorkspacePaneContent, } from "@/screens/workspace/workspace-pane-content"; import { buildBulkCloseConfirmationMessage, classifyBulkClosableTabs, } from "@/screens/workspace/workspace-bulk-close"; import { findAdjacentPane } from "@/utils/split-navigation"; const TERMINALS_QUERY_STALE_TIME = 5_000; const NEW_TAB_AGENT_OPTION_ID = "__new_tab_agent__"; const NEW_TAB_TERMINAL_OPTION_ID = "__new_tab_terminal__"; type NewTabOptionId = typeof NEW_TAB_AGENT_OPTION_ID | typeof NEW_TAB_TERMINAL_OPTION_ID; const EMPTY_UI_TABS: WorkspaceTab[] = []; const EMPTY_PINNED_AGENT_IDS = new Set(); type WorkspaceScreenProps = { serverId: string; workspaceId: string; }; function trimNonEmpty(value: string | null | undefined): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function decodeSegment(value: string): string { try { return decodeURIComponent(value); } catch { return value; } } function getFallbackTabOptionLabel(tab: WorkspaceTabDescriptor): string { 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 getFallbackTabOptionDescription(tab: WorkspaceTabDescriptor): string { if (tab.target.kind === "draft") { return "New Agent"; } if (tab.target.kind === "agent") { return "Agent"; } if (tab.target.kind === "terminal") { return "Terminal"; } return tab.target.path; } type MobileWorkspaceTabSwitcherProps = { tabs: WorkspaceTabDescriptor[]; activeTabKey: string; activeTab: WorkspaceTabDescriptor | null; tabSwitcherOptions: ComboboxOption[]; tabByKey: Map; normalizedServerId: string; normalizedWorkspaceId: string; onSelectSwitcherTab: (key: string) => void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; onCloseTab: (tabId: string) => Promise | void; onCloseTabsAbove: (tabId: string) => Promise | void; onCloseTabsBelow: (tabId: string) => Promise | void; onCloseOtherTabs: (tabId: string) => Promise | void; }; function MobileActiveTabTrigger({ activeTab, normalizedServerId, normalizedWorkspaceId, }: { activeTab: WorkspaceTabDescriptor | null; normalizedServerId: string; normalizedWorkspaceId: string; }) { if (!activeTab) { return null; } return ( ); } function ResolvedMobileActiveTabTrigger({ activeTab, normalizedServerId, normalizedWorkspaceId, }: { activeTab: WorkspaceTabDescriptor; normalizedServerId: string; normalizedWorkspaceId: string; }) { return ( {(presentation) => ( <> {presentation.titleState === "loading" ? "Loading..." : presentation.label} )} ); } function WorkspaceDocumentTitleEffect({ label, titleState, }: { label: string; titleState: "ready" | "loading"; }) { useEffect(() => { if (Platform.OS !== "web" || typeof document === "undefined") { return; } const resolvedLabel = label.trim(); document.title = titleState === "loading" ? "Loading..." : resolvedLabel || "Workspace"; }, [label, titleState]); return null; } function MobileWorkspaceTabOption({ tab, tabIndex, tabCount, normalizedServerId, normalizedWorkspaceId, selected, active, onPress, onCopyResumeCommand, onCopyAgentId, onCloseTab, onCloseTabsAbove, onCloseTabsBelow, onCloseOtherTabs, }: { tab: WorkspaceTabDescriptor; tabIndex: number; tabCount: number; normalizedServerId: string; normalizedWorkspaceId: string; selected: boolean; active: boolean; onPress: () => void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; onCloseTab: (tabId: string) => Promise | void; onCloseTabsAbove: (tabId: string) => Promise | void; onCloseTabsBelow: (tabId: string) => Promise | void; onCloseOtherTabs: (tabId: string) => Promise | void; }) { const { theme } = useUnistyles(); const menuTestIDBase = `workspace-tab-menu-${tab.key}`; const menuEntries = buildWorkspaceTabMenuEntries({ surface: "mobile", tab, index: tabIndex, tabCount, menuTestIDBase, onCopyResumeCommand, onCopyAgentId, onCloseTab, onCloseTabsBefore: onCloseTabsAbove, onCloseTabsAfter: onCloseTabsBelow, onCloseOtherTabs, }); return ( {(presentation) => ( [ styles.mobileTabMenuTrigger, (open || pressed) && styles.mobileTabMenuTriggerActive, ]} > {menuEntries.map((entry) => entry.kind === "separator" ? ( ) : ( {entry.label} ) )} } /> )} ); } const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({ tabs, activeTabKey, activeTab, tabSwitcherOptions, tabByKey, normalizedServerId, normalizedWorkspaceId, onSelectSwitcherTab, onCopyResumeCommand, onCopyAgentId, onCloseTab, onCloseTabsAbove, onCloseTabsBelow, onCloseOtherTabs, }: MobileWorkspaceTabSwitcherProps) { const { theme } = useUnistyles(); const [isOpen, setIsOpen] = useState(false); const anchorRef = useRef(null); const tabIndexByKey = useMemo(() => { const map = new Map(); tabs.forEach((tab, index) => { map.set(tab.key, index); }); return map; }, [tabs]); return ( [ styles.switcherTrigger, pressed && styles.switcherTriggerPressed, ]} onPress={() => setIsOpen(true)} > { const tab = tabByKey.get(option.id); if (!tab) { return ; } const tabIndex = tabIndexByKey.get(tab.key) ?? -1; if (tabIndex < 0) { return ; } return ( ); }} /> ); }); export function WorkspaceScreen({ serverId, workspaceId, }: WorkspaceScreenProps) { const isFocused = useIsFocused(); if (!isFocused) { return ; } return ( ); } function WorkspaceScreenContent({ serverId, workspaceId, }: WorkspaceScreenProps) { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); const isDarkMode = useColorScheme() === "dark"; const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0; const toast = useToast(); const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? ""; const normalizedWorkspaceId = normalizeWorkspaceIdentity(decodeWorkspaceIdFromPathSegment(workspaceId)) ?? ""; const workspaceTerminalScopeKey = normalizedServerId && normalizedWorkspaceId ? `${normalizedServerId}:${normalizedWorkspaceId}` : null; useWorkspaceTerminalSessionRetention({ scopeKey: workspaceTerminalScopeKey, }); const queryClient = useQueryClient(); const client = useHostRuntimeClient(normalizedServerId); const isConnected = useHostRuntimeIsConnected(normalizedServerId); const workspaceAgentVisibility = useStoreWithEqualityFn( useSessionStore, (state) => deriveWorkspaceAgentVisibility({ sessionAgents: state.sessions[normalizedServerId]?.agents, workspaceId: normalizedWorkspaceId, }), workspaceAgentVisibilityEqual, ); const terminalsQueryKey = useMemo( () => ["terminals", normalizedServerId, normalizedWorkspaceId] as const, [normalizedServerId, normalizedWorkspaceId] ); type ListTerminalsPayload = ListTerminalsResponse["payload"]; const terminalsQuery = useQuery({ queryKey: terminalsQueryKey, enabled: Boolean(client && isConnected) && normalizedWorkspaceId.length > 0 && normalizedWorkspaceId.startsWith("/"), queryFn: async () => { if (!client) { throw new Error("Host is not connected"); } return await client.listTerminals(normalizedWorkspaceId); }, staleTime: TERMINALS_QUERY_STALE_TIME, }); const terminals = terminalsQuery.data?.terminals ?? []; const createTerminalMutation = useMutation({ mutationFn: async (input?: { paneId?: string }) => { if (!client) { throw new Error("Host is not connected"); } return await client.createTerminal(normalizedWorkspaceId); }, onSuccess: (payload, input) => { const createdTerminal = payload.terminal; if (createdTerminal) { queryClient.setQueryData( terminalsQueryKey, (current) => { const nextTerminals = upsertTerminalListEntry({ terminals: current?.terminals ?? [], terminal: createdTerminal, }); return { cwd: current?.cwd ?? normalizedWorkspaceId, terminals: nextTerminals, requestId: current?.requestId ?? `terminal-create-${createdTerminal.id}`, }; } ); } void queryClient.invalidateQueries({ queryKey: terminalsQueryKey }); if (createdTerminal) { const workspaceKey = buildWorkspaceTabPersistenceKey({ serverId: normalizedServerId, workspaceId: normalizedWorkspaceId, }); if (!workspaceKey) { return; } if (input?.paneId) { focusWorkspacePane(workspaceKey, input.paneId); } const tabId = useWorkspaceLayoutStore .getState() .openTab(workspaceKey, { kind: "terminal", terminalId: createdTerminal.id }); if (tabId) { useWorkspaceLayoutStore.getState().focusTab(workspaceKey, tabId); } } }, }); const killTerminalMutation = useMutation({ mutationFn: async (terminalId: string) => { if (!client) { throw new Error("Host is not connected"); } const payload = await client.killTerminal(terminalId); if (!payload.success) { throw new Error("Unable to close terminal"); } return payload; }, }); const { archiveAgent, isArchivingAgent } = useArchiveAgent(); useEffect(() => { if (!client || !isConnected || !normalizedWorkspaceId.startsWith("/")) { return; } const unsubscribeChanged = client.on("terminals_changed", (message) => { if (message.type !== "terminals_changed") { return; } if (message.payload.cwd !== normalizedWorkspaceId) { return; } queryClient.setQueryData(terminalsQueryKey, (current) => ({ cwd: message.payload.cwd, terminals: message.payload.terminals, requestId: current?.requestId ?? `terminals-changed-${Date.now()}`, })); }); const unsubscribeStreamExit = client.on("terminal_stream_exit", (message) => { if (message.type !== "terminal_stream_exit") { return; } }); client.subscribeTerminals({ cwd: normalizedWorkspaceId }); return () => { unsubscribeChanged(); unsubscribeStreamExit(); client.unsubscribeTerminals({ cwd: normalizedWorkspaceId }); }; }, [client, isConnected, normalizedWorkspaceId, queryClient, terminalsQueryKey]); const checkoutQuery = useQuery({ queryKey: checkoutStatusQueryKey(normalizedServerId, normalizedWorkspaceId), enabled: Boolean(client && isConnected) && normalizedWorkspaceId.length > 0 && normalizedWorkspaceId.startsWith("/"), queryFn: async () => { if (!client) { throw new Error("Host is not connected"); } return (await client.getCheckoutStatus( normalizedWorkspaceId )) as CheckoutStatusPayload; }, staleTime: 15_000, }); const workspaceDescriptor = useSessionStore( (state) => state.sessions[normalizedServerId]?.workspaces.get(normalizedWorkspaceId) ?? null ); const hasHydratedWorkspaces = useSessionStore( (state) => state.sessions[normalizedServerId]?.hasHydratedWorkspaces ?? false ); const hasHydratedAgents = useSessionStore( (state) => state.sessions[normalizedServerId]?.hasHydratedAgents ?? false ); const workspaceHeader = workspaceDescriptor ? resolveWorkspaceHeader({ workspace: workspaceDescriptor }) : null; const isWorkspaceHeaderLoading = workspaceHeader === null; const isGitCheckout = checkoutQuery.data?.isGit ?? false; const currentBranchName = checkoutQuery.data?.isGit && checkoutQuery.data.currentBranch !== "HEAD" ? trimNonEmpty(checkoutQuery.data.currentBranch) : null; const mobileView = usePanelStore((state) => state.mobileView); const desktopFileExplorerOpen = usePanelStore( (state) => state.desktop.fileExplorerOpen ); const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer); const openFileExplorer = usePanelStore((state) => state.openFileExplorer); const activateExplorerTabForCheckout = usePanelStore( (state) => state.activateExplorerTabForCheckout ); const closeToAgent = usePanelStore((state) => state.closeToAgent); const setActiveExplorerCheckout = usePanelStore( (state) => state.setActiveExplorerCheckout ); const isExplorerOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen; const activeExplorerCheckout = useMemo(() => { if (!normalizedServerId || !normalizedWorkspaceId.startsWith("/")) { return null; } return { serverId: normalizedServerId, cwd: normalizedWorkspaceId, isGit: isGitCheckout, }; }, [isGitCheckout, normalizedServerId, normalizedWorkspaceId]); useEffect(() => { setActiveExplorerCheckout(activeExplorerCheckout); }, [activeExplorerCheckout, setActiveExplorerCheckout]); const openExplorerForWorkspace = useCallback(() => { if (!activeExplorerCheckout) { return; } activateExplorerTabForCheckout(activeExplorerCheckout); openFileExplorer(); }, [ activateExplorerTabForCheckout, activeExplorerCheckout, openFileExplorer, ]); const handleToggleExplorer = useCallback(() => { if (isExplorerOpen) { toggleFileExplorer(); return; } openExplorerForWorkspace(); }, [isExplorerOpen, openExplorerForWorkspace, toggleFileExplorer]); const explorerOpenGesture = useExplorerOpenGesture({ enabled: isMobile && mobileView === "agent", onOpen: openExplorerForWorkspace, }); useEffect(() => { if (Platform.OS === "web" || !isExplorerOpen) { return; } const handler = BackHandler.addEventListener("hardwareBackPress", () => { if (isExplorerOpen) { closeToAgent(); return true; } return false; }); return () => handler.remove(); }, [closeToAgent, isExplorerOpen]); const persistenceKey = useMemo( () => buildWorkspaceTabPersistenceKey({ serverId: normalizedServerId, workspaceId: normalizedWorkspaceId, }), [normalizedServerId, normalizedWorkspaceId] ); const workspaceLayout = useWorkspaceLayoutStore((state) => persistenceKey ? state.layoutByWorkspace[persistenceKey] ?? null : null ); const uiTabs = useMemo( () => (workspaceLayout ? collectAllTabs(workspaceLayout.root) : EMPTY_UI_TABS), [workspaceLayout] ); const openWorkspaceTab = useWorkspaceLayoutStore((state) => state.openTab); const focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab); const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab); const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent); const retargetWorkspaceTab = useWorkspaceLayoutStore((state) => state.retargetTab); const splitWorkspacePane = useWorkspaceLayoutStore((state) => state.splitPane); const splitWorkspacePaneEmpty = useWorkspaceLayoutStore((state) => state.splitPaneEmpty); const moveWorkspaceTabToPane = useWorkspaceLayoutStore((state) => state.moveTabToPane); const focusWorkspacePane = useWorkspaceLayoutStore((state) => state.focusPane); const resizeWorkspaceSplit = useWorkspaceLayoutStore((state) => state.resizeSplit); const reorderWorkspaceTabsInPane = useWorkspaceLayoutStore((state) => state.reorderTabsInPane); const pinnedAgentIds = useWorkspaceLayoutStore((state) => persistenceKey ? state.pinnedAgentIdsByWorkspace[persistenceKey] ?? EMPTY_PINNED_AGENT_IDS : EMPTY_PINNED_AGENT_IDS ); const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId); const pendingCloseTabIdsRef = useRef(new Set()); const closeWorkspaceTabWithCleanup = useCallback( function closeWorkspaceTabWithCleanup(input: { tabId: string; target?: WorkspaceTabTarget | null; }) { const normalizedTabId = trimNonEmpty(input.tabId); if (!normalizedTabId || !persistenceKey) { return; } if (input.target?.kind === "agent") { unpinWorkspaceAgent(persistenceKey, input.target.agentId); } closeWorkspaceTab(persistenceKey, normalizedTabId); }, [closeWorkspaceTab, persistenceKey, unpinWorkspaceAgent] ); const focusedPaneTabState = useMemo( () => deriveWorkspacePaneState({ layout: workspaceLayout, tabs: uiTabs, }), [uiTabs, workspaceLayout] ); const setFocusedAgentId = useSessionStore((state) => state.setFocusedAgentId); const focusedPaneAgentId = useMemo(() => { const target = focusedPaneTabState.activeTab?.descriptor.target; if (target?.kind !== "agent") { return null; } return target.agentId; }, [focusedPaneTabState.activeTab]); useEffect(() => { setFocusedAgentId(normalizedServerId, focusedPaneAgentId); }, [focusedPaneAgentId, normalizedServerId, setFocusedAgentId]); useEffect(() => { return () => { setFocusedAgentId(normalizedServerId, null); }; }, [normalizedServerId, setFocusedAgentId]); const ensureWorkspaceTab = useCallback( function ensureWorkspaceTab(target: WorkspaceTabTarget): string | null { if (!persistenceKey) { return null; } const normalizedTarget = normalizeWorkspaceTabTarget(target); if (!normalizedTarget) { return null; } const existingTab = uiTabs.find((tab) => workspaceTabTargetsEqual(tab.target, normalizedTarget)) ?? null; if (existingTab) { return existingTab.tabId; } const previousFocusedTabId = focusedPaneTabState.activeTabId; const tabId = openWorkspaceTab(persistenceKey, normalizedTarget); if (tabId && previousFocusedTabId && previousFocusedTabId !== tabId) { focusWorkspaceTab(persistenceKey, previousFocusedTabId); } return tabId; }, [focusWorkspaceTab, focusedPaneTabState, openWorkspaceTab, persistenceKey, uiTabs] ); const openWorkspaceDraftTab = useCallback( function openWorkspaceDraftTab(input?: { draftId?: string; focus?: boolean }) { if (!persistenceKey) { return null; } const target = normalizeWorkspaceTabTarget({ kind: "draft", draftId: trimNonEmpty(input?.draftId) ?? generateDraftId(), }); invariant(target?.kind === "draft", "Draft tab target must be valid"); if (input?.focus === false) { return ensureWorkspaceTab(target); } const tabId = openWorkspaceTab(persistenceKey, target); if (tabId) { focusWorkspaceTab(persistenceKey, tabId); } return tabId; }, [ensureWorkspaceTab, focusWorkspaceTab, openWorkspaceTab, persistenceKey] ); useEffect(() => { if (!normalizedServerId || !normalizedWorkspaceId || !persistenceKey) { return; } const terminalIds = new Set(terminals.map((terminal) => terminal.id)); const hasActivePendingDraftCreateInWorkspace = uiTabs.some((tab) => { if (tab.target.kind !== "draft") { return false; } const pending = pendingByDraftId[tab.target.draftId]; return pending?.serverId === normalizedServerId && pending.lifecycle === "active"; }); for (const agentId of workspaceAgentVisibility.activeAgentIds) { const representedByTarget = uiTabs.some( (tab) => tab.target.kind === "agent" && tab.target.agentId === agentId ); const representedByDeterministicTabId = uiTabs.some( (tab) => tab.tabId === `agent_${agentId}` ); if ( hasActivePendingDraftCreateInWorkspace && !representedByTarget && !representedByDeterministicTabId ) { continue; } ensureWorkspaceTab({ kind: "agent", agentId }); } for (const terminal of terminals) { ensureWorkspaceTab({ kind: "terminal", terminalId: terminal.id }); } const canPruneAgentTabs = hasHydratedAgents; const canPruneTerminalTabs = terminalsQuery.isSuccess; for (const tab of uiTabs) { if ( canPruneAgentTabs && tab.target.kind === "agent" && !pinnedAgentIds.has(tab.target.agentId) && shouldPruneWorkspaceAgentTab({ agentId: tab.target.agentId, agentsHydrated: hasHydratedAgents, knownAgentIds: workspaceAgentVisibility.knownAgentIds, }) ) { closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target }); } if ( canPruneTerminalTabs && tab.target.kind === "terminal" && !terminalIds.has(tab.target.terminalId) ) { closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target }); } } }, [ closeWorkspaceTabWithCleanup, ensureWorkspaceTab, hasHydratedAgents, pendingByDraftId, pinnedAgentIds, persistenceKey, terminals, terminalsQuery.isSuccess, uiTabs, workspaceAgentVisibility, ]); const activeTabId = focusedPaneTabState.activeTabId; const activeTab = focusedPaneTabState.activeTab; useEffect(() => { if (!activeTabId || !persistenceKey) { return; } focusWorkspaceTab(persistenceKey, activeTabId); }, [activeTabId, focusWorkspaceTab, persistenceKey]); const tabs = useMemo( () => focusedPaneTabState.tabs.map((tab) => tab.descriptor), [focusedPaneTabState.tabs] ); const navigateToTabId = useCallback( function navigateToTabId(tabId: string) { if (!tabId || !persistenceKey) { return; } focusWorkspaceTab(persistenceKey, tabId); }, [focusWorkspaceTab, persistenceKey] ); const emptyWorkspaceSeedRef = useRef(null); useEffect(() => { if (!persistenceKey) { return; } if (workspaceAgentVisibility.activeAgentIds.size > 0 || terminals.length > 0) { emptyWorkspaceSeedRef.current = null; return; } if (tabs.length > 0) { emptyWorkspaceSeedRef.current = null; return; } const workspaceKey = `${normalizedServerId}:${normalizedWorkspaceId}`; if (emptyWorkspaceSeedRef.current === workspaceKey) { return; } emptyWorkspaceSeedRef.current = workspaceKey; openWorkspaceDraftTab(); }, [ normalizedServerId, normalizedWorkspaceId, openWorkspaceDraftTab, persistenceKey, terminals.length, tabs.length, workspaceAgentVisibility.activeAgentIds.size, ]); const handleOpenFileFromExplorer = useCallback( function handleOpenFileFromExplorer(filePath: string) { if (isMobile) { closeToAgent(); } if (!persistenceKey) { return; } const tabId = openWorkspaceTab(persistenceKey, { kind: "file", path: filePath }); if (tabId) { navigateToTabId(tabId); } }, [closeToAgent, isMobile, navigateToTabId, openWorkspaceTab, persistenceKey] ); const handleOpenFileFromChat = useCallback( ({ filePath }: { filePath: string }) => { const normalizedFilePath = filePath.trim(); if (!normalizedFilePath) { return; } handleOpenFileFromExplorer(normalizedFilePath); }, [handleOpenFileFromExplorer] ); const [hoveredTabKey, setHoveredTabKey] = useState(null); const [hoveredCloseTabKey, setHoveredCloseTabKey] = useState( null ); const tabByKey = useMemo(() => { const map = new Map(); for (const tab of tabs) { map.set(tab.key, tab); } return map; }, [tabs]); const allTabDescriptorsById = useMemo(() => { const map = new Map(); for (const tab of uiTabs) { map.set(tab.tabId, { key: tab.tabId, tabId: tab.tabId, kind: tab.target.kind, target: tab.target, }); } return map; }, [uiTabs]); const activeTabKey = activeTabId ?? ""; const tabSwitcherOptions = useMemo( () => tabs.map((tab) => ({ id: tab.key, label: getFallbackTabOptionLabel(tab), description: getFallbackTabOptionDescription(tab), })), [tabs] ); const handleCreateDraftTab = useCallback(() => { openWorkspaceDraftTab(); }, [openWorkspaceDraftTab]); const handleCreateTerminal = useCallback((input?: { paneId?: string }) => { if (createTerminalMutation.isPending) { return; } if (!normalizedWorkspaceId.startsWith("/")) { return; } createTerminalMutation.mutate(input); }, [createTerminalMutation, normalizedWorkspaceId]); const handleSelectSwitcherTab = useCallback( (key: string) => { navigateToTabId(key); }, [navigateToTabId] ); const handleSelectNewTabOption = useCallback( (selection: { optionId: NewTabOptionId; paneId?: string }) => { if (selection.paneId && persistenceKey) { focusWorkspacePane(persistenceKey, selection.paneId); } if (selection.optionId === NEW_TAB_AGENT_OPTION_ID) { handleCreateDraftTab(); } else if (selection.optionId === NEW_TAB_TERMINAL_OPTION_ID) { handleCreateTerminal({ paneId: selection.paneId }); } }, [focusWorkspacePane, handleCreateDraftTab, handleCreateTerminal, persistenceKey] ); const handleCreateDraftSplit = useCallback( (input: { targetPaneId: string; position: "left" | "right" | "top" | "bottom" }) => { if (!persistenceKey) { return; } const paneId = splitWorkspacePaneEmpty(persistenceKey, input); if (!paneId) { return; } focusWorkspacePane(persistenceKey, paneId); openWorkspaceDraftTab(); }, [focusWorkspacePane, openWorkspaceDraftTab, persistenceKey, splitWorkspacePaneEmpty] ); const runCloseFlowForTab = useCallback( async (input: { tabId: string; action: () => Promise }): Promise => { const normalizedTabId = input.tabId.trim(); if (!normalizedTabId || pendingCloseTabIdsRef.current.has(normalizedTabId)) { return; } pendingCloseTabIdsRef.current.add(normalizedTabId); try { await input.action(); } finally { pendingCloseTabIdsRef.current.delete(normalizedTabId); } }, [] ); const handleCloseTerminalTab = useCallback( async (input: { tabId: string; terminalId: string }) => { const { tabId, terminalId } = input; await runCloseFlowForTab({ tabId, action: async () => { if ( killTerminalMutation.isPending && killTerminalMutation.variables === terminalId ) { return; } const confirmed = await confirmDialog({ title: "Close terminal?", message: "Any running process in this terminal will be stopped immediately.", confirmLabel: "Close", cancelLabel: "Cancel", destructive: true, }); if (!confirmed) { return; } await killTerminalMutation.mutateAsync(terminalId); setHoveredTabKey((current) => (current === tabId ? null : current)); setHoveredCloseTabKey((current) => (current === tabId ? null : current)); queryClient.setQueryData( terminalsQueryKey, (current) => { if (!current) { return current; } return { ...current, terminals: current.terminals.filter( (terminal) => terminal.id !== terminalId ), }; } ); if (persistenceKey) { closeWorkspaceTabWithCleanup({ tabId, target: { kind: "terminal", terminalId }, }); } }, }); }, [ closeWorkspaceTabWithCleanup, killTerminalMutation, persistenceKey, queryClient, runCloseFlowForTab, terminalsQueryKey, ] ); const handleCloseAgentTab = useCallback( async (input: { tabId: string; agentId: string }) => { const { tabId, agentId } = input; await runCloseFlowForTab({ tabId, action: async () => { if ( !normalizedServerId || isArchivingAgent({ serverId: normalizedServerId, agentId }) ) { return; } const confirmed = await confirmDialog({ title: "Archive agent?", message: "This closes the tab and archives the agent.", confirmLabel: "Archive", cancelLabel: "Cancel", destructive: true, }); if (!confirmed) { return; } await archiveAgent({ serverId: normalizedServerId, agentId }); setHoveredTabKey((current) => (current === tabId ? null : current)); setHoveredCloseTabKey((current) => (current === tabId ? null : current)); if (persistenceKey) { closeWorkspaceTabWithCleanup({ tabId, target: { kind: "agent", agentId }, }); } }, }); }, [ archiveAgent, closeWorkspaceTabWithCleanup, isArchivingAgent, normalizedServerId, persistenceKey, runCloseFlowForTab, ] ); const handleCloseDraftOrFileTab = useCallback( function handleCloseDraftOrFileTab(tabId: string) { setHoveredTabKey((current) => (current === tabId ? null : current)); setHoveredCloseTabKey((current) => (current === tabId ? null : current)); if (persistenceKey) { closeWorkspaceTabWithCleanup({ tabId }); } }, [closeWorkspaceTabWithCleanup, persistenceKey] ); const handleCloseTabById = useCallback( async (tabId: string) => { const tab = allTabDescriptorsById.get(tabId); if (!tab) { return; } if (tab.target.kind === "terminal") { await handleCloseTerminalTab({ tabId, terminalId: tab.target.terminalId }); return; } if (tab.target.kind === "agent") { await handleCloseAgentTab({ tabId, agentId: tab.target.agentId }); return; } handleCloseDraftOrFileTab(tabId); }, [allTabDescriptorsById, handleCloseAgentTab, handleCloseDraftOrFileTab, handleCloseTerminalTab] ); const handleCopyAgentId = useCallback( async (agentId: string) => { if (!agentId) return; try { await Clipboard.setStringAsync(agentId); toast.copied("Agent ID"); } catch { toast.error("Copy failed"); } }, [toast] ); const handleCopyResumeCommand = useCallback( async (agentId: string) => { if (!agentId) return; const agent = useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null; const providerSessionId = agent?.runtimeInfo?.sessionId ?? agent?.persistence?.sessionId ?? null; if (!agent || !providerSessionId) { toast.error("Resume ID not available"); return; } const command = buildProviderCommand({ provider: agent.provider, id: "resume", sessionId: providerSessionId, }) ?? null; if (!command) { toast.error("Resume command not available"); return; } try { await Clipboard.setStringAsync(command); toast.copied("resume command"); } catch { toast.error("Copy failed"); } }, [normalizedServerId, toast] ); const handleCopyWorkspacePath = useCallback(async () => { if (!normalizedWorkspaceId.startsWith("/")) { toast.error("Workspace path not available"); return; } try { await Clipboard.setStringAsync(normalizedWorkspaceId); toast.copied("Workspace path"); } catch { toast.error("Copy failed"); } }, [normalizedWorkspaceId, toast]); const handleCopyBranchName = useCallback(async () => { if (!currentBranchName) { toast.error("Branch name not available"); return; } try { await Clipboard.setStringAsync(currentBranchName); toast.copied("Branch name"); } catch { toast.error("Copy failed"); } }, [currentBranchName, toast]); const handleBulkCloseTabs = useCallback( async (input: { tabsToClose: WorkspaceTabDescriptor[]; title: string; logLabel: string }) => { const { tabsToClose, title, logLabel } = input; if (tabsToClose.length === 0) { return; } const groups = classifyBulkClosableTabs(tabsToClose); const confirmed = await confirmDialog({ title, message: buildBulkCloseConfirmationMessage(groups), confirmLabel: "Close", cancelLabel: "Cancel", destructive: true, }); if (!confirmed) { return; } for (const { tabId, terminalId } of groups.terminalTabs) { try { await killTerminalMutation.mutateAsync(terminalId); queryClient.setQueryData(terminalsQueryKey, (current) => { if (!current) { return current; } return { ...current, terminals: current.terminals.filter((terminal) => terminal.id !== terminalId), }; }); if (persistenceKey) { closeWorkspaceTabWithCleanup({ tabId, target: { kind: "terminal", terminalId }, }); } } catch (error) { console.warn(`[WorkspaceScreen] Failed to close terminal tab ${logLabel}`, { terminalId, error }); } } for (const { tabId, agentId } of groups.agentTabs) { if (!normalizedServerId) { continue; } try { await archiveAgent({ serverId: normalizedServerId, agentId }); if (persistenceKey) { closeWorkspaceTabWithCleanup({ tabId, target: { kind: "agent", agentId }, }); } } catch (error) { console.warn(`[WorkspaceScreen] Failed to archive agent tab ${logLabel}`, { agentId, error }); } } for (const { tabId } of groups.otherTabs) { if (persistenceKey) { closeWorkspaceTabWithCleanup({ tabId }); } } const closedKeys = new Set(tabsToClose.map((tab) => tab.key)); setHoveredTabKey((current) => (current && closedKeys.has(current) ? null : current)); setHoveredCloseTabKey((current) => (current && closedKeys.has(current) ? null : current)); }, [ archiveAgent, closeWorkspaceTabWithCleanup, killTerminalMutation, normalizedServerId, persistenceKey, queryClient, terminalsQueryKey, ] ); const handleCloseTabsToLeftInPane = useCallback( async (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => { const index = paneTabs.findIndex((tab) => tab.tabId === tabId); if (index < 0) { return; } await handleBulkCloseTabs({ tabsToClose: paneTabs.slice(0, index), title: "Close tabs to the left?", logLabel: "to the left", }); }, [handleBulkCloseTabs] ); const handleCloseTabsToLeft = useCallback( async (tabId: string) => { await handleCloseTabsToLeftInPane(tabId, tabs); }, [handleCloseTabsToLeftInPane, tabs] ); const handleCloseTabsToRightInPane = useCallback( async (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => { const index = paneTabs.findIndex((tab) => tab.tabId === tabId); if (index < 0) { return; } await handleBulkCloseTabs({ tabsToClose: paneTabs.slice(index + 1), title: "Close tabs to the right?", logLabel: "to the right", }); }, [handleBulkCloseTabs] ); const handleCloseTabsToRight = useCallback( async (tabId: string) => { await handleCloseTabsToRightInPane(tabId, tabs); }, [handleCloseTabsToRightInPane, tabs] ); const handleCloseOtherTabsInPane = useCallback( async (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => { const tabsToClose = paneTabs.filter((tab) => tab.tabId !== tabId); await handleBulkCloseTabs({ tabsToClose, title: "Close other tabs?", logLabel: "from close other tabs", }); }, [handleBulkCloseTabs] ); const handleCloseOtherTabs = useCallback( async (tabId: string) => { await handleCloseOtherTabsInPane(tabId, tabs); }, [handleCloseOtherTabsInPane, tabs] ); const handleWorkspaceTabAction = useCallback( (action: KeyboardActionDefinition): boolean => { switch (action.id) { case "workspace.tab.new": handleCreateDraftTab(); return true; case "workspace.tab.close-current": if (activeTabId) { void handleCloseTabById(activeTabId); } return true; case "workspace.tab.navigate-index": { const next = tabs[action.index - 1] ?? null; if (next?.tabId) { navigateToTabId(next.tabId); } return true; } case "workspace.tab.navigate-relative": { if (tabs.length > 0) { const currentIndex = tabs.findIndex((tab) => tab.tabId === activeTabId); const fromIndex = currentIndex >= 0 ? currentIndex : 0; const nextIndex = (fromIndex + action.delta + tabs.length) % tabs.length; const next = tabs[nextIndex] ?? null; if (next?.tabId) { navigateToTabId(next.tabId); } } return true; } default: return false; } }, [activeTabId, handleCloseTabById, handleCreateDraftTab, navigateToTabId, tabs] ); const handleWorkspacePaneAction = useCallback( (action: KeyboardActionDefinition): boolean => { if (!persistenceKey || !workspaceLayout) { return true; } const focusedPane = focusedPaneTabState.pane; if (!focusedPane) { return true; } if (action.id === "workspace.pane.split.right") { handleCreateDraftSplit({ targetPaneId: focusedPane.id, position: "right", }); return true; } if (action.id === "workspace.pane.split.down") { handleCreateDraftSplit({ targetPaneId: focusedPane.id, position: "bottom", }); return true; } if ( action.id === "workspace.pane.focus.left" || action.id === "workspace.pane.focus.right" || action.id === "workspace.pane.focus.up" || action.id === "workspace.pane.focus.down" ) { const direction = action.id.split(".").pop(); if ( direction === "left" || direction === "right" || direction === "up" || direction === "down" ) { const adjacentPaneId = findAdjacentPane(workspaceLayout.root, focusedPane.id, direction); if (adjacentPaneId) { focusWorkspacePane(persistenceKey, adjacentPaneId); } } return true; } if ( action.id === "workspace.pane.move-tab.left" || action.id === "workspace.pane.move-tab.right" || action.id === "workspace.pane.move-tab.up" || action.id === "workspace.pane.move-tab.down" ) { const direction = action.id.split(".").pop(); if ( direction === "left" || direction === "right" || direction === "up" || direction === "down" ) { const activePaneTabId = focusedPaneTabState.activeTabId; const adjacentPaneId = findAdjacentPane(workspaceLayout.root, focusedPane.id, direction); if (activePaneTabId && adjacentPaneId) { moveWorkspaceTabToPane(persistenceKey, activePaneTabId, adjacentPaneId); } } return true; } if (action.id === "workspace.pane.close") { for (const tabId of focusedPane.tabIds) { closeWorkspaceTabWithCleanup({ tabId, target: allTabDescriptorsById.get(tabId)?.target ?? null, }); } return true; } return false; }, [ allTabDescriptorsById, closeWorkspaceTabWithCleanup, focusWorkspacePane, handleCreateDraftSplit, moveWorkspaceTabToPane, persistenceKey, focusedPaneTabState.activeTabId, focusedPaneTabState.pane, workspaceLayout, ] ); useKeyboardActionHandler({ handlerId: `workspace-tab-actions:${normalizedServerId}:${normalizedWorkspaceId}`, actions: [ "workspace.tab.new", "workspace.tab.close-current", "workspace.tab.navigate-index", "workspace.tab.navigate-relative", ] as const, enabled: Boolean(normalizedServerId && normalizedWorkspaceId), priority: 100, isActive: () => true, handle: handleWorkspaceTabAction, }); useKeyboardActionHandler({ handlerId: `workspace-pane-actions:${normalizedServerId}:${normalizedWorkspaceId}`, actions: [ "workspace.pane.split.right", "workspace.pane.split.down", "workspace.pane.focus.left", "workspace.pane.focus.right", "workspace.pane.focus.up", "workspace.pane.focus.down", "workspace.pane.move-tab.left", "workspace.pane.move-tab.right", "workspace.pane.move-tab.up", "workspace.pane.move-tab.down", "workspace.pane.close", ] as const, enabled: Boolean(normalizedServerId && normalizedWorkspaceId), priority: 100, isActive: () => true, handle: handleWorkspacePaneAction, }); const activeTabDescriptor = activeTab?.descriptor ?? null; useEffect(() => { if (Platform.OS !== "web" || typeof document === "undefined" || activeTabDescriptor) { return; } document.title = "Workspace"; }, [activeTabDescriptor]); const buildPaneContentModel = useCallback( (input: { tab: WorkspaceTabDescriptor; paneId?: string | null; isPaneFocused: boolean; focusPaneBeforeOpen?: boolean; }) => buildWorkspacePaneContentModel({ tab: input.tab, normalizedServerId, normalizedWorkspaceId, isPaneFocused: input.isPaneFocused, onOpenTab: (target) => { if (!persistenceKey) { return; } if (input.focusPaneBeforeOpen && input.paneId) { focusWorkspacePane(persistenceKey, input.paneId); } const tabId = openWorkspaceTab(persistenceKey, target); if (tabId) { navigateToTabId(tabId); } }, onCloseCurrentTab: () => { void handleCloseTabById(input.tab.tabId); }, onRetargetCurrentTab: (target) => { if (!persistenceKey) { return; } retargetWorkspaceTab(persistenceKey, input.tab.tabId, target); }, onOpenWorkspaceFile: (filePath) => { if (input.focusPaneBeforeOpen && input.paneId && persistenceKey) { focusWorkspacePane(persistenceKey, input.paneId); } handleOpenFileFromChat({ filePath }); }, }), [ handleCloseTabById, handleOpenFileFromChat, focusWorkspacePane, navigateToTabId, normalizedServerId, normalizedWorkspaceId, openWorkspaceTab, persistenceKey, retargetWorkspaceTab, ] ); const activePaneContent = useMemo( () => activeTabDescriptor ? buildPaneContentModel({ tab: activeTabDescriptor, paneId: focusedPaneTabState.pane?.id ?? null, isPaneFocused: true, focusPaneBeforeOpen: false, }) : null, [activeTabDescriptor, buildPaneContentModel, focusedPaneTabState.pane?.id] ); const content = shouldRenderMissingWorkspaceDescriptor({ workspace: workspaceDescriptor, hasHydratedWorkspaces, }) ? ( ) : !activeTabDescriptor ? ( !hasHydratedAgents ? ( ) : ( No tabs are available yet. Use New tab to create an agent or terminal. ) ) : ( ); return ( {Platform.OS === "web" && activeTabDescriptor ? ( {(presentation) => ( )} ) : null} {isWorkspaceHeaderLoading ? ( <> ) : ( <> {workspaceHeader.title} {workspaceHeader.subtitle} )} {({ hovered, open }) => { const Icon = isMobile ? EllipsisVertical : Ellipsis; return ( ); }} } onSelect={handleCreateDraftTab} > New agent } disabled={createTerminalMutation.isPending} onSelect={handleCreateTerminal} > New terminal } disabled={!normalizedWorkspaceId.startsWith("/")} onSelect={handleCopyWorkspacePath} > Copy workspace path {currentBranchName ? ( } onSelect={handleCopyBranchName} > Copy branch name ) : null} } right={ {!isMobile && isGitCheckout ? ( <> [ styles.sourceControlButton, workspaceDescriptor?.diffStat && styles.sourceControlButtonWithStats, (hovered || pressed || isExplorerOpen) && styles.sourceControlButtonHovered, ]} > {({ hovered, pressed }) => { const active = isExplorerOpen || hovered || pressed; const iconColor = active ? theme.colors.foreground : theme.colors.foregroundMuted; return ( <> {workspaceDescriptor?.diffStat ? ( +{workspaceDescriptor.diffStat.additions} -{workspaceDescriptor.diffStat.deletions} ) : null} ); }} ) : null} {!isMobile && !isGitCheckout ? ( {({ hovered }) => { const color = isExplorerOpen || hovered ? theme.colors.foreground : theme.colors.foregroundMuted; return ; }} ) : null} {isMobile ? ( {({ hovered }) => { const color = isExplorerOpen || hovered ? theme.colors.foreground : theme.colors.foregroundMuted; return isGitCheckout ? : ; }} ) : null} } /> {isMobile ? ( ) : null} {isMobile ? ( {content} ) : ( {workspaceLayout && persistenceKey ? ( buildPaneContentModel({ tab, paneId, isPaneFocused, focusPaneBeforeOpen: true, }) } onFocusPane={(paneId) => { focusWorkspacePane(persistenceKey, paneId); }} onNewTerminalTab={handleCreateTerminal} onSplitPane={(input) => { splitWorkspacePane(persistenceKey, input); }} onSplitPaneEmpty={(input) => { handleCreateDraftSplit(input); }} onMoveTabToPane={(tabId, toPaneId) => { moveWorkspaceTabToPane(persistenceKey, tabId, toPaneId); }} onResizeSplit={(groupId, sizes) => { resizeWorkspaceSplit(persistenceKey, groupId, sizes); }} onReorderTabsInPane={(paneId, tabIds) => { reorderWorkspaceTabsInPane(persistenceKey, paneId, tabIds); }} renderPaneEmptyState={() => ( No tabs in this pane. )} /> ) : ( content )} )} ); } const styles = StyleSheet.create((theme) => ({ container: { flex: 1, backgroundColor: theme.colors.surface0, }, threePaneRow: { flex: 1, minHeight: 0, flexDirection: "row", alignItems: "stretch", }, centerColumn: { flex: 1, minHeight: 0, }, headerTitle: { fontSize: theme.fontSize.base, fontWeight: { xs: "400", md: "300", }, color: theme.colors.foreground, flexShrink: 1, }, headerTitleContainer: { flex: 1, minWidth: 0, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, headerProjectTitle: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.base, flexShrink: 1, }, headerTitleSkeleton: { width: 190, maxWidth: "45%", height: 22, borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.surface3, opacity: 0.25, }, headerProjectTitleSkeleton: { width: 300, maxWidth: "45%", height: 22, borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.surface3, opacity: 0.18, }, headerRight: { flexDirection: "row", alignItems: "center", gap: { xs: theme.spacing[1], md: theme.spacing[2], }, }, headerActionButton: { paddingVertical: theme.spacing[2], paddingHorizontal: theme.spacing[2], borderRadius: theme.borderRadius.lg, }, sourceControlButton: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[1], paddingVertical: theme.spacing[1], minHeight: Math.ceil(theme.fontSize.sm * 1.5) + theme.spacing[1] * 2, minWidth: Math.ceil(theme.fontSize.sm * 1.5) + theme.spacing[1] * 2, borderRadius: theme.borderRadius.md, }, sourceControlButtonWithStats: { paddingHorizontal: theme.spacing[3], }, sourceControlButtonHovered: { backgroundColor: theme.colors.surface2, }, diffStatRow: { flexDirection: "row", alignItems: "center", gap: 6, flexShrink: 0, }, diffStatAdditions: { fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, color: theme.colors.palette.green[400], }, diffStatDeletions: { fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, color: theme.colors.palette.red[500], }, newTabActions: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], }, newTabActionButton: { width: 30, height: 30, borderRadius: theme.borderRadius.md, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, backgroundColor: theme.colors.surface1, alignItems: "center", justifyContent: "center", }, newTabActionButtonHovered: { backgroundColor: theme.colors.surface2, }, newTabTooltipText: { fontSize: theme.fontSize.sm, color: theme.colors.popoverForeground, }, newTabTooltipRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, newTabTooltipShortcut: { backgroundColor: theme.colors.surface3, borderColor: theme.colors.borderAccent, }, mobileTabsRow: { backgroundColor: theme.colors.surface0, borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, }, switcherTrigger: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[2] + theme.spacing[3], paddingVertical: theme.spacing[2], }, switcherTriggerPressed: { backgroundColor: theme.colors.surface1, }, switcherTriggerLeft: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], flex: 1, minWidth: 0, }, switcherTriggerIcon: { flexShrink: 0, }, switcherTriggerText: { minWidth: 0, flex: 1, color: theme.colors.foreground, fontSize: theme.fontSize.sm, }, mobileTabMenuTrigger: { width: 28, height: 28, borderRadius: theme.borderRadius.md, alignItems: "center", justifyContent: "center", }, mobileTabMenuTriggerActive: { backgroundColor: theme.colors.surface2, }, tabsContainer: { borderBottomWidth: 1, borderBottomColor: theme.colors.border, backgroundColor: theme.colors.surface0, flexDirection: "row", alignItems: "center", }, tabsScroll: { flex: 1, minWidth: 0, }, tabsContent: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], paddingHorizontal: theme.spacing[2], paddingVertical: theme.spacing[1], }, tabsActions: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], paddingRight: theme.spacing[2], paddingVertical: theme.spacing[1], }, centerContent: { flex: 1, minHeight: 0, }, tab: { paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], borderRadius: theme.borderRadius.md, flexDirection: "row", alignItems: "center", gap: theme.spacing[1], maxWidth: 260, }, tabHandle: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], flex: 1, minWidth: 0, }, tabIcon: { flexShrink: 0, }, tabActive: { backgroundColor: theme.colors.surface2, }, tabHovered: { backgroundColor: theme.colors.surface2, }, tabLabel: { flexShrink: 1, minWidth: 0, color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, 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, }, tabCloseButtonHidden: { opacity: 0, }, tabCloseButtonActive: { backgroundColor: theme.colors.surface3, }, content: { flex: 1, minHeight: 0, backgroundColor: theme.colors.surface0, }, contentPlaceholder: { flex: 1, minHeight: 0, backgroundColor: theme.colors.surface0, }, emptyState: { flex: 1, alignItems: "center", justifyContent: "center", gap: theme.spacing[3], paddingHorizontal: theme.spacing[6], }, emptyStateText: { color: theme.colors.foregroundMuted, textAlign: "center", }, }));