diff --git a/packages/app/src/components/command-center.tsx b/packages/app/src/components/command-center.tsx index c329e6fd4..0ba00a273 100644 --- a/packages/app/src/components/command-center.tsx +++ b/packages/app/src/components/command-center.tsx @@ -7,7 +7,7 @@ import { View, Platform, } from "react-native"; -import { memo, useEffect, useMemo, useRef, type ReactNode } from "react"; +import { memo, useEffect, useRef, type ReactNode } from "react"; import { Plus, Settings } from "lucide-react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useCommandCenter } from "@/hooks/use-command-center"; @@ -69,6 +69,9 @@ export function CommandCenter() { const resultsRef = useRef(null); useEffect(() => { + if (!open) { + return; + } const row = rowRefs.current.get(activeIndex); if (!row || typeof document === "undefined") { return; @@ -99,18 +102,12 @@ export function CommandCenter() { if (rowBottom > visibleBottom) { scrollNode.scrollTop = rowBottom - scrollNode.clientHeight; } - }, [activeIndex]); + }, [activeIndex, open]); - if (Platform.OS !== "web") return null; + if (Platform.OS !== "web" || !open) return null; - const actionItems = useMemo( - () => items.filter((item) => item.kind === "action"), - [items] - ); - const agentItems = useMemo( - () => items.filter((item) => item.kind === "agent"), - [items] - ); + const actionItems = items.filter((item) => item.kind === "action"); + const agentItems = items.filter((item) => item.kind === "agent"); return ( { + onFocusPane(pane.id); + }} onPointerDownCapture={() => { onFocusPane(pane.id); }} @@ -769,7 +772,9 @@ const styles = StyleSheet.create((theme) => ({ backgroundColor: theme.colors.surface0, overflow: "hidden", }, - paneTabs: {}, + paneTabs: { + minWidth: 0, + }, paneContent: { position: "relative", flex: 1, diff --git a/packages/app/src/hooks/use-command-center.ts b/packages/app/src/hooks/use-command-center.ts index f220550fa..46a31a6b8 100644 --- a/packages/app/src/hooks/use-command-center.ts +++ b/packages/app/src/hooks/use-command-center.ts @@ -20,6 +20,10 @@ import { import type { ShortcutKey } from "@/utils/format-shortcut"; import { focusWithRetries } from "@/utils/web-focus"; +const EMPTY_AGENTS: AggregatedAgent[] = []; +const EMPTY_ACTION_ITEMS: CommandCenterActionItem[] = []; +const EMPTY_COMMAND_CENTER_ITEMS: CommandCenterItem[] = []; + function isMatch(agent: AggregatedAgent, query: string): boolean { if (!query) return true; const q = query.toLowerCase(); @@ -118,6 +122,9 @@ export function useCommandCenter() { const [activeIndex, setActiveIndex] = useState(0); const activeServerId = useMemo(() => { + if (!open) { + return null; + } const serverIdFromPath = parseServerIdFromPathname(pathname); if (serverIdFromPath) { const routeMatch = daemons.find((entry) => entry.serverId === serverIdFromPath); @@ -126,17 +133,20 @@ export function useCommandCenter() { } } return daemons[0]?.serverId ?? null; - }, [daemons, pathname]); + }, [daemons, open, pathname]); const { agents } = useAllAgentsList({ serverId: activeServerId, }); const agentResults = useMemo(() => { + if (!open || agents.length === 0) { + return EMPTY_AGENTS; + } const filtered = agents.filter((agent) => isMatch(agent, query)); filtered.sort(sortAgents); return filtered; - }, [agents, query]); + }, [agents, open, query]); const newAgentRoute = useMemo(() => { const serverIdFromPath = activeServerId; @@ -149,6 +159,9 @@ export function useCommandCenter() { }, [activeServerId]); const actionItems = useMemo(() => { + if (!open) { + return EMPTY_ACTION_ITEMS; + } return COMMAND_CENTER_ACTIONS.filter((action) => matchesActionQuery(query, action) ).map((action) => ({ @@ -159,9 +172,12 @@ export function useCommandCenter() { route: action.buildRoute({ newAgentRoute, settingsRoute }), shortcutKeys: action.shortcutKeys, })); - }, [newAgentRoute, query, settingsRoute]); + }, [newAgentRoute, open, query, settingsRoute]); const items = useMemo(() => { + if (!open) { + return EMPTY_COMMAND_CENTER_ITEMS; + } const next: CommandCenterItem[] = []; for (const action of actionItems) { next.push({ @@ -176,7 +192,7 @@ export function useCommandCenter() { }); } return next; - }, [actionItems, agentResults]); + }, [actionItems, agentResults, open]); const handleClose = useCallback(() => { setOpen(false); 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 e9de5a4f3..70ce09272 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -513,6 +513,7 @@ function ResolvedDesktopTabChip({ const styles = StyleSheet.create((theme) => ({ tabsContainer: { + minWidth: 0, borderBottomWidth: 1, borderBottomColor: theme.colors.border, backgroundColor: theme.colors.surface0, @@ -523,8 +524,7 @@ const styles = StyleSheet.create((theme) => ({ minWidth: 0, }, tabsScrollFitContent: { - flexGrow: 0, - flexShrink: 1, + flex: 1, }, tabsScrollOverflow: { flex: 1, diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index ced9b3f7c..267fcecdf 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -2008,12 +2008,12 @@ function WorkspaceScreenContent({ onSelectNewTabOption={handleSelectNewTabOption} newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID} buildPaneContentModel={({ paneId, tab }) => - buildPaneContentModel({ - tab, - paneId, - focusPaneBeforeOpen: true, - }) - } + buildPaneContentModel({ + tab, + paneId, + focusPaneBeforeOpen: true, + }) + } onFocusPane={(paneId) => { focusWorkspacePane(persistenceKey, paneId); }} 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 1c9c0f3d9..22bdace19 100644 --- a/packages/app/src/screens/workspace/workspace-tab-layout.test.ts +++ b/packages/app/src/screens/workspace/workspace-tab-layout.test.ts @@ -14,7 +14,7 @@ const metrics = { }; describe("computeWorkspaceTabLayout", () => { - it("keeps fit-content tab widths when there is extra horizontal space", () => { + it("caps equal-width tabs at the ideal width when there is extra horizontal space", () => { const result = computeWorkspaceTabLayout({ viewportWidth: 1200, tabLabelLengths: [8, 10, 7], @@ -25,10 +25,10 @@ describe("computeWorkspaceTabLayout", () => { expect(result.requiresHorizontalScrollFallback).toBe(false); expect(result.items).toHaveLength(3); expect(result.items.every((item) => item.showLabel)).toBe(true); - expect(result.items.map((item) => item.width)).toEqual([126, 140, 119]); + expect(result.items.map((item) => item.width)).toEqual([200, 200, 200]); }); - it("shrinks tab widths proportionally before hiding labels", () => { + it("shrinks equal-width tabs proportionally to fit the pane", () => { const result = computeWorkspaceTabLayout({ viewportWidth: 520, tabLabelLengths: [24, 12, 8], @@ -37,10 +37,27 @@ describe("computeWorkspaceTabLayout", () => { expect(result.closeButtonPolicy).toBe("all"); expect(result.requiresHorizontalScrollFallback).toBe(false); - expect(result.items.map((item) => item.width)).toEqual([151, 121, 103]); + expect(result.items.map((item) => item.width)).toEqual([125, 125, 125]); expect(result.items.every((item) => item.showLabel)).toBe(true); }); + it("uses the split width for evenly sized tabs when space is available", () => { + const result = computeWorkspaceTabLayout({ + viewportWidth: 743, + tabLabelLengths: [8, 8, 8, 8], + metrics: { + ...metrics, + actionsReservedWidth: 44, + rowPaddingHorizontal: 0, + tabGap: 0, + }, + }); + + expect(result.closeButtonPolicy).toBe("all"); + expect(result.requiresHorizontalScrollFallback).toBe(false); + expect(result.items.map((item) => item.width)).toEqual([175, 175, 175, 175]); + }); + it("collapses to icon-only before allowing horizontal scroll fallback", () => { const result = computeWorkspaceTabLayout({ viewportWidth: 388, diff --git a/packages/app/src/screens/workspace/workspace-tab-layout.ts b/packages/app/src/screens/workspace/workspace-tab-layout.ts index 28c99dc06..6ee7bc9f6 100644 --- a/packages/app/src/screens/workspace/workspace-tab-layout.ts +++ b/packages/app/src/screens/workspace/workspace-tab-layout.ts @@ -38,14 +38,6 @@ function clamp(value: number, min: number, max: number): number { return value; } -function sum(values: number[]): number { - let total = 0; - for (const value of values) { - total += value; - } - return total; -} - export function computeWorkspaceTabLayout( input: WorkspaceTabLayoutInput ): WorkspaceTabLayoutResult { @@ -67,33 +59,13 @@ export function computeWorkspaceTabLayout( const availableTabsWidth = Math.max(0, availableWidth - rowOverhead); const iconOnlyTabWidth = input.metrics.tabIconWidth + input.metrics.tabHorizontalPadding * 2 + input.metrics.closeButtonWidth; - const labelSafetyPadding = 10; - const estimateLabelWidth = (labelLength: number) => labelLength * input.metrics.estimatedCharWidth; - const desiredTabWidths = input.tabLabelLengths.map((rawLength) => { - const labelLength = Math.max(rawLength, 1); - const estimatedWidth = iconOnlyTabWidth + estimateLabelWidth(labelLength) + labelSafetyPadding; - return clamp(estimatedWidth, iconOnlyTabWidth, input.metrics.maxTabWidth); - }); - - const desiredTotalTabsWidth = sum(desiredTabWidths); const iconOnlyTotalTabsWidth = iconOnlyTabWidth * tabCount; const requiresHorizontalScrollFallback = availableTabsWidth < iconOnlyTotalTabsWidth; - - let resolvedWidths: number[]; - if (desiredTotalTabsWidth <= availableTabsWidth) { - // Fit-content baseline: do not stretch tabs when there is spare space. - resolvedWidths = desiredTabWidths; - } else if (availableTabsWidth <= iconOnlyTotalTabsWidth) { - resolvedWidths = new Array(tabCount).fill(iconOnlyTabWidth); - } else { - const desiredExpandableWidth = desiredTotalTabsWidth - iconOnlyTotalTabsWidth; - const availableExpandableWidth = availableTabsWidth - iconOnlyTotalTabsWidth; - const proportionalRatio = - desiredExpandableWidth > 0 ? availableExpandableWidth / desiredExpandableWidth : 0; - resolvedWidths = desiredTabWidths.map( - (desiredWidth) => iconOnlyTabWidth + (desiredWidth - iconOnlyTabWidth) * proportionalRatio - ); - } + const resolvedWidths = new Array(tabCount).fill( + requiresHorizontalScrollFallback + ? iconOnlyTabWidth + : clamp(availableTabsWidth / tabCount, iconOnlyTabWidth, input.metrics.maxTabWidth) + ); const roundedWidths = resolvedWidths.map((width) => Math.round(clamp(width, iconOnlyTabWidth, input.metrics.maxTabWidth))