fix(app): choose workspace header by measured column width

Co-authored-by: Elsa Granger <zeyugao@outlook.com>
This commit is contained in:
Mohamed Boudra
2026-11-04 07:15:43 +01:00
co-authored by Elsa Granger
parent fced746a27
commit 3e6f91dc7b
4 changed files with 135 additions and 84 deletions
@@ -1,6 +1,8 @@
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import React, {
Fragment,
createContext,
useContext,
useCallback,
useEffect,
useLayoutEffect,
@@ -485,6 +487,13 @@ interface WorkspaceTabLabelMeasurement {
width: number;
}
const TabMeasurementCache = createContext<Map<string, WorkspaceTabLabelMeasurement> | null>(null);
export function WorkspaceTabMeasurementScope({ children }: { children: React.ReactNode }) {
const cache = useMemo(() => new Map<string, WorkspaceTabLabelMeasurement>(), []);
return <TabMeasurementCache.Provider value={cache}>{children}</TabMeasurementCache.Provider>;
}
interface WorkspaceTabTrackSnapshot {
signature: string;
tabs: ResolvedWorkspaceDesktopTabRowItem[];
@@ -1059,9 +1068,13 @@ function ResolvedWorkspaceDesktopTabsRow({
const tabsScrollRef = useRef<ScrollView>(null);
const [exitFocusModeWidth, setExitFocusModeWidth] = useState<number>(0);
const tabScrollBoundary = useHorizontalScrollBoundary();
const [labelMeasurements, setLabelMeasurements] = useState(
() => new Map<string, WorkspaceTabLabelMeasurement>(),
);
const measurementCache = useContext(TabMeasurementCache);
const [labelMeasurements, setLabelMeasurements] = useState(() => new Map(measurementCache ?? []));
useLayoutEffect(() => {
if (!measurementCache) return;
measurementCache.clear();
for (const [key, value] of labelMeasurements) measurementCache.set(key, value);
}, [measurementCache, labelMeasurements]);
const [trackSnapshot, setTrackSnapshot] = useState<WorkspaceTabTrackSnapshot | null>(null);
const handleTabsContainerLayout = useCallback((event: LayoutChangeEvent) => {
@@ -1,3 +1,4 @@
import { shouldUseCompactWorkspaceHeader } from "./workspace-tab-layout";
import { planTimelineTailFetch } from "@/timeline/timeline-sync-plan";
import { useHostFeature } from "@/runtime/host-features";
import {
@@ -147,6 +148,7 @@ import {
} from "@/screens/workspace/use-workspace-tab-rename";
import {
WorkspaceDesktopTabsRow,
WorkspaceTabMeasurementScope,
type WorkspaceDesktopTabRowItem,
} from "@/screens/workspace/workspace-desktop-tabs-row";
import { useDesktopBrowserNewTabRequests } from "@/desktop/browser/new-tab-requests";
@@ -786,6 +788,27 @@ function WorkspaceContentProviders({
);
}
function AdaptiveWorkspaceChrome({
compactHeader,
children,
}: {
compactHeader: ReactNode;
children: ReactNode;
}) {
const [width, setWidth] = useState<number | null>(null);
const measure = useCallback(
(event: LayoutChangeEvent) => setWidth(event.nativeEvent.layout.width),
[],
);
return (
<WorkspaceTabMeasurementScope>
<View onLayout={measure}>
{width !== null && shouldUseCompactWorkspaceHeader(width) ? compactHeader : children}
</View>
</WorkspaceTabMeasurementScope>
);
}
function WorkspacePanelSurface({
launcher,
children,
@@ -3362,57 +3385,56 @@ function WorkspaceScreenContent({
],
);
const compactWorkspaceHeader = useMemo(
() =>
isMobile ? (
<CompactWorkspaceHeader
isLoading={isWorkspaceHeaderLoading}
title={workspaceHeaderTitle}
subtitle={workspaceHeaderSubtitle}
isSubtitleDistinct={isWorkspaceHeaderSubtitleDistinct}
serverId={normalizedServerId}
onToggleExplorer={handleToggleExplorerSidebar}
explorerToggleLabel={explorerSidebarToggleLabel}
explorerToggleAccessibilityState={explorerSidebarToggleAccessibilityState}
>
<MobileWorkspaceTabSwitcher
tabs={tabs}
activeTabKey={activeTabKey}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
onSelectSwitcherTab={handleSelectSwitcherTab}
createTerminalDisabled={createTerminalDisabled}
onCreateDraftTab={handleCreateDraftTab}
onCreateTerminal={handleCreateTerminal}
onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId}
onCopyTerminalId={handleCopyTerminalId}
onCopyFilePath={handleCopyFilePath}
onReloadAgent={handleReloadAgent}
onRenameTab={handleRenameTab}
onCloseTab={handleCloseTabById}
onCloseTabsAbove={handleCloseTabsToLeft}
onCloseTabsBelow={handleCloseTabsToRight}
onCloseOtherTabs={handleCloseOtherTabs}
/>
<WorkspaceHeaderMenuMobile
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
scripts={compactHeaderScripts}
currentBranchName={currentBranchName}
showWorkspaceSetup={showWorkspaceSetup}
showCreateBrowserTab={showCreateBrowserTab}
createTerminalDisabled={createTerminalDisabled}
importAgentDisabled={!canOpenImportSheet}
copyPathDisabled={!workspaceDirectory}
onCreateTerminalWithProfile={handleCreateTerminalWithProfile}
onCreateBrowser={handleCreateBrowserTab}
onOpenImportSheet={openImportSheet}
onCopyWorkspacePath={handleCopyWorkspacePath}
onCopyBranchName={handleCopyBranchName}
onOpenSetupTab={handleOpenSetupTab}
/>
</CompactWorkspaceHeader>
) : null,
() => (
<CompactWorkspaceHeader
isLoading={isWorkspaceHeaderLoading}
title={workspaceHeaderTitle}
subtitle={workspaceHeaderSubtitle}
isSubtitleDistinct={isWorkspaceHeaderSubtitleDistinct}
serverId={normalizedServerId}
onToggleExplorer={handleToggleExplorerSidebar}
explorerToggleLabel={explorerSidebarToggleLabel}
explorerToggleAccessibilityState={explorerSidebarToggleAccessibilityState}
>
<MobileWorkspaceTabSwitcher
tabs={tabs}
activeTabKey={activeTabKey}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
onSelectSwitcherTab={handleSelectSwitcherTab}
createTerminalDisabled={createTerminalDisabled}
onCreateDraftTab={handleCreateDraftTab}
onCreateTerminal={handleCreateTerminal}
onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId}
onCopyTerminalId={handleCopyTerminalId}
onCopyFilePath={handleCopyFilePath}
onReloadAgent={handleReloadAgent}
onRenameTab={handleRenameTab}
onCloseTab={handleCloseTabById}
onCloseTabsAbove={handleCloseTabsToLeft}
onCloseTabsBelow={handleCloseTabsToRight}
onCloseOtherTabs={handleCloseOtherTabs}
/>
<WorkspaceHeaderMenuMobile
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
scripts={compactHeaderScripts}
currentBranchName={currentBranchName}
showWorkspaceSetup={showWorkspaceSetup}
showCreateBrowserTab={showCreateBrowserTab}
createTerminalDisabled={createTerminalDisabled}
importAgentDisabled={!canOpenImportSheet}
copyPathDisabled={!workspaceDirectory}
onCreateTerminalWithProfile={handleCreateTerminalWithProfile}
onCreateBrowser={handleCreateBrowserTab}
onOpenImportSheet={openImportSheet}
onCopyWorkspacePath={handleCopyWorkspacePath}
onCopyBranchName={handleCopyBranchName}
onOpenSetupTab={handleOpenSetupTab}
/>
</CompactWorkspaceHeader>
),
[
activeTabKey,
canOpenImportSheet,
@@ -3440,7 +3462,6 @@ function WorkspaceScreenContent({
handleRenameTab,
handleSelectSwitcherTab,
handleToggleExplorerSidebar,
isMobile,
isWorkspaceHeaderLoading,
isWorkspaceHeaderSubtitleDistinct,
normalizedServerId,
@@ -3559,36 +3580,37 @@ function WorkspaceScreenContent({
</View>
) : (
<View style={styles.centerColumn}>
{rendersDesktopSplitContent ? null : renderWorkspaceScreenHeader()}
{shouldRenderDesktopPaneFallback ? (
<NewTabLauncherProvider value={newTabLauncher}>
<WorkspaceDesktopTabsRow
paneId={focusedPaneIdOrUndefined}
isFocused={isRouteFocused}
tabs={desktopTabRowItems}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
setHoveredCloseTabKey={setHoveredCloseTabKey}
onNavigateTab={navigateToTabId}
onCloseTab={handleCloseTabById}
onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId}
onCopyTerminalId={handleCopyTerminalId}
onCopyFilePath={handleCopyFilePath}
onReloadAgent={handleReloadAgent}
onRenameTab={handleRenameTab}
onCloseTabsToLeft={handleCloseTabsToLeft}
onCloseTabsToRight={handleCloseTabsToRight}
onCloseOtherTabs={handleCloseOtherTabs}
onCreateNewTab={handleCreateNewTab}
onReorderTabs={handleReorderTabsInFocusedPane}
focusModeEnabled={desktopFocusModeEnabled}
onExitFocusMode={toggleFocusMode}
/>
</NewTabLauncherProvider>
) : null}
<AdaptiveWorkspaceChrome compactHeader={compactWorkspaceHeader}>
{rendersDesktopSplitContent ? null : renderWorkspaceScreenHeader()}
{shouldRenderDesktopPaneFallback ? (
<NewTabLauncherProvider value={newTabLauncher}>
<WorkspaceDesktopTabsRow
paneId={focusedPaneIdOrUndefined}
isFocused={isRouteFocused}
tabs={desktopTabRowItems}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
setHoveredCloseTabKey={setHoveredCloseTabKey}
onNavigateTab={navigateToTabId}
onCloseTab={handleCloseTabById}
onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId}
onCopyTerminalId={handleCopyTerminalId}
onCopyFilePath={handleCopyFilePath}
onReloadAgent={handleReloadAgent}
onRenameTab={handleRenameTab}
onCloseTabsToLeft={handleCloseTabsToLeft}
onCloseTabsToRight={handleCloseTabsToRight}
onCloseOtherTabs={handleCloseOtherTabs}
onCreateNewTab={handleCreateNewTab}
onReorderTabs={handleReorderTabsInFocusedPane}
focusModeEnabled={desktopFocusModeEnabled}
onExitFocusMode={toggleFocusMode}
/>
</NewTabLauncherProvider>
) : null}
</AdaptiveWorkspaceChrome>
<View style={styles.centerContent}>
<View style={styles.content}>{desktopContent}</View>
</View>
@@ -256,3 +256,13 @@ describe("resolveWorkspaceTabChipBackdrop", () => {
).toBe("surface0");
});
});
import { shouldUseCompactWorkspaceHeader } from "./workspace-tab-layout";
it.each([
[349, true],
[499, true],
[500, false],
[800, false],
])("workspace width %s selects compact=%s", (width, compact) => {
expect(shouldUseCompactWorkspaceHeader(width)).toBe(compact);
});
@@ -1,3 +1,9 @@
import { COMPACT_CONTENT_WIDTH } from "@/constants/layout";
export function shouldUseCompactWorkspaceHeader(width: number): boolean {
return width < COMPACT_CONTENT_WIDTH;
}
export type WorkspaceTabCloseButtonPolicy = "all" | "none";
export interface WorkspaceTabLayoutMetrics {