From 5ddd5f3726782ef6793b6cfd304aab0097b089c0 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 15 Jul 2026 11:54:59 +0200 Subject: [PATCH] perf(app): keep desktop sidebar ready to reopen Retain the hidden sidebar tree so common toggles preserve local UI state while existing activity gates pause expensive hidden work. --- docs/mobile-panels.md | 3 ++ packages/app/e2e/sidebar-workspace.spec.ts | 41 ++++++++++++++++++++ packages/app/src/app/_layout.tsx | 22 ++++++----- packages/app/src/components/left-sidebar.tsx | 39 +++++++++++-------- 4 files changed, 79 insertions(+), 26 deletions(-) diff --git a/docs/mobile-panels.md b/docs/mobile-panels.md index 46c385d20..28ee87f1d 100644 --- a/docs/mobile-panels.md +++ b/docs/mobile-panels.md @@ -75,6 +75,9 @@ definition, no longer eligible to begin. so its injected `collapsable={false}` reaches Android/Fabric. - Mobile sidebars render through `MobilePanelOverlay`; do not duplicate overlay lifecycle or motion styles in sidebar components. +- The desktop left sidebar is retained too. App chrome owns separate mounted and visible decisions: + closing it or yielding its width marks it inactive and applies `display: none` without conditionally + removing the sidebar tree. - Animated panel nodes use React Native static styles plus inline theme values. Do not attach Unistyles-generated styles to those nodes; Unistyles and Reanimated patching the same Fabric node has caused native crashes. diff --git a/packages/app/e2e/sidebar-workspace.spec.ts b/packages/app/e2e/sidebar-workspace.spec.ts index 2fd0f5868..d23085852 100644 --- a/packages/app/e2e/sidebar-workspace.spec.ts +++ b/packages/app/e2e/sidebar-workspace.spec.ts @@ -169,6 +169,47 @@ test.describe("Mobile sidebar panelState transition", () => { test.describe("Half-screen desktop layout", () => { test.use({ viewport: { width: 751, height: 982 } }); + test("keeps the sidebar scroll position across close and reopen", async ({ page }) => { + const workspace = await seedWorkspace({ repoPrefix: "sidebar-retained-scroll-" }); + + try { + let lastWorkspaceId = workspace.workspaceId; + for (let index = 0; index < 24; index += 1) { + const created = await workspace.client.createWorkspace({ + source: { + kind: "directory", + path: workspace.repoPath, + projectId: workspace.projectId, + }, + title: `Retained sidebar ${index + 1}`, + }); + if (!created.workspace) { + throw new Error(created.error ?? "Failed to fill the retained sidebar"); + } + lastWorkspaceId = created.workspace.id; + } + + await gotoAppShell(page); + await waitForSidebarWorkspace(page, lastWorkspaceId); + + const sidebarScroll = page.getByTestId("sidebar-project-workspace-list-scroll"); + const scrollTop = await sidebarScroll.evaluate((element) => { + element.scrollTop = 160; + return element.scrollTop; + }); + expect(scrollTop).toBe(160); + + await page.getByTestId("menu-button").click(); + await expect(page.getByTestId("sidebar-global-new-workspace")).not.toBeVisible(); + + await page.getByTestId("menu-button").click(); + await expect(page.getByTestId("sidebar-global-new-workspace")).toBeVisible(); + await expect(sidebarScroll).toHaveJSProperty("scrollTop", scrollTop); + } finally { + await workspace.cleanup(); + } + }); + test("keeps the pinned sidebar at half of a 14-inch Mac display", async ({ page }) => { await gotoAppShell(page); await expect(page.getByTestId("sidebar-global-new-workspace")).toBeVisible(); diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 606d4b530..bbd751d4b 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -482,10 +482,10 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon requestedExplorerWidth: explorerWidth, viewportWidth, }); - const desktopSidebarRendered = + const desktopSidebarMounted = chromeEnabled && !isFocusModeEnabled; + const desktopSidebarVisible = !isCompactLayout && - chromeEnabled && - !isFocusModeEnabled && + desktopSidebarMounted && isDesktopAgentListOpen && canDesktopAppSidebarShare({ contentMinimumWidth: appContentMinimumWidth, @@ -494,13 +494,14 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon }); const hasTopLeftWindowControls = useHasWindowChromeObstruction("top-left"); const appChromeLayout = resolveDesktopAppChromeLayout({ - desktopSidebarRendered, + desktopSidebarRendered: desktopSidebarVisible, hasTopLeftWindowControls, sidebarControlsEnabled: chromeEnabled && !isFocusModeEnabled, }); const sidebarChrome = ( ); @@ -567,19 +568,22 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon } function SidebarChrome({ - showSidebar, + mounted, + visible, keyboardShortcutsEnabled, }: { - showSidebar: boolean; + mounted: boolean; + visible: boolean; keyboardShortcutsEnabled: boolean; }) { const isCompactLayout = useIsCompactFormFactor(); const isOpen = usePanelStore((state) => selectIsAgentListOpen(state, { isCompact: isCompactLayout }), ); + const active = visible && isOpen; return ( - - {showSidebar ? : null} + + {mounted ? : null} ); diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index 11adaa60f..cd5be3247 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -51,7 +51,7 @@ import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { useHosts } from "@/runtime/host-runtime"; import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store"; import { useWorkspace } from "@/stores/session-store-hooks"; -import { selectIsAgentListOpen, usePanelStore } from "@/stores/panel-store"; +import { usePanelStore } from "@/stores/panel-store"; import { useOwnsWindowChromeCorner, WindowChromeSafeArea } from "@/utils/desktop-window"; import { useCloseAgentListGesture } from "@/mobile-panels/gestures"; import { MobilePanelOverlay } from "@/mobile-panels/presentation"; @@ -117,19 +117,16 @@ interface MobileSidebarProps extends SidebarSharedProps { interface DesktopSidebarProps extends SidebarSharedProps { insetsTop: number; - isOpen: boolean; + active: boolean; handleViewMore: () => void; handleViewSchedules: () => void; } -export const LeftSidebar = memo(function LeftSidebar() { +export const LeftSidebar = memo(function LeftSidebar({ active }: { active: boolean }) { const { theme } = useUnistyles(); const { t } = useTranslation(); const insets = useSafeAreaInsets(); const isCompactLayout = useIsCompactFormFactor(); - const isOpen = usePanelStore((state) => - selectIsAgentListOpen(state, { isCompact: isCompactLayout }), - ); const showMobileAgent = usePanelStore((state) => state.showMobileAgent); const { @@ -256,7 +253,7 @@ export const LeftSidebar = memo(function LeftSidebar() { if (isCompactLayout) { return ( - + + [staticStyles.desktopSidebar, resizeAnimatedStyle], - [resizeAnimatedStyle], + () => [ + staticStyles.desktopSidebar, + !active && staticStyles.desktopSidebarHidden, + resizeAnimatedStyle, + ], + [active, resizeAnimatedStyle], ); const desktopSidebarBorderStyle = useMemo( () => [styles.desktopSidebarBorder, { flex: 1, paddingTop: insetsTop }], @@ -769,12 +770,13 @@ function DesktopSidebar({ [], ); - if (!isOpen) { - return null; - } - return ( - + {ownsTopLeft ? ( @@ -917,6 +919,9 @@ const staticStyles = RNStyleSheet.create({ desktopSidebar: { position: "relative" as const, }, + desktopSidebarHidden: { + display: "none", + }, }); const styles = StyleSheet.create((theme) => ({