From 7703625e76f4bd92c394e049b0cad4236d3b3a85 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 21 Mar 2026 11:38:52 +0700 Subject: [PATCH] feat(app): redesign mobile tab switcher as secondary header row Move tab switcher from bottom to top (after header), restyle as a clean pressable row with chevron instead of count badge. Add "New agent" and "New terminal" to header kebab menu. Fix BottomSheet portal crash by nesting BottomSheetModalProvider inside QueryProvider. --- packages/app/src/app/_layout.tsx | 8 +- .../app/src/components/split-container.tsx | 4 +- .../workspace/workspace-desktop-tabs-row.tsx | 2 +- .../screens/workspace/workspace-screen.tsx | 116 ++++++------------ 4 files changed, 47 insertions(+), 83 deletions(-) diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 8e0840052..fff114c09 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -616,8 +616,8 @@ export default function RootLayout() { - - + + @@ -656,8 +656,8 @@ export default function RootLayout() { - - + + diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 23215f177..9ecc93c84 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -67,9 +67,9 @@ interface SplitContainerProps { onCloseTabsToLeft: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise | void; onCloseTabsToRight: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise | void; onCloseOtherTabs: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise | void; - onSelectNewTabOption: (selection: { optionId: "__new_tab_agent__"; paneId?: string }) => void; + onSelectNewTabOption: (selection: { optionId: "__new_tab_agent__" | "__new_tab_terminal__"; paneId?: string }) => void; onNewTerminalTab: (input: { paneId?: string }) => void; - newTabAgentOptionId?: "__new_tab_agent__"; + newTabAgentOptionId?: "__new_tab_agent__" | "__new_tab_terminal__"; buildPaneContentModel: (input: { paneId: string; isPaneFocused: boolean; 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 6370cef31..2fd44bd6f 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -27,7 +27,7 @@ import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs- const DROPDOWN_WIDTH = 220; const LOADING_TAB_LABEL_SKELETON_WIDTH = 80; -type NewTabOptionId = "__new_tab_agent__"; +type NewTabOptionId = "__new_tab_agent__" | "__new_tab_terminal__"; type NewTabSelection = { optionId: NewTabOptionId; paneId?: string; diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index fbecfc0f7..df1ef8540 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -22,6 +22,8 @@ import { SquareTerminal, } from "lucide-react-native"; import { GestureDetector } from "react-native-gesture-handler"; +import Animated from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import invariant from "tiny-invariant"; import { SidebarMenuToggle } from "@/components/headers/menu-header"; @@ -115,6 +117,8 @@ import { findAdjacentPane } from "@/utils/split-navigation"; const TERMINALS_QUERY_STALE_TIME = 5_000; const NEW_TAB_AGENT_OPTION_ID = "__new_tab_agent__"; +const NEW_TAB_TERMINAL_OPTION_ID = "__new_tab_terminal__"; +type NewTabOptionId = typeof NEW_TAB_AGENT_OPTION_ID | typeof NEW_TAB_TERMINAL_OPTION_ID; const EMPTY_UI_TABS: WorkspaceTab[] = []; type WorkspaceScreenProps = { @@ -217,10 +221,6 @@ type MobileWorkspaceTabSwitcherProps = { normalizedServerId: string; normalizedWorkspaceId: string; onSelectSwitcherTab: (key: string) => void; - onSelectNewTabOption: (selection: { - optionId: typeof NEW_TAB_AGENT_OPTION_ID; - paneId?: string; - }) => void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; onCloseTab: (tabId: string) => Promise | void; @@ -415,7 +415,6 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({ normalizedServerId, normalizedWorkspaceId, onSelectSwitcherTab, - onSelectNewTabOption, onCopyResumeCommand, onCopyAgentId, onCloseTab, @@ -435,21 +434,18 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({ }, [tabs]); return ( - + [ + accessibilityRole="button" + accessibilityLabel={`Switch tabs (${tabs.length} open)`} + style={({ pressed }) => [ styles.switcherTrigger, - (hovered || pressed || isOpen) && styles.switcherTriggerActive, - { borderWidth: 0, borderColor: "transparent" }, - Platform.OS === "web" - ? { - outlineStyle: "solid", - outlineWidth: 0, - outlineColor: "transparent", - } - : null, + pressed && styles.switcherTriggerPressed, ]} onPress={() => setIsOpen(true)} > @@ -460,37 +456,9 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({ normalizedWorkspaceId={normalizedWorkspaceId} /> - - - - - onSelectNewTabOption({ - optionId: NEW_TAB_AGENT_OPTION_ID, - }) - } - accessibilityRole="button" - accessibilityLabel="New agent tab" - style={({ hovered, pressed }) => [ - styles.newTabActionButton, - (hovered || pressed) && styles.newTabActionButtonHovered, - ]} - > - - - - - New agent tab - - - - - - { + (selection: { optionId: NewTabOptionId; paneId?: string }) => { + if (selection.paneId && persistenceKey) { + focusWorkspacePane(persistenceKey, selection.paneId); + } if (selection.optionId === NEW_TAB_AGENT_OPTION_ID) { - if (selection.paneId && persistenceKey) { - focusWorkspacePane(persistenceKey, selection.paneId); - } handleCreateDraftTab(); + } else if (selection.optionId === NEW_TAB_TERMINAL_OPTION_ID) { + handleCreateTerminal({ paneId: selection.paneId }); } }, - [focusWorkspacePane, handleCreateDraftTab, persistenceKey] + [focusWorkspacePane, handleCreateDraftTab, handleCreateTerminal, persistenceKey] ); const handleCreateDraftSplit = useCallback( @@ -1962,6 +1933,18 @@ function WorkspaceScreenContent({ width={220} testID="workspace-header-menu" > + + } + onSelect={handleCreateDraftTab} + > + New agent + - ) : ( - null - )} + ) : null} {isMobile ? ( @@ -2323,35 +2303,19 @@ const styles = StyleSheet.create((theme) => ({ borderColor: theme.colors.borderAccent, }, mobileTabsRow: { - borderBottomWidth: 1, - borderBottomColor: theme.colors.border, backgroundColor: theme.colors.surface0, - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], - paddingHorizontal: theme.spacing[2], - paddingVertical: theme.spacing[1], - }, - mobileTabsActions: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[1], + borderBottomWidth: theme.borderWidth[1], + borderBottomColor: theme.colors.border, }, switcherTrigger: { flexDirection: "row", alignItems: "center", - gap: theme.spacing[1], - flex: 1, - minWidth: 0, - paddingHorizontal: theme.spacing[2], - paddingVertical: theme.spacing[1], - borderRadius: theme.borderRadius.md, - borderWidth: theme.borderWidth[1], - borderColor: theme.colors.border, - justifyContent: "space-between", + gap: theme.spacing[2], + paddingHorizontal: theme.spacing[2] + theme.spacing[3], + paddingVertical: theme.spacing[2], }, - switcherTriggerActive: { - backgroundColor: theme.colors.surface2, + switcherTriggerPressed: { + backgroundColor: theme.colors.surface1, }, switcherTriggerLeft: { flexDirection: "row",