From 96f9a2f6c0ed010f12ee46d12bafb4381ee086b2 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:21:11 +0800 Subject: [PATCH] fix(ui-chat): clip horizontal overflow in the chat transcript --- .../src/client/chat/ChatView.module.css | 9 ++++++- .../tests/chat-scroll-overflow.client.spec.ts | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 packages/client/ui-chat/tests/chat-scroll-overflow.client.spec.ts diff --git a/packages/client/ui-chat/src/client/chat/ChatView.module.css b/packages/client/ui-chat/src/client/chat/ChatView.module.css index 5c28650c35..341a33f612 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.module.css +++ b/packages/client/ui-chat/src/client/chat/ChatView.module.css @@ -22,6 +22,10 @@ .scroll { flex: 1 1 auto; min-height: 0; + /* Never expose horizontal scrollable overflow: the absolutely positioned + text-interaction surface adds it transiently, and the changing scroll range + makes the transcript jitter while the tail is pinned. */ + overflow-x: clip; overflow-y: auto; /* Sides = composer clearance + 16px: on narrow viewports the transcript stays exactly 32px narrower than the input card (the shared width rule). */ @@ -46,7 +50,10 @@ } :global([data-conversation-scroll]) .scroll { - overflow: visible; + /* A host scrollport still owns vertical overflow; horizontal overflow is + clipped for the same reason as the self-scrolled case above. */ + overflow-x: clip; + overflow-y: visible; flex: 0 0 auto; min-height: auto; } diff --git a/packages/client/ui-chat/tests/chat-scroll-overflow.client.spec.ts b/packages/client/ui-chat/tests/chat-scroll-overflow.client.spec.ts new file mode 100644 index 0000000000..875c6e91cf --- /dev/null +++ b/packages/client/ui-chat/tests/chat-scroll-overflow.client.spec.ts @@ -0,0 +1,24 @@ +/** The transcript scroller never exposes horizontal scrollable overflow. */ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +const read = (name: string): string => + readFileSync(fileURLToPath(new URL(`../src/client/chat/${name}`, import.meta.url)), 'utf8') + +const rule = (css: string, selector: string): string => { + const start = css.indexOf(`${selector} {`) + return css.slice(start, css.indexOf('}', start) + 1) +} + +describe('Transcript scroll overflow', () => { + it('clips the horizontal axis in both scrollport modes', () => { + // The text-interaction surface is absolutely positioned inside the message + // column and can reach past its right edge; an `auto` horizontal axis turns + // that into a scroll range whose appearance and resolution jitter the + // transcript while the tail is pinned. + const css = read('ChatView.module.css') + expect(rule(css, '.scroll')).toMatch(/overflow-x:\s*clip/) + expect(rule(css, ':global([data-conversation-scroll]) .scroll')).toMatch(/overflow-x:\s*clip/) + }) +})