Skip to content

fix(transcript): persist the scroll offset alongside the at-bottom flag - #1348

Merged
chr1syy merged 3 commits into
RunMaestro:rcfrom
chr1syy:fix/y1-transcript-scroll-restore
Aug 5, 2026
Merged

fix(transcript): persist the scroll offset alongside the at-bottom flag#1348
chr1syy merged 3 commits into
RunMaestro:rcfrom
chr1syy:fix/y1-transcript-scroll-restore

Conversation

@chr1syy

@chr1syy chr1syy commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Closes finding Y1.

Problem

A deliberately scrolled-up transcript snapped back to the live bottom after an agent swap, instead of holding the position the user chose. This is the inverse of J1/O1 (#1323), which were about failing to reach the bottom.

useTerminalOutputScroll restores a saved offset only when both initialScrollTop > 0 and initialIsAtBottom === false are persisted. The two halves were written on different schedules:

So a user who scrolled up and swapped within ~200ms persisted isAtBottom: false with no matching scrollTop. The restore was skipped and the mount-time bottom jump won.

Fix

Persist the offset alongside the synchronous at-bottom transition, so both halves are written together and can never disagree. This deliberately does not flush the debounce on unmount - #1323 rejected that explicitly, and its reasoning still holds.

Testing

Scoped tests only. Adds 208 lines to useTerminalOutputScroll.test.ts, including a test that pins the race by unmounting before the 200ms debounce fires. Existing #1323 follow-the-bottom coverage and the #1140 scroll-event guard bookkeeping are untouched.

Human verification

Confirmed on a local RC build: following the bottom, swapping away and back, still lands at the live bottom (the O1 behaviour must not regress).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved transcript scroll-position preservation when switching agents after scrolling up.
    • Restored saved scroll offsets, including position 0, when returning to a transcript.
    • Kept saved position and bottom-of-transcript state synchronized during scrolling.
    • Prevented unintended position persistence when the user has not manually scrolled.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f9dd5e2e-5705-4c6f-b346-ed6849acf719

📥 Commits

Reviewing files that changed from the base of the PR and between bcf44a1 and cd50b83.

📒 Files selected for processing (2)
  • src/__tests__/renderer/components/TerminalOutput/useTerminalOutputScroll.test.ts
  • src/renderer/components/TerminalOutput/hooks/useTerminalOutputScroll.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/components/TerminalOutput/hooks/useTerminalOutputScroll.ts
  • src/tests/renderer/components/TerminalOutput/useTerminalOutputScroll.test.ts

📝 Walkthrough

Walkthrough

The terminal scroll hook now saves scrollTop synchronously with bottom-state changes. Restoration supports saved offset 0. Tests cover persistence, debounce cleanup, agent swaps, and remount restoration.

Changes

Terminal scroll persistence

Layer / File(s) Summary
Synchronous scroll-state persistence
src/renderer/components/TerminalOutput/hooks/useTerminalOutputScroll.ts, src/__tests__/renderer/components/TerminalOutput/useTerminalOutputScroll.test.ts
The hook persists scrollTop with bottom-state changes. Tests cover debounce behavior, cleanup, continued scrolling, and no-scroll cases.
Scroll restoration from saved offsets
src/renderer/components/TerminalOutput/hooks/useTerminalOutputScroll.ts, src/__tests__/renderer/components/TerminalOutput/useTerminalOutputScroll.test.ts
Restoration accepts offset 0 when initialIsAtBottom is false. Tests cover fast swaps, paused restoration, and bottom-state gating.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes persisting the scroll offset with the at-bottom flag, which is the pull request's main change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown

Greptile Summary

The PR synchronously persists a transcript’s scroll offset whenever its at-bottom state changes, keeping the two saved values coherent across fast agent swaps.

  • Adds transition-time offset persistence while retaining debounced refinement updates.
  • Expands hook tests for fast unmounts, boundary transitions, refinements, and remount restoration.
  • Leaves the valid zero-offset restoration case unhandled.

Confidence Score: 4/5

The zero-offset restoration defect should be fixed before merging because users who deliberately scroll to the transcript’s absolute top can still be snapped to the live bottom after a fast swap.

The new synchronous write correctly preserves positive offsets, but it can persist zero with isAtBottom false while the existing restore condition accepts only offsets greater than zero.

Files Needing Attention: src/renderer/components/TerminalOutput/hooks/useTerminalOutputScroll.ts

Important Files Changed

Filename Overview
src/renderer/components/TerminalOutput/hooks/useTerminalOutputScroll.ts Adds coherent transition-time persistence, but a saved top-of-transcript offset of zero still fails the remount restore gate.
src/tests/renderer/components/TerminalOutput/useTerminalOutputScroll.test.ts Adds extensive race and restoration coverage, but all restoration cases use positive offsets and omit the valid zero-offset boundary.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[User scrolls] --> B{At-bottom state changed?}
    B -- Yes --> C[Persist flag and offset synchronously]
    B -- No --> D[Debounce offset refinement]
    C --> E[Agent swap and unmount]
    D --> E
    E --> F{Saved offset greater than zero?}
    F -- Yes --> G[Restore saved position]
    F -- No --> H[Skip restore and jump to bottom]
Loading

Reviews (1): Last reviewed commit: "MAESTRO: persist the scroll offset along..." | Re-trigger Greptile

// mount-time bottom jump snaps the user back down. Writing both
// halves in the same tick, in both directions, keeps the saved pair
// coherent no matter when the component goes away. (Y1)
onScrollPositionChange?.(scrollTop);

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 Zero offset remains unrestorable

When a user scrolls to the absolute top of an overflowing transcript, this call persists scrollTop = 0 with isAtBottom = false, but the remount path requires initialScrollTop > 0; restoration is therefore skipped and the transcript snaps to the live bottom.

chr1syy added a commit to chr1syy/Maestro that referenced this pull request Aug 4, 2026
…eview)

