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.
This commit is contained in:
Mohamed Boudra
2026-07-11 19:27:24 +00:00
parent a257a1c5f6
commit 052d6830fc
3 changed files with 87 additions and 15 deletions
+32
View File
@@ -8,6 +8,7 @@ import {
} from "./helpers/agent-stream"; } from "./helpers/agent-stream";
import { import {
expectScrollStaysFixed, expectScrollStaysFixed,
clickToolCallBesideScrollToBottomButton,
readScrollMetrics, readScrollMetrics,
scrollAgentChatToBottom, scrollAgentChatToBottom,
scrollChatAwayFromBottom, scrollChatAwayFromBottom,
@@ -205,6 +206,37 @@ test.describe("Agent stream UI", () => {
await expectScrollStaysFixed(page, baseline); 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("working-indicator transitions to copy-button when stream ends", async ({ page }) => {
test.setTimeout(60_000); test.setTimeout(60_000);
const agent = await startRunningMockAgent(page, { const agent = await startRunningMockAgent(page, {
@@ -124,6 +124,57 @@ export async function scrollChatAwayFromBottom(
return readScrollMetrics(page); 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<HTMLElement>('[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( export async function expectScrollStaysFixed(
page: Page, page: Page,
baseline: ScrollMetrics, baseline: ScrollMetrics,
+4 -15
View File
@@ -905,12 +905,8 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
})} })}
</MessageOuterSpacingProvider> </MessageOuterSpacingProvider>
{!isNearBottom && ( {!isNearBottom && (
<Animated.View <View style={stylesheet.scrollToBottomContainer} pointerEvents="box-none">
style={stylesheet.scrollToBottomContainer} <Animated.View entering={scrollIndicatorFadeIn} exiting={scrollIndicatorFadeOut}>
entering={scrollIndicatorFadeIn}
exiting={scrollIndicatorFadeOut}
>
<View style={stylesheet.scrollToBottomInner}>
<Pressable <Pressable
style={stylesheet.scrollToBottomButton} style={stylesheet.scrollToBottomButton}
onPress={scrollToBottom} onPress={scrollToBottom}
@@ -920,8 +916,8 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
> >
<ChevronDown size={24} color={stylesheet.scrollToBottomIcon.color} /> <ChevronDown size={24} color={stylesheet.scrollToBottomIcon.color} />
</Pressable> </Pressable>
</View> </Animated.View>
</Animated.View> </View>
)} )}
</View> </View>
</ToolCallSheetProvider> </ToolCallSheetProvider>
@@ -1393,13 +1389,6 @@ const stylesheet = StyleSheet.create((theme) => ({
left: 0, left: 0,
right: 0, right: 0,
alignItems: "center", alignItems: "center",
pointerEvents: "box-none",
},
scrollToBottomInner: {
width: "100%",
maxWidth: MAX_CONTENT_WIDTH,
alignSelf: "center",
alignItems: "center",
}, },
scrollToBottomButton: { scrollToBottomButton: {
width: 48, width: 48,