From 052d6830fc3c522fd1dfbcb58c785729a399c6f5 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 11 Jul 2026 19:27:24 +0000 Subject: [PATCH] fix(app): keep stream controls clickable under scroll button The full-width animated overlay won browser hit testing even outside the circular control. Keep positioning on a pass-through container and animate only the button-sized child. --- packages/app/e2e/agent-stream-ui.spec.ts | 32 ++++++++++++ .../app/e2e/helpers/agent-bottom-anchor.ts | 51 +++++++++++++++++++ packages/app/src/agent-stream/view.tsx | 19 ++----- 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/packages/app/e2e/agent-stream-ui.spec.ts b/packages/app/e2e/agent-stream-ui.spec.ts index c0fee77854..aeeb6cff73 100644 --- a/packages/app/e2e/agent-stream-ui.spec.ts +++ b/packages/app/e2e/agent-stream-ui.spec.ts @@ -8,6 +8,7 @@ import { } from "./helpers/agent-stream"; import { expectScrollStaysFixed, + clickToolCallBesideScrollToBottomButton, readScrollMetrics, scrollAgentChatToBottom, scrollChatAwayFromBottom, @@ -205,6 +206,37 @@ test.describe("Agent stream UI", () => { await expectScrollStaysFixed(page, baseline); }); + test("keeps tool calls clickable beside the scroll-to-bottom button", async ({ page }) => { + test.setTimeout(60_000); + const agent = await seedMockAgentWorkspace({ + repoPrefix: "stream-scroll-button-hit-area-", + title: "Scroll button hit area", + model: "ten-second-stream", + initialPrompt: "Stream enough content to exercise the scroll button hit area.", + }); + try { + await agent.client.waitForFinish(agent.agentId, 30_000); + await openAgentRoute(page, { + workspaceId: agent.workspaceId, + agentId: agent.agentId, + }); + await waitForScrollableChat(page, { + minScrollableDistance: SCROLL_AWAY_MIN_SCROLLABLE_DISTANCE, + timeout: 30_000, + }); + + const hitArea = await clickToolCallBesideScrollToBottomButton(page); + + expect(hitArea).toEqual({ + outsideButton: true, + toolCallReceivesPointer: true, + withinButtonBand: true, + }); + } finally { + await agent.cleanup(); + } + }); + test("working-indicator transitions to copy-button when stream ends", async ({ page }) => { test.setTimeout(60_000); const agent = await startRunningMockAgent(page, { diff --git a/packages/app/e2e/helpers/agent-bottom-anchor.ts b/packages/app/e2e/helpers/agent-bottom-anchor.ts index a0ff65ba2e..f871afac29 100644 --- a/packages/app/e2e/helpers/agent-bottom-anchor.ts +++ b/packages/app/e2e/helpers/agent-bottom-anchor.ts @@ -124,6 +124,57 @@ export async function scrollChatAwayFromBottom( return readScrollMetrics(page); } +export async function clickToolCallBesideScrollToBottomButton(page: Page): Promise<{ + outsideButton: boolean; + toolCallReceivesPointer: boolean; + withinButtonBand: boolean; +}> { + await scrollChatAwayFromBottom(page, { + deltaY: -900, + minDistanceFromBottom: 300, + }); + + const scrollToBottomButton = page.getByRole("button", { name: "Scroll to bottom" }); + await expect(scrollToBottomButton).toBeVisible(); + + const hitArea = await page.evaluate(() => { + const button = document.querySelector('[data-testid="scroll-to-bottom-button"]'); + if (!(button instanceof HTMLElement)) { + throw new Error("Expected visible scroll-to-bottom button"); + } + const buttonBounds = button.getBoundingClientRect(); + const toolCall = Array.from( + document.querySelectorAll('[data-testid="tool-call-badge"] [role="button"]'), + ).find((element) => { + const bounds = element.getBoundingClientRect(); + const centerY = bounds.top + bounds.height / 2; + return bounds.width > 0 && centerY >= buttonBounds.top && centerY <= buttonBounds.bottom; + }); + if (!toolCall) { + throw new Error("Expected a tool call beside the scroll-to-bottom button"); + } + + const toolCallBounds = toolCall.getBoundingClientRect(); + const clickPoint = { + x: toolCallBounds.left + 24, + y: toolCallBounds.top + toolCallBounds.height / 2, + }; + const hit = document.elementFromPoint(clickPoint.x, clickPoint.y); + return { + clickPoint, + outsideButton: clickPoint.x < buttonBounds.left || clickPoint.x > buttonBounds.right, + toolCallReceivesPointer: hit !== null && toolCall.contains(hit), + withinButtonBand: clickPoint.y >= buttonBounds.top && clickPoint.y <= buttonBounds.bottom, + }; + }); + await page.mouse.click(hitArea.clickPoint.x, hitArea.clickPoint.y); + return { + outsideButton: hitArea.outsideButton, + toolCallReceivesPointer: hitArea.toolCallReceivesPointer, + withinButtonBand: hitArea.withinButtonBand, + }; +} + export async function expectScrollStaysFixed( page: Page, baseline: ScrollMetrics, diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index 17afca016c..0a93327c9b 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -905,12 +905,8 @@ const AgentStreamViewComponent = forwardRef {!isNearBottom && ( - - + + - - + + )} @@ -1393,13 +1389,6 @@ const stylesheet = StyleSheet.create((theme) => ({ left: 0, right: 0, alignItems: "center", - pointerEvents: "box-none", - }, - scrollToBottomInner: { - width: "100%", - maxWidth: MAX_CONTENT_WIDTH, - alignSelf: "center", - alignItems: "center", }, scrollToBottomButton: { width: 48,