Skip to content

feat: handle whitespace scroll#1365

Open
liamhess wants to merge 2 commits intogeneralaction:mainfrom
liamhess:liam/forward-scroll-7s4
Open

feat: handle whitespace scroll#1365
liamhess wants to merge 2 commits intogeneralaction:mainfrom
liamhess:liam/forward-scroll-7s4

Conversation

@liamhess
Copy link
Contributor

@liamhess liamhess commented Mar 9, 2026

Summary

On wider screens the width of the chat interface is limited and then scrolling only works in the middle where the pty is actually rendered. This is a bit unintuitive, because visually there's no border/separation between the whitespace and the pty. Generally I think the nice fix would be to allow scrolling in the whitespace. But I understand if it's too much code / too hacky to forward the scroll for just a little usability improvement

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g. code style improvements, linting)
  • This change requires a documentation update

Mandatory Tasks

  • I have self-reviewed the code
  • A decent size PR without self-review might be rejected

Checklist

  • I have read the contributing guide
  • My code follows the style guidelines of this project (pnpm run format)
  • I have commented my code, particularly in hard-to-understand areas
  • I have checked if my PR needs changes to the documentation
  • I have checked if my changes generate no new warnings (pnpm run lint)
  • I have added tests that prove my fix is effective or that my feature works
  • I haven't checked if new and existing unit tests pass locally with my changes

@vercel
Copy link

vercel bot commented Mar 9, 2026

@liamhess is attempting to deploy a commit to the General Action Team on Vercel.

A member of the Team first needs to authorize it.

@arnestrickmann
Copy link
Contributor

Hey @liamhess, thanks for taking this on.

Is this already ready for review?

@liamhess
Copy link
Contributor Author

Hey @liamhess, thanks for taking this on.

Is this already ready for review?

It is now @arnestrickmann, looking forward to your review:)

@liamhess liamhess marked this pull request as ready for review March 11, 2026 10:15
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 11, 2026

Greptile Summary

This PR adds wheel-event forwarding so that scrolling in the empty whitespace on either side of the terminal pane (visible when the viewport is wider than max-w-4xl) correctly scrolls the terminal's scroll buffer, matching user expectation that the whole terminal region is interactive.

Key changes:

  • New useTerminalViewportWheelForwarding hook that intercepts onWheelCapture events, skips events originating inside [data-terminal-container] (letting xterm.js handle those natively), and programmatically forwards the rest to the terminal.
  • TerminalPaneHandle type extended with scrollViewportFromWheelDelta, wired through useImperativeHandle to the underlying TerminalSessionManager.
  • TerminalSessionManager gains pixel/line/page delta normalization, a sub-line carry accumulator (wheelLineRemainder), and a guarded scrollToLine call clamped within [0, buffer.baseY].
  • Both ChatInterface and MultiAgentTask integrate the new hook consistently.

Two minor polish issues were identified:

  • wheelLineRemainder is not cleared when the scroll direction reverses, which can create a small "dead zone" on trackpads.
  • metaKey (Cmd+scroll) is not guarded against alongside ctrlKey, which could forward unintended OS-level gestures to the terminal scroll path on macOS.

Confidence Score: 4/5

  • This PR is safe to merge with minor UX polish items remaining.
  • The core scroll-forwarding logic is well-implemented with proper guards (ctrlKey, dominant deltaX, data-terminal-container target check), try-catch around all DOM operations, and correct buffer clamping. The two flagged issues are style/UX improvements rather than correctness bugs — they won't cause crashes or data loss.
  • src/renderer/terminal/TerminalSessionManager.ts — the wheelLineRemainder direction-reversal edge case and src/renderer/hooks/useTerminalViewportWheelForwarding.ts — the missing metaKey guard.

Important Files Changed

Filename Overview
src/renderer/hooks/useTerminalViewportWheelForwarding.ts New hook that intercepts wheel events on the whitespace area surrounding the terminal and forwards them to the terminal's scroll API; guards against Ctrl+scroll, horizontal scrolling, and events already targeting the terminal container.
src/renderer/terminal/TerminalSessionManager.ts Adds public scrollViewportFromWheelDelta with sub-line accumulation (wheelLineRemainder), pixel-to-line normalization, and DOM-clamped scrolling via xterm's scrollToLine; one edge case where the remainder is not cleared on direction reversal.
src/renderer/components/TerminalPane.tsx Exports the new TerminalPaneHandle type (adds scrollViewportFromWheelDelta alongside existing focus) and wires it through useImperativeHandle; clean and backward-compatible.
src/renderer/components/ChatInterface.tsx Updates terminalRef to TerminalPaneHandle, creates the forwarding handler, and attaches it via onWheelCapture on the terminal wrapper div — straightforward integration.
src/renderer/components/MultiAgentTask.tsx Same TerminalPaneHandle + wheel-forwarding integration as ChatInterface; the single handler is correctly shared across variant containers since only the active variant can receive pointer events (visibility: hidden on inactive ones).

