Skip to content

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

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

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

Conversation

@aakhter

@aakhter aakhter commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #508

This changes the last-resort page-key scroll in _maybePageCliTranscript and its tile twin TerminalTile._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, or a tile's discounted localRows). It sends plain PageUp/PageDown keys so the CLI pages its own transcript.

This is not SGR wheel forwarding. Codex and opencode still ignore SGR wheel reports (#227, #555), their wheelForward stays never, and _shouldForwardWheelToApp is unchanged.

  1. Codex can use the page-key fallback, and the paging list is registry data. 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 or an opencode pane. Scrolling the wheel over it did nothing (Codex 0.159.1 hybrid sessions lose wheel/touch scrolling, leaving mobile without transcript navigation #508).

    • 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. 0.147.0 still draws inline; nothing between 0.147.0 and 0.157.1 has been checked.
    • 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 (an optional boolean in the schema), set for claude, codex and opencode in stock.ts. opencode keeps the paging fix(terminal): opencode — a drag selects text again, and the wheel pages the transcript #555 gave it.
    • The server publishes the list to the page as window.__codemanTranscriptPageKeys, also in /session/:id windows, so terminal-ui.js holds no CLI id for it. The mode !== 'claude' && mode !== 'opencode' check in _localScrollbackIsHollow(target) is gone; the target form tiles use is kept.
  2. Gesture handling, shared by the main pane and tiles. The rules live in one pure helper, CodemanTerminalInput.pageKeysForGesture(state, ev, lines, rows, now), next to wheelDeltaLines and pageKeysForTravel. The main pane and every TerminalTile (grid tiles and the split's Pane B) call it with their own state, so a Codex or opencode tile pages exactly like the main pane.

    • The first trackpad-sized event of a gesture (under 2 rows) 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. That 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 larger opening event is a mouse-wheel notch (100 px is 4 rows) and accumulates as before, so slow notches still send one page per half screen rather than one each.
    • One touch contact, including its momentum, is one gesture regardless of pauses between whole-cell movements. A new contact is eligible for an immediate first page. On a 38-row pane, twelve one-line movements 170 ms apart send one PageUp; forty send two.
    • Wheel gestures normally end after 150 ms of quiet. While a pre-paid page is still owed in the same direction, that quiet interval extends to 1 second, so small slow ticks repay the page instead of each buying another. Once repaid, the normal 150 ms interval applies again. A rapid reversal (or a reversal within one touch contact) drops the debt and accumulates without another immediate page, so a small back-and-forth wobble does not page on every event. Tab switches reset the gesture.
    • A pinch (ctrlKey wheel events) and a mostly horizontal swipe (both deltas present, |deltaX| > |deltaY|) are consumed without paging.
  3. Nothing pages into an open dialog, checked per pane. While a session's tab alert is 'action' (a pending permission prompt or question), a wheel over its pane is consumed without sending a key. The main pane checks the active session; a tile checks its own sessionId, so a tile over a session with a pending prompt does not page while the focused tab is clear, and a clear tile still pages while another tab has a dialog. The main pane logs a consumed dialog gesture as page-keys-dialog-open, so the routing decision remains visible.

Tests.

  • test/terminal-scroll-routing.test.ts: the pure helper (first-event page and repayment, notch accumulation, debt reset, direction change and jitter, pinch and swipe, the batch cap); Codex pages only while hollow; opencode keeps paging; gesture reset; tab isolation; the active session's dialog blocks, another tab's does not; modes come only from the injected map; the main pane's state matches the helper over a mixed sequence.
  • test/terminal-tile-scroll.test.ts: a Codex tile pages; first-event paging, notch accumulation, pinch and swipe in a tile; byte-for-byte parity with the main pane over a mixed gesture sequence; the dialog check reads the tile's session in both directions.
  • test/terminal-touch-paging.browser.test.ts: real Chromium touch listeners and the key coalescer; slow whole-cell movement, momentum retaining the same contact ID, and immediate paging on a new contact. Unit tests also cover contact boundaries, 40-line drags, small pixel/line wheel ticks, debt repayment and expiry, rapid reversals, and dialog logging.
  • test/mobile/keyboard.test.ts: the local-scrollback touch case now writes real xterm history, asserts a visible textarea and a non-hollow normal buffer, then verifies that a real touch drag moves the viewport and sends no keys to the CLI.
  • test/cli-registry-schema.test.ts: the capability's shape, and exactly claude, codex and opencode set in stock.
  • test/render-index-html.test.ts: injection in the main page and the solo window.

docs/architecture-invariants.md describes the gesture rules, and the Dashboard manual now includes newer Codex alongside OpenCode in its wheel and swipe paging guidance.

Validation of the review fixes: typecheck, lint, formatting, lockfile, frontend/assets, browser exclusions, catalogue and build checks pass, as do 259 targeted tests, the three touch/tile browser tests, and the corrected mobile scrollback case. The wider mobile keyboard file still has five failures also reproduced on upstream aa3730aa; those are not claimed fixed here.

One trade-off: small trackpad gestures now send a key where they used to send nothing, for Claude and opencode as well. The dialog check covers permission prompts and questions; a CLI menu with no hook behind it (for example a slash-command picker) could still react to PageUp, which is the caveat this fallback already carried.

@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.

@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
@opticon454

opticon454 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Independent confirmation of the cause on a second install, in case it helps the review.

With codex-cli 0.160.1 running under a Codeman session, the pane shows tmux alternate_on=1, history_size=0 and mouse_any_flag=0. xterm's normal buffer therefore never grows past one screen (baseY === 0), so the wheel, which for Codex goes to local scrollback, scrolls nothing. PageUp/PageDown still page the Codex transcript. That matches the description here (page keys work, SGR wheel reports and local scrolling do nothing).

I had a smaller branch that only allowed codex in _localScrollbackIsHollow, to reuse the existing page-key fallback. It reaches the same behaviour as the first item here, so I'm not opening a duplicate.

@Ark0N

Ark0N commented Oct 9, 2026

Copy link
Copy Markdown
Owner

@aakhter, sorry this one sat again. All four items from my review are in and I have nothing more on them, but 1.40.0 landed underneath it, so it needs a rebase before I can merge it. It now conflicts in src/web/public/terminal-ui.js (7 hunks), test/terminal-scroll-routing.test.ts and docs/architecture-invariants.md, and it isn't a mechanical rebase anymore. Three things changed under you:

1. opencode pages too now. #555 (Randalix) added opencode to the hollow check, since it draws on the alternate screen and ignores SGR wheel reports like Codex (master's _localScrollbackIsHollow() reads mode !== 'claude' && mode !== 'opencode'). Your version reads window.__codemanTranscriptPageKeys instead, so taking your side as it is would quietly turn opencode paging off again. Please set transcriptPageKeys on the opencode entry in stock.ts too, so the list is claude, codex and opencode, and keep the opencode cases #555 added to test/terminal-scroll-routing.test.ts passing.

2. Tiles page through shared helpers. The tile grid moved the paging math into two pure helpers on CodemanTerminalInput (wheelDeltaLines and pageKeysForTravel), and _localScrollbackIsHollow(target) now takes a { terminal, sessionId, localRows } target so a TerminalTile (grid tiles and the split's Pane B) can ask for its own session. TerminalTile._maybePageCliTranscript (terminal-tile.js:1121) keeps its own pending travel and pages with pageKeysForTravel. Once Codex is in the list, a Codex tile becomes pageable, but with the old arithmetic: no page on the first event, no guard for a mostly horizontal swipe or a pinch, and no dialog check. Please put the new gesture logic where both panes reach it (in or next to pageKeysForTravel, kept pure), keep the two _maybePageCliTranscript copies in step as the comment at terminal-tile.js:1114 asks, and add the matching cases to test/terminal-tile-scroll.test.ts.

3. The dialog check has to use the pane's own session. this.tabAlerts?.get(this.activeSessionId) === 'action' is right for the main pane, but a tile shows its own session, which is usually not the active one. The tile's check needs to read this.sessionId, or a wheel over one tile pages into that session's permission prompt whenever the focused tab happens to be clear.

GitHub won't let you mark it ready, so I'll do that on my side once the rebase is up. With those three in, I'll merge it, and it closes #508 as well (Codex hybrid sessions losing wheel and touch scrolling), so feel free to add a Fixes #508 line to the description.

johoja12 and others added 3 commits October 9, 2026 20: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.
…opencode

Review follow-up after the rebase onto 1.40.0, where tiles began paging a
hollow buffer through the shared CodemanTerminalInput helpers and opencode
joined the hollow check (Ark0N#555).

- opencode declares transcriptPageKeys in stock.ts, so the paging list read
  by both panes is claude, codex and opencode. Without it, reading the
  capability instead of the old id check would have turned opencode paging
  off again. The stock-set and render tests name it.
- The gesture rules (first trackpad-sized event pages at once and is owed
  back, wheel notches accumulate, a pre-paid debt dies with its gesture,
  pinch and mostly horizontal swipes are consumed without paging) move out
  of the primary pane into the pure CodemanTerminalInput.pageKeysForGesture.
  The primary pane and TerminalTile both call it with their own state, so a
  Codex or opencode tile pages exactly like the main pane.
- The dialog check stays per pane: the primary pane reads tabAlerts for the
  active session, a tile for its own sessionId, so a wheel over a tile never
  pages into that session's permission prompt while the focused tab is clear,
  and a tile is not blocked by a dialog in another tab.

Tests: pure helper cases in terminal-scroll-routing, tile cases for Codex
paging, first-event paging, notch accumulation, pinch and swipe, byte parity
with the primary pane over a mixed sequence, and the per-session dialog
check in both directions.
@aakhter
aakhter force-pushed the pr/codex-hollow-paging branch from 8d6e578 to d7198dd Compare October 10, 2026 00:30
@aakhter

aakhter commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review, and for pointing out exactly what moved underneath it. It's rebased onto 1.40.0 now, with the review fixes in one new commit on top (d7198dd):

  1. opencode. It now sets transcriptPageKeys in stock.ts, so the paging list is claude, codex and opencode. The opencode cases fix(terminal): opencode — a drag selects text again, and the wheel pages the transcript #555 added to terminal-scroll-routing still pass, and the stock-set and render tests now name opencode.

  2. Shared helper for tiles. The gesture rules now live in one pure helper next to the others, CodemanTerminalInput.pageKeysForGesture(state, ev, lines, rows, now), built on pageKeysForTravel. It covers:

    • the first-event page for openings under 2 rows, and the travel it owes back;
    • notch accumulation, and dropping a prepaid debt when its gesture ends;
    • consuming pinches and mostly horizontal swipes without paging.

    Both _maybePageCliTranscript copies call it with their own state, and their twin comments point at each other, so a Codex or opencode tile pages exactly like the main pane. terminal-tile-scroll has the matching cases, including a byte-for-byte parity test against the main pane over a mixed sequence with timing gaps.

  3. Dialog check per pane. The tile now reads its own session (app.tabAlerts.get(this.sessionId)), and the main pane keeps the active session. Tests cover both directions: a tile with a pending prompt doesn't page while the focused tab is clear, and a clear tile still pages while the active tab has a dialog.

The description now has Fixes #508, and covers opencode, tiles and the per-pane dialog check. One note for bisecting: the opencode capability arrives in the review-fix commit, so the two earlier commits on their own still fail #555's two opencode cases. The head is green throughout.

Thanks again!

Conflicts resolved by keeping both sides:
- terminal-ui.js: master's wheelDeltaWholeLines/sgrWheelReports next to
  this branch's pageKeyRows/pageKeysForGesture.
- terminal-tile.js: master's tile SGR wheel forwarding and Shift+wheel
  local scroll (_shiftScrollPending) kept; tile paging stays on the
  shared pageKeysForGesture state (_pageKeyGesture) with its per-tile
  dialog check.
- architecture-invariants.md: this branch's hollow-paging paragraph,
  with master's tile SGR forwarding and Shift+wheel sentences replacing
  the old "do not forward SGR wheel" note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aakhter
aakhter marked this pull request as ready for review October 10, 2026 22:24
@Ark0N

Ark0N commented Oct 11, 2026

Copy link
Copy Markdown
Owner

Thanks again @aakhter, and thanks for turning both review rounds around so carefully. This PR lets a hollow Codex pane (alternate screen, no local history) page its transcript with PageUp/PageDown from the wheel and touch, moves the paging mode list into the CLI registry as transcriptPageKeys, and shares the new gesture rules between the main pane and tiles through CodemanTerminalInput.pageKeysForGesture.

Everything from the earlier rounds is in and correct: opencode keeps paging, tiles use the shared helper on their own state, the dialog check reads each pane's own session, and Codex still never gets SGR wheel reports. Typecheck, lint, format, the frontend checks, the catalogue check and the touched test files all pass here.

One regression needs fixing before merge, on the phone path that #508 is about:

  1. A slow touch drag pages a full screen per line (src/web/public/terminal-ui.js:192-205, reached from the touch paths at terminal-ui.js:1135 and :1312). touchmove only calls _maybePageCliTranscript when the finger crosses a whole cell (about 15 px). In a slow drag those crossings are more than 150 ms apart, so each one starts a new gesture, drops the previous pre-paid debt and earns another immediate page. Through the real _maybePageCliTranscript on a hollow 38-row pane:

    • 12 lines of finger travel at 170 ms per line sends 12 PageUps (master: 0);
    • the same 12 lines at 32 ms per line sends 1.

    So the slower and more careful the drag, the faster the transcript runs away. Please make one touch contact one gesture that the 150 ms gap never splits. For example:

    • increment an id on touchstart and pass it on the synthetic event at both touch call sites ({ shiftKey: false, touchId }), including the momentum frames;
    • in pageKeysForGesture, only apply the time-gap test when ev.touchId is absent or differs from the previous event's.

    I tried that change in simulation: a 12-line slow drag sends 1 PageUp, a 40-line one sends 2, separate swipes each page at once, and the trackpad results are unchanged. Please add a test in test/terminal-scroll-routing.test.ts: twelve -1 calls 170 ms apart with one touchId send exactly one PageUp, and a new touchId pages again at once.

Smaller things, ideally in the same push:

  1. Small slow wheel ticks and back-and-forth jitter (terminal-ui.js:192-205). A wheel whose slow ticks report under 2 rows still pages once per tick when they are more than 150 ms apart:

    • five 40 px ticks 250 ms apart send 5 PageUps;
    • Firefox at 1 line per notch behaves the same way.

    Separately, every reversal pre-pays, so a +/-3 px trackpad wobble at 16 ms sends PageUp, PageDown, PageUp, ... on every event. Either bound it (for example, let a same-direction pre-paid debt survive the gap for about a second, and skip the immediate page on a reversal inside the gap) or pin the intended behaviour with a test.

  2. The dialog check logs nothing (terminal-ui.js:5871). A wheel consumed because the session has an 'action' alert returns before _logScrollRouting, so a dead wheel under a stale alert leaves no [scroll] line. The invariants doc asks to keep that log when touching the routing. A this._logScrollRouting('page-keys-dialog-open') before the return true covers it.

  3. User manual (docs/wiki/The-Dashboard.md:224-226) still says only OpenCode's wheel and swipes page and other CLIs scroll locally. Please add newer Codex there, since that is the visible fix for Codex 0.159.1 hybrid sessions lose wheel/touch scrolling, leaving mobile without transcript navigation #508.

I'll re-wrap the one 127-character comment line at terminal-ui.js:5862 at merge time, so no need to touch it.

With item 1 (plus 3 and 4 if you have a minute) pushed, I'll merge this and it closes #508.

…cks and wobble

Review of Ark0N#527 (Ark0N, 2026-10-11):

1. A slow touch drag paged a full screen per line. touchmove calls the
   fallback once per whole cell, so a slow drag's calls were further apart
   than the 150 ms gesture gap and each pre-paid a page (12 lines at 170 ms
   sent 12 PageUps). The touch paths now tag every call, touchmove and
   momentum frames alike, with a per-contact touchId (incremented on
   touchstart), and pageKeysForGesture never splits a contact by time; a
   new contact is a new gesture.
2. A pre-paid debt now survives up to PAGE_KEY_DEBT_MS (1 s) of quiet when
   the next event goes the same way, so slow small wheel ticks (40 px every
   250 ms, a Firefox 1-line notch) pay it back instead of each pre-paying.
   A reversal inside the gap (or inside one contact) drops the debt and
   accumulates without pre-paying, so a +/-3 px wobble no longer pages
   back and forth. A reversal after the gap still pages at once. Main pane
   and tiles share the helper, so both get it.
3. A wheel consumed by the dialog check now logs page-keys-dialog-open.
4. The Dashboard wiki names newer Codex beside OpenCode; the invariants
   paragraph describes the new gesture boundaries.

Tests: unit regressions through the real _maybePageCliTranscript, helper
cases, tile/main parity for ticks and wobble, and a Chromium test that
drives the real initTerminal touch listeners (12 PageUps before, 1 after).
pageKeysForGesture kept `prepaid` set after the travel had fully repaid a
pre-paid page, so the one-second debt lifetime still applied: on a 38-row
pane, 19 lines of -1 at 32 ms left { pending: 0, prepaid: true } and a -1
300 ms later sent nothing. `prepaid` now clears as soon as the pending
travel reaches or crosses zero, so the leftover is plain travel and the
150 ms gap applies again. Touch contacts are unaffected (never split by
time). The mixed parity step's reversal grows from 20 to 25 rows because
the -5 rows left after repayment are now plain travel and cancel against it.
"keeps terminal touch drag available for scrollback with the visible
textarea enabled" built a Codex session on a blank terminal. With Codex
in transcriptPageKeys that pane is hollow, so the drag is correctly
consumed by the page-key fallback and never reaches scrollLines; the
fixture no longer described local scrollback.

The case now writes 200 lines into xterm (awaiting the write), turns the
visible CJK textarea on explicitly, and asserts the scenario before the
drag: normal buffer, baseY > 0, viewport at the bottom, not hollow, wheel
forwarding off, cjk-input-visible set. A real touch drag back into history
must then call scrollLines with negative lines, move the viewport up, and
send nothing to the CLI. Hollow Codex paging stays covered by
terminal-touch-paging.browser.test.ts.
aakhter added a commit to aakhter/Codeman that referenced this pull request Oct 11, 2026
…cks and wobble

Review of Ark0N#527 (Ark0N, 2026-10-11):

1. A slow touch drag paged a full screen per line. touchmove calls the
   fallback once per whole cell, so a slow drag's calls were further apart
   than the 150 ms gesture gap and each pre-paid a page (12 lines at 170 ms
   sent 12 PageUps). The touch paths now tag every call, touchmove and
   momentum frames alike, with a per-contact touchId (incremented on
   touchstart), and pageKeysForGesture never splits a contact by time; a
   new contact is a new gesture.
2. A pre-paid debt now survives up to PAGE_KEY_DEBT_MS (1 s) of quiet when
   the next event goes the same way, so slow small wheel ticks (40 px every
   250 ms, a Firefox 1-line notch) pay it back instead of each pre-paying.
   A reversal inside the gap (or inside one contact) drops the debt and
   accumulates without pre-paying, so a +/-3 px wobble no longer pages
   back and forth. A reversal after the gap still pages at once. Main pane
   and tiles share the helper, so both get it.
3. A wheel consumed by the dialog check now logs page-keys-dialog-open.
4. The Dashboard wiki names newer Codex beside OpenCode; the invariants
   paragraph describes the new gesture boundaries.

Tests: unit regressions through the real _maybePageCliTranscript, helper
cases, tile/main parity for ticks and wobble, and a Chromium test that
drives the real initTerminal touch listeners (12 PageUps before, 1 after).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aakhter
aakhter force-pushed the pr/codex-hollow-paging branch from bb52a8d to dd7774f Compare October 12, 2026 02:29
@aakhter

aakhter commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, all four are in (head dd7774f9). Going down your list:

  1. One touch contact is one gesture. Both touch call sites now pass { shiftKey: false, touchId }, with the id bumped on each touchstart and carried by that contact's momentum frames. In pageKeysForGesture a different touchId (or a wheel event after a touch) starts a new gesture, and within one contact the 150 ms gap never applies. The test you asked for is in test/terminal-scroll-routing.test.ts: twelve -1 calls 170 ms apart on one touchId send exactly one PageUp, and a new touchId pages again at once. There's also a slow 40-line drag that pages twice. I added a real-Chromium test too (test/terminal-touch-paging.browser.test.ts, browser suite) that drives the actual touch listeners and momentum loop. With the contact rule and the debt rule below both turned off, it reproduces your numbers exactly: 12 PageUps for 12 lines.

  2. Slow ticks and wobble. I went with the bounded version you suggested:

    • A pre-paid debt in the same direction now survives the gap for up to a second (PAGE_KEY_DEBT_MS). So five 40 px ticks 250 ms apart, or Firefox 1-line notches, pay the debt back instead of each pre-paying a page. After a second of quiet the debt lapses, and the next small tick pages at once again.
    • The debt also ends as soon as travel repays it, so the longer lifetime never outlasts the page it belongs to.
    • A reversal inside a gesture drops the debt and accumulates without pre-paying, so a +/-3 px wobble at 16 ms pages once for its opening event and then nothing.
    • A reversal after the gap is still a new gesture and pages at once.

    There are tests for each of these.

  3. Dialog log. this._logScrollRouting('page-keys-dialog-open') now runs before the return true, with a test that checks for the [scroll] line.

  4. User manual. The-Dashboard.md now lists newer Codex next to OpenCode: alternate screen, no history in the terminal, and an older inline Codex still scrolls locally. It also gives the gesture rule in one sentence. The invariants paragraph describes the contact and debt lifetimes.

I left the 127-character line at terminal-ui.js:5862 for you, as you said.

Typecheck, lint, format, the frontend, public-asset and browser-exclude checks, and the build all pass. The scroll-routing, tile-scroll and both no-id-branching test files pass (114 tests), and so do the two browser files.

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.

Codex 0.159.1 hybrid sessions lose wheel/touch scrolling, leaving mobile without transcript navigation

4 participants