feat: add chat outline navigation strip#1917
feat: add chat outline navigation strip#1917Abhijitam01 wants to merge 13 commits intopingdotgg:mainfrom
Conversation
Minimap-style strip beside chat content showing user message bars. Hover to expand popover with previews, click to scroll to message. Active messages highlighted via IntersectionObserver.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
ApprovabilityVerdict: Needs human review 1 blocking correctness issue found. This PR introduces a new user-facing chat outline navigation feature with non-trivial complexity. An unresolved high-severity bug has been identified: the component lacks a You can customize Macroscope's approvability policy. Learn more. |
Use functional setState updaters to eliminate shared mutable Set between IntersectionObserver and MutationObserver, preventing race conditions. Batch removal updates into a single state update. Fix theme colors (bg-foreground instead of bg-white) and add max-h-40 scroll cap.
- Traverse nested children in removedNodes handler to find data-message-id on wrapper divs (mirrors the addedNodes traversal pattern) - Move popover outside the overflow-y-auto scrollable strip container to prevent CSS clipping
|
nothin happens when i click here: CleanShot.2026-04-11.at.11.07.05.mp4 |
|
its happening only when the messages passes the number 20 or from starting @juliusmarminge |
|
idk. just tested on a large thread as that's where i thought i could find some bugs but i tried clicking many of the "links" and none worked on this thread |
Wire scrollToIndex through a shared ref so clicking any outline bar scrolls to that message even when it's virtualized off-screen. Pre-compute message-id→row-index Map for O(1) lookups.
Messages in the last ~8 rows are rendered directly in the DOM (not tracked by the virtualizer). scrollToIndex with an out-of-bounds index silently fails for these. Fall back to scrollIntoView when the target index >= virtualizedRowCount.
issue - When you click message #25 in the outline panel, our code looks up its index (25) and tells the virtualizer “hey, scroll to row 25.” But the virtualizer only knows about rows 0–22. It has no idea what row 25 is, so it just… does nothing. No error, no scroll, nothing happens. Fix - |
- Remove behavior: "smooth" from scrollToIndex — conflicts with virtualizer's re-measure correction for distant items, causing silent scroll failures. Matches existing codebase pattern. - Return prev Set when IntersectionObserver fires without actual visibility changes, avoiding unnecessary re-renders during scroll.
- Remove unused OutlineEntry interface (dead code) - Scope MutationObserver removal handler to user-message elements only, matching the IntersectionObserver's observation scope - Skip no-op state updates in removal handler to prevent unnecessary re-renders when virtualizer recycles assistant rows
- Take upstream's LegendList approach, drop useVirtualizer - Adapt ChatOutlinePanel props (scrollContainer=null for now) - Remove duplicate /.well-known proxy entry in vite.config.ts
Null out hoverTimerRef after clearTimeout and inside the timeout callback to prevent orphaned timers when handleMouseLeave fires twice from both the wrapper and popover elements.
Replace hardcoded null props (scrollContainer={null}, onScrollToMessage={{
current: null }}) with a stable listRef prop backed by LegendList's ref.
- Derive scroll container via listRef.current.getScrollableNode()
- IntersectionObserver and MutationObserver now track visible messages
- Click-to-scroll uses querySelector on the real scroll container
- Stable ref prop no longer defeats React.memo on every render
| /> | ||
|
|
||
| {/* Chat outline strip — aligned to right edge of message content */} | ||
| <ChatOutlinePanel |
There was a problem hiding this comment.
🟠 High components/ChatView.tsx:3268
ChatOutlinePanel is rendered without a key prop, so it does not remount when the user switches threads. Its useEffect that captures the scroll container depends only on [listRef], and since the ref object identity is stable, the effect never re-runs after legendListRef.current is replaced. This leaves the component observing a detached DOM element; the IntersectionObserver, MutationObserver, and click handlers all target the wrong scroll container after a thread switch. Add key={activeThread.id} to force remount on thread change, matching the behavior of MessagesTimeline.
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/ChatView.tsx around line 3268:
`ChatOutlinePanel` is rendered without a `key` prop, so it does not remount when the user switches threads. Its `useEffect` that captures the scroll container depends only on `[listRef]`, and since the ref object identity is stable, the effect never re-runs after `legendListRef.current` is replaced. This leaves the component observing a detached DOM element; the IntersectionObserver, MutationObserver, and click handlers all target the wrong scroll container after a thread switch. Add `key={activeThread.id}` to force remount on thread change, matching the behavior of `MessagesTimeline`.
Evidence trail:
apps/web/src/components/ChatView.tsx lines 3268-3271 (ChatOutlinePanel without key), line 3243 (MessagesTimeline with key={activeThread.id}), line 701 (legendListRef = useRef). apps/web/src/components/chat/ChatOutlinePanel.tsx lines 41-52 (useEffect with [listRef] dependency), lines 58-78 (IntersectionObserver using scrollContainer as root), lines 88-116 (MutationObserver observing scrollContainer), lines 122-131 (click handler using scrollContainer.querySelector).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 558e8fe. Configure here.
The useEffect that resolves the scroll container from LegendList's ref
depended only on [listRef], which is a stable useRef that never changes
identity. When the user switches threads, MessagesTimeline remounts via
key={activeThread.id}, but the effect never re-ran, leaving
scrollContainer pointing at the old detached DOM node.
Adding timelineEntries to the dependency array ensures the effect
re-runs on thread switch since the entries change with each thread.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@juliusmarminge after alot of changes , it is error free , can you please have a look |
|
|
||
| // Observe any user-message element currently in the DOM | ||
| const observeUserMessages = (root: Element) => { | ||
| root.querySelectorAll('[data-message-role="user"]').forEach((el) => { |
|
#2348 is cleaner |



Minimap-style strip beside chat content showing user message bars. Hover to expand popover with previews, click to scroll to message. Active messages highlighted via IntersectionObserver.
What Changed
Add a minimap-style outline strip on the right side of the chat, beside the message content area:
Small horizontal bars representing each user message
Currently visible messages highlighted with brighter bars (IntersectionObserver)
Hover the strip to reveal a popover with message text previews
Click any bar or popover entry to smooth-scroll to that message
Only shows user messages, hidden when there are none
Why
Its a simple ui change to make the ux better
UI Changes
Before :
After :
Checklist
Note
Medium Risk
Adds a DOM-observer-driven UI overlay tied to the virtualized message list, which could introduce scroll/visibility bugs or performance regressions. Also tweaks primary environment WS URL derivation behavior, potentially affecting connection setup in unusual protocol cases.
Overview
Adds a minimap-style chat outline strip beside the message timeline that renders one marker per user message, highlights currently visible messages, and provides hover previews; clicking markers/previews smooth-scrolls to the corresponding message.
Wires the new
ChatOutlinePanelintoChatViewand adjusts layout positioning to support the overlay. Separately refactors primary environment target resolution by introducingderiveWsFromHttp()and using it for the window-originwsBaseUrl.Reviewed by Cursor Bugbot for commit 63e0b02. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add chat outline navigation strip with click-to-scroll and hover previews
IntersectionObserver, and supports click-to-scroll (smooth, centered) and hover-to-expand previews.timelineEntriesandlegendListRef; the chat column container is set toposition: relativeto anchor the absolutely positioned strip.deriveWsFromHttphelper in target.ts that converts HTTP(S) URLs to WS(S), replacing a prior code path that threw on unsupported protocols.Macroscope summarized 63e0b02.