From 24c8183832b8d578900fb4f65954165c10abf3a9 Mon Sep 17 00:00:00 2001 From: Harry Bayliss Date: Fri, 15 May 2026 15:34:00 +0100 Subject: [PATCH] Auto-snap child viewport to bottom when typing into scrollback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing into a focused child while its emulator viewport was scrolled up left the keystroke heading to the PTY but the input box invisible below the visible region — it looked like typing did nothing. processStdin's flushForward now sets pendingViewportBottom whenever bytes are actually injected, so the existing post-loop handler snaps the viewport and repaints. Wheel events and Ctrl-B paths are untouched: both are intercepted before reaching forward, so wheel still scrolls into history and Ctrl-B is still the explicit escape hatch. Only bytes that would actually reach the child PTY trigger the snap. --- CHANGELOG.md | 6 ++++++ internal/app/app.go | 22 ++++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e1e07..0168daa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Fixed +- Typing into a focused child while its emulator viewport is + scrolled up into scrollback history now auto-snaps the viewport + back to the live area. Previously the keystroke reached the + child PTY but the input box was off-screen below the visible + region, so it looked like typing did nothing. Wheel scrolling + and Ctrl-B are unchanged; only forwarded keystrokes snap. - Top tab bar now keeps the top-level agent's tab highlighted when focus is on one of its sub-agents (or on a Processes pane entry, matching the existing agent-tree behavior). Previously diff --git a/internal/app/app.go b/internal/app/app.go index 3aca4b7..df0d622 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1323,6 +1323,15 @@ func (st *uiState) processStdin(chunk []byte) { } forward := make([]byte, 0, len(chunk)) + + var pendingAction *paletteAction + var pendingNav navEntry + var pendingRestartID string + var pendingViewportDelta int + var pendingViewportBottom bool + var pendingPadStep int + var pendingPadExit bool + flushForward := func() { if len(forward) == 0 { return @@ -1337,19 +1346,16 @@ func (st *uiState) processStdin(chunk []byte) { if prev != OwnerUser { go st.drawStatusLine() } + // Auto-snap the emulator viewport to the live area + // on any forwarded keystroke. Without this, typing + // while scrolled into history leaves the cursor / + // echoed bytes off-screen below the visible region. + pendingViewportBottom = true } } forward = forward[:0] } - var pendingAction *paletteAction - var pendingNav navEntry - var pendingRestartID string - var pendingViewportDelta int - var pendingViewportBottom bool - var pendingPadStep int - var pendingPadExit bool - // childOnPrimary captures whether the focused child is on its primary // screen at the start of this chunk. Wheel events on the primary // screen scroll the emulator viewport (inline scrollback); on the