From 2e88a6c00a7a71059511a94ca2cb30c2bba15a7b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 28 Jul 2026 09:56:49 +0000 Subject: [PATCH 1/2] Fix search modal misalignment from collapsed sidebar (#282) Mount a single SearchModal at the app level and portal it to document.body so position:fixed stays viewport-centered. The collapsed sidebar's CSS transform was becoming the containing block when Search opened from a peeked/sidebar mount, shifting the palette off-center. Co-authored-by: Bhavsar Dhruv Ajit --- surfaces/gui/e2e/search-modal.spec.ts | 68 +++++++++++++++++++++ surfaces/gui/src/App.tsx | 10 +-- surfaces/gui/src/components/SearchModal.tsx | 18 ++++-- surfaces/gui/src/components/Sidebar.tsx | 25 +++----- 4 files changed, 98 insertions(+), 23 deletions(-) create mode 100644 surfaces/gui/e2e/search-modal.spec.ts diff --git a/surfaces/gui/e2e/search-modal.spec.ts b/surfaces/gui/e2e/search-modal.spec.ts new file mode 100644 index 0000000000..d218164291 --- /dev/null +++ b/surfaces/gui/e2e/search-modal.spec.ts @@ -0,0 +1,68 @@ +// Search command palette (#282): must stay viewport-centered even when opened from the +// collapsed/peeked sidebar, whose CSS transform would otherwise become the containing block +// for a nested `position: fixed` overlay. +import { expect } from "@playwright/test"; +import { test } from "./fixtures"; + +test("search from expanded sidebar is centered in the viewport", async ({ page }) => { + await page.goto("/"); + await page.locator(".sidebar").getByRole("button", { name: "Search", exact: true }).click(); + + const panel = page.getByTestId("search-modal-panel"); + await expect(panel).toBeVisible(); + await expect(page.getByPlaceholder("Search chats")).toBeVisible(); + + const box = await panel.boundingBox(); + const viewport = page.viewportSize(); + expect(box).toBeTruthy(); + expect(viewport).toBeTruthy(); + const panelCenter = box!.x + box!.width / 2; + const viewportCenter = viewport!.width / 2; + expect(Math.abs(panelCenter - viewportCenter)).toBeLessThan(8); +}); + +test("search from peeked collapsed sidebar stays viewport-centered", async ({ page }) => { + await page.goto("/"); + const app = page.locator(".app"); + + await page.keyboard.press("Meta+b"); + await expect(app).toHaveClass(/nav-collapsed/); + + // Hover the left-edge zone to peek the floating sidebar, then open Search from it. + await page.locator(".nav-hover-zone").hover(); + await expect(app).toHaveClass(/nav-peek/); + await page.locator(".sidebar").getByRole("button", { name: "Search", exact: true }).click(); + + const panel = page.getByTestId("search-modal-panel"); + await expect(panel).toBeVisible(); + + // Peek should dismiss so the floating sidebar does not cover the palette. + await expect(app).not.toHaveClass(/nav-peek/); + + const box = await panel.boundingBox(); + const viewport = page.viewportSize(); + expect(box).toBeTruthy(); + expect(viewport).toBeTruthy(); + const panelCenter = box!.x + box!.width / 2; + const viewportCenter = viewport!.width / 2; + expect(Math.abs(panelCenter - viewportCenter)).toBeLessThan(8); +}); + +test("collapsed topbar search is also viewport-centered", async ({ page }) => { + await page.goto("/"); + await page.keyboard.press("Meta+b"); + + const cluster = page.getByTestId("topbar-cluster"); + await cluster.getByRole("button", { name: "Search" }).click(); + + const panel = page.getByTestId("search-modal-panel"); + await expect(panel).toBeVisible(); + + const box = await panel.boundingBox(); + const viewport = page.viewportSize(); + expect(box).toBeTruthy(); + expect(viewport).toBeTruthy(); + const panelCenter = box!.x + box!.width / 2; + const viewportCenter = viewport!.width / 2; + expect(Math.abs(panelCenter - viewportCenter)).toBeLessThan(8); +}); diff --git a/surfaces/gui/src/App.tsx b/surfaces/gui/src/App.tsx index 603d1e8e69..5ac97a9584 100644 --- a/surfaces/gui/src/App.tsx +++ b/surfaces/gui/src/App.tsx @@ -284,8 +284,10 @@ export function App() { window.addEventListener("ocw-open-artifact", show); return () => window.removeEventListener("ocw-open-artifact", show); }, []); - // The command-palette search, openable from the collapsed-sidebar topbar cluster (§22). The - // expanded sidebar owns its own instance; this one exists so search never disappears with it. + // The command-palette search — single app-level instance for both the expanded sidebar's + // Search row and the collapsed-sidebar topbar cluster (§22). Mounting it here (and portaling + // to document.body) keeps `position: fixed` viewport-centered even when the collapsed sidebar + // applies a CSS transform for peek/slide (#282). const [searchOpen, setSearchOpen] = useState(false); // A pending composer prefill (text + attachments) pushed from the session start panel. const [composerPrefill, setComposerPrefill] = useState<{ text: string; attachments?: Attachment[]; nonce: number }>(); @@ -1285,6 +1287,7 @@ export function App() { collapsed={navCollapsed} onCollapse={toggleNav} onPeekLeave={() => setNavPeek(false)} + onOpenSearch={() => setSearchOpen(true)} /> {surface === "scheduled" ? ( )} - {/* Search from the collapsed-sidebar topbar cluster (the sidebar's own instance is - unreachable while it's collapsed). */} + {/* Single SearchModal for sidebar Search and the collapsed topbar cluster. */} {searchOpen && ( (b.updated_at || "").localeCompare(a.updated_at || ""); @@ -109,10 +114,14 @@ export function SearchModal({ ); }; - return ( -
+ // z-[70] sits above the collapsed sidebar peek (z-60) so the palette is never covered. + return createPortal( +
-
+
-
+
, + document.body, ); } diff --git a/surfaces/gui/src/components/Sidebar.tsx b/surfaces/gui/src/components/Sidebar.tsx index 9f0215dd13..472d0bf61f 100644 --- a/surfaces/gui/src/components/Sidebar.tsx +++ b/surfaces/gui/src/components/Sidebar.tsx @@ -24,7 +24,6 @@ import { isProjectScoped, shortPersonaName } from "../personaScope"; import { ConnectorIcon } from "../connectors/ConnectorIcon"; import { Icon, type IconName } from "./Icon"; import { PersonaGlyph, personaGlyph } from "./personaIcon"; -import { SearchModal } from "./SearchModal"; import { baseName } from "../paths"; import { showPersonas } from "../flags"; @@ -146,6 +145,9 @@ interface Props { collapsed?: boolean; onCollapse?: () => void; onPeekLeave?: () => void; + // Opens the app-level SearchModal. Kept out of this tree so the collapsed sidebar's + // `transform` cannot become the containing block for `position: fixed` (#282). + onOpenSearch?: () => void; } // Compact age for project session rows: "now" / "5m" / "6h" / "3d" / "2w" / "4mo" / "2y". @@ -171,7 +173,6 @@ const compactAge = (iso?: string | null): string => { // Sessions shown per group before "Show more" comes from Settings (sessions_peek, default 5). export function Sidebar(props: Props) { - const [searchModalOpen, setSearchModalOpen] = useState(false); const [appMenuOpen, setAppMenuOpen] = useState(false); // The account row (§26): cloud sign-in status drives the avatar/name/dot; refreshed on // focus and whenever the menu opens (sign-in completes out-of-band in the browser). @@ -1014,11 +1015,16 @@ export function Sidebar(props: Props) { /> {/* Search: a borderless nav-style entry (not a boxed input) that opens the command-palette - SearchModal over the whole app. Matches the bottom-nav rows to reduce the boxy look. */} + SearchModal over the whole app. Matches the bottom-nav rows to reduce the boxy look. + Opens via App so the palette is never mounted under the transformed collapsed sidebar. */}
@@ -1270,17 +1276,6 @@ export function Sidebar(props: Props) {
- {searchModalOpen && ( - { - setSearchModalOpen(false); - props.onSelectSession(id, ws, ag); - }} - onClose={() => setSearchModalOpen(false)} - /> - )}
); } From a610cef3a43c5a18a24e98e7e25a01ea95d569c0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 28 Jul 2026 09:58:23 +0000 Subject: [PATCH 2/2] Harden search modal e2e: wait for boot, use collapse button Avoid Meta+b races during boot-splash by waiting for the app shell and collapsing via the sidebar button before asserting palette centering. Co-authored-by: Bhavsar Dhruv Ajit --- surfaces/gui/e2e/search-modal.spec.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/surfaces/gui/e2e/search-modal.spec.ts b/surfaces/gui/e2e/search-modal.spec.ts index d218164291..84f737e076 100644 --- a/surfaces/gui/e2e/search-modal.spec.ts +++ b/surfaces/gui/e2e/search-modal.spec.ts @@ -4,8 +4,14 @@ import { expect } from "@playwright/test"; import { test } from "./fixtures"; -test("search from expanded sidebar is centered in the viewport", async ({ page }) => { +async function ready(page: import("@playwright/test").Page) { await page.goto("/"); + await expect(page.locator(".app")).not.toHaveClass(/boot-splash/); + await expect(page.locator(".sidebar")).toBeVisible(); +} + +test("search from expanded sidebar is centered in the viewport", async ({ page }) => { + await ready(page); await page.locator(".sidebar").getByRole("button", { name: "Search", exact: true }).click(); const panel = page.getByTestId("search-modal-panel"); @@ -22,10 +28,10 @@ test("search from expanded sidebar is centered in the viewport", async ({ page } }); test("search from peeked collapsed sidebar stays viewport-centered", async ({ page }) => { - await page.goto("/"); + await ready(page); const app = page.locator(".app"); - await page.keyboard.press("Meta+b"); + await page.getByRole("button", { name: "Collapse sidebar" }).click(); await expect(app).toHaveClass(/nav-collapsed/); // Hover the left-edge zone to peek the floating sidebar, then open Search from it. @@ -49,10 +55,12 @@ test("search from peeked collapsed sidebar stays viewport-centered", async ({ pa }); test("collapsed topbar search is also viewport-centered", async ({ page }) => { - await page.goto("/"); - await page.keyboard.press("Meta+b"); + await ready(page); + await page.getByRole("button", { name: "Collapse sidebar" }).click(); + await expect(page.locator(".app")).toHaveClass(/nav-collapsed/); const cluster = page.getByTestId("topbar-cluster"); + await expect(cluster).toBeVisible(); await cluster.getByRole("button", { name: "Search" }).click(); const panel = page.getByTestId("search-modal-panel");