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) => ({