Greptile P1 on PR RunMaestro#1348: the restore gate required a POSITIVE saved offset
(`initialScrollTop > 0`), so the one position a user reaches by scrolling all
the way up - offset 0 with `isAtBottom: false` - was unrestorable. Returning to
such a tab skipped the restore and the mount-time bottom jump snapped the view
to the live bottom, which is the exact Y1 symptom this PR set out to fix.

Zero is a real saved position, not a missing one. `initialIsAtBottom !== false`
is the actual gate; the offset only needs to exist, so widen the check to
`>= 0`.

Two tests: offset 0 with the flag false restores to the top and pauses
auto-scroll; offset 0 with the flag absent (a legacy tab) still falls through
to the bottom jump, so the other half of the gate cannot silently regress.
Verified the first test fails against the old `> 0` gate.
@chr1syy

chr1syy commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Good catch - fixed in 2f08827d4.

Scrolling to the absolute top of an overflowing transcript persists scrollTop: 0 with isAtBottom: false, which is a perfectly ordinary deliberate position. Requiring initialScrollTop > 0 made that one spot unrestorable, so returning to a tab left at the very top snapped it to the live bottom - the exact Y1 symptom this PR set out to fix.

initialIsAtBottom !== false is the real gate; the offset only needs to exist, so the check is now >= 0.

Two tests added: offset 0 with the flag false restores to the top and pauses auto-scroll, and offset 0 with the flag absent (a legacy tab that never persisted it) still falls through to the mount-time bottom jump - so the other half of the gate cannot silently regress. Verified the first fails against the old > 0 gate.

chr1syy and others added 3 commits August 5, 2026 11:42
A deliberate scroll-up persists its two halves on different schedules: the
at-bottom flag goes out synchronously on the transition, the absolute offset
is debounced 200ms and DROPPED (not flushed) on unmount for the RunMaestro#1323
wrong-tab reason. Swapping agents inside that window saves isAtBottom: false
with no matching scrollTop, so the remount restore gate (initialScrollTop > 0)
skips and the mount-time bottom jump wins.

Adds a 'scrolled-up persistence across unmount (Y1)' describe block pinning
that current broken behaviour as documentation of the race. No product code
touched; it is inverted when the fix lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A deliberately scrolled-up transcript position snapped back to the live
bottom after swapping agents away and back. The two halves of the saved
scroll state were written on different schedules: the at-bottom flag went
out synchronously on the boundary transition, the absolute offset only
after a 200ms debounce. The unmount cleanup drops that pending save on
purpose (RunMaestro#1323: onScrollPositionChange resolves its target tab at call
time, so flushing during a swap would write the outgoing agent's offset
into the incoming agent's tab), so a swap inside the debounce window
persisted isAtBottom: false with no matching scrollTop. The remount
restore requires initialScrollTop > 0, skipped, and the mount-time bottom
jump won.

Write both halves in the same tick instead, inside the existing
transition block and in both directions, so the saved pair can never
disagree. The debounced save stays as the refinement path, so a dropped
flush now costs at most ~200ms of scrolling precision, never the whole
position. No change to the cleanup behaviour, the 200ms debounce, the
16ms throttle, the 50px at-bottom threshold or the 50px restore epsilon.

Inverts the Task 1 pin test and adds return-to-bottom pair coherence,
debounce refinement, no-scroll-no-save and end-to-end restore coverage.
17/17 in the scoped file; the new tests go red when the hook change is
reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eview)

Greptile P1 on PR RunMaestro#1348: the restore gate required a POSITIVE saved offset
(`initialScrollTop > 0`), so the one position a user reaches by scrolling all
the way up - offset 0 with `isAtBottom: false` - was unrestorable. Returning to
such a tab skipped the restore and the mount-time bottom jump snapped the view
to the live bottom, which is the exact Y1 symptom this PR set out to fix.

Zero is a real saved position, not a missing one. `initialIsAtBottom !== false`
is the actual gate; the offset only needs to exist, so widen the check to
`>= 0`.

Two tests: offset 0 with the flag false restores to the top and pauses
auto-scroll; offset 0 with the flag absent (a legacy tab) still falls through
to the bottom jump, so the other half of the gate cannot silently regress.
Verified the first test fails against the old `> 0` gate.
@chr1syy
chr1syy force-pushed the fix/y1-transcript-scroll-restore branch from 2f08827 to cd50b83 Compare August 5, 2026 09:44
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@chr1syy
chr1syy merged commit 61eea1d into RunMaestro:rc Aug 5, 2026
6 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.

1 participant