diff --git a/apps/desktop/src/renderer/styles/chat-message.css b/apps/desktop/src/renderer/styles/chat-message.css index c8c2074922..50c376205e 100644 --- a/apps/desktop/src/renderer/styles/chat-message.css +++ b/apps/desktop/src/renderer/styles/chat-message.css @@ -136,6 +136,12 @@ anchoring when a skipped block leaves and re-enters the viewport. */ .maka-chat-message-list [data-maka-transcript-boundary] { content-visibility: auto; + /* `content-visibility` supplies paint containment as well as skipping. + Assistant ghost bubbles are deliberately flush-left, and Chromium can + rasterize the first CJK glyph a fraction outside that content box; at a + zero-width clip edge it then shaves the glyph's leading stroke. Preserve + a tiny paint apron while keeping the boundary independently skippable. */ + overflow-clip-margin: 2px; /* First-paint intrinsic-size ESTIMATE for scroll-anchor stability, not a fixed height: `auto px` still grows to the block's real size after paint. 96px is the single-line answer baseline; tall multi-line blocks diff --git a/apps/desktop/stories/app-shell.stories.tsx b/apps/desktop/stories/app-shell.stories.tsx index 43ce44f709..f39dd02b14 100644 --- a/apps/desktop/stories/app-shell.stories.tsx +++ b/apps/desktop/stories/app-shell.stories.tsx @@ -964,9 +964,16 @@ export const WideAssistantProse: Story = { const paragraph = await within(answer).findByRole('paragraph'); const turn = paragraph.closest('.maka-turn'); if (!turn) throw new Error('Wide assistant paragraph did not render inside a turn'); + const boundary = paragraph.closest('[data-maka-transcript-boundary]'); + if (!boundary) throw new Error('Wide assistant paragraph has no paint-containment boundary'); const turnRect = turn.getBoundingClientRect(); expect(turnRect.width).toBeGreaterThan(680); expect(turnRect.right - paragraph.getBoundingClientRect().right).toBeLessThanOrEqual(1); + // `content-visibility: auto` implies paint containment. CJK glyph ink can + // extend a fraction past its line box, so a flush-left answer needs a + // small clip margin or Chromium shaves the first glyph on every line. + expect(getComputedStyle(boundary).contentVisibility).toBe('auto'); + expect(Number.parseFloat(getComputedStyle(boundary).overflowClipMargin)).toBeGreaterThanOrEqual(2); }, };