Skip to content

feat(web): vertical minimap for user messages#2348

Open
akarabach wants to merge 22 commits intopingdotgg:mainfrom
akarabach:feat/chat-user-messages-minimap
Open

feat(web): vertical minimap for user messages#2348
akarabach wants to merge 22 commits intopingdotgg:mainfrom
akarabach:feat/chat-user-messages-minimap

Conversation

@akarabach
Copy link
Copy Markdown
Contributor

@akarabach akarabach commented Apr 25, 2026

Summary

When a user has a long chat - it make sense to jump between user messages and rollback to some point in time, minimap allows to do it very fast

  • Adds a compact minimap rail in the top-right of the chat that shows one dash per user message in the current thread.
  • Hovering the rail expands it into a dropdown-style list of message previews; clicking a row scrolls the conversation to that message.
  • The active dash tracks scroll position so you always know where you are in a long thread.
  • Internally event-driven (native scroll + Legend List lastPositionUpdate) — no rAF polling, no per-frame work when nothing changes.

Demo

Screenshot 2026-04-26 at 00 26 47 Screenshot 2026-04-26 at 00 26 53

What changed

File Purpose
apps/web/src/components/chat/ChatMinimap.tsx (new) The component itself: collapsed dashes strip, hover-expanded preview menu, click-to-navigate, active-dash tracking.
apps/web/src/components/chat/ChatMinimap.logic.ts (new) Pure helpers: selectUserMessageMinimapEntries(rows) and computeActiveMinimapIndex(state, entries). Kept separate so both can be unit-tested without DOM.
apps/web/src/components/chat/ChatMinimap.browser.tsx (new) 7 vitest-browser-react tests covering empty state, dash count, scroll-driven activation, thread reset, hover-to-open, click-to-navigate, mouse-leave collapse.
apps/web/src/components/ui/preview-card.tsx (new) Thin wrapper around @base-ui/react/preview-card. Used here for built-in delay/closeDelay hover handling — no manual timer plumbing.
apps/web/src/components/chat/MessagesTimeline.tsx Adds two lines: a useMemo selecting minimap entries from the rendered rows, and the <ChatMinimap> render. No state/refs/effects added to the timeline.
apps/web/src/components/chat/MessagesTimeline.logic.ts Re-exports the row type for consumers; no behavior change.
apps/web/src/components/chat/MessagesTimeline.logic.test.ts New unit tests for selectUserMessageMinimapEntries and computeActiveMinimapIndex (viewport-top threshold + advance-to-next refinement, empty/unmeasured guards, terminal-context preview fallback).

Architecture notes

Active-dash tracking is event-driven, not polled. Two listeners in ChatMinimap:

  • Native scroll on listRef.current.getScrollableNode() — fires once per frame while scrolling.
  • state.listen("lastPositionUpdate", …) — fires on Legend List (re)measurement. Required because scrollLength reads as 0 before the async layout pass, and we want to avoid the "first dash flashes active" jitter.

Both paths call the same pure computeActiveMinimapIndex helper. setActiveIndex short-circuits when the value is unchanged, so 60Hz scroll events don't translate into 60Hz React renders.

Encapsulation. MessagesTimeline owns nothing minimap-specific beyond const minimapEntries = useMemo(() => selectUserMessageMinimapEntries(rows), [rows]) and the JSX. All activation state lives in ChatMinimap.

Hover delays. Handled by Base UI's <PreviewCard> (delay={60} / closeDelay={150}) — no manual setTimeout refs, no cleanup-on-unmount effect.

Menu placement. The expanded menu replaces the dashes strip in the same DOM position (no Portal/Positioner) so the rail stays visually anchored to the top-right corner regardless of state.

Test plan

  • bun --cwd apps/web run typecheck — clean
  • bun --cwd apps/web run test — 919 passing
  • bun --cwd apps/web run test:browser — 148 passing (including 7 new ChatMinimap browser tests)
  • Manual smoke (reviewer):
    • Open a thread with 10+ user messages → dashes appear, last visible dash is active
    • Scroll up/down → active dash updates in sync
    • Hover the rail → menu expands; mouse-leave → menu collapses after a short delay
    • Click any preview → conversation scrolls to that message and menu closes
    • Switch threads → active highlight resets, menu closes if open
    • Resize to mobile width → rail still fits, menu width caps at 22rem

Out of scope / follow-ups

  • Keyboard navigation through the menu (arrow keys, Enter to navigate). Today the menu is hover/click only.
  • Showing assistant messages in the rail. Intentionally limited to user messages for v1 — they're the most useful navigation anchors.

Note

Medium Risk
Adds a new interactive chat navigation UI plus a new persisted client setting, which can affect layout and settings serialization across web/desktop if defaults or schema handling are incorrect.

Overview
Adds a chat minimap rail to MessagesTimeline that renders a right-side strip of dashes (sampled to a max of 10 with a +N overflow label) and expands on hover into a preview menu; clicking an item scrolls the underlying LegendList to that user message and the active item tracks scroll/remeasure events.

Introduces ChatMinimap + pure helpers in ChatMinimap.logic.ts (selectUserMessageMinimapEntries, selectVisibleMinimapEntries, computeActiveMinimapIndex), adds browser + unit test coverage for the new behaviors, and adds a PreviewCard wrapper for hover open/close delays.

Extends ClientSettingsSchema with hideChatMinimap (default false) and wires a toggle into Settings, updating related persistence tests to construct default settings via schema decoding.

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

Note

