import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactElement, type ReactNode, } from "react"; import { useStoreWithEqualityFn } from "zustand/traditional"; import { useIsFocused } from "@react-navigation/native"; import { ActivityIndicator, BackHandler, Keyboard, Pressable, Text, View } from "react-native"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useRouter, type Href } from "expo-router"; import * as Clipboard from "expo-clipboard"; import { useTranslation } from "react-i18next"; import { DiffStat } from "@/components/diff-stat"; import { CopyX, ArrowLeftToLine, ArrowRightToLine, ChevronDown, Copy, Ellipsis, EllipsisVertical, Globe, Import as ImportIcon, PanelRight, Pencil, RotateCw, Settings, SquarePen, SquareTerminal, X, } from "lucide-react-native"; import { GestureDetector } from "react-native-gesture-handler"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import type { Theme } from "@/styles/theme"; 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 { ScreenTitle } from "@/components/headers/screen-title"; import { Combobox, type ComboboxOption } from "@/components/ui/combobox"; import { Shortcut } from "@/components/ui/shortcut"; import type { ShortcutKey } from "@/utils/format-shortcut"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { FloatingPanelPortalHost, FloatingPanelPortalHostNameProvider, } from "@/components/ui/floating-panel-portal"; import { ExplorerSidebar } from "@/components/explorer-sidebar"; import { MountedTabActiveContext, SplitContainer } from "@/components/split-container"; import { SourceControlPanelIcon } from "@/components/icons/source-control-panel-icon"; import { WorkspaceGitActions } from "@/git/workspace-actions"; import { WorkspaceOpenInEditorButton } from "@/screens/workspace/workspace-open-in-editor-button"; import { WorkspaceScriptsButton } from "@/screens/workspace/workspace-scripts-button"; import { ImportSessionSheet } from "@/components/import-session-sheet"; import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context"; import { useToast } from "@/contexts/toast-context"; import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture"; import { selectIsFileExplorerOpen, usePanelStore } from "@/stores/panel-store"; import { type ExplorerCheckoutContext } from "@/stores/explorer-checkout-context"; import { useSessionStore, useWorkspaceRestoreStatus, type WorkspaceDescriptor, } from "@/stores/session-store"; import { buildWorkspaceTabPersistenceKey, collectAllTabs, getFocusedBrowserId, type WorkspaceLayout, useWorkspaceLayoutStore, useWorkspaceLayoutStoreHydrated, } 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 { buildDeterministicWorkspaceTabId, normalizeWorkspaceTabTarget, workspaceTabTargetsEqual, } from "@/workspace-tabs/identity"; import { getHostRuntimeStore, useHostRuntimeClient, useHostRuntimeIsConnected, useHostRuntimeSnapshot, useHosts, } from "@/runtime/host-runtime"; import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; import { shouldShowWorkspaceSetup, useWorkspaceSetupStore } from "@/stores/workspace-setup-store"; import { useWorkspace } from "@/stores/session-store-hooks"; import { useWorkspaceTerminalSessionRetention } from "@/terminal/hooks/use-workspace-terminal-session-retention"; import type { CheckoutStatusPayload } from "@/git/use-status-query"; import { checkoutStatusQueryKey } from "@/git/query-keys"; import { fetchCheckoutStatus } from "@/git/checkout-status-cache"; import { confirmDialog } from "@/utils/confirm-dialog"; import { useArchiveAgent } from "@/hooks/use-archive-agent"; import { useStableEvent } from "@/hooks/use-stable-event"; import { createWorkspaceBrowser, useBrowserStore } from "@/stores/browser-store"; import { getDesktopHost } from "@/desktop/host"; import { buildProviderCommand } from "@/utils/provider-command-templates"; import { generateDraftId } from "@/stores/draft-keys"; import { resolveWorkspaceRouteId } from "@/utils/workspace-identity"; import { WorkspaceTabPresentationResolver, WorkspaceTabIcon, WorkspaceTabOptionRow, type WorkspaceTabPresentation, } from "@/screens/workspace/workspace-tab-presentation"; import { useWorkspaceTabRename, WorkspaceTabRenameModal, } from "@/screens/workspace/use-workspace-tab-rename"; import { WorkspaceDesktopTabsRow, type WorkspaceDesktopTabRowItem, } from "@/screens/workspace/workspace-desktop-tabs-row"; import { buildWorkspaceTabMenuEntries, type WorkspaceTabMenuEntry, type WorkspaceTabMenuLabels, } from "@/screens/workspace/workspace-tab-menu"; import { useDesktopBrowserNewTabRequests } from "@/browser/new-tab-requests"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; import { resolveWorkspaceHeaderRenderState, type WorkspaceHeaderCheckoutState, } from "@/screens/workspace/workspace-header-source"; import { resolveWorkspaceRouteState, type WorkspaceRouteState, } from "@/screens/workspace/workspace-route-state"; import { renderWorkspaceRouteGate } from "@/screens/workspace/workspace-route-state-views"; import { buildWorkspaceTabSnapshot, deriveWorkspaceAgentVisibility, workspaceAgentVisibilityEqual, } from "@/workspace-tabs/agent-visibility"; import { deriveWorkspacePaneState, resolveSideFileOpenPlacement, } from "@/screens/workspace/workspace-pane-state"; import { buildWorkspacePaneContentModel, WorkspacePaneContent, type WorkspacePaneContentModel, } from "@/screens/workspace/workspace-pane-content"; import { useMountedTabSet } from "@/screens/workspace/use-mounted-tab-set"; import { WorkspaceFocusProvider } from "@/workspace/focus"; import { shouldSeedEmptyWorkspaceDraft } from "@/screens/workspace/workspace-empty-draft-seed"; import { buildBulkCloseConfirmationMessage, type BulkCloseConfirmationLabels, classifyBulkClosableTabs, closeBulkWorkspaceTabs, } from "@/screens/workspace/workspace-bulk-close"; import { resolveCloseAgentTabPolicy } from "@/subagents"; import { findAdjacentPane } from "@/utils/split-navigation"; import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout"; import { getIsElectron, isNative, isWeb } from "@/constants/platform"; import { useContainerWidthBelow } from "@/hooks/use-container-width"; import { buildHostRootRoute, buildSettingsHostRoute, buildSettingsHostSectionRoute, } from "@/utils/host-routes"; import { canCreateWorkspaceTerminal } from "@/screens/workspace/terminals/state"; import { useWorkspaceTerminals, type TerminalProfileInput, } from "@/screens/workspace/terminals/use-workspace-terminals"; import { useDaemonConfig } from "@/hooks/use-daemon-config"; import { getTerminalProfileIcon, resolveTerminalProfiles, } from "@getpaseo/protocol/terminal-profiles"; import { getProviderIcon } from "@/components/provider-icons"; import { createWorkspaceFileTabTarget, normalizeWorkspaceFileLocation, type WorkspaceFileLocation, type WorkspaceFileOpenRequest, } from "@/workspace/file-open"; import { RenderProfile } from "@/utils/render-profiler"; const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000; const WORKSPACE_FLOATING_PANEL_PORTAL_HOST_PREFIX = "workspace-floating-panels"; const EMPTY_UI_TABS: WorkspaceTab[] = []; const EMPTY_WORKSPACE_SCRIPTS: WorkspaceDescriptor["scripts"] = []; const EMPTY_PINNED_AGENT_IDS = new Set(); const EMPTY_SET = new Set(); const COMPACT_WEB_GESTURE_TOUCH_ACTION = isWeb ? "auto" : "pan-y"; function getWorkspaceScripts( workspaceDescriptor: WorkspaceDescriptor | null | undefined, ): WorkspaceDescriptor["scripts"] { return workspaceDescriptor?.scripts ?? EMPTY_WORKSPACE_SCRIPTS; } interface WorkspaceFileLocationFields { path: string | null; lineStart?: number; lineEnd?: number; } function getWorkspaceFileLocationFields( tab: WorkspaceTabDescriptor | null, ): WorkspaceFileLocationFields { const target = tab?.target; if (target?.kind !== "file") { return { path: null }; } return { path: target.path, lineStart: target.lineStart, lineEnd: target.lineEnd }; } function buildWorkspaceFileLocation( fields: WorkspaceFileLocationFields, ): WorkspaceFileLocation | null { if (fields.path === null) { return null; } return { path: fields.path, lineStart: fields.lineStart, lineEnd: fields.lineEnd }; } const ThemedActivityIndicator = withUnistyles(ActivityIndicator); const ThemedEllipsis = withUnistyles(Ellipsis); const ThemedEllipsisVertical = withUnistyles(EllipsisVertical); const ThemedChevronDown = withUnistyles(ChevronDown); const ThemedCopy = withUnistyles(Copy); const ThemedRotateCw = withUnistyles(RotateCw); const ThemedArrowLeftToLine = withUnistyles(ArrowLeftToLine); const ThemedArrowRightToLine = withUnistyles(ArrowRightToLine); const ThemedCopyX = withUnistyles(CopyX); const ThemedPencil = withUnistyles(Pencil); const ThemedX = withUnistyles(X); const ThemedSquarePen = withUnistyles(SquarePen); const ThemedSquareTerminal = withUnistyles(SquareTerminal); const ThemedGlobe = withUnistyles(Globe); const ThemedImport = withUnistyles(ImportIcon); const ThemedSettings = withUnistyles(Settings); const ThemedPanelRight = withUnistyles(PanelRight); const ThemedSourceControlPanelIcon = withUnistyles(SourceControlPanelIcon); interface DynamicProviderIconProps { iconKey: string; size: number; color?: string; } function DynamicProviderIcon({ iconKey, size, color = "" }: DynamicProviderIconProps) { const Icon = getProviderIcon(iconKey); return ; } const ThemedDynamicProviderIcon = withUnistyles(DynamicProviderIcon); const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); const sourceControlPanelStrokeWidth15 = { strokeWidth: 1.5 }; const MENU_NEW_AGENT_ICON = ; const MENU_NEW_TERMINAL_ICON = ; const MENU_NEW_BROWSER_ICON = ; const MENU_IMPORT_ICON = ; const MENU_COPY_ICON = ; const MENU_SETTINGS_ICON = ; const GATED_WORKSPACE_HEADER_LEFT = ; interface WorkspaceScreenProps { serverId: string; workspaceId: string; isRouteFocused?: boolean; } type WorkspaceScreenContentProps = WorkspaceScreenProps & { isRouteFocused: boolean; }; 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 useSyncWorkspaceActiveBrowser(input: { workspaceLayout: WorkspaceLayout | null; isRouteFocused: boolean; }) { const focusedBrowserId = useMemo( () => getFocusedBrowserId(input.workspaceLayout), [input.workspaceLayout], ); const desktopActiveBrowserId = input.isRouteFocused ? focusedBrowserId : null; useEffect(() => { if (!getIsElectron()) { return; } void getDesktopHost()?.browser?.setWorkspaceActiveBrowser?.(desktopActiveBrowserId); }, [desktopActiveBrowserId]); } function getFallbackTabOptionLabel( tab: WorkspaceTabDescriptor, labels: { newAgent: string; setup: string; terminal: string; browser: string; agent: string; }, ): string { if (tab.target.kind === "draft") { return labels.newAgent; } if (tab.target.kind === "setup") { return labels.setup; } if (tab.target.kind === "terminal") { return labels.terminal; } if (tab.target.kind === "browser") { return labels.browser; } if (tab.target.kind === "file") { return tab.target.path.split("/").findLast(Boolean) ?? tab.target.path; } return labels.agent; } function getFallbackTabOptionDescription( tab: WorkspaceTabDescriptor, labels: { newAgent: string; workspaceSetup: string; agent: string; terminal: string; browser: string; }, ): string { if (tab.target.kind === "draft") { return labels.newAgent; } if (tab.target.kind === "setup") { return labels.workspaceSetup; } if (tab.target.kind === "agent") { return labels.agent; } if (tab.target.kind === "terminal") { return labels.terminal; } if (tab.target.kind === "browser") { return labels.browser; } return tab.target.path; } interface 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; onCopyFilePath: (path: string) => Promise | void; onReloadAgent: (agentId: string) => Promise | void; onRenameTab: (tab: WorkspaceTabDescriptor) => 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; }) { const { t } = useTranslation(); return ( {(presentation) => ( <> {presentation.titleState === "loading" ? t("workspace.tabs.loading") : presentation.label} )} ); } function WorkspaceDocumentTitleEffect({ label, titleState, }: { label: string; titleState: "ready" | "loading"; }) { const { t } = useTranslation(); useEffect(() => { if (isNative || typeof document === "undefined") { return; } const resolvedLabel = label.trim(); document.title = titleState === "loading" ? t("workspace.tabs.loading") : resolvedLabel || t("workspace.tabs.fallback.workspace"); }, [label, titleState, t]); return null; } function noop() {} function mobileTabMenuTriggerStyle({ open, pressed }: { open?: boolean; pressed?: boolean }) { return [ styles.mobileTabMenuTrigger, (Boolean(open) || Boolean(pressed)) && styles.mobileTabMenuTriggerActive, ]; } function switcherTriggerStyle({ pressed }: { pressed?: boolean }) { return [styles.switcherTrigger, Boolean(pressed) && styles.switcherTriggerPressed]; } function MobileTabTrailingAccessory({ menuTestIDBase, presentationLabel, menuEntries, }: { menuTestIDBase: string; presentationLabel: string; menuEntries: WorkspaceTabMenuEntry[]; }) { const { t } = useTranslation(); return ( {menuEntries.map((entry) => entry.kind === "separator" ? ( ) : ( ), )} ); } function MobileTabDropdownMenuItem({ entry, }: { entry: Extract; }) { const leading = useMemo(() => { switch (entry.icon) { case "copy": return ; case "rotate-cw": return ; case "arrow-left-to-line": return ; case "arrow-right-to-line": return ; case "copy-x": return ; case "pencil": return ; case "x": return ; default: return undefined; } }, [entry.icon]); const trailing = useMemo( () => (entry.hint ? {entry.hint} : undefined), [entry.hint], ); return ( {entry.label} ); } function MobileWorkspaceTabOption({ tab, tabIndex, tabCount, normalizedServerId, normalizedWorkspaceId, selected, active, onPress, onCopyResumeCommand, onCopyAgentId, onCopyFilePath, onReloadAgent, onRenameTab, 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; onCopyFilePath: (path: string) => Promise | void; onReloadAgent: (agentId: string) => Promise | void; onRenameTab: (tab: WorkspaceTabDescriptor) => void; onCloseTab: (tabId: string) => Promise | void; onCloseTabsAbove: (tabId: string) => Promise | void; onCloseTabsBelow: (tabId: string) => Promise | void; onCloseOtherTabs: (tabId: string) => Promise | void; }) { const { t } = useTranslation(); const tabMenuLabels = useMemo( () => ({ copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"), copyAgentId: t("workspace.tabs.menu.copyAgentId"), copyFilePath: t("workspace.tabs.menu.copyFilePath"), rename: t("workspace.tabs.menu.rename"), closeAbove: t("workspace.tabs.menu.closeAbove"), closeBelow: t("workspace.tabs.menu.closeBelow"), closeLeft: t("workspace.tabs.menu.closeLeft"), closeRight: t("workspace.tabs.menu.closeRight"), closeOthers: t("workspace.tabs.menu.closeOthers"), reloadAgent: t("workspace.tabs.menu.reloadAgent"), reloadAgentTooltip: t("workspace.tabs.menu.reloadAgentTooltip"), close: t("workspace.tabs.menu.close"), }), [t], ); const menuTestIDBase = `workspace-tab-menu-${buildDeterministicWorkspaceTabId(tab.target)}`; const menuEntries = buildWorkspaceTabMenuEntries({ surface: "mobile", tab, index: tabIndex, tabCount, menuTestIDBase, onCopyResumeCommand, onCopyAgentId, onCopyFilePath, onReloadAgent, onRenameTab, onCloseTab, onCloseTabsBefore: onCloseTabsAbove, onCloseTabsAfter: onCloseTabsBelow, onCloseOtherTabs, labels: tabMenuLabels, }); const fallbackLabels = useMemo( () => ({ newAgent: t("workspace.tabs.fallback.newAgent"), setup: t("workspace.tabs.fallback.setup"), terminal: t("workspace.tabs.fallback.terminal"), browser: t("workspace.tabs.fallback.browser"), agent: t("workspace.tabs.fallback.agent"), }), [t], ); const fallbackLabel = getFallbackTabOptionLabel(tab, fallbackLabels); const trailingAccessory = useMemo( () => ( ), [menuTestIDBase, fallbackLabel, menuEntries], ); const renderPresentation = useCallback( (presentation: WorkspaceTabPresentation) => ( ), [selected, active, onPress, trailingAccessory], ); return ( {renderPresentation} ); } const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({ tabs, activeTabKey, activeTab, tabSwitcherOptions, tabByKey, normalizedServerId, normalizedWorkspaceId, onSelectSwitcherTab, onCopyResumeCommand, onCopyAgentId, onCopyFilePath, onReloadAgent, onRenameTab, onCloseTab, onCloseTabsAbove, onCloseTabsBelow, onCloseOtherTabs, }: MobileWorkspaceTabSwitcherProps) { const { t } = useTranslation(); 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]); const handleOpenSwitcher = useCallback(() => { Keyboard.dismiss(); setIsOpen(true); }, []); const renderTabOption = useCallback( ({ option, selected, active, onPress, }: { option: ComboboxOption; selected: boolean; active: boolean; onPress: () => void; }) => { const tab = tabByKey.get(option.id); if (!tab) { return ; } const tabIndex = tabIndexByKey.get(tab.key) ?? -1; if (tabIndex < 0) { return ; } return ( ); }, [ tabByKey, tabIndexByKey, tabs.length, normalizedServerId, normalizedWorkspaceId, onCopyResumeCommand, onCopyAgentId, onCopyFilePath, onReloadAgent, onRenameTab, onCloseTab, onCloseTabsAbove, onCloseTabsBelow, onCloseOtherTabs, ], ); return ( ); }); interface MobileMountedTabSlotProps { tabDescriptor: WorkspaceTabDescriptor; isVisible: boolean; isWorkspaceFocused: boolean; isPaneFocused: boolean; paneId: string | null; buildPaneContentModel: (input: { paneId: string | null; tab: WorkspaceTabDescriptor; }) => WorkspacePaneContentModel; } const MobileMountedTabSlot = memo(function MobileMountedTabSlot({ tabDescriptor, isVisible, isWorkspaceFocused, isPaneFocused, paneId, buildPaneContentModel, }: MobileMountedTabSlotProps) { const content = useMemo( () => buildPaneContentModel({ paneId, tab: tabDescriptor, }), [buildPaneContentModel, paneId, tabDescriptor], ); const slotStyle = isVisible ? styles.mobileMountedTabSlotVisible : styles.mobileMountedTabSlotHidden; return ( ); }); interface MobileExplorerOpenGestureSurfaceProps { children: ReactNode; enabled: boolean; onOpenExplorer: () => void; } function MobileExplorerOpenGestureSurface({ children, enabled, onOpenExplorer, }: MobileExplorerOpenGestureSurfaceProps) { const explorerOpenGesture = useExplorerOpenGesture({ enabled, onOpen: onOpenExplorer, }); return ( {children} ); } function useStableTabDescriptorMap(tabDescriptors: WorkspaceTabDescriptor[]) { const cacheRef = useRef(new Map()); const tabDescriptorMap = useMemo(() => { const next = new Map(); for (const tabDescriptor of tabDescriptors) { const cachedDescriptor = cacheRef.current.get(tabDescriptor.tabId); if ( cachedDescriptor && cachedDescriptor.key === tabDescriptor.key && cachedDescriptor.kind === tabDescriptor.kind && workspaceTabTargetsEqual(cachedDescriptor.target, tabDescriptor.target) ) { next.set(tabDescriptor.tabId, cachedDescriptor); continue; } next.set(tabDescriptor.tabId, tabDescriptor); } return next; }, [tabDescriptors]); useEffect(() => { cacheRef.current = tabDescriptorMap; }, [tabDescriptorMap]); return tabDescriptorMap; } export const WorkspaceScreen = memo(function WorkspaceScreen({ serverId, workspaceId, isRouteFocused, }: WorkspaceScreenProps) { const navigationFocused = useIsFocused(); const effectiveRouteFocused = isRouteFocused ?? navigationFocused; return ( ); }); interface UseCloseTabsResult { closingTabIds: Set; closeTab: (tabId: string, action: () => Promise) => Promise; } function useCloseTabs(): UseCloseTabsResult { const pendingRef = useRef(new Set()); const [closingTabIds, setClosingTabIds] = useState>(EMPTY_SET); const closeTab = useCallback(async (tabId: string, action: () => Promise) => { const normalized = tabId.trim(); if (!normalized || pendingRef.current.has(normalized)) { return; } pendingRef.current.add(normalized); setClosingTabIds(new Set(pendingRef.current)); try { await action(); } finally { pendingRef.current.delete(normalized); setClosingTabIds(new Set(pendingRef.current)); } }, []); return { closingTabIds, closeTab }; } interface WorkspaceHeaderMenuProps { normalizedServerId: string; currentBranchName: string | null; showWorkspaceSetup: boolean; showCreateBrowserTab: boolean; isMobile: boolean; createTerminalDisabled: boolean; importAgentDisabled: boolean; copyPathDisabled: boolean; menuNewAgentIcon: ReactElement; menuNewTerminalIcon: ReactElement; menuNewBrowserIcon: ReactElement; menuImportIcon: ReactElement; menuCopyIcon: ReactElement; menuSettingsIcon: ReactElement; onCreateDraftTab: () => void; onCreateTerminal: () => void; onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void; onCreateBrowser: () => void; onOpenImportSheet: () => void; onCopyWorkspacePath: () => void; onCopyBranchName: () => void; onOpenSetupTab: () => void; } interface HeaderMenuProfileItemProps { profile: { id: string; name: string; command: string; args?: string[]; icon?: string }; disabled: boolean; onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void; } function HeaderMenuProfileItem({ profile, disabled, onCreateTerminalWithProfile, }: HeaderMenuProfileItemProps) { const handleSelect = useCallback(() => { onCreateTerminalWithProfile({ name: profile.name, command: profile.command, args: profile.args, }); }, [onCreateTerminalWithProfile, profile]); const icon = getTerminalProfileIcon(profile); const leading = useMemo(() => { if (!icon) { return ( ); } return ( ); }, [icon]); return ( {profile.name} ); } function WorkspaceHeaderMenuTriggerIcon({ hovered, open, isMobile, }: { hovered: boolean; open: boolean; isMobile: boolean; }) { const Icon = isMobile ? ThemedEllipsisVertical : ThemedEllipsis; const colorMapping = hovered || open ? foregroundColorMapping : mutedColorMapping; return ; } function WorkspaceHeaderMenu({ normalizedServerId, currentBranchName, showWorkspaceSetup, showCreateBrowserTab, isMobile, createTerminalDisabled, importAgentDisabled, copyPathDisabled, menuNewAgentIcon, menuNewTerminalIcon, menuNewBrowserIcon, menuImportIcon, menuCopyIcon, menuSettingsIcon, onCreateDraftTab, onCreateTerminal, onCreateTerminalWithProfile, onCreateBrowser, onOpenImportSheet, onCopyWorkspacePath, onCopyBranchName, onOpenSetupTab, }: WorkspaceHeaderMenuProps) { const { t } = useTranslation(); const router = useRouter(); const { config } = useDaemonConfig(normalizedServerId); const profiles = useMemo( () => resolveTerminalProfiles(config?.terminalProfiles), [config?.terminalProfiles], ); const handleEditProfiles = useCallback(() => { router.push(buildSettingsHostSectionRoute(normalizedServerId, "terminals") as Href); }, [normalizedServerId, router]); const renderTriggerIcon = useCallback( ({ hovered, open }: { hovered: boolean; open: boolean }) => ( ), [isMobile], ); return ( {renderTriggerIcon} {t("workspace.header.actions.newAgent")} {showCreateBrowserTab ? ( {t("workspace.header.actions.newBrowser")} ) : null} {t("workspace.header.actions.importSession")} {t("workspace.header.actions.copyPath")} {currentBranchName ? ( {t("workspace.header.actions.copyBranchName")} ) : null} {showWorkspaceSetup ? ( <> {t("workspace.header.actions.showSetup")} ) : null} {t("workspace.tabs.actions.terminalProfilesMenu")} {t("workspace.header.actions.newTerminal")} {profiles.map((profile) => ( ))} {t("workspace.tabs.actions.editTerminalProfiles")} ); } interface WorkspaceHeaderTitleBarProps { isLoading: boolean; title: string; subtitle: string; showSubtitle: boolean; currentBranchName: string | null; normalizedServerId: string; normalizedWorkspaceId: string; workspaceScripts: WorkspaceDescriptor["scripts"]; liveTerminalIds: string[]; showWorkspaceSetup: boolean; showCreateBrowserTab: boolean; isMobile: boolean; createTerminalDisabled: boolean; importAgentDisabled: boolean; copyPathDisabled: boolean; menuNewAgentIcon: ReactElement; menuNewTerminalIcon: ReactElement; menuNewBrowserIcon: ReactElement; menuImportIcon: ReactElement; menuCopyIcon: ReactElement; menuSettingsIcon: ReactElement; onCreateDraftTab: () => void; onCreateTerminal: () => void; onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void; onCreateBrowser: () => void; onOpenImportSheet: () => void; onCopyWorkspacePath: () => void; onCopyBranchName: () => void; onOpenSetupTab: () => void; onScriptTerminalStarted: (terminalId: string) => void; onViewScriptTerminal: (terminalId: string) => void; onOpenUrlInBrowserTab: (url: string) => void; } function WorkspaceHeaderTitleBar({ isLoading, title, subtitle, showSubtitle, currentBranchName, normalizedServerId, normalizedWorkspaceId, workspaceScripts, liveTerminalIds, showWorkspaceSetup, showCreateBrowserTab, isMobile, createTerminalDisabled, importAgentDisabled, copyPathDisabled, menuNewAgentIcon, menuNewTerminalIcon, menuNewBrowserIcon, menuImportIcon, menuCopyIcon, menuSettingsIcon, onCreateDraftTab, onCreateTerminal, onCreateTerminalWithProfile, onCreateBrowser, onOpenImportSheet, onCopyWorkspacePath, onCopyBranchName, onOpenSetupTab, onScriptTerminalStarted, onViewScriptTerminal, onOpenUrlInBrowserTab, }: WorkspaceHeaderTitleBarProps) { return ( {isLoading ? ( ) : ( {title} {showSubtitle ? ( {subtitle} ) : null} )} {isMobile && workspaceScripts.length > 0 ? ( ) : null} ); } type PaneDirection = "left" | "right" | "up" | "down"; function parsePaneDirection(actionId: string): PaneDirection | null { const direction = actionId.split(".").pop(); if (direction === "left" || direction === "right" || direction === "up" || direction === "down") { return direction; } return null; } interface RenderWorkspaceContentInput { isMissingWorkspaceDirectory: boolean; activeTabDescriptor: WorkspaceTabDescriptor | null; hasHydratedAgents: boolean; mountedFocusedPaneTabIds: string[]; focusedPaneTabDescriptorMap: Map; isRouteFocused: boolean; focusedPaneId: string | null; buildMobilePaneContentModel: (input: { paneId: string | null; tab: WorkspaceTabDescriptor; }) => WorkspacePaneContentModel; } function renderWorkspaceContent(input: RenderWorkspaceContentInput): React.ReactNode { const { isMissingWorkspaceDirectory, activeTabDescriptor, hasHydratedAgents, mountedFocusedPaneTabIds, focusedPaneTabDescriptorMap, isRouteFocused, focusedPaneId, buildMobilePaneContentModel, } = input; if (isMissingWorkspaceDirectory) { return ( Workspace directory is missing. Reload workspace data before opening tabs. ); } if (!activeTabDescriptor && !hasHydratedAgents) { return ( ); } if (!activeTabDescriptor) { return ( No tabs are available yet. Use New tab to create an agent or terminal. ); } return mountedFocusedPaneTabIds.map((tabId) => { const tabDescriptor = focusedPaneTabDescriptorMap.get(tabId); if (!tabDescriptor) { return null; } return ( ); }); } interface WorkspaceHeaderFields { isWorkspaceHeaderLoading: boolean; workspaceHeaderTitle: string; workspaceHeaderSubtitle: string; shouldShowWorkspaceHeaderSubtitle: boolean; isGitCheckout: boolean; currentBranchName: string | null; } function buildWorkspaceHeaderCheckoutState(input: { isCheckoutStatusLoading: boolean; isError: boolean; data: CheckoutStatusPayload | undefined; }): WorkspaceHeaderCheckoutState { if (input.isCheckoutStatusLoading) { return { kind: "pending" }; } if (input.isError || !input.data) { return { kind: "error" }; } return { kind: "ready", checkout: { isGit: input.data.isGit, currentBranch: input.data.currentBranch, }, }; } function deriveWorkspaceHeaderFields(input: { workspace: WorkspaceDescriptor | null; checkoutState: WorkspaceHeaderCheckoutState; }): WorkspaceHeaderFields { const renderState = resolveWorkspaceHeaderRenderState(input); if (renderState.kind !== "ready") { return { isWorkspaceHeaderLoading: true, workspaceHeaderTitle: "", workspaceHeaderSubtitle: "", shouldShowWorkspaceHeaderSubtitle: false, isGitCheckout: false, currentBranchName: null, }; } return { isWorkspaceHeaderLoading: false, workspaceHeaderTitle: renderState.title, workspaceHeaderSubtitle: renderState.subtitle, shouldShowWorkspaceHeaderSubtitle: renderState.shouldShowSubtitle, isGitCheckout: renderState.isGitCheckout, currentBranchName: renderState.currentBranchName, }; } function getHostDisplayName(host: { label?: string | null } | null, fallback: string): string { const trimmed = host?.label?.trim(); return trimmed ? trimmed : fallback; } function useWorkspaceRouteActions(normalizedServerId: string): { handleRetryHost: () => void; handleManageHost: () => void; handleDismissMissingWorkspace: () => void; } { const router = useRouter(); const handleRetryHost = useCallback(() => { if (!normalizedServerId) { return; } void getHostRuntimeStore().runProbeCycleNow(normalizedServerId); }, [normalizedServerId]); const handleManageHost = useCallback(() => { if (!normalizedServerId) { return; } router.push(buildSettingsHostRoute(normalizedServerId) as Href); }, [normalizedServerId, router]); const handleDismissMissingWorkspace = useCallback(() => { if (router.canGoBack()) { router.back(); return; } if (normalizedServerId) { router.replace(buildHostRootRoute(normalizedServerId) as Href); return; } router.replace("/" as Href); }, [normalizedServerId, router]); return { handleRetryHost, handleManageHost, handleDismissMissingWorkspace, }; } function useResolvedWorkspaceRouteState(input: { serverId: string; workspaceId: string; workspace: WorkspaceDescriptor | null; hasHydratedWorkspaces: boolean; }): WorkspaceRouteState { const hosts = useHosts(); const host = useMemo( () => hosts.find((entry) => entry.serverId === input.serverId) ?? null, [hosts, input.serverId], ); const hostSnapshot = useHostRuntimeSnapshot(input.serverId); const hostName = useMemo(() => getHostDisplayName(host, input.serverId), [host, input.serverId]); const restoreStatus = useWorkspaceRestoreStatus(input.serverId, input.workspaceId); return useMemo( () => resolveWorkspaceRouteState({ hostName, connectionStatus: hostSnapshot?.connectionStatus ?? "connecting", lastError: hostSnapshot?.lastError ?? null, workspace: input.workspace, hasHydratedWorkspaces: input.hasHydratedWorkspaces, restoreStatus, }), [ hostName, hostSnapshot?.connectionStatus, hostSnapshot?.lastError, input.workspace, input.hasHydratedWorkspaces, restoreStatus, ], ); } function WorkspaceScreenGateFrame({ children }: { children: ReactNode }) { return ( <> {children} ); } function renderWorkspaceScreenGateShell(input: { gate: ReactNode; workspaceKey: string | null; }): ReactElement | null { if (!input.gate) { return null; } return ( {input.gate} ); } function WorkspaceDocumentTitleEffectSlot({ tab, serverId, workspaceId, isRouteFocused, }: { tab: WorkspaceTabDescriptor | null; serverId: string; workspaceId: string; isRouteFocused: boolean; }) { if (!isRouteFocused || !isWeb || !tab) { return null; } return ( {(presentation) => ( )} ); } function shouldShowWorkspaceScreenHeader(input: { isFocusModeEnabled: boolean; isMobile: boolean; }): boolean { return !input.isFocusModeEnabled || input.isMobile; } function shouldShowWorkspaceExplorerSidebar(input: { isRouteFocused: boolean; isFocusModeEnabled: boolean; isMobile: boolean; }): boolean { return input.isRouteFocused && shouldShowWorkspaceScreenHeader(input); } function buildWorkspaceTerminalScopeKey(serverId: string, workspaceId: string): string | null { if (!serverId || !workspaceId) { return null; } return `${serverId}:${workspaceId}`; } interface WorkspaceTerminalTabActionsInput { persistenceKey: string | null; focusWorkspacePane: (workspaceKey: string, paneId: string) => void; openWorkspaceTabFocused: (workspaceKey: string, target: WorkspaceTabTarget) => string | null; labels: { workspacePathUnavailable: string; terminalQueued: string; }; toast: { error: (message: string) => void; show: (message: string) => void; }; } interface WorkspaceTerminalTabActions { handleTerminalCreated: (input: { terminalId: string; paneId?: string }) => void; handleScriptTerminalSelected: (terminalId: string) => void; handleWorkspacePathUnavailable: () => void; handleTerminalCreateQueued: () => void; handleTerminalCreateFailed: (reason: string) => void; } function useWorkspaceTerminalTabActions({ persistenceKey, focusWorkspacePane, openWorkspaceTabFocused, labels, toast, }: WorkspaceTerminalTabActionsInput): WorkspaceTerminalTabActions { const handleTerminalCreated = useCallback( ({ terminalId, paneId }: { terminalId: string; paneId?: string }) => { if (!persistenceKey) { return; } if (paneId) { focusWorkspacePane(persistenceKey, paneId); } openWorkspaceTabFocused(persistenceKey, { kind: "terminal", terminalId }); }, [focusWorkspacePane, openWorkspaceTabFocused, persistenceKey], ); const handleScriptTerminalSelected = useCallback( (terminalId: string) => { if (!persistenceKey) { return; } openWorkspaceTabFocused(persistenceKey, { kind: "terminal", terminalId }); }, [openWorkspaceTabFocused, persistenceKey], ); const handleWorkspacePathUnavailable = useCallback(() => { toast.error(labels.workspacePathUnavailable); }, [labels.workspacePathUnavailable, toast]); const handleTerminalCreateQueued = useCallback(() => { toast.show(labels.terminalQueued); }, [labels.terminalQueued, toast]); const handleTerminalCreateFailed = useCallback( (reason: string) => { toast.error(reason); }, [toast], ); return { handleTerminalCreated, handleScriptTerminalSelected, handleWorkspacePathUnavailable, handleTerminalCreateQueued, handleTerminalCreateFailed, }; } function useWorkspaceCheckoutStatus(input: { client: ReturnType; isConnected: boolean; isRouteFocused: boolean; normalizedServerId: string; normalizedWorkspaceId: string; workspaceDirectory: string | null; }) { const { t } = useTranslation(); const isCheckoutQueryEnabled = useMemo( () => canCreateWorkspaceTerminal({ isRouteFocused: input.isRouteFocused, client: input.client, isConnected: input.isConnected, workspaceDirectory: input.workspaceDirectory, }), [input.isRouteFocused, input.client, input.isConnected, input.workspaceDirectory], ); const checkoutQuery = useQuery({ queryKey: checkoutStatusQueryKey( input.normalizedServerId, input.workspaceDirectory ?? `missing-workspace-directory:${input.normalizedWorkspaceId}`, ), enabled: isCheckoutQueryEnabled, queryFn: async () => { if (!input.client || !input.workspaceDirectory) { throw new Error(t("workspace.terminal.hostDisconnected")); } return await fetchCheckoutStatus({ client: input.client, serverId: input.normalizedServerId, cwd: input.workspaceDirectory, }); }, staleTime: Infinity, // Refetch on mount only after explicit invalidation (e.g. reconnect) — see // useCheckoutStatusQuery for the rationale. refetchOnMount: true, refetchOnReconnect: false, refetchOnWindowFocus: false, }); const isCheckoutStatusLoading = useMemo( () => isCheckoutQueryEnabled && checkoutQuery.data === undefined && !checkoutQuery.isError, [isCheckoutQueryEnabled, checkoutQuery.data, checkoutQuery.isError], ); return { checkoutQuery, isCheckoutStatusLoading }; } function WorkspaceScreenContent({ serverId, workspaceId, isRouteFocused, }: WorkspaceScreenContentProps) { const { t } = useTranslation(); const _insets = useSafeAreaInsets(); const toast = useToast(); const isMobile = useIsCompactFormFactor(); const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); const normalizedServerId = useMemo(() => trimNonEmpty(decodeSegment(serverId)) ?? "", [serverId]); const normalizedWorkspaceId = useMemo( () => resolveWorkspaceRouteId({ routeWorkspaceId: workspaceId }) ?? "", [workspaceId], ); const workspaceDescriptor = useWorkspace(normalizedServerId, normalizedWorkspaceId); const workspaceScripts = getWorkspaceScripts(workspaceDescriptor); const { handleRetryHost, handleManageHost, handleDismissMissingWorkspace } = useWorkspaceRouteActions(normalizedServerId); const workspaceTerminalScopeKey = useMemo( () => buildWorkspaceTerminalScopeKey(normalizedServerId, normalizedWorkspaceId), [normalizedServerId, normalizedWorkspaceId], ); useWorkspaceTerminalSessionRetention({ scopeKey: workspaceTerminalScopeKey, }); const client = useHostRuntimeClient(normalizedServerId); const isConnected = useHostRuntimeIsConnected(normalizedServerId); const workspaceDirectory = workspaceDescriptor?.workspaceDirectory || null; const isMissingWorkspaceDirectory = Boolean(workspaceDescriptor) && !workspaceDirectory; const [isImportSheetVisible, setIsImportSheetVisible] = useState(false); const canOpenImportSheet = [client, isConnected, workspaceDirectory].every(Boolean); const openImportSheet = useCallback(() => { setIsImportSheetVisible(true); }, []); const closeImportSheet = useCallback(() => { setIsImportSheetVisible(false); }, []); // Warm the workspace-scoped provider snapshot so the model picker is ready when opened. useProvidersSnapshot(normalizedServerId, { cwd: workspaceDirectory, enabled: isRouteFocused, }); const persistenceKey = useMemo( () => buildWorkspaceTabPersistenceKey({ serverId: normalizedServerId, workspaceId: normalizedWorkspaceId, }), [normalizedServerId, normalizedWorkspaceId], ); const openWorkspaceTabFocused = useWorkspaceLayoutStore((state) => state.openTabFocused); const openWorkspaceChildTabFocused = useWorkspaceLayoutStore( (state) => state.openChildTabFocused, ); const focusWorkspacePane = useWorkspaceLayoutStore((state) => state.focusPane); const hasHydratedWorkspaces = useSessionStore( (state) => state.sessions[normalizedServerId]?.hasHydratedWorkspaces ?? false, ); const workspaceAgentVisibility = useStoreWithEqualityFn( useSessionStore, (state) => deriveWorkspaceAgentVisibility({ sessionAgents: state.sessions[normalizedServerId]?.agents, agentDetails: state.sessions[normalizedServerId]?.agentDetails, workspaceId: normalizedWorkspaceId, }), workspaceAgentVisibilityEqual, ); const { handleTerminalCreated, handleScriptTerminalSelected, handleWorkspacePathUnavailable, handleTerminalCreateQueued, handleTerminalCreateFailed, } = useWorkspaceTerminalTabActions({ persistenceKey, focusWorkspacePane, openWorkspaceTabFocused, labels: { workspacePathUnavailable: t("workspace.header.toasts.workspacePathUnavailable"), terminalQueued: t("workspace.header.toasts.terminalQueued"), }, toast, }); const queryClient = useQueryClient(); const { createMutation: createTerminalMutation, createTerminal, handleScriptTerminalStarted, handleViewScriptTerminal, invalidateTerminals, killMutation: killTerminalMutation, knownTerminalIds, liveTerminalIds, pendingCreateInput: pendingTerminalCreateInput, query: terminalsQuery, queryKey: terminalsQueryKey, removeTerminalFromCache, standaloneTerminalIds, terminals, } = useWorkspaceTerminals({ client, isConnected, isRouteFocused, normalizedServerId, normalizedWorkspaceId, workspaceDirectory, workspaceScripts, hasHydratedWorkspaces, isMissingWorkspaceDirectory, onTerminalCreated: handleTerminalCreated, onScriptTerminalSelected: handleScriptTerminalSelected, onWorkspacePathUnavailable: handleWorkspacePathUnavailable, onTerminalCreateQueued: handleTerminalCreateQueued, onTerminalCreateFailed: handleTerminalCreateFailed, }); const { archiveAgent } = useArchiveAgent(); const { checkoutQuery, isCheckoutStatusLoading } = useWorkspaceCheckoutStatus({ client, isConnected, isRouteFocused, normalizedServerId, normalizedWorkspaceId, workspaceDirectory, }); const hasHydratedAgents = useSessionStore( (state) => state.sessions[normalizedServerId]?.hasHydratedAgents ?? false, ); const workspaceRouteState = useResolvedWorkspaceRouteState({ serverId: normalizedServerId, workspaceId: normalizedWorkspaceId, workspace: workspaceDescriptor, hasHydratedWorkspaces, }); const workspaceHeaderCheckoutState = buildWorkspaceHeaderCheckoutState({ isCheckoutStatusLoading, isError: checkoutQuery.isError, data: checkoutQuery.data, }); const { isWorkspaceHeaderLoading, workspaceHeaderTitle, workspaceHeaderSubtitle, shouldShowWorkspaceHeaderSubtitle, isGitCheckout, currentBranchName, } = deriveWorkspaceHeaderFields({ workspace: workspaceDescriptor, checkoutState: workspaceHeaderCheckoutState, }); const isExplorerOpen = usePanelStore((state) => selectIsFileExplorerOpen(state, { isCompact: isMobile }), ); const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout); const toggleFileExplorerForCheckout = usePanelStore( (state) => state.toggleFileExplorerForCheckout, ); const showMobileAgent = usePanelStore((state) => state.showMobileAgent); const activeExplorerCheckout = useMemo(() => { if (!normalizedServerId || !workspaceDirectory) { return null; } return { serverId: normalizedServerId, cwd: workspaceDirectory, isGit: isGitCheckout, }; }, [isGitCheckout, normalizedServerId, workspaceDirectory]); const openExplorerForWorkspace = useCallback(() => { if (!activeExplorerCheckout) { return; } openFileExplorerForCheckout({ isCompact: isMobile, checkout: activeExplorerCheckout, }); }, [activeExplorerCheckout, isMobile, openFileExplorerForCheckout]); const handleToggleExplorer = useCallback(() => { if (!activeExplorerCheckout) { return; } toggleFileExplorerForCheckout({ isCompact: isMobile, checkout: activeExplorerCheckout, }); }, [activeExplorerCheckout, isMobile, toggleFileExplorerForCheckout]); const hasDiffStat = useMemo(() => Boolean(workspaceDescriptor?.diffStat), [workspaceDescriptor]); const explorerToggleStyle = useCallback( ({ hovered, pressed }: { hovered?: boolean; pressed?: boolean }) => [ styles.sourceControlButton, hasDiffStat && styles.sourceControlButtonWithStats, (Boolean(hovered) || Boolean(pressed) || isExplorerOpen) && styles.sourceControlButtonHovered, ], [hasDiffStat, isExplorerOpen], ); const explorerToggleAccessibilityState = useMemo( () => ({ expanded: isExplorerOpen }), [isExplorerOpen], ); useEffect(() => { if (!isRouteFocused || isWeb || !isExplorerOpen) { return; } const handler = BackHandler.addEventListener("hardwareBackPress", () => { if (isExplorerOpen) { showMobileAgent(); return true; } return false; }); return () => handler.remove(); }, [isExplorerOpen, isRouteFocused, showMobileAgent]); const workspaceLayout = useWorkspaceLayoutStore((state) => persistenceKey ? (state.layoutByWorkspace[persistenceKey] ?? null) : null, ); const hasHydratedWorkspaceLayoutStore = useWorkspaceLayoutStoreHydrated(); const workspaceSetupSnapshot = useWorkspaceSetupStore((state) => persistenceKey ? (state.snapshots[persistenceKey] ?? null) : null, ); const upsertWorkspaceSetupProgress = useWorkspaceSetupStore((state) => state.upsertProgress); const showWorkspaceSetup = shouldShowWorkspaceSetup(workspaceSetupSnapshot); const uiTabs = useMemo( () => (workspaceLayout ? collectAllTabs(workspaceLayout.root) : EMPTY_UI_TABS), [workspaceLayout], ); useSyncWorkspaceActiveBrowser({ workspaceLayout, isRouteFocused }); const openWorkspaceTabInBackground = useWorkspaceLayoutStore( (state) => state.openTabInBackground, ); const focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab); const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab); const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent); const hideWorkspaceAgent = useWorkspaceLayoutStore((state) => state.hideAgent); const retargetWorkspaceTab = useWorkspaceLayoutStore((state) => state.retargetTab); const reconcileWorkspaceTabs = useWorkspaceLayoutStore((state) => state.reconcileTabs); const splitWorkspacePane = useWorkspaceLayoutStore((state) => state.splitPane); const splitWorkspacePaneEmpty = useWorkspaceLayoutStore((state) => state.splitPaneEmpty); const moveWorkspaceTabToPane = useWorkspaceLayoutStore((state) => state.moveTabToPane); const paneFocusSuppressedRef = useRef(false); 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 _hiddenAgentIds = useWorkspaceLayoutStore((state) => persistenceKey ? (state.hiddenAgentIdsByWorkspace[persistenceKey] ?? EMPTY_SET) : EMPTY_SET, ); const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId); const { closingTabIds, closeTab } = useCloseTabs(); const { onLayout: onHeaderLayout, isBelow: showCompactButtonLabels } = useContainerWidthBelow(700); 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); hideWorkspaceAgent(persistenceKey, input.target.agentId); } if (input.target?.kind === "browser") { const { browserId } = input.target; useBrowserStore.getState().removeBrowser(browserId); void getDesktopHost()?.browser?.clearPartition?.(browserId); } closeWorkspaceTab(persistenceKey, normalizedTabId); }, [closeWorkspaceTab, hideWorkspaceAgent, persistenceKey, unpinWorkspaceAgent], ); const focusedPaneTabState = useMemo( () => deriveWorkspacePaneState({ layout: workspaceLayout, tabs: uiTabs, }), [uiTabs, workspaceLayout], ); const setFocusedAgentId = useSessionStore((state) => state.setFocusedAgentId); const setFocusedTerminalId = useSessionStore((state) => state.setFocusedTerminalId); const focusedPaneAgentId = useMemo(() => { const target = focusedPaneTabState.activeTab?.descriptor.target; if (target?.kind !== "agent") { return null; } return target.agentId; }, [focusedPaneTabState.activeTab]); const focusedPaneTerminalId = useMemo(() => { const target = focusedPaneTabState.activeTab?.descriptor.target; if (target?.kind !== "terminal") { return null; } return target.terminalId; }, [focusedPaneTabState.activeTab]); useEffect(() => { if (!isRouteFocused) { return; } setFocusedAgentId(normalizedServerId, focusedPaneAgentId); setFocusedTerminalId(normalizedServerId, focusedPaneTerminalId); }, [ focusedPaneAgentId, focusedPaneTerminalId, isRouteFocused, normalizedServerId, setFocusedAgentId, setFocusedTerminalId, ]); useEffect(() => { if (!isRouteFocused) { return; } return () => { setFocusedAgentId(normalizedServerId, null); setFocusedTerminalId(normalizedServerId, null); }; }, [isRouteFocused, normalizedServerId, setFocusedAgentId, setFocusedTerminalId]); 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 openWorkspaceTabInBackground(persistenceKey, target); } return openWorkspaceTabFocused(persistenceKey, target); }, [openWorkspaceTabFocused, openWorkspaceTabInBackground, persistenceKey], ); useEffect(() => { if (!isRouteFocused) { return; } if (!normalizedServerId || !normalizedWorkspaceId || !persistenceKey) { return; } if (!hasHydratedWorkspaceLayoutStore) { return; } 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"; }); reconcileWorkspaceTabs( persistenceKey, buildWorkspaceTabSnapshot({ agentVisibility: workspaceAgentVisibility, agentsHydrated: hasHydratedAgents, terminalsHydrated: terminalsQuery.isSuccess, knownTerminalIds, standaloneTerminalIds, hasActivePendingDraftCreate: hasActivePendingDraftCreateInWorkspace, }), ); }, [ hasHydratedAgents, hasHydratedWorkspaceLayoutStore, isRouteFocused, normalizedServerId, normalizedWorkspaceId, pendingByDraftId, persistenceKey, reconcileWorkspaceTabs, knownTerminalIds, standaloneTerminalIds, terminalsQuery.isSuccess, uiTabs, workspaceAgentVisibility, ]); const activeTabId = focusedPaneTabState.activeTabId; const activeTab = focusedPaneTabState.activeTab; const tabs = useMemo( () => focusedPaneTabState.tabs.map((tab) => tab.descriptor), [focusedPaneTabState.tabs], ); const hasSetupTab = useMemo( () => uiTabs.some( (tab) => tab.target.kind === "setup" && tab.target.workspaceId === normalizedWorkspaceId, ), [normalizedWorkspaceId, uiTabs], ); const navigateToTabId = useCallback( function navigateToTabId(tabId: string) { if (!tabId || !persistenceKey) { return; } focusWorkspaceTab(persistenceKey, tabId); }, [focusWorkspaceTab, persistenceKey], ); const handleImportedAgent = useCallback( (agentId: string) => { if (!persistenceKey) { return; } const tabId = openWorkspaceTabFocused(persistenceKey, { kind: "agent", agentId }); if (tabId) { navigateToTabId(tabId); } }, [navigateToTabId, openWorkspaceTabFocused, persistenceKey], ); const emptyWorkspaceSeedRef = useRef(null); const autoOpenedSetupTabWorkspaceRef = useRef(null); const requestedWorkspaceSetupStatusKeyRef = useRef(null); useEffect(() => { if (!isRouteFocused) { return; } if (!client || !normalizedServerId || !normalizedWorkspaceId || !persistenceKey) { return; } if (workspaceSetupSnapshot) { return; } if (requestedWorkspaceSetupStatusKeyRef.current === persistenceKey) { return; } requestedWorkspaceSetupStatusKeyRef.current = persistenceKey; let isCancelled = false; client .fetchWorkspaceSetupStatus(normalizedWorkspaceId) .then((response) => { if (isCancelled || response.workspaceId !== normalizedWorkspaceId || !response.snapshot) { return; } upsertWorkspaceSetupProgress({ serverId: normalizedServerId, payload: { workspaceId: response.workspaceId, ...response.snapshot }, }); return; }) .catch(() => { if (requestedWorkspaceSetupStatusKeyRef.current === persistenceKey) { requestedWorkspaceSetupStatusKeyRef.current = null; } }); return () => { isCancelled = true; }; }, [ client, isRouteFocused, normalizedServerId, normalizedWorkspaceId, persistenceKey, upsertWorkspaceSetupProgress, workspaceSetupSnapshot, ]); useEffect(() => { if ( !shouldSeedEmptyWorkspaceDraft({ isRouteFocused, hasPersistenceKey: Boolean(persistenceKey), hasWorkspaceDirectory: Boolean(workspaceDirectory), hasHydratedWorkspaceLayoutStore, hasHydratedAgents, hasLoadedTerminals: terminalsQuery.isSuccess, activeAgentCount: workspaceAgentVisibility.activeAgentIds.size, terminalCount: terminals.length, tabCount: tabs.length, }) ) { emptyWorkspaceSeedRef.current = null; return; } const workspaceKey = `${normalizedServerId}:${normalizedWorkspaceId}`; if (emptyWorkspaceSeedRef.current === workspaceKey) { return; } emptyWorkspaceSeedRef.current = workspaceKey; openWorkspaceDraftTab(); }, [ normalizedServerId, normalizedWorkspaceId, openWorkspaceDraftTab, persistenceKey, hasHydratedAgents, hasHydratedWorkspaceLayoutStore, isRouteFocused, terminals.length, terminalsQuery.isSuccess, tabs.length, workspaceDirectory, workspaceAgentVisibility.activeAgentIds.size, ]); useEffect(() => { if (!isRouteFocused) { return; } if (!persistenceKey) { return; } if (!workspaceSetupSnapshot || !showWorkspaceSetup) { if (autoOpenedSetupTabWorkspaceRef.current === persistenceKey) { autoOpenedSetupTabWorkspaceRef.current = null; } return; } const snapshotAge = Date.now() - workspaceSetupSnapshot.updatedAt; const shouldAutoOpen = workspaceSetupSnapshot.status === "running" || snapshotAge <= WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS; if (!shouldAutoOpen) { return; } if (hasSetupTab) { autoOpenedSetupTabWorkspaceRef.current = persistenceKey; return; } if (autoOpenedSetupTabWorkspaceRef.current === persistenceKey) { return; } const target = normalizeWorkspaceTabTarget({ kind: "setup", workspaceId: normalizedWorkspaceId, }); if (!target) { return; } const tabId = openWorkspaceTabInBackground(persistenceKey, target); if (!tabId) { return; } autoOpenedSetupTabWorkspaceRef.current = persistenceKey; }, [ hasSetupTab, isRouteFocused, normalizedWorkspaceId, openWorkspaceTabInBackground, persistenceKey, showWorkspaceSetup, workspaceSetupSnapshot, ]); const handleOpenFileFromExplorer = useCallback( function handleOpenFileFromExplorer(filePath: string) { if (isMobile) { showMobileAgent(); } if (!persistenceKey) { return; } const location = normalizeWorkspaceFileLocation({ path: filePath }); if (!location) { return; } const tabId = openWorkspaceTabFocused(persistenceKey, createWorkspaceFileTabTarget(location)); if (tabId) { navigateToTabId(tabId); } }, [isMobile, navigateToTabId, openWorkspaceTabFocused, persistenceKey, showMobileAgent], ); const handleOpenFileFromChat = useCallback( (location: WorkspaceFileLocation, options?: { parentTabId?: string | null }) => { const normalizedLocation = normalizeWorkspaceFileLocation(location); if (!normalizedLocation) { return; } if (isMobile) { showMobileAgent(); } if (!persistenceKey) { return; } const target = createWorkspaceFileTabTarget(normalizedLocation); const tabId = options?.parentTabId ? openWorkspaceChildTabFocused(persistenceKey, target, options.parentTabId) : openWorkspaceTabFocused(persistenceKey, target); if (tabId) { navigateToTabId(tabId); } }, [ isMobile, navigateToTabId, openWorkspaceChildTabFocused, openWorkspaceTabFocused, persistenceKey, showMobileAgent, ], ); const handleOpenFileFromChatInSidePane = useCallback( (input: { location: WorkspaceFileLocation; sourcePaneId?: string; parentTabId?: string | null; }) => { const location = normalizeWorkspaceFileLocation(input.location); if (!location) { return; } if (!persistenceKey || isMobile || !input.sourcePaneId) { handleOpenFileFromChat(location, { parentTabId: input.parentTabId }); return; } const target: WorkspaceTabTarget = createWorkspaceFileTabTarget(location); const placement = resolveSideFileOpenPlacement({ layout: workspaceLayout, sourcePaneId: input.sourcePaneId, tabs: uiTabs, target, }); if (placement.kind === "focus-side-pane") { focusWorkspacePane(persistenceKey, placement.paneId); } else if (placement.kind === "split-side-pane") { splitWorkspacePaneEmpty(persistenceKey, { targetPaneId: placement.paneId, position: "right", }); } const tabId = input.parentTabId ? openWorkspaceChildTabFocused(persistenceKey, target, input.parentTabId) : openWorkspaceTabFocused(persistenceKey, target); if (tabId) { navigateToTabId(tabId); } }, [ handleOpenFileFromChat, isMobile, focusWorkspacePane, navigateToTabId, openWorkspaceChildTabFocused, openWorkspaceTabFocused, persistenceKey, splitWorkspacePaneEmpty, uiTabs, workspaceLayout, ], ); const handleOpenWorkspaceFileFromPane = useStableEvent(function handleOpenWorkspaceFileFromPane({ request, paneId, parentTabId, focusPaneBeforeOpen, }: { request: WorkspaceFileOpenRequest; paneId?: string | null; parentTabId: string; focusPaneBeforeOpen?: boolean; }) { if (focusPaneBeforeOpen && paneId && persistenceKey) { focusWorkspacePane(persistenceKey, paneId); } if (request.disposition === "side") { handleOpenFileFromChatInSidePane({ location: request.location, sourcePaneId: paneId ?? undefined, parentTabId, }); return; } handleOpenFileFromChat(request.location, { parentTabId }); }); const [hoveredCloseTabKey, setHoveredCloseTabKey] = useState(null); const { handleRenameTab, renamingTab, handleRenameModalSubmit, handleRenameModalClose } = useWorkspaceTabRename({ client, normalizedServerId, queryClient, terminalsData: terminalsQuery.data, terminalsQueryKey, }); 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 bulkCloseConfirmationLabels = useMemo( () => ({ all: ({ agents, terminals: terminalCount, tabs: tabCount }) => t("workspace.tabs.confirmations.bulk.all", { agents, terminals: terminalCount, tabs: tabCount, }), agentsAndTerminals: ({ agents, terminals: terminalCount }) => t("workspace.tabs.confirmations.bulk.agentsAndTerminals", { agents, terminals: terminalCount, }), terminalsAndTabs: ({ terminals: terminalCount, tabs: tabCount }) => t("workspace.tabs.confirmations.bulk.terminalsAndTabs", { terminals: terminalCount, tabs: tabCount, }), agentsAndTabs: ({ agents, tabs: tabCount }) => t("workspace.tabs.confirmations.bulk.agentsAndTabs", { agents, tabs: tabCount }), terminals: ({ terminals: terminalCount }) => t("workspace.tabs.confirmations.bulk.terminals", { terminals: terminalCount }), tabs: ({ tabs: tabCount }) => t("workspace.tabs.confirmations.bulk.tabs", { tabs: tabCount }), agents: ({ agents }) => t("workspace.tabs.confirmations.bulk.agents", { agents }), }), [t], ); const explorerToggleLabel = isExplorerOpen ? t("workspace.tabs.explorer.close") : t("workspace.tabs.explorer.open"); const activeTabKey = useMemo(() => activeTabId ?? "", [activeTabId]); const tabFallbackLabels = useMemo( () => ({ newAgent: t("workspace.tabs.fallback.newAgent"), setup: t("workspace.tabs.fallback.setup"), workspaceSetup: t("workspace.tabs.fallback.workspaceSetup"), terminal: t("workspace.tabs.fallback.terminal"), browser: t("workspace.tabs.fallback.browser"), agent: t("workspace.tabs.fallback.agent"), }), [t], ); const tabSwitcherOptions = useMemo( () => tabs.map((tab) => ({ id: tab.key, label: getFallbackTabOptionLabel(tab, tabFallbackLabels), description: getFallbackTabOptionDescription(tab, tabFallbackLabels), })), [tabFallbackLabels, tabs], ); const handleCreateDraftTab = useCallback( (input?: { paneId?: string }) => { if (input?.paneId && persistenceKey) { focusWorkspacePane(persistenceKey, input.paneId); } openWorkspaceDraftTab(); }, [focusWorkspacePane, openWorkspaceDraftTab, persistenceKey], ); const handleCreateTerminal = useStableEvent(createTerminal); const handleCreateTerminalWithProfile = useCallback( (profile: TerminalProfileInput) => { createTerminal({ profile }); }, [createTerminal], ); const handleCreateBrowserTab = useCallback( (input?: { paneId?: string }) => { if (!persistenceKey || !getIsElectron()) { return; } if (input?.paneId) { focusWorkspacePane(persistenceKey, input.paneId); } const { browserId } = createWorkspaceBrowser(); openWorkspaceTabFocused(persistenceKey, { kind: "browser", browserId }); }, [focusWorkspacePane, openWorkspaceTabFocused, persistenceKey], ); const handleOpenUrlInBrowserTab = useCallback( (url: string) => { if (!persistenceKey || !getIsElectron()) { return; } const { browserId } = createWorkspaceBrowser({ initialUrl: url }); openWorkspaceTabFocused(persistenceKey, { kind: "browser", browserId }); }, [openWorkspaceTabFocused, persistenceKey], ); useDesktopBrowserNewTabRequests({ enabled: Boolean(persistenceKey), workspaceLayout, openUrl: handleOpenUrlInBrowserTab, }); const handleSelectSwitcherTab = useCallback( (key: string) => { navigateToTabId(key); }, [navigateToTabId], ); const handleCreateDraftSplit = useCallback( (input: { targetPaneId: string; position: "left" | "right" | "top" | "bottom" }) => { if (!persistenceKey) { return; } const paneId = splitWorkspacePaneEmpty(persistenceKey, input); if (!paneId) { return; } handleCreateDraftTab({ paneId }); }, [handleCreateDraftTab, persistenceKey, splitWorkspacePaneEmpty], ); const killTerminalAsync = killTerminalMutation.mutateAsync; const handleCloseTerminalTab = useCallback( async (input: { tabId: string; terminalId: string }) => { const { tabId, terminalId } = input; await closeTab(tabId, async () => { const confirmed = await confirmDialog({ title: t("workspace.tabs.confirmations.closeTerminalTitle"), message: t("workspace.tabs.confirmations.closeTerminalMessage"), confirmLabel: t("workspace.tabs.confirmations.close"), cancelLabel: t("workspace.tabs.confirmations.cancel"), destructive: true, }); if (!confirmed) { return; } removeTerminalFromCache(terminalId); setHoveredCloseTabKey((current) => (current === tabId ? null : current)); if (persistenceKey) { closeWorkspaceTabWithCleanup({ tabId, target: { kind: "terminal", terminalId }, }); } void killTerminalAsync(terminalId).catch(invalidateTerminals); }); }, [ closeTab, closeWorkspaceTabWithCleanup, invalidateTerminals, killTerminalAsync, persistenceKey, removeTerminalFromCache, t, ], ); const handleCloseAgentTab = useCallback( async (input: { tabId: string; agentId: string }) => { const { tabId, agentId } = input; await closeTab(tabId, async () => { if (!normalizedServerId) { return; } const agent = useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null; const closePolicy = resolveCloseAgentTabPolicy(agent); const isRunning = agent?.status === "running"; if (isRunning && closePolicy.kind === "archive-on-close") { const confirmed = await confirmDialog({ title: t("workspace.tabs.confirmations.archiveRunningAgentTitle"), message: t("workspace.tabs.confirmations.archiveRunningAgentMessage"), confirmLabel: t("workspace.tabs.confirmations.archive"), cancelLabel: t("workspace.tabs.confirmations.cancel"), destructive: true, }); if (!confirmed) { return; } } setHoveredCloseTabKey((current) => (current === tabId ? null : current)); if (persistenceKey) { closeWorkspaceTabWithCleanup({ tabId, target: { kind: "agent", agentId }, }); } if (closePolicy.kind === "layout-only") { return; } // Errors (e.g. timeout) are handled by the mutation's onSettled callback void archiveAgent({ serverId: normalizedServerId, agentId }).catch(() => {}); }); }, [archiveAgent, closeTab, closeWorkspaceTabWithCleanup, normalizedServerId, persistenceKey, t], ); const handleCloseDraftOrFileTab = useCallback( function handleCloseDraftOrFileTab(input: { tabId: string; target?: WorkspaceTabTarget | null; }) { setHoveredCloseTabKey((current) => (current === input.tabId ? null : current)); if (persistenceKey) { closeWorkspaceTabWithCleanup({ tabId: input.tabId, target: input.target }); } }, [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, target: tab.target }); }, [allTabDescriptorsById, handleCloseAgentTab, handleCloseDraftOrFileTab, handleCloseTerminalTab], ); const handleCopyAgentId = useCallback( async (agentId: string) => { if (!agentId) return; try { await Clipboard.setStringAsync(agentId); toast.copied(t("workspace.tabs.toasts.agentIdCopiedLabel")); } catch { toast.error(t("workspace.tabs.toasts.copyFailed")); } }, [toast, t], ); const handleCopyFilePath = useCallback( async (path: string) => { if (!path) return; try { await Clipboard.setStringAsync(path); toast.copied(t("workspace.tabs.toasts.filePathCopiedLabel")); } catch { toast.error(t("workspace.tabs.toasts.copyFailed")); } }, [toast, t], ); 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(t("workspace.tabs.toasts.resumeIdUnavailable")); return; } const command = buildProviderCommand({ provider: agent.provider, id: "resume", sessionId: providerSessionId, }) ?? null; if (!command) { toast.error(t("workspace.tabs.toasts.resumeCommandUnavailable")); return; } try { await Clipboard.setStringAsync(command); toast.copied(t("workspace.tabs.toasts.resumeCommandCopiedLabel")); } catch { toast.error(t("workspace.tabs.toasts.copyFailed")); } }, [normalizedServerId, toast, t], ); const handleReloadAgent = useCallback( async (agentId: string) => { if (!client || !isConnected) { toast.error(t("workspace.terminal.hostDisconnected")); return; } toast.show(t("workspace.tabs.toasts.reloadingAgent"), { durationMs: null }); try { await client.refreshAgent(agentId); // Send the existing cursor so the server detects the new epoch and // returns reset:true. Without a cursor, the server returns reset:false // and the client takes the incremental path, where new-epoch rows are // dropped against the stale cursor. const sessionState = useSessionStore.getState().sessions[normalizedServerId]; const currentCursor = sessionState?.agentTimelineCursor.get(agentId); await client.fetchAgentTimeline(agentId, { direction: "tail", projection: "projected", ...(currentCursor ? { cursor: { epoch: currentCursor.epoch, seq: currentCursor.endSeq } } : {}), }); toast.show(t("workspace.tabs.toasts.reloadedAgent"), { variant: "success" }); } catch (error) { toast.error( error instanceof Error ? error.message : t("workspace.tabs.toasts.failedToReloadAgent"), ); } }, [client, isConnected, normalizedServerId, toast, t], ); const handleCopyWorkspacePath = useCallback(async () => { if (!workspaceDirectory) { toast.error(t("workspace.header.toasts.workspacePathUnavailable")); return; } try { await Clipboard.setStringAsync(workspaceDirectory); toast.copied(t("workspace.header.toasts.workspacePathCopiedLabel")); } catch { toast.error(t("workspace.tabs.toasts.copyFailed")); } }, [toast, workspaceDirectory, t]); const handleCopyBranchName = useCallback(async () => { if (!currentBranchName) { toast.error(t("workspace.header.toasts.branchNameUnavailable")); return; } try { await Clipboard.setStringAsync(currentBranchName); toast.copied(t("workspace.header.toasts.branchNameCopiedLabel")); } catch { toast.error(t("workspace.tabs.toasts.copyFailed")); } }, [currentBranchName, toast, t]); const handleOpenSetupTab = useCallback(() => { if (!persistenceKey) { return; } const target = normalizeWorkspaceTabTarget({ kind: "setup", workspaceId: normalizedWorkspaceId, }); if (!target) { return; } openWorkspaceTabFocused(persistenceKey, target); }, [normalizedWorkspaceId, openWorkspaceTabFocused, persistenceKey]); 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, bulkCloseConfirmationLabels), confirmLabel: t("workspace.tabs.confirmations.close"), cancelLabel: t("workspace.tabs.confirmations.cancel"), destructive: true, }); if (!confirmed) { return; } await closeBulkWorkspaceTabs({ client, groups, closeTab, closeWorkspaceTabWithCleanup: (cleanupInput) => { if (!persistenceKey) { return; } closeWorkspaceTabWithCleanup(cleanupInput); }, logLabel, warn: (message, payload) => { console.warn(message, payload); }, }); const closedKeys = new Set(tabsToClose.map((tab) => tab.key)); setHoveredCloseTabKey((current) => (current && closedKeys.has(current) ? null : current)); }, [ bulkCloseConfirmationLabels, client, closeTab, closeWorkspaceTabWithCleanup, persistenceKey, t, ], ); 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: t("workspace.tabs.confirmations.closeTabsLeftTitle"), logLabel: "to the left", }); }, [handleBulkCloseTabs, t], ); 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: t("workspace.tabs.confirmations.closeTabsRightTitle"), logLabel: "to the right", }); }, [handleBulkCloseTabs, t], ); 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: t("workspace.tabs.confirmations.closeOtherTabsTitle"), logLabel: "from close other tabs", }); }, [handleBulkCloseTabs, t], ); 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.terminal.new": handleCreateTerminal(); 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, handleCreateTerminal, navigateToTabId, tabs, ], ); const handleWorkspaceSidebarAction = useCallback( (action: KeyboardActionDefinition): boolean => { if (action.id !== "sidebar.toggle.right") { return false; } handleToggleExplorer(); return true; }, [handleToggleExplorer], ); 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.startsWith("workspace.pane.focus.")) { const direction = parsePaneDirection(action.id); if (direction) { const adjacentPaneId = findAdjacentPane(workspaceLayout.root, focusedPane.id, direction); if (adjacentPaneId) { focusWorkspacePane(persistenceKey, adjacentPaneId); } } return true; } if (action.id.startsWith("workspace.pane.move-tab.")) { const direction = parsePaneDirection(action.id); if (direction) { const activePaneTabId = focusedPaneTabState.activeTabId; const adjacentPaneId = findAdjacentPane(workspaceLayout.root, focusedPane.id, direction); if (activePaneTabId && adjacentPaneId) { paneFocusSuppressedRef.current = true; moveWorkspaceTabToPane(persistenceKey, activePaneTabId, adjacentPaneId); requestAnimationFrame(() => { paneFocusSuppressedRef.current = false; }); } } 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", "workspace.terminal.new", ] as const, enabled: Boolean(isRouteFocused && 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(isRouteFocused && normalizedServerId && normalizedWorkspaceId), priority: 100, isActive: () => true, handle: handleWorkspacePaneAction, }); useKeyboardActionHandler({ handlerId: `workspace-sidebar-actions:${normalizedServerId}:${normalizedWorkspaceId}`, actions: ["sidebar.toggle.right"] as const, enabled: Boolean(isRouteFocused && normalizedServerId && normalizedWorkspaceId), priority: 100, isActive: () => true, handle: handleWorkspaceSidebarAction, }); const activeTabDescriptor = useMemo(() => activeTab?.descriptor ?? null, [activeTab]); const activeFileFields = getWorkspaceFileLocationFields(activeTabDescriptor); const activeFilePath = activeFileFields.path; const activeFileLineStart = activeFileFields.lineStart; const activeFileLineEnd = activeFileFields.lineEnd; const activeFileLocation = useMemo( () => buildWorkspaceFileLocation({ path: activeFilePath, lineStart: activeFileLineStart, lineEnd: activeFileLineEnd, }), [activeFileLineEnd, activeFileLineStart, activeFilePath], ); const canRenderDesktopPaneSplits = supportsDesktopPaneSplits(); const shouldRenderDesktopPaneFallback = useMemo( () => !isMobile && !canRenderDesktopPaneSplits, [isMobile, canRenderDesktopPaneSplits], ); useEffect(() => { if (!isRouteFocused || isNative || typeof document === "undefined" || activeTabDescriptor) { return; } document.title = "Workspace"; }, [activeTabDescriptor, isRouteFocused]); const buildPaneContentModel = useCallback( (input: { tab: WorkspaceTabDescriptor; paneId?: string | null; focusPaneBeforeOpen?: boolean; }) => buildWorkspacePaneContentModel({ tab: input.tab, normalizedServerId, normalizedWorkspaceId, onOpenTab: (target) => { if (!persistenceKey) { return; } if (input.focusPaneBeforeOpen && input.paneId) { focusWorkspacePane(persistenceKey, input.paneId); } const tabId = openWorkspaceChildTabFocused(persistenceKey, target, input.tab.tabId); if (tabId) { navigateToTabId(tabId); } }, onCloseCurrentTab: () => { void handleCloseTabById(input.tab.tabId); }, onRetargetCurrentTab: (target) => { if (!persistenceKey) { return; } retargetWorkspaceTab(persistenceKey, input.tab.tabId, target); }, onOpenWorkspaceFile: (request: WorkspaceFileOpenRequest) => { handleOpenWorkspaceFileFromPane({ request, paneId: input.paneId, parentTabId: input.tab.tabId, focusPaneBeforeOpen: input.focusPaneBeforeOpen, }); }, onOpenImportSheet: openImportSheet, }), [ handleCloseTabById, focusWorkspacePane, handleOpenWorkspaceFileFromPane, navigateToTabId, normalizedServerId, normalizedWorkspaceId, openImportSheet, openWorkspaceChildTabFocused, persistenceKey, retargetWorkspaceTab, ], ); const focusedPaneId = useMemo( () => focusedPaneTabState.pane?.id ?? null, [focusedPaneTabState.pane], ); const focusedPaneTabIds = useMemo(() => tabs.map((tab) => tab.tabId), [tabs]); const focusedPaneTabDescriptorMap = useStableTabDescriptorMap(tabs); const { mountedTabIds: mountedFocusedPaneTabIdsSet } = useMountedTabSet({ activeTabId, allTabIds: focusedPaneTabIds, cap: 3, }); const mountedFocusedPaneTabIds = useMemo( () => focusedPaneTabIds.filter((tabId) => mountedFocusedPaneTabIdsSet.has(tabId)), [focusedPaneTabIds, mountedFocusedPaneTabIdsSet], ); const buildMobilePaneContentModel = useCallback( function buildMobilePaneContentModel(input: { paneId: string | null; tab: WorkspaceTabDescriptor; }) { return buildPaneContentModel({ tab: input.tab, paneId: input.paneId, focusPaneBeforeOpen: false, }); }, [buildPaneContentModel], ); const content = renderWorkspaceContent({ isMissingWorkspaceDirectory, activeTabDescriptor, hasHydratedAgents, mountedFocusedPaneTabIds, focusedPaneTabDescriptorMap, isRouteFocused, focusedPaneId, buildMobilePaneContentModel, }); const buildDesktopPaneContentModel = useCallback( function buildDesktopPaneContentModel(input: { paneId: string; tab: WorkspaceTabDescriptor }) { return buildPaneContentModel({ tab: input.tab, paneId: input.paneId, focusPaneBeforeOpen: true, }); }, [buildPaneContentModel], ); const desktopTabRowItems = useMemo( () => tabs.map((tab) => ({ tab, isActive: tab.tabId === activeTabDescriptor?.tabId, isCloseHovered: hoveredCloseTabKey === tab.key, isClosingTab: closingTabIds.has(tab.tabId), })), [activeTabDescriptor?.tabId, closingTabIds, hoveredCloseTabKey, tabs], ); const handleFocusPane = useStableEvent(function handleFocusPane(paneId: string) { if (!persistenceKey || paneFocusSuppressedRef.current) { return; } focusWorkspacePane(persistenceKey, paneId); }); const handleSplitPane = useCallback( function handleSplitPane(input: { tabId: string; targetPaneId: string; position: "left" | "right" | "top" | "bottom"; }) { if (!persistenceKey) { return; } splitWorkspacePane(persistenceKey, input); }, [persistenceKey, splitWorkspacePane], ); const handleMoveTabToPane = useCallback( function handleMoveTabToPane(tabId: string, toPaneId: string) { if (!persistenceKey) { return; } moveWorkspaceTabToPane(persistenceKey, tabId, toPaneId); }, [moveWorkspaceTabToPane, persistenceKey], ); const handleResizePaneSplit = useCallback( function handleResizePaneSplit(groupId: string, sizes: number[]) { if (!persistenceKey) { return; } resizeWorkspaceSplit(persistenceKey, groupId, sizes); }, [persistenceKey, resizeWorkspaceSplit], ); const handleReorderTabsInPane = useCallback( function handleReorderTabsInPane(paneId: string, tabIds: string[]) { if (!persistenceKey) { return; } reorderWorkspaceTabsInPane(persistenceKey, paneId, tabIds); }, [persistenceKey, reorderWorkspaceTabsInPane], ); const handleReorderTabsInFocusedPane = useCallback( (nextTabs: WorkspaceTabDescriptor[]) => { if (!focusedPaneId) { return; } handleReorderTabsInPane( focusedPaneId, nextTabs.map((tab) => tab.tabId), ); }, [focusedPaneId, handleReorderTabsInPane], ); const renderSplitPaneEmptyState = useCallback( function renderSplitPaneEmptyState() { return ( {t("workspace.tabs.emptyPane")} ); }, [t], ); const containerStyle = containerWithWorkspaceBackgroundStyle; const menuNewAgentIcon = MENU_NEW_AGENT_ICON; const menuNewTerminalIcon = MENU_NEW_TERMINAL_ICON; const menuCopyIcon = MENU_COPY_ICON; const menuSettingsIcon = MENU_SETTINGS_ICON; const workspaceScreenGate = renderWorkspaceRouteGate({ state: workspaceRouteState, actions: { onRetryHost: handleRetryHost, onManageHost: handleManageHost, onDismissMissingWorkspace: handleDismissMissingWorkspace, }, }); const gatedWorkspaceScreen = renderWorkspaceScreenGateShell({ gate: workspaceScreenGate, workspaceKey: persistenceKey, }); const headerRight = useMemo( () => ( {!isMobile && workspaceDescriptor && workspaceDescriptor.scripts.length > 0 ? ( ) : null} {!isMobile && workspaceDirectory ? ( ) : null} {!isMobile && isGitCheckout ? ( <> {workspaceDirectory ? ( ) : null} {({ hovered, pressed }) => { const active = isExplorerOpen || hovered || pressed; const colorMapping = active ? foregroundColorMapping : mutedColorMapping; return ( <> {workspaceDescriptor?.diffStat ? ( ) : null} ); }} {t("workspace.tabs.explorer.toggle")} ) : null} {!isMobile && !isGitCheckout ? ( {({ hovered }) => { const colorMapping = isExplorerOpen || hovered ? foregroundColorMapping : mutedColorMapping; return ; }} ) : null} {isMobile ? ( {({ hovered }) => { const colorMapping = isExplorerOpen || hovered ? foregroundColorMapping : mutedColorMapping; return isGitCheckout ? ( ) : ( ); }} ) : null} ), [ isMobile, workspaceDescriptor, normalizedServerId, normalizedWorkspaceId, workspaceDirectory, activeFileLocation, liveTerminalIds, handleScriptTerminalStarted, handleViewScriptTerminal, handleOpenUrlInBrowserTab, showCompactButtonLabels, isGitCheckout, handleToggleExplorer, isExplorerOpen, explorerToggleLabel, explorerToggleAccessibilityState, explorerToggleStyle, t, ], ); const showScreenHeader = useMemo( () => shouldShowWorkspaceScreenHeader({ isFocusModeEnabled, isMobile }), [isFocusModeEnabled, isMobile], ); const showExplorerSidebar = useMemo( () => shouldShowWorkspaceExplorerSidebar({ isRouteFocused, isFocusModeEnabled, isMobile }), [isRouteFocused, isFocusModeEnabled, isMobile], ); const createTerminalDisabled = useMemo( () => createTerminalMutation.isPending || pendingTerminalCreateInput !== null, [createTerminalMutation.isPending, pendingTerminalCreateInput], ); const showCreateBrowserTab = getIsElectron(); const focusedPaneIdOrUndefined = useMemo(() => focusedPaneId ?? undefined, [focusedPaneId]); const desktopFocusModeEnabled = useMemo( () => isFocusModeEnabled && !isMobile, [isFocusModeEnabled, isMobile], ); const workspaceFloatingPanelPortalHostName = useMemo( () => `${WORKSPACE_FLOATING_PANEL_PORTAL_HOST_PREFIX}:${normalizedServerId}:${normalizedWorkspaceId}`, [normalizedServerId, normalizedWorkspaceId], ); const desktopSplitContent = useMemo(() => { if (!canRenderDesktopPaneSplits || !workspaceLayout || !persistenceKey) { return null; } return ( ); }, [ canRenderDesktopPaneSplits, workspaceLayout, persistenceKey, desktopFocusModeEnabled, normalizedServerId, normalizedWorkspaceId, isRouteFocused, uiTabs, hoveredCloseTabKey, closingTabIds, navigateToTabId, handleCloseTabById, handleCopyResumeCommand, handleCopyAgentId, handleCopyFilePath, handleReloadAgent, handleRenameTab, handleCloseTabsToLeftInPane, handleCloseTabsToRightInPane, handleCloseOtherTabsInPane, handleCreateDraftTab, handleCreateTerminal, handleCreateBrowserTab, showCreateBrowserTab, buildDesktopPaneContentModel, handleFocusPane, handleSplitPane, handleCreateDraftSplit, handleMoveTabToPane, handleResizePaneSplit, handleReorderTabsInPane, renderSplitPaneEmptyState, ]); const desktopContent = desktopSplitContent ?? content; const workspaceCenterColumn = ( {showScreenHeader && ( } right={headerRight} /> )} {isMobile ? ( ) : null} {shouldRenderDesktopPaneFallback ? ( ) : null} {isMobile ? ( {content} ) : ( {desktopContent} )} ); return ( gatedWorkspaceScreen ?? ( {workspaceCenterColumn} {showExplorerSidebar && workspaceDirectory ? ( ) : null} ) ); } const styles = StyleSheet.create((theme) => ({ container: { flex: 1, backgroundColor: theme.colors.surface0, }, containerWorkspaceBackground: { backgroundColor: theme.colors.surfaceWorkspace, }, 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, flexShrink: 1, minWidth: 0, flexDirection: "row", alignItems: "center", gap: { xs: theme.spacing[1], md: theme.spacing[2], }, overflow: "hidden", }, headerTitleTextGroup: { minWidth: 0, overflow: "hidden", flexShrink: 1, flexGrow: { xs: 1, md: 0, }, flexDirection: { xs: "column", md: "row", }, alignItems: { xs: "flex-start", md: "center", }, justifyContent: "flex-start", gap: { xs: 0, md: theme.spacing[2], }, }, headerProjectTitle: { color: theme.colors.foregroundMuted, fontSize: { xs: theme.fontSize.sm, md: theme.fontSize.base, }, flexShrink: 1, minWidth: 0, maxWidth: "60%", }, headerTitleSkeleton: { width: 220, maxWidth: "100%", height: 22, borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.surface3, opacity: 0.25, }, 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, }, compactHeaderActionButton: { width: theme.spacing[8], height: theme.spacing[8], padding: 0, borderRadius: theme.borderRadius.lg, alignItems: "center", justifyContent: "center", }, compactHeaderMenuCluster: { flexDirection: "row", alignItems: "center", gap: { xs: 0, md: theme.spacing[2], }, }, 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, }, 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: {}, explorerTooltipRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, explorerTooltipText: { fontSize: theme.fontSize.sm, color: theme.colors.popoverForeground, }, explorerTooltipShortcut: {}, 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, }, menuItemHint: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, }, headerMenuProfileIconWrapper: { width: 16, height: 16, }, 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, position: "relative", }, mobileMountedTabSlotVisible: { ...StyleSheet.absoluteFillObject, opacity: 1, }, mobileMountedTabSlotHidden: { ...StyleSheet.absoluteFillObject, opacity: 0, }, 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", }, })); const containerWithWorkspaceBackgroundStyle = [ styles.container, styles.containerWorkspaceBackground, ]; const EXPLORER_TOGGLE_KEYS: ShortcutKey[] = ["mod", "E"];