feat(tui): ADR-0048 smart scroll-lock - #71
Merged
Conversation
…nt indicator (ADR-0048) Co-authored-by: Justin Wilkin <justin@entr.net.au>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR implements ADR-0048, adding smart scroll-lock functionality to the TUI viewport. The feature prevents the viewport from auto-scrolling to the bottom when users scroll up to read earlier output during agent streaming, addressing a key usability issue where the viewport would fight users attempting to review conversation history.
Changes:
- Added
followScrollandhasNewContentstate fields to track scroll behavior - Implemented key handlers (PgUp, PgDn, G) and mouse wheel handlers for scroll-lock control
- Created a salmon-pink banner indicator showing "↓ New content below — press G or PgDn to follow" when new content arrives during scroll-lock
- Replaced all unconditional
GotoBottom()calls with conditionalscrollToBottomOrMark()logic
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pkg/executor/tui/model.go | Added followScroll and hasNewContent boolean fields for ADR-0048 state tracking |
| pkg/executor/tui/init.go | Initialized scroll-lock fields with followScroll=true (auto-follow by default) and hasNewContent=false |
| pkg/executor/tui/events.go | Added scrollToBottomOrMark() helper and replaced GotoBottom calls; reset scroll-lock on turn end |
| pkg/executor/tui/update.go | Added handleScrollKey() for PgUp/PgDn/G keys; implemented mouse wheel scroll-lock tracking; updated viewport height calculation |
| pkg/executor/tui/view.go | Added buildScrollLockIndicator() and refactored assembleBaseView() to include scroll indicator |
| pkg/executor/tui/styles.go | Added scrollLockIndicatorStyle using salmon-pink color with bold text |
| pkg/executor/tui/scroll_lock_test.go | Added 8 unit tests covering scroll-lock behavior for all key handlers and helper functions |
| docs/adr/0048-tui-smart-scroll-lock.md | Updated status from "Proposed" to "Implemented" |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Copilot PR review identified that pressing 'g' while scroll-locked would insert an unwanted 'g' character into the textarea because handleScrollKey was called from handleKeyPress, which runs after textarea.Update(msg). Fix: pre-empt the textarea update with an early-return guard at the same call site as the command palette pre-emption (lines 100-137). When the model is scroll-locked (!followScroll) and the user presses 'g', we jump to the viewport bottom, resume auto-follow, and return before the textarea ever sees the keystroke. The 'g' branch in handleScrollKey is now only reached when followScroll is already true, so its !followScroll guard makes it a no-op — no duplicate logic, just belt-and-suspenders safety.
Co-authored-by: Justin Wilkin <justin@entr.net.au>
justinwilkin
added a commit
that referenced
this pull request
Mar 2, 2026
* feat(tui): implement smart scroll-lock with follow mode and new content indicator (ADR-0048) Co-authored-by: Justin Wilkin <justin@entr.net.au> * fix(tui): intercept 'g' scroll-lock key before textarea update Copilot PR review identified that pressing 'g' while scroll-locked would insert an unwanted 'g' character into the textarea because handleScrollKey was called from handleKeyPress, which runs after textarea.Update(msg). Fix: pre-empt the textarea update with an early-return guard at the same call site as the command palette pre-emption (lines 100-137). When the model is scroll-locked (!followScroll) and the user presses 'g', we jump to the viewport bottom, resume auto-follow, and return before the textarea ever sees the keystroke. The 'g' branch in handleScrollKey is now only reached when followScroll is already true, so its !followScroll guard makes it a no-op — no duplicate logic, just belt-and-suspenders safety. * style(tui): align struct field assignments in scroll_lock_test.go Co-authored-by: Justin Wilkin <justin@entr.net.au> --------- Co-authored-by: anvxl <anvxl@entr.net.au>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements ADR-0048 — smart scroll-lock for the TUI viewport.
When the user scrolls up during a streaming agent response the viewport stays put and a salmon-pink banner appears at the bottom of the content area:
Auto-follow resumes when the user presses G, PgDn (when at bottom), or mouse-wheel down (when at bottom).
Changes
Model state (
model.go/init.go)followScroll bool(defaulttrue) andhasNewContent boolfieldsCore helper (
events.go)scrollToBottomOrMark()— callsGotoBottom()when following, else setshasNewContent=truehandleTurnEnd()— resets both fields after each agent turnGotoBottom()call sites replaced withscrollToBottomOrMark()Key & mouse handling (
update.go)handleScrollKey()extracted fromhandleKeyPressto keep cyclomatic complexity ≤ 15Visual indicator (
view.go/styles.go)buildScrollLockIndicator()renders a centred bold banner in salmon-pinkcalculateViewportHeight()reserves one line for the indicator to avoid overlapADR
Proposed→ImplementedTests
8 new tests in
pkg/executor/tui/scroll_lock_test.go:TestScrollToBottomOrMark_WhenFollowingTestScrollToBottomOrMark_WhenLockedTestHandleScrollKey_PgUpTestHandleScrollKey_CtrlBTestHandleScrollKey_GKey_WhenLockedTestHandleScrollKey_GKey_WhenAlreadyFollowingTestHandleScrollKey_PgDn_ResumesAtBottomTestHandleScrollKey_UnknownKeyChecklist
go build ./pkg/executor/tui/...)make lint)go test ./pkg/executor/tui/...)