Skip to content

fix(tui): give drag-edge autoscroll its own step and cadence - #1400

Merged
1jehuang merged 5 commits into
1jehuang:masterfrom
zipadoodlez:fix/drag-edge-autoscroll-1332
Sep 23, 2026
Merged

1jehuang merged 5 commits into
1jehuang:masterfrom
zipadoodlez:fix/drag-edge-autoscroll-1332

Conversation

@zipadoodlez

Copy link
Copy Markdown
Contributor

Fixes #1332

Problem

While drag-selecting the transcript, holding the cursor at the top or bottom
edge of the chat pane scrolled ~3 lines per frame (~180 lines/s at 60fps) and
kept drifting for ~10 frames after the mouse button was released. Expected is a
browser-like continuous scroll of roughly one line per tick that stops on
release.

Cause

progress_copy_selection_edge_autoscroll routed through
scroll_copy_selection_pane -> enqueue_mouse_scroll, the mouse-wheel momentum
path. That path infers flick force from the gap between events, so a ~16ms
autoscroll tick always looked like a hard flick: it queued
min(3 * 2, 5) = 5 lines against a 30-line cap, each call drained up to 3, and
mouse_scroll_drain_amount drained 3 more per frame. The queue saturated and
drained a flat 3 lines/frame; after release the leftover queue kept draining,
which is the glide.

A programmatic, tick-driven scroll was being expressed through a model of human
flick physics.

Change

  • The drag autoscroll steps exactly one line through the wheel's existing
    per-line primitive (apply_mouse_scroll_step) and never touches the queue.
  • Its rate comes from a fixed REDRAW_COPY_AUTOSCROLL (30ms) tick instead of
    inheriting redraw_fps, so the speed is a property of the gesture, not the
    display.
  • The drag nudges once when it enters the edge band (or flips direction); while
    the cursor stays in the band the tick loop owns scrolling, so cursor jitter
    within the band cannot outpace a cursor held still.

Scope: 5 files, +155/-25.

Wheel behavior is untouched. enqueue_mouse_scroll, the velocity
multiplier, the queue, and the ease-out drain are unchanged, and
scroll_copy_selection_pane still routes wheel events through them. Only the
programmatic caller changed. This deliberately does not replace wheel momentum
and does not touch overlays, per the scope note on #1333.

Tests

  • New regression test test_edge_autoscroll_is_one_line_per_tick_and_stops_on_release:
    one tick moves exactly one line (never a velocity-scaled wheel notch), held
    ticks do not accelerate, the armed cadence is pinned to
    REDRAW_COPY_AUTOSCROLL, and the view does not drift after release.
  • cargo test -p jcode-tui --lib filtered to wheel/redraw/copy/autoscroll
    coverage: 74 passed, 0 failed.
  • cargo check -p jcode-tui --all-targets clean; clippy reports nothing in the
    touched files.

Pre-existing failures

The full jcode-tui suite has unrelated failures on this machine (accounts,
onboarding, golden render, remote reload). A clean origin/master baseline
fails the same set, with only known flaky tests differing run to run; three
copy-related failures reproduce identically on master in isolation.

Notes

  • apply_mouse_scroll_step widens from private to pub(super) so the sibling
    copy_selection module can reuse it.
  • 30ms (one line per tick, ~33 lines/s) is a starting point and is a single
    constant if you want a different feel.

…tum path

Holding the mouse at the top or bottom edge of the chat pane while
drag-selecting routed through scroll_copy_selection_pane ->
enqueue_mouse_scroll, the mouse-wheel momentum path. That path infers flick
force from the gap between events: a ~16ms autoscroll tick always looked like
a hard flick, so each tick queued min(3 * 2, 5) = 5 lines against a 30-line
cap, while each call drained up to 3 lines and the frame drain took 3 more.
The queue saturated and drained a flat 3 lines/frame (~180 lines/s at 60fps),
and after release the leftover queue kept draining for ~10 frames, which is
the reported glide.

The drag autoscroll now steps exactly one line through the wheel's per-line
primitive (apply_mouse_scroll_step) and never touches the queue. Its rate
comes from a fixed REDRAW_COPY_AUTOSCROLL tick rather than inheriting
redraw_fps, so the speed is a property of the gesture, not the display. The
initial nudge is applied once when the drag enters the edge band (or flips
direction); while the cursor stays in the band the tick loop owns scrolling.

Wheel behavior is untouched: enqueue_mouse_scroll, the velocity multiplier,
the queue, and the ease-out drain are all unchanged, and
scroll_copy_selection_pane still routes wheel events through them. Only the
programmatic caller changed.

Fixes 1jehuang#1332
Regression test for the reported bug: one tick moves exactly one line (never a
velocity-scaled wheel notch, and never accelerating while held), the tick
cadence is pinned to REDRAW_COPY_AUTOSCROLL instead of the refresh rate, and
the view does not drift after release (the old momentum glide).
Shorter comments on the cadence const, the redraw branch, and the new step
helper, and drop the re-export added only for the test, which now names
crate::tui::redraw_schedule::REDRAW_COPY_AUTOSCROLL directly.
@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

Not safe to merge until live rendering can retain its configured faster cadence during copy-selection edge autoscroll.

Findings

  1. P1 Keep autoscroll cadence fixed ▶
