diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 278317243a..fc70f91232 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -1,3 +1,7 @@ +import { + AdaptiveWorkspaceChrome, + type WorkspaceCompactHeaderActions, +} from "@/screens/workspace/adaptive-workspace-chrome"; import { useWorkspaceTabDrag } from "./workspace-tab-drag-context"; import { Fragment, @@ -42,6 +46,7 @@ import type { WorkspacePaneContentModel } from "@/screens/workspace/workspace-pa import { WorkspacePanelHost } from "@/screens/workspace/workspace-panel-host"; import { WorkspaceDesktopTabsRow, + useWorkspaceNewTabKeyboardAction, type WorkspaceDesktopTabRowItem, } from "@/screens/workspace/workspace-desktop-tabs-row"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; @@ -61,6 +66,11 @@ import { isNative } from "@/constants/platform"; interface SplitContainerProps { layout: WorkspaceLayout; renderMainHeader?: () => ReactNode; + renderCompactHeader: ( + tabs: WorkspaceTabDescriptor[], + activeTabKey: string, + actions: WorkspaceCompactHeaderActions, + ) => ReactNode; workspaceKey: string; normalizedServerId: string; normalizedWorkspaceId: string; @@ -145,7 +155,6 @@ const EMPTY_SPLIT_SIZES: number[] = []; export function SplitContainer({ layout, - renderMainHeader, workspaceKey, normalizedServerId, normalizedWorkspaceId, @@ -174,6 +183,8 @@ export function SplitContainer({ onReorderTabsInPane, focusModeEnabled, onExitFocusMode, + renderMainHeader, + renderCompactHeader, }: SplitContainerProps) { const inheritedWindowChromeCorners = useWindowChromeCorners(); const windowChromeCorners = focusModeEnabled ? inheritedWindowChromeCorners : "none"; @@ -237,7 +248,6 @@ export function SplitContainer({ - {renderMainHeader?.()} {splitRoot.usesFallbackStrip && } {renderRoot ? ( ) : null} @@ -410,6 +422,8 @@ function SplitNodeView({ onTogglePaneMaximized, focusModeEnabled, onExitFocusMode, + renderMainHeader, + renderCompactHeader, }: SplitNodeViewProps) { const [groupContainerSize, setGroupContainerSize] = useState(0); const groupId = node.kind === "group" ? node.group.id : null; @@ -496,6 +510,8 @@ function SplitNodeView({ onTogglePaneMaximized={onTogglePaneMaximized} focusModeEnabled={focusModeEnabled} onExitFocusMode={onExitFocusMode} + renderMainHeader={renderMainHeader} + renderCompactHeader={renderCompactHeader} /> @@ -550,6 +566,8 @@ function SplitNodeView({ onTogglePaneMaximized={onTogglePaneMaximized} focusModeEnabled={focusModeEnabled} onExitFocusMode={onExitFocusMode} + renderMainHeader={renderMainHeader} + renderCompactHeader={renderCompactHeader} /> {index < node.group.children.length - 1 && @@ -608,6 +626,8 @@ const SplitPaneView = memo(function SplitPaneView({ onTogglePaneMaximized, focusModeEnabled, onExitFocusMode, + renderMainHeader, + renderCompactHeader, }: SplitPaneViewProps) { recordRenderProfileReasons(`SplitPaneView:${pane.id}`, ["render"]); const paneRef = useRef(null); @@ -701,6 +721,23 @@ const SplitPaneView = memo(function SplitPaneView({ () => onTogglePaneMaximized(paneId), [onTogglePaneMaximized, paneId], ); + const compactHeaderActions = useMemo( + () => ({ + closeAbove: handleCloseTabsToLeft, + closeBelow: handleCloseTabsToRight, + closeOthers: handleCloseOtherTabs, + }), + [handleCloseTabsToLeft, handleCloseTabsToRight, handleCloseOtherTabs], + ); + const createNewTab = useCallback(() => onCreateNewTab({ paneId }), [onCreateNewTab, paneId]); + useWorkspaceNewTabKeyboardAction({ + handlerName: "workspace-chrome-new-tab", + ownsKeyboardShortcuts: isFocused && isWorkspaceFocused, + normalizedServerId, + normalizedWorkspaceId, + paneId, + createNewTab, + }); return ( - - - - + + {renderMainHeader?.()} + + + + + (null); + const measure = useCallback((event: LayoutChangeEvent) => { + const measured = event.nativeEvent.layout.width; + if (measured > 0) setWidth(measured); + }, []); + return ( + + + {selectWorkspaceChrome({ width, compact: compactHeader, full: children })} + + + ); +} + +export interface WorkspaceCompactHeaderActions { + closeAbove: (key: string) => Promise | void; + closeBelow: (key: string) => Promise | void; + closeOthers: (key: string) => Promise | void; +} diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx index 9d372f0cf3..540ddd8fde 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -967,6 +967,47 @@ function TabChip({ ); } +export function useWorkspaceNewTabKeyboardAction({ + handlerName = "workspace-new-tab", + ownsKeyboardShortcuts, + normalizedServerId, + normalizedWorkspaceId, + paneId, + createNewTab, +}: { + handlerName?: string; + ownsKeyboardShortcuts: boolean; + normalizedServerId: string; + normalizedWorkspaceId: string; + paneId?: string; + createNewTab: () => void; +}) { + const handleNewTabKeyboardAction = useCallback( + (action: KeyboardActionDefinition): boolean => { + if (!ownsKeyboardShortcuts) return false; + if (action.id === "workspace.tab.menu.open") { + createNewTab(); + return true; + } + return false; + }, + [createNewTab, ownsKeyboardShortcuts], + ); + + useKeyboardActionHandler({ + handlerId: buildWorkspaceKeyboardHandlerId({ + name: handlerName, + serverId: normalizedServerId, + workspaceId: normalizedWorkspaceId, + paneId, + }), + actions: ["workspace.tab.menu.open"], + enabled: ownsKeyboardShortcuts, + priority: 200, + handle: handleNewTabKeyboardAction, + }); +} + export function WorkspaceDesktopTabsRow(props: WorkspaceDesktopTabsRowProps) { const [presentations, setPresentations] = useState( () => new Map(), @@ -1259,29 +1300,12 @@ function ResolvedWorkspaceDesktopTabsRow({ const createNewTab = useCallback(() => onCreateNewTab({ paneId }), [onCreateNewTab, paneId]); - const handleNewTabKeyboardAction = useCallback( - (action: KeyboardActionDefinition): boolean => { - if (!ownsKeyboardShortcuts) return false; - if (action.id === "workspace.tab.menu.open") { - createNewTab(); - return true; - } - return false; - }, - [createNewTab, ownsKeyboardShortcuts], - ); - - useKeyboardActionHandler({ - handlerId: buildWorkspaceKeyboardHandlerId({ - name: "workspace-new-tab", - serverId: normalizedServerId, - workspaceId: normalizedWorkspaceId, - paneId, - }), - actions: ["workspace.tab.menu.open"], - enabled: ownsKeyboardShortcuts, - priority: 200, - handle: handleNewTabKeyboardAction, + useWorkspaceNewTabKeyboardAction({ + ownsKeyboardShortcuts, + normalizedServerId, + normalizedWorkspaceId, + paneId, + createNewTab, }); const rail: WorkspaceTabRailSurface = host === "explorer" ? "sidebar" : "surface"; diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 22839d103f..92d1cb5825 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -1,4 +1,7 @@ -import { shouldUseCompactWorkspaceHeader } from "./workspace-tab-layout"; +import { + AdaptiveWorkspaceChrome, + type WorkspaceCompactHeaderActions, +} from "./adaptive-workspace-chrome"; import { planTimelineTailFetch } from "@/timeline/timeline-sync-plan"; import { useHostFeature } from "@/runtime/host-features"; import { @@ -148,7 +151,7 @@ import { } from "@/screens/workspace/use-workspace-tab-rename"; import { WorkspaceDesktopTabsRow, - WorkspaceTabMeasurementScope, + useWorkspaceNewTabKeyboardAction, type WorkspaceDesktopTabRowItem, } from "@/screens/workspace/workspace-desktop-tabs-row"; import { useDesktopBrowserNewTabRequests } from "@/desktop/browser/new-tab-requests"; @@ -788,27 +791,6 @@ function WorkspaceContentProviders({ ); } -function AdaptiveWorkspaceChrome({ - compactHeader, - children, -}: { - compactHeader: ReactNode; - children: ReactNode; -}) { - const [width, setWidth] = useState(null); - const measure = useCallback( - (event: LayoutChangeEvent) => setWidth(event.nativeEvent.layout.width), - [], - ); - return ( - - - {width !== null && shouldUseCompactWorkspaceHeader(width) ? compactHeader : children} - - - ); -} - function WorkspacePanelSurface({ launcher, children, @@ -3384,69 +3366,71 @@ function WorkspaceScreenContent({ handleOpenUrlInBrowserTab, ], ); - const compactWorkspaceHeader = useMemo( - () => ( - - - - - ), + const renderCompactWorkspaceHeader = useCallback( + ( + headerTabs: WorkspaceTabDescriptor[], + headerActiveTabKey: string, + actions: WorkspaceCompactHeaderActions, + ) => { + return ( + + + + + ); + }, [ - activeTabKey, canOpenImportSheet, compactHeaderScripts, createTerminalDisabled, currentBranchName, explorerSidebarToggleAccessibilityState, explorerSidebarToggleLabel, - handleCloseOtherTabs, handleCloseTabById, - handleCloseTabsToLeft, - handleCloseTabsToRight, handleCopyAgentId, handleCopyBranchName, handleCopyFilePath, @@ -3469,12 +3453,16 @@ function WorkspaceScreenContent({ openImportSheet, showCreateBrowserTab, showWorkspaceSetup, - tabs, workspaceDirectory, workspaceHeaderSubtitle, workspaceHeaderTitle, ], ); + const compactWorkspaceHeader = renderCompactWorkspaceHeader(tabs, activeTabKey, { + closeAbove: handleCloseTabsToLeft, + closeBelow: handleCloseTabsToRight, + closeOthers: handleCloseOtherTabs, + }); const desktopSplitContent = useMemo(() => { if (!canRenderDesktopPaneSplits || !workspaceLayout || !persistenceKey) { return null; @@ -3483,6 +3471,7 @@ function WorkspaceScreenContent({ handleCreateNewTab({ paneId: focusedPaneIdOrUndefined }), + [handleCreateNewTab, focusedPaneIdOrUndefined], + ); + const ownsFallbackKeyboardShortcuts = useMemo( + () => shouldRenderDesktopPaneFallback && isRouteFocused, + [shouldRenderDesktopPaneFallback, isRouteFocused], + ); + useWorkspaceNewTabKeyboardAction({ + handlerName: "workspace-chrome-new-tab", + ownsKeyboardShortcuts: ownsFallbackKeyboardShortcuts, + normalizedServerId, + normalizedWorkspaceId, + paneId: focusedPaneIdOrUndefined, + createNewTab: createFallbackTab, + }); const compactHeaderLayout = resolveCompactHeaderLayout({ header: compactWorkspaceHeader, activeTab: activeTabDescriptor, @@ -3580,37 +3586,40 @@ function WorkspaceScreenContent({ ) : ( - - {rendersDesktopSplitContent ? null : renderWorkspaceScreenHeader()} + {rendersDesktopSplitContent ? null : ( + + {renderWorkspaceScreenHeader()} - {shouldRenderDesktopPaneFallback ? ( - - - - ) : null} - + {shouldRenderDesktopPaneFallback ? ( + + + + ) : null} + + )} {desktopContent} diff --git a/packages/app/src/screens/workspace/workspace-tab-layout.test.ts b/packages/app/src/screens/workspace/workspace-tab-layout.test.ts index 1ba6e05f01..afc5275a46 100644 --- a/packages/app/src/screens/workspace/workspace-tab-layout.test.ts +++ b/packages/app/src/screens/workspace/workspace-tab-layout.test.ts @@ -7,6 +7,7 @@ import { SidebarCloseButton } from "@/components/headers/sidebar-close-button"; import { resolveWorkspaceTabChipBackdrop } from "@/screens/workspace/workspace-tab-chip-backdrop"; import { darkPureBlackTheme, darkTheme, lightTheme } from "@/styles/theme"; import { + selectWorkspaceChrome, computeWorkspaceTabLayout, retainWorkspaceTabMeasuredWidth, } from "@/screens/workspace/workspace-tab-layout"; @@ -266,3 +267,14 @@ it.each([ ])("workspace width %s selects compact=%s", (width, compact) => { expect(shouldUseCompactWorkspaceHeader(width)).toBe(compact); }); + +describe("desktop pane chrome", () => { + it("replaces the pane header and rail together at the pane width boundary", () => { + const full = ["workspace header", "pane tab rail"]; + expect(selectWorkspaceChrome({ width: 440, compact: "pane dropdown", full })).toBe( + "pane dropdown", + ); + expect(selectWorkspaceChrome({ width: 500, compact: "pane dropdown", full })).toBe(full); + expect(selectWorkspaceChrome({ width: null, compact: "pane dropdown", full })).toBe(full); + }); +}); diff --git a/packages/app/src/screens/workspace/workspace-tab-layout.ts b/packages/app/src/screens/workspace/workspace-tab-layout.ts index 94aad3c1b6..137a6f7b7b 100644 --- a/packages/app/src/screens/workspace/workspace-tab-layout.ts +++ b/packages/app/src/screens/workspace/workspace-tab-layout.ts @@ -126,3 +126,12 @@ export function computeWorkspaceTabLayout( requiresHorizontalScrollFallback, }; } + +export function selectWorkspaceChrome(input: { + width: number | null; + compact: Compact; + full: Full; +}): Compact | Full { + const compact = input.width !== null && shouldUseCompactWorkspaceHeader(input.width); + return compact ? input.compact : input.full; +}