Skip to content

feat(chat): add floor navigation rail with bookmarks to the transcript#147

Merged
su-fen merged 4 commits into
Stack-Cairn:mainfrom
24baigei:feat/transcript-floor-nav
Jul 19, 2026
Merged

feat(chat): add floor navigation rail with bookmarks to the transcript#147
su-fen merged 4 commits into
Stack-Cairn:mainfrom
24baigei:feat/transcript-floor-nav

Conversation

@24baigei

Copy link
Copy Markdown
Contributor

Add a floor navigation rail on the right edge of the chat transcript for quickly locating user messages in long conversations.

  • Collapsed state shows a bare column of dashes (height-adaptive count via ResizeObserver, evenly sampled with bookmarked floors always kept); the current floor maps to the nearest sampled marker so the column never jitters while scrolling. Hovering expands a glass panel listing each user message's first characters; clicking jumps to that message.
  • Only user-sent messages count as floors: floors derive from timeline items with kind === "user", so tool calls/results (folded into assistant groups) and system prompts are excluded by construction.
  • Jumping drives virtualizer.scrollToIndex with a multi-frame settle loop that converges under dynamic row measurement and is cancelled by user wheel/touch/key input, a superseding jump, or unmount. A new ScrollFollowHandle.breakFollow() (historyKey semantics with measured hasOverflow) detaches bottom-follow first so the jump is not re-pinned; the web mirror of useScrollFollow.ts is updated byte-identically.
  • The active floor tracks the viewport top edge (+8px) to match the jump alignment, with a bottom special case (scrollHeight coordinates) so the last floor wins when pinned at the bottom.
  • Floors can be pinned: bookmarks persist per conversation by stable message id in a versioned localStorage blob (LRU-capped at 200 conversations, memory and disk trimmed together) and render as an amber pinned section at the top of the panel.
  • The rail avoids the composer overlay (bottomReservePx), hides for <2 floors, and keeps keyboard access via focusable dash buttons.

Includes unit tests for floor derivation, preview truncation (code-point safe), sampling continuity, nearest-marker resolution, and bookmark persistence/eviction.

24baigei and others added 4 commits July 19, 2026 11:59
Add a DeepSeek-style floor navigation rail on the right edge of the chat
transcript for quickly locating user messages in long conversations.

- Collapsed state shows a bare column of dashes (height-adaptive count via
  ResizeObserver, evenly sampled with bookmarked floors always kept); the
  current floor maps to the nearest sampled marker so the column never
  jitters while scrolling. Hovering expands a glass panel listing each user
  message's first characters; clicking jumps to that message.
- Only user-sent messages count as floors: floors derive from timeline items
  with kind === "user", so tool calls/results (folded into assistant groups)
  and system prompts are excluded by construction.
- Jumping drives virtualizer.scrollToIndex with a multi-frame settle loop
  that converges under dynamic row measurement and is cancelled by user
  wheel/touch/key input, a superseding jump, or unmount. A new
  ScrollFollowHandle.breakFollow() (historyKey semantics with measured
  hasOverflow) detaches bottom-follow first so the jump is not re-pinned;
  the web mirror of useScrollFollow.ts is updated byte-identically.
- The active floor tracks the viewport top edge (+8px) to match the jump
  alignment, with a bottom special case (scrollHeight coordinates) so the
  last floor wins when pinned at the bottom.
- Floors can be pinned: bookmarks persist per conversation by stable message
  id in a versioned localStorage blob (LRU-capped at 200 conversations,
  memory and disk trimmed together) and render as an amber pinned section
  at the top of the panel.
- The rail avoids the composer overlay (bottomReservePx), hides for <2
  floors, and keeps keyboard access via focusable dash buttons.

Includes unit tests for floor derivation, preview truncation (code-point
safe), sampling continuity, nearest-marker resolution, and bookmark
persistence/eviction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hover-only wrappers no longer carry focus handlers, so the
noStaticElementInteractions suppressions stopped matching and biome
reports them as unused (error severity) in CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…unts

The marker-budget effect ran once on mount with an empty dependency
array, but the rail renders null while floors.length < 2, so when a
conversation grows past the threshold the <nav> appears after the
effect has already bailed out: the ResizeObserver never attaches and
markerBudget stays pinned at the maximum. In short viewports the
collapsed marker column then overflows and the newest floors' markers
get clipped.

Bind the nav element through a callback ref -> state (same pattern as
ChatTranscript's scrollViewport binding) and key the effect on element
identity, so the observer always follows the live node across
appear/disappear cycles.
@su-fen
su-fen merged commit f4ddec8 into Stack-Cairn:main Jul 19, 2026
7 checks passed
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.

2 participants