Sequence Diagram

sequenceDiagram
    participant User as User (whitespace scroll)
    participant WrapperDiv as Wrapper Div (onWheelCapture)
    participant Hook as useTerminalViewportWheelForwarding
    participant TerminalPane as TerminalPane (ref)
    participant TSM as TerminalSessionManager
    participant XTerm as xterm.js

    User->>WrapperDiv: WheelEvent (deltaY, deltaMode)
    WrapperDiv->>Hook: onWheelCapture fires (capture phase)
    Hook->>Hook: Guard: ctrlKey / deltaX dominant / data-terminal-container?
    alt target inside [data-terminal-container]
        Hook-->>WrapperDiv: return (xterm handles natively)
    else whitespace area
        Hook->>TerminalPane: scrollViewportFromWheelDelta(deltaY, deltaMode)
        TerminalPane->>TSM: scrollViewportFromWheelDelta(deltaY, deltaMode)
        TSM->>TSM: normalizeWheelDeltaToLines → lineDelta
        TSM->>TSM: accumulate wheelLineRemainder → wholeLines
        TSM->>TSM: scrollViewportByLineDelta(wholeLines)
        TSM->>XTerm: terminal.scrollToLine(targetLine)
        XTerm-->>TSM: done
        TSM-->>TerminalPane: true (scrolled)
        TerminalPane-->>Hook: true
        Hook->>WrapperDiv: event.preventDefault()
    end
Loading

Last reviewed commit: 038e95b

Comment on lines +547 to +549
const totalDelta = this.wheelLineRemainder + lineDelta;
const wholeLines = totalDelta > 0 ? Math.floor(totalDelta) : Math.ceil(totalDelta);
this.wheelLineRemainder = totalDelta - wholeLines;
Copy link
Contributor

Choose a reason for hiding this comment

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

Accumulated remainder not cleared on direction reversal

wheelLineRemainder is a running carry-over value that is never reset when the scroll direction changes. This means after a sustained scroll in one direction that stops on a fractional boundary, reversing direction will silently "burn through" up to one line's worth of accumulated carry before the terminal actually moves the other way.

For example, if wheelLineRemainder = 0.85 (scrolling down) and the user immediately starts scrolling up with a single-line pixel delta of -0.15:

totalDelta = 0.85 + (-0.15) = 0.70   →   wholeLines = 0   (no movement)

The terminal won't respond until the carry is fully cancelled. Consider resetting the remainder when the incoming lineDelta has the opposite sign to wheelLineRemainder:

const totalDelta = this.wheelLineRemainder + lineDelta;

could become:

// Reset carry on direction reversal to avoid an invisible "dead zone"
if ((this.wheelLineRemainder > 0 && lineDelta < 0) || (this.wheelLineRemainder < 0 && lineDelta > 0)) {
  this.wheelLineRemainder = 0;
}
const totalDelta = this.wheelLineRemainder + lineDelta;

This is a UX polish issue rather than a correctness bug, but it can feel noticeably sluggish on trackpads where sub-line scroll events are very common.

Comment on lines +9 to +10
if (!Number.isFinite(event.deltaY) || event.deltaY === 0) return;
if (event.ctrlKey || Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

metaKey (Cmd+scroll) not guarded against

The hook correctly short-circuits for ctrlKey (pinch-zoom on some platforms) and for dominant-horizontal swipes, but metaKey (Cmd+scroll) is not checked. On macOS inside Electron, Cmd+scroll is sometimes used for zoom or OS-level gestures. Forwarding those events to the terminal scroll path will produce unexpected behaviour.

Suggested change
if (!Number.isFinite(event.deltaY) || event.deltaY === 0) return;
if (event.ctrlKey || Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;
if (event.ctrlKey || event.metaKey || Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;

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.

2 participants