diff --git a/packages/app/e2e/browser/context-window-meter.spec.ts b/packages/app/e2e/browser/context-window-meter.spec.ts index be0835b116..f79b7fd760 100644 --- a/packages/app/e2e/browser/context-window-meter.spec.ts +++ b/packages/app/e2e/browser/context-window-meter.spec.ts @@ -429,6 +429,32 @@ test("context details support keyboard entry and dismiss when the viewport moves await expect(details).toHaveCount(0); }); +test("context details stay open while the chat streams", async ({ page }) => { + test.setTimeout(120_000); + await page.setViewportSize(DESKTOP); + const session = await seedMockAgentWorkspace({ + repoPrefix: "context-window-streaming-", + title: "Context details while streaming", + model: "ten-second-stream", + }); + try { + await openAgentRoute(page, session); + await expectComposerVisible(page); + await submitMessageWithButton(page, "Stream while the context details are open."); + const stop = page.getByRole("button", { name: /stop|cancel/i }).first(); + await expect(stop).toBeVisible(); + const details = await pressContextWindowMeter(page, /^Context window/); + await expect(stop).toBeVisible(); + await qaScreenshot(page, "context-details-while-streaming"); + + await expect(stop).toHaveCount(0, { timeout: 30_000 }); + await expect(details).toBeVisible(); + await qaScreenshot(page, "context-details-after-streaming"); + } finally { + await session.cleanup(); + } +}); + test("pointer hovering context details preserves composer focus", async ({ page, agent }) => { const usage = await scriptAgentUsage(page); await page.setViewportSize(DESKTOP); diff --git a/packages/app/e2e/browser/sidebar-workspace.spec.ts b/packages/app/e2e/browser/sidebar-workspace.spec.ts index d96f68580c..0270235168 100644 --- a/packages/app/e2e/browser/sidebar-workspace.spec.ts +++ b/packages/app/e2e/browser/sidebar-workspace.spec.ts @@ -13,6 +13,7 @@ import { movePointerAwayFromWorkspaceHoverCard, openMobileAgentSidebar, pinWorkspaceFromSidebar, + scrollWorkspaceList, } from "../support/helpers/sidebar"; import { seedWorkspace } from "../support/helpers/seed-client"; import { expectWorkspaceHeader } from "../support/helpers/workspace-ui"; @@ -582,6 +583,12 @@ test.describe("Half-screen desktop layout", () => { await page.getByTestId("menu-button").click(); await expect(page.getByTestId("sidebar-global-new-workspace")).toBeVisible(); await expect(sidebarScroll).toHaveJSProperty("scrollTop", scrollTop); + + await movePointerAwayFromWorkspaceHoverCard(page); + await focusWorkspaceRowWithKeyboard(page, lastWorkspaceId); + await expectWorkspaceHoverCardOpen(page); + await scrollWorkspaceList(page); + await expectWorkspaceHoverCardClosed(page); } finally { await workspace.cleanup(); } diff --git a/packages/app/e2e/support/helpers/context-window.ts b/packages/app/e2e/support/helpers/context-window.ts index afc91f859c..5320b73b32 100644 --- a/packages/app/e2e/support/helpers/context-window.ts +++ b/packages/app/e2e/support/helpers/context-window.ts @@ -54,12 +54,15 @@ export async function leaveContextWindowMeter(page: Page): Promise { await expect(contextWindowDetails(page)).toHaveCount(0); } -/** Compact screens open the meter's details in a sheet. */ -export async function pressContextWindowMeter(page: Page, name = METER_NAME): Promise { +/** Clicking the meter opens its details in the active layout. */ +export async function pressContextWindowMeter( + page: Page, + name: string | RegExp = METER_NAME, +): Promise { await page.getByRole("button", { name, exact: true }).click({ timeout: 30_000 }); - const sheet = contextWindowDetails(page); - await expect(sheet).toBeVisible(); - return sheet; + const details = contextWindowDetails(page); + await expect(details).toBeVisible(); + return details; } export async function closeContextWindowSheet(page: Page): Promise { diff --git a/packages/app/e2e/support/helpers/sidebar.ts b/packages/app/e2e/support/helpers/sidebar.ts index 66a913e7b3..0943eba0b4 100644 --- a/packages/app/e2e/support/helpers/sidebar.ts +++ b/packages/app/e2e/support/helpers/sidebar.ts @@ -71,6 +71,15 @@ export async function focusWorkspaceRowWithKeyboard( await expect(row).toBeFocused(); } +export async function scrollWorkspaceList(page: Page): Promise { + const list = page.getByTestId("sidebar-project-workspace-list-scroll"); + const before = await list.evaluate((element) => element.scrollTop); + await list.evaluate((element) => + element.scrollBy({ top: element.scrollTop > 0 ? -40 : 40, behavior: "instant" }), + ); + await expect.poll(() => list.evaluate((element) => element.scrollTop)).not.toBe(before); +} + export async function expectWorkspaceHoverCardOpen(page: Page): Promise { await expect(page.getByTestId("workspace-hover-card")).toBeVisible(); } diff --git a/packages/app/src/components/ui/hover-card.tsx b/packages/app/src/components/ui/hover-card.tsx index a93ce92193..164120c491 100644 --- a/packages/app/src/components/ui/hover-card.tsx +++ b/packages/app/src/components/ui/hover-card.tsx @@ -208,9 +208,9 @@ function WebHoverCard({ ) keyPressed(event); }; + // Only a scroll that moves the trigger detaches the card. A streaming chat beside it does not. const scrolled = (event: Event) => { - if (!(contentRef.current as unknown as HTMLElement | null)?.contains(event.target as Node)) - close(); + if ((event.target as Node).contains(trigger)) close(); }; document.addEventListener("focusin", focusEntered); document.addEventListener("focusout", focusLeft);