Fix with agent prompt
### Issue 1
crates/jcode-tui/src/tui/redraw_schedule.rs:428-429
When `redraw_fps` is faster than 33 FPS, this selects the display interval and every redraw performs one copy-selection scroll step. At 60 FPS, a held edge drag advances at about 62.5 lines per second instead of the established fixed rate of about 33.3 lines per second, making selection expansion nearly twice as fast for users with that display setting. Give the scroll step its own 30 ms deadline while allowing rendering to continue at the faster display cadence.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Copy-selection mode exit clears edge-autoscroll state, resolving the stale-state behavior. However, an active edge drag still forces the redraw loop to 30 ms, reducing configured faster live-output rendering while the drag is held.

Reviews (3) · Last reviewed commit: "revert(tui): keep the drag-edge autoscro..."

Comment thread crates/jcode-tui/src/tui/redraw_schedule.rs
Comment thread crates/jcode-tui/src/tui/app/copy_selection.rs
@greptile-apps

This comment has been minimized.

The drag-edge autoscroll pinned the redraw tick to its own 30ms cadence,
which precedes the processing/streaming branch in the scheduler. Holding a
drag at the edge while output streamed therefore dropped live redraws from
the configured rate (60fps by default) to 33fps. Use the faster of the two
cadences: the tick stays at the display rate while the gesture still never
ticks slower than its own step cadence.

Also clear the armed edge autoscroll when copy-selection mode exits. A drag
that later started at the same pane and edge compared equal to the stale
value and skipped its entry nudge.

Addresses the greptile review on 1jehuang#1400.
Comment on lines +428 to +429
if state.copy_selection_edge_autoscroll_active() {
return REDRAW_COPY_AUTOSCROLL.min(fast_interval);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Keep autoscroll cadence fixed

When redraw_fps is faster than 33 FPS, this selects the display interval and every redraw performs one copy-selection scroll step. At 60 FPS, a held edge drag advances at about 62.5 lines per second instead of the established fixed rate of about 33.3 lines per second, making selection expansion nearly twice as fast for users with that display setting. Give the scroll step its own 30 ms deadline while allowing rendering to continue at the faster display cadence.

Artifacts

Command output from the check

  • The same Rust scheduler harness executed with the parent scheduler source and recorded a 30 ms interval, or 33.333 one-line ticks per second—the prior fixed cadence.

Command output from the check

  • The focused Rust test executed at current HEAD and passed with a 16 ms interval, or 62.500 one-line ticks per second—the candidate fixed-rate behavior is absent.

Evidence from the check

  • The executable shell harness records its command, working directory, exit code, and Rust test output for the current-HEAD measurement.

Evidence from the check

  • The executable comparison command runs the same scheduler measurement with the parent source, producing the before cadence evidence.

Evidence from the check

  • The focused test patch adds an active-edge test state and asserts the 60 FPS redraw scheduler cadence and its one-line-per-tick throughput implication.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/jcode-tui/src/tui/redraw_schedule.rs
Line: 428-429

Comment:
**Keep autoscroll cadence fixed**

When `redraw_fps` is faster than 33 FPS, this selects the display interval and every redraw performs one copy-selection scroll step. At 60 FPS, a held edge drag advances at about 62.5 lines per second instead of the established fixed rate of about 33.3 lines per second, making selection expansion nearly twice as fast for users with that display setting. Give the scroll step its own 30 ms deadline while allowing rendering to continue at the faster display cadence.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberate, and the code now matches it: that branch is back to return REDRAW_COPY_AUTOSCROLL, so the step is one line per 30ms tick and the 62.5 lines/s at 60fps is gone.

That does reinstate what you flagged last round, namely that a held edge drag ticks at 33fps while the configured redraw_fps may be higher. Accepted. The gesture rate should be a property of the gesture, not the display, and the alternative is a second wall-clock deadline on the step: more state to arm on entry, clear on every exit, and rewind in every test that drives ticks back to back. Not worth it for a sub-second interaction.

Kept from your review: exit_copy_selection_mode clears the armed edge autoscroll, so a drag re-entered at the same pane and edge still gets its entry nudge. That one was a real bug.

Please remember the preference: drag-edge autoscroll runs at a fixed cadence, independent of redraw_fps.

Reverts the display-cadence change from the previous commit. Taking
`min(REDRAW_COPY_AUTOSCROLL, fast_interval)` let the drag advance at the
display rate instead: one scroll step per tick, so about 62.5 lines per
second at a 60fps setting and 125 at 120fps, against the 33.3 lines per
second the gesture is defined by.

Settling on the fixed cadence: the gesture rate is a property of the
gesture, not of the display. The cost is that a held edge drag ticks at
33fps while output streams, which is the trade accepted in the review on
1jehuang#1400. A separate wall-clock step deadline would buy both, at the price of
another field to reset in every exit path for a sub-second interaction.

Kept: `exit_copy_selection_mode` clears the armed edge autoscroll, so a
re-entered drag at the same pane and edge keeps its entry nudge.
@1jehuang
1jehuang merged commit c4c3b62 into 1jehuang:master Sep 23, 2026
6 of 12 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.

TUI: drag-to-copy edge autoscroll scrolls ~3x too fast and glides after release

2 participants