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();
+ }
+ });
+});