Add vertical minimap for user messages in the chat timeline

  • Adds a ChatMinimap component rendered to the right of the message list in MessagesTimeline.tsx, showing a vertical strip of dashes representing user messages.
  • Hovering the minimap opens an expanded PreviewCard menu listing user message previews; clicking one scrolls to that message via listRef.scrollToIndex.
  • Active dash tracks scroll position using computeActiveMinimapIndex, which accounts for partial measurements and end-of-list state.
  • When there are more entries than the display cap (10), visible dashes are sampled evenly between first and last, with a +N overflow indicator.
  • A hideChatMinimap boolean flag (default false) is added to ClientSettingsSchema and exposed as a toggle in the General settings panel.

Macroscope summarized 69510a1.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

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: 20976eb4-ec59-4719-8828-71f9de2de78a

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

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 and usage tips.

@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 25, 2026
@akarabach akarabach changed the title feat(web): chat minimap for user messages feat(web): vertical minimap for user messages Apr 25, 2026
Comment thread apps/web/src/components/ui/preview-card.tsx
Comment thread apps/web/src/components/chat/ChatMinimap.tsx Outdated
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented Apr 25, 2026

Approvability

Verdict: Needs human review

This PR introduces a complete new feature — a vertical minimap for navigating user messages in chat — including new React components, logic modules, browser tests, and a user-facing settings toggle. New features with this scope warrant human review. There is also an unresolved comment about listener reattachment during streaming that should be addressed.

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

Comment thread apps/web/src/components/chat/ChatMinimap.tsx
- Wrap expanded menu in an outer div carrying rounded-lg + overflow-hidden so the
  inner scrollbar no longer paints over the rounded corners on the right side.
- Reduce menu corner radius from rounded-xl to rounded-lg to match other popover
  surfaces (combobox, menu, select).
- Add 8px of asymmetric right padding to the chat list so the minimap has more
  visual breathing room from right-aligned user messages.
…tener

Optional chaining on `getState?.()` only protects against `getState` being
undefined, not against it returning undefined. Add a second `?.` so the
`.listen` access short-circuits cleanly when the list isn't yet measured.
@juliusmarminge
Copy link
Copy Markdown
Member

#1917 added this feature but was buggy last time i tested it. haven't checked it in a while

@akarabach akarabach closed this Apr 27, 2026
@akarabach akarabach reopened this Apr 27, 2026
@akarabach
Copy link
Copy Markdown
Contributor Author

#1917 added this feature but was buggy last time i tested it. haven't checked it in a while

I reviewed it a bit, and I think this PR is better because:

  1. it uses default shadcn component for overlay menu
  2. don't do any heavy (or lets say any) DOM query lookcups
  3. dont add new observers
  4. using legendapp onscroll

Comment thread apps/web/src/components/chat/ChatMinimap.logic.ts
@juliusmarminge
Copy link
Copy Markdown
Member

Overflows on long threads:
CleanShot 2026-04-27 at 09 31 01@2x

@juliusmarminge
Copy link
Copy Markdown
Member

but works very well other than that!

- Sample minimap dashes to fit the available column height
- Add a setting to hide the chat minimap and restore it cleanly
- Cover the new minimap selection behavior with tests
Comment thread apps/web/src/components/chat/ChatMinimap.logic.ts
- Decode `ClientSettingsSchema` in desktop and web persistence tests
- Simplify chat minimap trigger and strip layout so it can collapse correctly
Comment thread apps/web/src/components/chat/ChatMinimap.logic.ts
- Prevent stale or sparse minimap indices from skipping entries
- Render dash markers as spans instead of buttons
- Add regression tests for clamping and unmeasured gaps
Comment thread apps/web/src/components/chat/ChatMinimap.tsx
@akarabach
Copy link
Copy Markdown
Contributor Author

Overflows on long threads: CleanShot 2026-04-27 at 09 31 01@2x

Im on it

akarabach and others added 2 commits April 27, 2026 21:27
- Cap the initial unmeasured minimap render to avoid overflowing long threads
- Update minimap logic tests for short and long threads
@akarabach
Copy link
Copy Markdown
Contributor Author

akarabach commented Apr 27, 2026

Screenshot 2026-04-27 at 21 43 24 @juliusmarminge wdyt? 80 is nice, but, maybe amount of messages is better

I have one more fix, please let me know that u are not working on this branch right now ;)

A short final prompt with no content below it never lets its top reach
the viewport top, so the existing viewport-top rule kept an earlier
prompt lit while the reader was plainly looking at the latest. Add an
`isAtEnd` flag to the minimap state snapshot and short-circuit
`computeActiveMinimapIndex` to the last entry when it's true.

Adds two unit tests: the canonical at-end case, and a sanity-check
that `isAtEnd=false` falls through to the existing viewport-top rule.
…akarabach/t3code into feat/chat-user-messages-minimap

# Conflicts:
#	apps/web/src/components/chat/ChatMinimap.logic.ts
#	apps/web/src/components/chat/MessagesTimeline.logic.test.ts
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Apr 28, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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 and found 2 potential issues.

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 4447b00. Configure here.

Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
],
);
const rows = useStableRows(rawRows);
const minimapEntries = useMemo(() => selectUserMessageMinimapEntries(rows), [rows]);
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 entries reattach listeners every streaming chunk

Low Severity

selectUserMessageMinimapEntries allocates fresh entry objects on every call, so minimapEntries gets a new array identity whenever any row in useStableRows changes — including each assistant streaming chunk where user messages are unchanged. Because entries is in the active-tracking effect's dependency list, the native scroll listener and lastPositionUpdate subscription tear down and re-attach on every chunk, undermining the PR's "no per-frame work when nothing changes" claim. Stabilizing identity (e.g., shallow-equal memo on the entry list) would avoid the thrash.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4447b00. 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: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