diff --git a/src/web-ui/src/flow_chat/components/CodePreview.test.tsx b/src/web-ui/src/flow_chat/components/CodePreview.test.tsx
index dd50f0c31..c9bdf73ec 100644
--- a/src/web-ui/src/flow_chat/components/CodePreview.test.tsx
+++ b/src/web-ui/src/flow_chat/components/CodePreview.test.tsx
@@ -107,4 +107,25 @@ describe('CodePreview', () => {
expect(highlighter.textContent).not.toContain('line 090');
expect(Number(highlighter.dataset.startingLineNumber)).toBeLessThanOrEqual(104);
});
+
+ it('fits the streaming tail in the viewport when nested autoscroll is disabled', async () => {
+ await act(async () => {
+ root.render(
+
+ );
+ });
+
+ const highlighter = container.querySelector('[data-testid="syntax-highlighter"]') as HTMLElement;
+ expect(highlighter).not.toBeNull();
+ expect(highlighter.textContent).toContain('line 120');
+ expect(highlighter.textContent).toContain('line 117');
+ expect(highlighter.textContent).not.toContain('line 116');
+ expect(Number(highlighter.dataset.startingLineNumber)).toBe(117);
+ });
});
diff --git a/src/web-ui/src/flow_chat/components/CodePreview.tsx b/src/web-ui/src/flow_chat/components/CodePreview.tsx
index 4ff2a3561..a9d137956 100644
--- a/src/web-ui/src/flow_chat/components/CodePreview.tsx
+++ b/src/web-ui/src/flow_chat/components/CodePreview.tsx
@@ -64,19 +64,24 @@ function countNewlines(value: string, endExclusive = value.length): number {
return count;
}
-function getStreamingTailLineLimit(maxHeight: number): number {
- const visibleLines = Math.ceil(maxHeight / CODE_PREVIEW_STREAMING_LINE_HEIGHT_PX);
+function getStreamingTailLineLimit(maxHeight: number, includeOverscan: boolean): number {
+ const visibleLines = Math.max(1, Math.ceil(maxHeight / CODE_PREVIEW_STREAMING_LINE_HEIGHT_PX));
+ const desiredLines = includeOverscan
+ ? visibleLines + STREAMING_TAIL_OVERSCAN_LINES
+ : visibleLines;
+ const minimumLines = includeOverscan ? STREAMING_TAIL_MIN_LINES : 1;
+
return Math.min(
STREAMING_TAIL_MAX_LINES,
- Math.max(STREAMING_TAIL_MIN_LINES, visibleLines + STREAMING_TAIL_OVERSCAN_LINES),
+ Math.max(minimumLines, desiredLines),
);
}
-function getStreamingTailDisplayContent(content: string, maxHeight: number): {
+function getStreamingTailDisplayContent(content: string, maxHeight: number, includeOverscan: boolean): {
content: string;
startingLineNumber: number;
} {
- const tailLineLimit = getStreamingTailLineLimit(maxHeight);
+ const tailLineLimit = getStreamingTailLineLimit(maxHeight, includeOverscan);
const totalLineCount = countNewlines(content) + 1;
if (totalLineCount <= tailLineLimit && content.length <= STREAMING_TAIL_MAX_CHARS) {
@@ -142,8 +147,8 @@ export const CodePreview: React.FC = memo(({
return { content: deferredContent, startingLineNumber: 1 };
}
- return getStreamingTailDisplayContent(deferredContent, maxHeight);
- }, [isStreaming, deferredContent, maxHeight]);
+ return getStreamingTailDisplayContent(deferredContent, maxHeight, autoScrollToBottom);
+ }, [isStreaming, deferredContent, maxHeight, autoScrollToBottom]);
const displayContent = displayContentInfo.content;
diff --git a/src/web-ui/src/flow_chat/tool-cards/README.md b/src/web-ui/src/flow_chat/tool-cards/README.md
index f9791c088..15342ff65 100644
--- a/src/web-ui/src/flow_chat/tool-cards/README.md
+++ b/src/web-ui/src/flow_chat/tool-cards/README.md
@@ -39,9 +39,10 @@ Current examples:
When the preview uses a nested scrolling code viewer, avoid forcing that nested
viewer to auto-scroll while params are streaming. Streaming code previews already
-render the latest viewport-sized tail, and the outer conversation list owns the
-high-level follow behavior. Writing `scrollTop` on every preview batch can force
-layout work inside the WebView and make long code output less responsive.
+render the latest viewport-sized tail without overscan when nested auto-scroll is
+disabled, and the outer conversation list owns the high-level follow behavior.
+Writing `scrollTop` on every preview batch can force layout work inside the
+WebView and make long code output less responsive.
Preferred pattern: