From ce7b14a143767f37aab1a490bcba91565b3fa9c7 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 07:05:49 +0000 Subject: [PATCH] Keep context details open while chat streams (#8742) * fix(app): keep hover cards open while unrelated content scrolls Hover cards closed on any scroll outside their content. A streaming chat scrolls on every new line, so the context window details closed while an agent was responding. Close only when the scrolled element contains the trigger, which is the case that moves the card's anchor. * test(app): confirm the turn is still streaming once the details open * test(app): cover context clicks and trigger scrolling Exercise the reported click path without replacing usage transport, and preserve dismissal when a sidebar scroll moves its trigger. Co-authored-by: 3ae3ae --------- Co-authored-by: 3ae3ae Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../e2e/browser/context-window-meter.spec.ts | 26 +++++++++++++++++++ .../app/e2e/browser/sidebar-workspace.spec.ts | 7 +++++ .../app/e2e/support/helpers/context-window.ts | 13 ++++++---- packages/app/e2e/support/helpers/sidebar.ts | 9 +++++++ packages/app/src/components/ui/hover-card.tsx | 4 +-- 5 files changed, 52 insertions(+), 7 deletions(-) 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);