Skip to content

Add native keyboard scrolling to the chat history - #9

Open
neo773 wants to merge 1 commit into
zeronsh:mainfrom
neo773:feat/transcript-keyboard-scrolling
Open

Add native keyboard scrolling to the chat history#9
neo773 wants to merge 1 commit into
zeronsh:mainfrom
neo773:feat/transcript-keyboard-scrolling

Conversation

@neo773

@neo773 neo773 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

What

Keyboard control for the transcript, tuned to feel like a native macOS scroll view — no one should be able to tell this is a custom GPUI renderer.

  • ↑/↓ — tap for one crisp line step (~50ms, Chrome-parity); hold for continuous scrolling that ramps to cruise speed and stops on key-up. OS key-repeats are treated as hold-liveness signals, not scroll quanta, so there's no line…pause…line-line stutter.
  • ⌥↑/⌥↓ — glide between segments (the rail's user-prompt ticks), riding the same animation as a rail-tick click. Past the last segment it hands off to the scroll-to-bottom spring and re-engages autoscroll.
  • PageUp/PageDown — page scrolling everywhere, even while typing (Messages-style). Home/End, ⌘↑/⌘↓ — top/bottom when the chat holds focus.
  • Focus handles itself — click into the chat to scroll with bare arrows; arrows fall through from an empty composer; typing while the chat is focused hops back to the composer with the keystroke; Escape returns to the composer. ⌘C still copies the markdown selection.

How

  • Every keyboard animation is frame-driven from render via window.on_next_frame — vsync-locked, 120Hz on ProMotion — never wall-clock timer loops. The rail-click glide moved onto the same driver.
  • The row glide gets an overtime landing: when its fixed timeline expires short (tall unmeasured rows, e.g. 20–40 line prompts), it closes the remainder with a quick exponential tail instead of hard-snapping.
  • The stick-to-bottom spring gets a terminal phase (minimum closing velocity) so its asymptotic tail can't leave "at bottom" consumers (rail active tick) lagging a visually finished scroll; it lands in the glued representation the app already keys off.
  • Bottom re-pinning is intent-based: only deliberate downward keyboard motion re-engages the pin — upward motion never can. Wheel input always cancels any keyboard/rail glide.
  • Reduced-motion snaps instantly throughout.

Testing

  • cargo test -p comet-ui: 313 passed.
  • Hand-tested feel: tap/hold arrows from every state (pinned bottom, mid-history, top edge), segment jumps across tall prompts, wheel interrupts, streaming autoscroll handoff, empty vs non-empty composer, focus round-trips.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Arrow keys drive the transcript like a native macOS scroll view:

- Tap Up/Down: one crisp line step (~50ms ease-out, Chrome-parity).
  Holding scrolls continuously from the first frame — a velocity-fed
  hold with a spin-up ramp, ended by key-up — instead of stuttering
  through OS key-repeat quanta. Key-repeats serve only as
  hold-liveness signals.
- Option+Up/Down: glide between segments (user prompts, the rail's
  tick anchors), reusing the rail-click glide; past the last segment
  it hands off to the jump-to-bottom spring and re-engages autoscroll.
- PageUp/PageDown work everywhere (Messages-style, even while
  typing); Home/End and Cmd+Up/Down go top/bottom when the chat
  holds focus.
- Focus takes care of itself: click into the chat to scroll with
  bare arrows; empty-composer arrows fall through to the chat; typing
  a character while the chat is focused hops focus back to the
  composer with the keystroke; Escape returns to the composer.
  Cmd+C still copies the markdown selection when the chat has focus.

Mechanics worth noting:

- All keyboard animation is frame-driven from render via
  window.on_next_frame (vsync-locked, 120Hz on ProMotion), never
  timer loops; the rail-click glide moved onto the same driver.
- The row glide gets an overtime landing: when its timeline expires
  short (tall unmeasured rows — 20-40 line prompts), it closes the
  remainder with an exponential tail instead of hard-snapping.
- The stick-to-bottom spring gets a terminal phase (minimum closing
  velocity) so its asymptotic tail can't leave "at bottom" state
  lagging a visually finished scroll, and it lands in the glued
  representation the rest of the app keys off.
- Re-pinning to the bottom is intent-based: only deliberate downward
  keyboard motion re-engages the pin; upward motion never does.
  Wheel input always cancels any keyboard/rail glide.

Co-Authored-By: Claude Fable 5 <[email protected]>
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.

1 participant