From b01a54de6f0cb2df4a137c28a62d985c308cf723 Mon Sep 17 00:00:00 2001 From: NianJiu <3235467914@qq.com> Date: Sun, 14 Jun 2026 01:13:29 +0800 Subject: [PATCH] fix(ui): restore sidebar session picker interactivity above desktop workbench (#92705) * fix(ui): restore sidebar session picker interactivity above desktop workbench The collapsed sidebar session picker was covered by the chat content area when the workspace rail was visible at wider viewports. Two issues caused this: 1. .sidebar-session-select--collapsed .chat-session-picker used var(--z-dropdown) which was never defined, creating an invalid z-index declaration (falls back to auto). 2. .shell-nav and .content--chat are grid siblings with equal z-index (auto), and .content--chat (later DOM) paints above .shell-nav, covering the session picker that extends from the nav column into the content column. Fix: add position:relative + z-index:10 to .shell-nav so it stacks above .content--chat; change overflow from hidden to visible so the session picker extends beyond the nav rail; replace undefined var(--z-dropdown) with z-index:100. * fix(ui): keep sidebar picker z-index tokenized --------- Co-authored-by: Vincent Koc --- ui/src/styles/base.css | 3 + ui/src/styles/layout.css | 4 +- .../sidebar-session-picker.browser.test.ts | 292 ++++++++++++++++++ 3 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 ui/src/ui/chat/sidebar-session-picker.browser.test.ts diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index abe25a10299b..85b45b827fd7 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -120,6 +120,9 @@ --safe-area-bottom: env(safe-area-inset-bottom, 0px); --safe-area-left: env(safe-area-inset-left, 0px); + /* Layering */ + --z-dropdown: 100; + color-scheme: dark; } diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index 5f139b0df975..c8e61f87ba32 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -460,9 +460,11 @@ .shell-nav { grid-area: nav; + position: relative; + z-index: 10; display: flex; min-height: 100%; - overflow: hidden; + overflow: visible; border-right: 1px solid color-mix(in srgb, var(--border) 74%, transparent); transition: width var(--shell-focus-duration) var(--shell-focus-ease); } diff --git a/ui/src/ui/chat/sidebar-session-picker.browser.test.ts b/ui/src/ui/chat/sidebar-session-picker.browser.test.ts new file mode 100644 index 000000000000..545bd06b3097 --- /dev/null +++ b/ui/src/ui/chat/sidebar-session-picker.browser.test.ts @@ -0,0 +1,292 @@ +// Control UI tests cover sidebar session picker layering and interaction. +import { existsSync } from "node:fs"; +import { chromium, type Browser, type Page } from "playwright"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { readStyleSheet } from "../../../../test/helpers/ui-style-fixtures.js"; + +const describeBrowserLayout = existsSync(chromium.executablePath()) ? describe : describe.skip; + +let browser: Browser; + +function readUiCss(): string { + const files = [ + "ui/src/styles/base.css", + "ui/src/styles/layout.css", + "ui/src/styles/layout.mobile.css", + "ui/src/styles/components.css", + "ui/src/styles/chat/layout.css", + "ui/src/styles/chat/text.css", + "ui/src/styles/chat/grouped.css", + "ui/src/styles/chat/tool-cards.css", + "ui/src/styles/chat/sidebar.css", + ]; + return files.map((file) => readStyleSheet(file)).join("\n"); +} + +function iconSvg() { + return ``; +} + +function sidebarSessionPickerHtml(opts: { workspaceRail?: boolean } = {}) { + const optionButtons = Array.from({ length: 18 }, (_, index) => { + const sessionKey = `dashboard-session-${index + 1}`; + const selected = index === 0; + return ` + + `; + }).join(""); + const workspaceRail = opts.workspaceRail + ? ` + + ` + : ""; + return ` +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+

Keep the sidebar session picker interactive even when the desktop chat workbench is visible.

+
+
+
+
+
+ + + + +
+
+
+
+
+ ${workspaceRail} +
+
+
+
+ + `; +} + +async function openSidebarSessionPickerFixture( + width: number, + height: number, + opts: { workspaceRail?: boolean } = {}, +): Promise { + const page = await browser.newPage({ viewport: { width, height } }); + await page.setContent( + `${sidebarSessionPickerHtml(opts)}`, + ); + return page; +} + +async function expectNoHorizontalOverflow(page: Page) { + const metrics = await page.evaluate(() => ({ + body: document.body.scrollWidth, + html: document.documentElement.scrollWidth, + viewport: window.innerWidth, + })); + expect(metrics.html).toBeLessThanOrEqual(metrics.viewport + 1); + expect(metrics.body).toBeLessThanOrEqual(metrics.viewport + 1); +} + +beforeAll(async () => { + browser = await chromium.launch({ headless: true }); +}); + +afterAll(async () => { + await browser.close(); +}); + +describeBrowserLayout("sidebar session picker browser layout", () => { + it("keeps the collapsed sidebar session picker interactive above the desktop workbench when the workspace rail is visible", async () => { + const page = await openSidebarSessionPickerFixture(1366, 900, { workspaceRail: true }); + try { + await expectNoHorizontalOverflow(page); + const input = page.locator('[data-chat-session-picker-search="true"]'); + const list = page.locator(".chat-session-picker__list"); + const targetOption = page.locator( + '[data-chat-session-picker-option="true"][data-session-key="dashboard-session-12"]', + ); + + const inputHit = await input.evaluate((node) => { + const rect = (node as HTMLElement).getBoundingClientRect(); + const hit = document.elementFromPoint( + rect.left + rect.width / 2, + rect.top + rect.height / 2, + ); + return hit === node || node.contains(hit); + }); + expect(inputHit).toBe(true); + + await input.click(); + await input.fill("telegram"); + await expect + .poll(() => page.evaluate(() => document.body.dataset.searchValue ?? "")) + .toBe("telegram"); + + const listBox = await list.boundingBox(); + if (!listBox) { + throw new Error("Expected session picker list bounding box"); + } + const listScrollBefore = await list.evaluate((node) => (node as HTMLElement).scrollTop); + await page.mouse.move(listBox.x + listBox.width / 2, listBox.y + listBox.height / 2); + await page.mouse.wheel(0, 420); + await expect + .poll(() => list.evaluate((node) => (node as HTMLElement).scrollTop)) + .toBeGreaterThan(listScrollBefore); + + const optionHit = await targetOption.evaluate((node) => { + const rect = (node as HTMLElement).getBoundingClientRect(); + const hit = document.elementFromPoint( + rect.left + rect.width / 2, + rect.top + Math.min(rect.height / 2, rect.height - 4), + ); + return hit === node || node.contains(hit); + }); + expect(optionHit).toBe(true); + + await targetOption.click(); + await expect + .poll(() => page.evaluate(() => document.body.dataset.clickedSession ?? "")) + .toBe("dashboard-session-12"); + } finally { + await page.close(); + } + }); +});