Skip to content

fix/restore thread scroll position - #5552

Open
bansalarnav wants to merge 4 commits into
pingdotgg:mainfrom
bansalarnav:fix/restore-thread-scroll-position
Open

fix/restore thread scroll position#5552
bansalarnav wants to merge 4 commits into
pingdotgg:mainfrom
bansalarnav:fix/restore-thread-scroll-position

Conversation

@bansalarnav

@bansalarnav bansalarnav commented Aug 6, 2026

Copy link
Copy Markdown

What Changed

Small changes to scroll behavior -

  1. Threads remember scroll position. If you scroll up in a thread, go to another thread, and come back you're at the scroll position you left
  2. Completed threads that you're opening for the first time are at the top of the agent response instead of the bottom

Why

I work with multiple threads quite a bit and it's annoying to scroll up every single time

UI Changes

t3codepr.mp4

(sorry in advance for the crappy video)

Checklist

  • [ -] This PR is small and focused
  • [-] I explained what changed and why
  • [-] I included before/after screenshots for any UI changes
  • [-] I included a video for animation/interaction changes

AI Description -

Restores each web thread’s timeline scroll position when navigating away and back. Active turns reopen at the current response, while manual scrolling is preserved and opts out of auto-follow so streaming updates don’t pull the user away from their chosen position.


Note

Medium Risk
Touches core chat scrolling, LegendList lifecycle, and auto-follow during streaming; regressions could mis-position threads or fight user scroll, but no auth or data changes.

Overview
Per-thread scroll memory — The chat timeline now keeps each thread’s scroll offset in module-level state (keyed by scoped thread key) so switching threads and returning restores where you left off; a full page reload still resets positions.

Smarter initial placement — On open, resolveTimelineEntryScrollMode chooses follow-end, restore saved offset, or anchor to the user message for the active/latest turn. First-time opens of completed threads land at the top of the agent response instead of the bottom; threads with a running turn anchor to that response unless you had manually scrolled that same turn.

Manual scroll vs auto-follow — User navigation (wheel, touch, pointer on the scroll surface, and arrow/page/home/end/space keys) records a manual position and opts out of live-follow. MessagesTimeline accepts initialScrollOffset and reports onScrollOffsetChange; the list remount key is routeThreadKey so restoration applies on thread switches. Sending a message or using “Scroll to end” still jumps to the live edge and clears the preserved-response mode.

Reviewed by Cursor Bugbot for commit a7cd406. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Restore thread scroll position and detect manual navigation in chat timeline

  • Adds a module-scoped timelineScrollPositionByThreadKey map in ChatView.tsx to persist each thread's scroll offset across route changes and restore it on return.
  • Introduces resolveTimelineEntryScrollMode to compute whether the timeline should follow the end, restore a saved offset, or anchor to a turn response, based on running/latest turn state.
  • Detects manual navigation via wheel, touch, pointer, and keyboard events; cancels live-follow when the user scrolls manually, and resumes follow-end when sending a new message.
  • Extends MessagesTimeline.tsx with initialScrollOffset and onScrollOffsetChange props so the parent can seed and track scroll position.
  • Behavioral Change: threads no longer always open at the bottom — returning to a thread restores the previous scroll position, and threads with an in-progress turn anchor to the latest response rather than auto-following until the user scrolls to the end.

Macroscope summarized a7cd406.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 167536dc-5e36-494b-b6ca-bf34dad0a240

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread apps/web/src/components/ChatView.tsx
onClick={() => {
beginTimelineManualNavigation();
scrollToEnd(true);
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scroll pill saves partial offset

Medium Severity

The “Scroll to end” control calls beginTimelineManualNavigation before the animated scrollToEnd. That arms manual-scroll tracking, so the first onTimelineScrollOffsetChange where the offset moves more than 0.5px from the starting value persists a manual position at an in-between offset. Leaving the thread before the animation finishes restores the wrong scroll position on return.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6705a75. Configure here.

timelineScrollPositionByThreadKey.set(
routeThreadKey,
existing?.kind === "manual" ? { ...existing, offset } : { kind: "automatic", offset },
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early scroll overwrites saved offset

High Severity

onTimelineScrollOffsetChange writes every scroll offset into timelineScrollPositionByThreadKey with no entry/restore guard. MessagesTimeline also invokes the same callback from a requestAnimationFrame handleScroll when rows mount or change, which can run before initialScrollOffset is applied. A transient offset (often near zero) can overwrite the saved position so the next visit restores the wrong place.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6705a75. Configure here.

Comment thread apps/web/src/components/ChatView.tsx
@macroscopeapp

macroscopeapp Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new scroll position restoration logic with module-level state management. There are 3 unresolved review comments identifying potential bugs, including a high-severity race condition where transient scroll offsets may overwrite saved positions.

You can customize Macroscope's approvability policy. Learn more.

@ChristmasSun

Copy link
Copy Markdown

+1 on this finding it very annoying that scroll is not preserved when navigating across threads as it is in Codex app

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 7, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a7cd406. Configure here.

return { kind: "restore-position", offset: input.savedPosition.offset };
}
return input.latestTurnId
? { kind: "anchor-response", turnId: input.latestTurnId }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automatic offset blocks response anchor

Medium Severity

The new anchor-response path for latestTurnId is skipped whenever any saved timeline position exists, including kind: "automatic" offsets written during initial list layout. After a brief first visit that only auto-settled at the bottom, returning to the same thread restores that offset instead of anchoring at the latest response top described in the PR.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a7cd406. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants