Skip to content

Restore chat scroll affordances and add timeline minimap#3587

Open
juliusmarminge wants to merge 2 commits into
mainfrom
t3code/restore-chat-scroll-button
Open

Restore chat scroll affordances and add timeline minimap#3587
juliusmarminge wants to merge 2 commits into
mainfrom
t3code/restore-chat-scroll-button

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jun 28, 2026

Copy link
Copy Markdown
Member

Summary

  • Restores the chat scroll-to-end affordance with clearer live-edge behavior and updated labeling.
  • Prevents scroll-pill flicker during thread switches and ignores invalid composer height reads.
  • Adds a timeline minimap for jumping between user messages, with live in-view state and tooltip previews.
  • Introduces shared timeline logic for resolving LegendList end-state semantics and adds coverage for the new helper.

Testing

  • apps/web/src/components/chat/MessagesTimeline.test.tsx: added coverage for resolveTimelineIsAtEnd behavior.
  • Not run: project lint scripts.
  • Not run: vp check.
  • Not run: vp run typecheck.
  • Not run: broader app test suite beyond the added unit test.

Note

Medium Risk
Touches chat scroll anchoring and programmatic restore logic, where regressions could fight user scrolling or mis-show the live-edge pill; changes are localized to web chat UI with a small unit test for end-state resolution.

Overview
Adds a timeline minimap on medium+ viewports: one strip per user turn, in-view highlighting on scroll, hover previews, and animated jump to the row.

Live-edge detection now prefers LegendList isNearEnd via resolveTimelineIsAtEnd, so the scroll-to-end pill matches “near the bottom” instead of only strict isAtEnd. The pill is renamed to Scroll to end with accessibility labels; scrollToEnd immediately clears the pill and marks at-end.

ChatView scroll behavior is tightened: composer overlay height ignores zero reads; anchor offset restore is skipped after user wheel/touch/pointer scroll (generation counter) and only when scroll is still within ~2px of the saved offset.

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

Note

Restore chat scroll affordances and add a timeline minimap for user messages

  • Adds a left-side vertical minimap in MessagesTimeline.tsx that appears on medium+ screens when there are at least 2 user messages; each strip shows a compact preview and clicking it scrolls to that message.
  • Fixes anchor scroll restoration in ChatView.tsx so it no longer overrides user-initiated scrolls — restoration is skipped if the current offset has moved more than 2px from the saved offset or if the user has interacted with the scroll container.
  • Updates scrollToEnd to immediately hide the scroll-to-end pill and mark the list as at-end before scrolling, and renames the button label from 'Scroll to bottom' to 'Scroll to end'.
  • Introduces resolveTimelineIsAtEnd in MessagesTimeline.logic.ts so the is-at-end state reported to parents prefers isNearEnd over isAtEnd when available.

Macroscope summarized 8fdd7de.

- Prevent the scroll-to-end pill from flickering during thread switches
- Preserve anchor scroll position during user-driven scrolling
- Add timeline minimap navigation and live-edge detection
@coderabbitai

coderabbitai Bot commented Jun 28, 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

Run ID: 1b05286d-efde-4e1e-b4ab-16fa629d7d17

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/restore-chat-scroll-button

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

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jun 28, 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 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Inverted anchor scroll restore
    • Removed the inverted <= 2px pixel-threshold guard that caused scroll restoration to only fire when drift was negligible (a no-op) and skip when drift was large (when it was actually needed); the userScrollGeneration check already correctly handles user-initiated scroll suppression.

Create PR

Or push these changes by commenting:

@cursor push cb3843c97d
Preview (cb3843c97d)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -3273,14 +3273,7 @@
         settledTimelineAnchorRef.current === pending.messageId &&
         pending.userScrollGeneration === anchorUserScrollGenerationRef.current
       ) {
-        const list = legendListRef.current;
-        const currentScrollOffset = list?.getState().scroll;
-        if (
-          typeof currentScrollOffset === "number" &&
-          Math.abs(currentScrollOffset - pending.offset) <= 2
-        ) {
-          void list?.scrollToOffset({ offset: pending.offset, animated: false });
-        }
+        void legendListRef.current?.scrollToOffset({ offset: pending.offset, animated: false });
       }
     });
   }, []);

You can send follow-ups to the cloud agent here.

Math.abs(currentScrollOffset - pending.offset) <= 2
) {
void list?.scrollToOffset({ offset: pending.offset, animated: false });
}

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.

Inverted anchor scroll restore

Medium Severity

In onTimelineAnchorSizeChanged, scroll restoration only applies if the current scroll offset is within 2px of the saved position. This causes the viewport to jump instead of staying pinned to the anchored message when layout drift, common during streaming, shifts the scroll position by more than 2px.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit da040f6. Configure here.

@macroscopeapp

macroscopeapp Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new timeline minimap UI component and modifies scroll behavior logic, which constitutes new user-facing functionality requiring human review. Additionally, two unresolved medium-severity findings identify potential UX issues with scroll restoration and minimap overflow.

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

- Switch the minimap from a fixed-height rail to item-spaced positioning
- Preserve hover/focus width cues while keeping the scroll-jump targets visible

@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 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Minimap grows past viewport
    • Added TIMELINE_MINIMAP_MAX_HEIGHT (180px) cap so the minimap height never exceeds a bounded value, and when items exceed that cap the spacing compresses proportionally to keep all strips visible and clickable.

Create PR

Or push these changes by commenting:

@cursor push db100acdc9
Preview (db100acdc9)
diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts
--- a/apps/web/src/components/chat/MessagesTimeline.logic.ts
+++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts
@@ -11,6 +11,7 @@
 
 export const MAX_VISIBLE_WORK_LOG_ENTRIES = 1;
 export const TIMELINE_MINIMAP_ITEM_SPACING = 8;
+export const TIMELINE_MINIMAP_MAX_HEIGHT = 180;
 export const TIMELINE_MINIMAP_MIN_ITEMS = 2;
 
 export interface TimelineEndState {

diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx
--- a/apps/web/src/components/chat/MessagesTimeline.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.tsx
@@ -73,6 +73,7 @@
   type StableMessagesTimelineRowsState,
   type MessagesTimelineRow,
   TIMELINE_MINIMAP_ITEM_SPACING,
+  TIMELINE_MINIMAP_MAX_HEIGHT,
   TIMELINE_MINIMAP_MIN_ITEMS,
   type TimelineLatestTurn,
 } from "./MessagesTimeline.logic";
@@ -562,7 +563,10 @@
     return null;
   }
 
-  const minimapHeight = Math.max(1, (items.length - 1) * TIMELINE_MINIMAP_ITEM_SPACING);
+  const naturalHeight = Math.max(1, (items.length - 1) * TIMELINE_MINIMAP_ITEM_SPACING);
+  const minimapHeight = Math.min(naturalHeight, TIMELINE_MINIMAP_MAX_HEIGHT);
+  const effectiveSpacing =
+    items.length > 1 ? minimapHeight / (items.length - 1) : 0;
 
   return (
     <div
@@ -592,7 +596,7 @@
       <div className="relative w-10 select-none" style={{ height: minimapHeight }}>
         <div className="absolute top-0 left-3 h-full w-px bg-border/15" />
         {items.map((item, index) => {
-          const top = index * TIMELINE_MINIMAP_ITEM_SPACING;
+          const top = index * effectiveSpacing;
           return (
             <button
               aria-label={`Jump to message: ${item.userText ?? "User message"}`}

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 8fdd7de. Configure here.

}
`}
</style>
<div className="relative w-10 select-none" style={{ height: minimapHeight }}>

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.

Minimap grows past viewport

Medium Severity

Replacing the fixed minimap height with minimapHeight = (items.length - 1) * TIMELINE_MINIMAP_ITEM_SPACING lets the rail grow without a cap. The minimap stays vertically centered in the chat column, which lives under overflow-hidden, so strips for early and late user turns can be clipped and their jump buttons become unreachable on long threads.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8fdd7de. Configure here.

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant