Skip to content

fix: scroll to bottom button didnt show during streaming#1007

Open
ethanwinters wants to merge 3 commits intocarbon-design-system:mainfrom
ethanwinters:markdown-fast-render
Open

fix: scroll to bottom button didnt show during streaming#1007
ethanwinters wants to merge 3 commits intocarbon-design-system:mainfrom
ethanwinters:markdown-fast-render

Conversation

@ethanwinters
Copy link
Contributor

@ethanwinters ethanwinters commented Feb 26, 2026

closes #999

I broke this PR into three different commits with instructions for each one because its a lot of stuff changing here.

I have one pending fix for Safari I'm not including in this PR. If the streamed message is for something async rendered like a code snippet, Safari can get a little confused during scrolling.

fix: remove unneeded scrolling features

Fix scroll to bottom button not showing during streaming and remove extra legacy streaming stuff we shouldn't need with new scroll behavior. Removes the doAutoScroll prop that was being threaded through MessageComponent and MessageTypeComponent but was no longer needed. This is a cleanup pass to reduce prop surface area and simplify the component interfaces.

Pay attention to: MessageComponent.tsx and MessageTypeComponent.tsx — these are the files where the prop was removed and callers were updated.

Changelog

New

  • Nothing new.

Changed

  • MessageComponent.tsx: Removed HasDoAutoScroll from the MessageProps interface and removed doAutoScroll from the destructured render props and from the props passed down to child components.
  • MessageTypeComponent.tsx: Removed doAutoScroll from the props passed to UserDefinedResponse and ConversationalSearch, and from the local destructure in renderUserDefinedResponse.

Removed

Testing / Reviewing

  • Verify that message rendering still works end-to-end: send a message, receive a streaming response, and confirm no console errors about missing props.
  • Verify that UserDefinedResponse and ConversationalSearch message types still render correctly.
  • Check that no other callers of MessageComponent or MessageTypeComponent were passing doAutoScroll and are now broken (search for doAutoScroll in the codebase — it should return zero results after this change).

fix: scrolling safari issues

Refactors MessagesComponent to fix Safari scroll anchoring issues. The core problem was that Safari's scroll anchoring fires synchronously on layout, which caused scrollTop to be capped or snapped unexpectedly when the bottom spacer was written. The fix is to restore scrollTop immediately after any spacer DOM write.

As part of this fix, the auto-scroll logic and render logic were extracted out of the monolithic MessagesComponent into dedicated modules, and the JSX was split into focused sub-components to make the component easier to reason about.

Pay attention to: MessagesComponent.tsx — this is the orchestrator that wires everything together and is where the Safari fix lives. Also review messagesAutoScrollController.ts — this is the new home for all scroll geometry and spacer math.

Changelog

New

  • MessagesScrollHandle.tsx: New sub-component for the accessible scroll handle button (extracted from MessagesComponent).
  • MessagesScrollToBottomButton.tsx: New sub-component for the scroll-to-bottom button (extracted from MessagesComponent).
  • MessagesTypingIndicator.tsx: New sub-component for the typing/processing indicator (extracted from MessagesComponent).
  • MessagesView.tsx: New sub-component that owns the messages container DOM structure (extracted from MessagesComponent).
  • messagesAutoScrollController.ts: New module containing all auto-scroll policy decisions, spacer geometry math, and action types (resolveAutoScrollAction, pinMessageAndScroll, recalculatePinnedMessageSpacer, consumeStreamingChunk).
  • messagesRenderUtils.ts: New module containing buildRenderableMessageMetadata, extracted from MessagesComponent's render logic.

Changed

  • MessagesComponent.tsx: Reduced to an orchestrator — delegates rendering to the new sub-components and delegates scroll math to messagesAutoScrollController. Added scrollTop restore after spacer DOM writes to override Safari scroll anchoring.
  • workspace-manager.ts: Added { leading: true, trailing: true } to the throttle options for throttledHandleHostResize to ensure the final resize event is always processed.
  • markdown.ts: Changed throttle options from { leading: true } to { leading: true, trailing: true } to ensure the final update is never dropped.

Removed

  • Inline auto-scroll logic, spacer math, and render metadata logic from MessagesComponent.tsx (moved to dedicated modules).
  • Inline JSX for scroll handle, scroll-to-bottom button, and typing indicator from MessagesComponent.tsx (moved to dedicated sub-components).

