Skip to content

feat(tui): ADR-0048 smart scroll-lock - #71

Merged
justinwilkin merged 3 commits into
feat/tui-upliftfrom
feat/tui-uplift-smart-scroll
Feb 26, 2026
Merged

feat(tui): ADR-0048 smart scroll-lock#71
justinwilkin merged 3 commits into
feat/tui-upliftfrom
feat/tui-uplift-smart-scroll

Conversation

@justinwilkin

Copy link
Copy Markdown
Member

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:

↓ New content below — press G or PgDn to follow

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)

  • Added followScroll bool (default true) and hasNewContent bool fields

Core helper (events.go)

  • scrollToBottomOrMark() — calls GotoBottom() when following, else sets hasNewContent=true
  • handleTurnEnd() — resets both fields after each agent turn
  • All GotoBottom() call sites replaced with scrollToBottomOrMark()

Key & mouse handling (update.go)

  • handleScrollKey() extracted from handleKeyPress to keep cyclomatic complexity ≤ 15
  • PgUp / ctrl+b → lock scroll
  • PgDn at bottom / G → resume follow
  • Mouse-wheel up → lock; wheel-down at bottom → resume

Visual indicator (view.go / styles.go)

  • buildScrollLockIndicator() renders a centred bold banner in salmon-pink
  • calculateViewportHeight() reserves one line for the indicator to avoid overlap

ADR

  • Status updated: ProposedImplemented

Tests

8 new tests in pkg/executor/tui/scroll_lock_test.go:

Test Covers
TestScrollToBottomOrMark_WhenFollowing GotoBottom called, hasNewContent stays false
TestScrollToBottomOrMark_WhenLocked viewport unchanged, hasNewContent set true
TestHandleScrollKey_PgUp followScroll set false
TestHandleScrollKey_CtrlB ctrl+b alias behaves identically to PgUp
TestHandleScrollKey_GKey_WhenLocked resume follow, clear flag, jump to bottom
TestHandleScrollKey_GKey_WhenAlreadyFollowing 'g' not swallowed during normal typing
TestHandleScrollKey_PgDn_ResumesAtBottom follow resumes when PgDn reaches bottom
TestHandleScrollKey_UnknownKey unrecognised keys pass through

Checklist

  • Build clean (go build ./pkg/executor/tui/...)
  • Lint: 0 issues (make lint)
  • All TUI tests pass (go test ./pkg/executor/tui/...)
  • ADR-0048 status updated to Implemented

…nt indicator (ADR-0048)

Co-authored-by: Justin Wilkin <justin@entr.net.au>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 followScroll and hasNewContent state 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 conditional scrollToBottomOrMark() 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.

Comment thread pkg/executor/tui/update.go
justinwilkin and others added 2 commits February 27, 2026 10:49
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
justinwilkin merged commit 7231e05 into feat/tui-uplift Feb 26, 2026
@justinwilkin
justinwilkin deleted the feat/tui-uplift-smart-scroll branch February 26, 2026 23:56
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>
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.

3 participants