Skip to content

fix(terminal): page a hollow Codex transcript with PageUp/PageDown from the first wheel event - #527

Draft
aakhter wants to merge 2 commits into
Ark0N:masterfrom
aakhter:pr/codex-hollow-paging
Draft

aakhter wants to merge 2 commits into
Ark0N:masterfrom
aakhter:pr/codex-hollow-paging

Conversation

@aakhter

@aakhter aakhter commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

This changes the last-resort page-key scroll in _maybePageCliTranscript. That path runs when a wheel or touch gesture is not forwarded to the CLI and the local xterm buffer holds no history (normal buffer, baseY === 0). It sends plain PageUp/PageDown keys so the CLI pages its own transcript.

This is not SGR wheel forwarding. Codex still ignores SGR wheel reports (#227), its wheelForward stays never, and _shouldForwardWheelToApp is unchanged.

  1. Codex can use the page-key fallback. Newer Codex draws on tmux's alternate screen, and the full strip hides that from xterm, so a Codex pane has no local scrollback, just like a repaint-mode Claude pane. Scrolling the wheel over it did nothing.

    • Measured with Codex 0.157.1 locally and 0.160 over a remote session: PageUp moves the visible transcript, while SGR wheel reports and local scrolling do nothing.
    • An older inline Codex keeps real local history (baseY > 0), so the guard never fires for it.
    • Which modes may page is a new registry capability, capabilities.transcriptPageKeys (set for claude and codex in stock.ts, an optional boolean in the schema).
    • The server publishes it to the page as window.__codemanTranscriptPageKeys, also in /session/:id windows, so terminal-ui.js holds no CLI id for it. The old mode !== 'claude' check is gone.
  2. The first event of a gesture pages straight away. The fallback used to send a key only after half a screen of accumulated travel (19 rows, about 475 px on a 38-row pane), so an ordinary trackpad flick sent nothing.

    • The first page is paid in advance: the travel it skipped is owed back by the rest of the gesture, so the rate stays at one page per half screen.
    • A pause of more than 150 ms, a change of direction or a tab switch starts a new gesture.
    • Sub-row jitter does not page.

Tests. These fail on master and pass with this change:

  • test/terminal-scroll-routing.test.ts: Codex pages only while hollow; first-event paging and repayment; gesture reset; tab isolation; modes come only from the injected map.
  • test/cli-registry-schema.test.ts: the capability's shape, and exactly claude and codex set in stock.
  • test/render-index-html.test.ts: injection in the main page and the solo window.

docs/architecture-invariants.md is updated.

One trade-off: small gestures now send a key where they used to send nothing, for Claude as well. A Claude menu or dialog open at that moment could react to PageUp, which is the caveat this fallback already carries.

@aakhter

aakhter commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

This is a draft only because GitHub currently won't let my account open regular PRs or mark drafts ready (same as #525 and #526). It's ready for review. It stands alone on master and is independent of the tab-rail stack.

@Ark0N

Ark0N commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Thanks for this, @aakhter, and sorry it sat while the rail stack went in. This extends the hollow-pane PageUp/PageDown fallback to Codex through a new transcriptPageKeys capability, and makes it page on a gesture's first event. The registry wiring is right: optional in the schema, set for claude and codex in stock.ts, injected into the main page and the solo windows, read at call time, and _shouldForwardWheelToApp untouched, so Codex still never gets SGR wheel reports. The new tests fail without the change: the pre-pay, the old mode !== 'claude' check, the injection and the stock flag each fail their own tests.

I ran this PR's terminal-ui.js side by side with master's on a hollow 38-row pane. Four things change that I'd like guarded before it merges:

  1. A mostly horizontal trackpad swipe pages (_maybePageCliTranscript). Six wheel events with deltaX 60 and deltaY 3 send one PageDown; master sent nothing, because 3 px already clears PAGE_KEY_MIN_START_ROWS. When both deltas are present and |deltaX| > |deltaY|, consume the event without paging. The touch path already locks its axis, so it's unaffected.
  2. A pinch pages. Chrome reports a trackpad pinch as wheel events with ctrlKey set; four of them (deltaY 4) send a PageDown. if (ev?.ctrlKey) return true (consume, don't page) covers it.
  3. Slow wheel notches page a full screen each. Five 100 px notches 250 ms apart send five PageUps (master: one). The same five notches 40 ms apart send one in both. So "one page per half screen" only holds inside the 150 ms window. I'd keep the immediate first page for trackpad-sized starts only (say an opening event under 2 rows) and let wheel notches accumulate as before.
  4. A small gesture can now reach an open Claude dialog. 2.5 px of travel sends PageUp/PageDown to a hollow Claude pane that is showing a permission prompt or an AskUserQuestion dialog, where it used to take half a screen of travel. I haven't checked what Claude's selector does with PageUp, but staying out of it is cheap: skip paging while this.tabAlerts?.get(this.activeSessionId) === 'action', which is set exactly while a permission_prompt or elicitation_dialog is pending (updateTabAlertFromHooks in app.js).

Each one is a few lines in _maybePageCliTranscript plus a case in test/terminal-scroll-routing.test.ts.

One thing I couldn't check here: the installed Codex is 0.147.0, so I couldn't reproduce the alt-screen Codex you measured on 0.157.1 and 0.160. If you know which Codex version started drawing on the alt screen, a line about it in the invariants paragraph would help whoever reads it next.

#525 and #526 just shipped in 1.35.0, so this is next in line, and it still merges cleanly onto master.

johoja12 and others added 2 commits October 5, 2026 21:19
…om the first wheel event

Two changes to the last-resort page-key scroll (_maybePageCliTranscript), the
path a wheel or touch gesture takes when it is not forwarded to the CLI and
the local xterm buffer has no history (normal buffer, baseY 0).

Codex now takes that path too. Newer codex draws on tmux's alternate screen,
which the full strip hides from xterm, so a codex pane is as hollow as a
repaint-mode Claude pane and the wheel did nothing. Codex still ignores SGR
wheel reports and is still never forwarded the wheel; this sends plain
PageUp/PageDown keys, which codex does page its transcript on (measured on
0.157.1 locally and 0.160 over a remote session). Which modes may page is a
new registry capability, capabilities.transcriptPageKeys (claude and codex in
stock.ts), published to the page as window.__codemanTranscriptPageKeys, so
terminal-ui.js holds no CLI id for it.

The first event of a gesture now pages at once. The fallback used to send a
key only after half a screen of accumulated travel (19 rows, about 475px on a
38-row pane), so an ordinary trackpad flick sent nothing. The skipped travel is
owed back by the rest of the gesture, keeping one page per half screen; a pause
of more than 150 ms, a direction change or a tab switch starts a new gesture.
…el notches and dialogs

Review follow-up for the hollow-pane PageUp/PageDown fallback
(_maybePageCliTranscript):

- A mostly horizontal wheel swipe (both deltas present, |deltaX| > |deltaY|)
  is consumed without paging. The touch path already locks its axis.
- A pinch (wheel events with ctrlKey, as Chrome reports one) is consumed
  without paging.
- Only a trackpad-sized opening event (under 2 rows) pages at once. A wheel
  notch (100 px, 4 rows) accumulates toward half a screen as before, so five
  slow notches send one PageUp again instead of five. A debt from a pre-paid
  page still dies with its gesture; plain wheel travel carries across notches.
- Nothing pages while the active tab's alert is 'action' (a pending
  permission_prompt or elicitation_dialog), so a page key never reaches an
  open dialog's selector.

Docs: the invariants paragraph now says which Codex versions were seen
drawing on the alt screen (0.157.1, 0.160) and that 0.147.0 still draws
inline, without claiming a cutoff.
@aakhter

aakhter commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for running it side by side with master; that made each of these easy to reproduce. All four are in a new commit on top (8d6e578), rebased onto 1.35.0, each with its own case in test/terminal-scroll-routing.test.ts:

  1. Horizontal swipe. A mostly horizontal swipe no longer pages: when both deltas are present and |deltaX| > |deltaY|, the event is consumed without sending a key. Your six-event case (deltaX 60, deltaY 3) now sends nothing, and a mostly vertical event with a little sideways drift still pages.
  2. Pinch. Any wheel event with ctrlKey set is consumed and sends nothing.
  3. Wheel notches. Only a trackpad-sized opening event (under 2 rows) pages at once. Wheel notches add up toward half a screen as on master, so five 100 px notches send one PageUp whether they are 250 ms or 40 ms apart. The debt from an immediate page still ends with its gesture, while plain wheel travel carries across notches the way it used to.
  4. Dialogs. Nothing pages while the active tab's alert is 'action', so a pending permission_prompt or elicitation_dialog never gets a PageUp. The check reads the alert type, not a CLI id, and both no-id-branching tests still pass.

Codex version. I can't pin down when Codex started drawing on the alt screen. It was seen on 0.157.1 locally and on 0.160 over a remote session, and your 0.147.0 draws inline. Nothing in between has been checked, so the invariants paragraph now says exactly that, without naming a cutoff.

Each new case fails without its guard. Typecheck, lint, format, the frontend checks, the catalogue check and the touched test files all pass.

It's still a draft only because GitHub won't let my account mark PRs ready; it's ready for review.

@aakhter
aakhter force-pushed the pr/codex-hollow-paging branch from 17fb7f8 to 8d6e578 Compare October 6, 2026 01:25
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