Testing / Reviewing

  • Safari specifically: During a streaming response, scroll down manually into the blank spacer area. Confirm the view does not snap back to the pin position. This was the core Safari regression.
  • Verify the scroll-to-bottom button appears and disappears correctly during and after streaming.
  • Verify the typing indicator appears while waiting for a response and disappears when streaming begins.
  • Verify the scroll handle (accessible button) still works for keyboard navigation.
  • Verify that resolveAutoScrollAction, pinMessageAndScroll, recalculatePinnedMessageSpacer, and consumeStreamingChunk in messagesAutoScrollController.ts are the single source of truth — no duplicate scroll math should remain in MessagesComponent.

fix: remove extra spacer while streaming

Changes the streaming spacer strategy so that the bottom spacer shrinks progressively as content fills in, rather than staying at its full pin-time height until the stream ends. Previously, users would see a large blank area below the streaming response throughout the entire stream. Now the spacer shrinks in real time as content grows.

The Safari scrollTop restore pattern (introduced in the previous commit) is applied here too: after each mid-stream spacer write, scrollTop is immediately restored to prevent the browser from capping it when scrollHeight decreases.

Pay attention to: MessagesComponent.tsx — specifically handleStreamingChunk — and messagesAutoScrollController.ts — specifically consumeStreamingChunk and its updated doc comment. The test file messagesAutoScrollController_spec.ts covers the key behaviors.

Changelog

New

Changed

  • MessagesComponent.tsx: handleStreamingChunk now writes the reduced spacer height to the DOM immediately when currentSpacerHeight decreases, and restores scrollTop right after to prevent Safari from capping it. Updated domSpacerHeight comment to reflect that it is now updated mid-stream on each spacer write.
  • messagesAutoScrollController.ts: Updated consumeStreamingChunk doc comment to clarify that the caller is responsible for writing the reduced spacer to the DOM and restoring scrollTop.

Removed

  • Nothing removed.

Testing / Reviewing

  • During a streaming response, watch the area below the response. The blank spacer space should visibly shrink as content fills in — there should be no large persistent blank area below the streaming text.
  • Confirm that if the user has scrolled down into the spacer area, the browser caps them at the new content bottom (correct behavior) rather than snapping them back to the pin.
  • Run the new unit tests in messagesAutoScrollController_spec.ts and confirm they all pass.
  • Safari specifically: Confirm that mid-stream spacer writes do not cause the view to jump back to the pin position.

@ethanwinters ethanwinters requested a review from a team as a code owner February 26, 2026 15:32
@netlify
Copy link

netlify bot commented Feb 26, 2026

Deploy Preview for carbon-ai-chat-components ready!

Name Link
🔨 Latest commit beb5785
🔍 Latest deploy log https://app.netlify.com/projects/carbon-ai-chat-components/deploys/69a20095ee75e6000865259b
😎 Deploy Preview https://deploy-preview-1007--carbon-ai-chat-components.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Feb 26, 2026

Deploy Preview for carbon-ai-chat-demo ready!

Name Link
🔨 Latest commit beb5785
🔍 Latest deploy log https://app.netlify.com/projects/carbon-ai-chat-demo/deploys/69a2009545c25200087a4520
😎 Deploy Preview https://deploy-preview-1007--carbon-ai-chat-demo.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Feb 26, 2026

Deploy Preview for ai-chat-components-react ready!

Name Link
🔨 Latest commit beb5785
🔍 Latest deploy log https://app.netlify.com/projects/ai-chat-components-react/deploys/69a200956d8d120007d2da2b
😎 Deploy Preview https://deploy-preview-1007--ai-chat-components-react.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@ethanwinters ethanwinters marked this pull request as draft February 26, 2026 16:00
@ethanwinters ethanwinters force-pushed the markdown-fast-render branch 11 times, most recently from 056833d to 828357f Compare February 26, 2026 21:10
@ethanwinters ethanwinters force-pushed the markdown-fast-render branch 2 times, most recently from 3bc7ac8 to 621d6f0 Compare February 27, 2026 15:05
@ethanwinters ethanwinters force-pushed the markdown-fast-render branch 2 times, most recently from 2e5a4db to 8331186 Compare February 27, 2026 19:59
@ethanwinters ethanwinters marked this pull request as ready for review February 27, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Scroll positioning keeps resetting when scrolling through an actively streaming message response

1 participant