Merge pull request #699: fix(terminal): a mouse wheel notch pages the CLI's transcript at once

Conflict in docs/architecture-invariants.md resolved by keeping both
sides: the hollow-pane paragraph carries #681's unloaded-scrollback
sentence, #698's Codex-menu rewording and #699's notch rewording (a
word-level three-way merge; the three edits touch disjoint sentences).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-24 15:22:43 +02:00
commit edf3cb19e4
6 files changed
+153 -35

No files matched your search

+1 -1
View File
@@ -1173,7 +1173,7 @@
// pages the CLI's own transcript with PageUp/PageDown. Every gate is the
// primary pane's own, asked for THIS pane (its terminal, its session, never
// the active one), so the CLI rules stay in terminal-ui.js and this file
// names no CLI. The gesture rules (first-event page, notch accumulation,
// names no CLI. The gesture rules (first-event page, notch debt,
// pinch and sideways-swipe consume) are the shared pure
// CodemanTerminalInput.pageKeysForGesture, run on this tile's own state.
// The primary pane's unloaded-scrollback fall-through (`paneHistoryLines`
+53 -22
View File
@@ -98,10 +98,20 @@
const PAGE_KEY_GESTURE_GAP_MS = 150;
// ...but not for sub-row jitter (0.1 row, ~2px)...
const PAGE_KEY_MIN_START_ROWS = 0.1;
// ...and only for a trackpad-sized opening event. A mouse-wheel notch (100px,
// 4 rows) accumulates toward half a screen as before; paging at once on it made
// slow notches, each its own gesture past the gap, page a full screen apiece.
// ...for any opening event under one page. A trackpad-sized opener (under this many rows) and a
// mouse-wheel notch (100px, 4 rows) both page at once; this size is what tells them apart for the debt
// (see PAGE_KEY_NOTCH_DEBT_MS). The signal is the opener's size, not the device: a touch contact whose
// first touchmove is this many rows or more is treated as a notch too, and so is a brisk trackpad
// flick that opens with a large event. Both are fine, and both get the longer debt.
const PAGE_KEY_IMMEDIATE_MAX_ROWS = 2;
// A mouse-wheel notch used to just accumulate toward half a screen, so a mouse showed nothing for the
// first 5 notches (7 on Firefox's 3-line notches) and a gesture of a few notches seemed dead. The first
// notch of a gesture now pre-pays its page like a trackpad flick does, and the notches that follow repay
// it: the same gesture sends the same pages, the first one sooner. The old objection was slow notches,
// each its own gesture past the gap, paging a full screen apiece, so a notch's debt outlives the pauses
// between notches for this long (a trackpad's lasts PAGE_KEY_DEBT_MS). It applies to any opener of
// PAGE_KEY_IMMEDIATE_MAX_ROWS rows or more, whatever device it came from.
const PAGE_KEY_NOTCH_DEBT_MS = 3000;
// A pre-paid page's debt outlives the gesture gap for this long 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 page of its own.
@@ -205,19 +215,22 @@
// (`ev.touchId`, set by the touch paths per touchstart and carried by that
// contact's momentum; a wheel event has none, so wheel and touch never
// share a gesture), or, for events outside any contact, after quiet:
// PAGE_KEY_GESTURE_GAP_MS, stretched to PAGE_KEY_DEBT_MS while a pre-paid
// page is still owed in the same direction. One contact is never split by
// time, so a slow drag (one line per 170 ms) is one gesture;
// - a new gesture's trackpad-sized opening event (at least
// PAGE_KEY_MIN_START_ROWS, under PAGE_KEY_IMMEDIATE_MAX_ROWS) pages AT ONCE,
// PAGE_KEY_GESTURE_GAP_MS, stretched to PAGE_KEY_DEBT_MS (PAGE_KEY_NOTCH_DEBT_MS
// when a notch-sized opener pre-paid it) while a pre-paid page is still owed
// in the same direction. One contact is never split by time, so a slow drag
// (one line per 170 ms) is one gesture;
// - a new gesture's opening event (at least PAGE_KEY_MIN_START_ROWS and
// under one page: a trackpad flick, or a mouse notch) pages AT ONCE,
// pre-paid: the half screen it skipped is owed back by the rest of the
// gesture, so the rate stays one page per half screen. A debt left by a
// pre-paid page dies with its gesture;
// - a direction change inside a gesture (a reversal within the gap, or
// within the same contact) drops a pre-paid debt and accumulates, never
// pre-paying, so a +/-3 px trackpad wobble cannot page back and forth;
// - everything else, a wheel notch included, accumulates through
// pageKeysForTravel, carried across notches.
// - a mouse-wheel notch (any opening event under one page, past the trackpad size) pre-pays its page
// the same way, and its debt lasts PAGE_KEY_NOTCH_DEBT_MS, so the notches that follow repay it
// however slowly they come instead of each paging a screen of its own;
// - everything else accumulates through pageKeysForTravel, carried across notches.
// Returns { state, keys } with keys '' when nothing is due.
function pageKeysForGesture(state, ev, lines, rows, now) {
const prev = state || {};
@@ -239,7 +252,8 @@
gestureStart = false; // the same contact, however long it has been
} else {
const quiet = now - prev.lastAt;
gestureStart = quiet > (prepaid && !reversed ? PAGE_KEY_DEBT_MS : PAGE_KEY_GESTURE_GAP_MS);
const debtMs = prev.notch ? PAGE_KEY_NOTCH_DEBT_MS : PAGE_KEY_DEBT_MS;
gestureStart = quiet > (prepaid && !reversed ? debtMs : PAGE_KEY_GESTURE_GAP_MS);
}
if (gestureStart || reversed) {
// A pre-paid debt belongs to its gesture and its direction.
@@ -248,20 +262,34 @@
}
if (gestureStart) {
const size = Math.abs(lines);
if (size >= PAGE_KEY_MIN_START_ROWS && size < Math.min(perPage, PAGE_KEY_IMMEDIATE_MAX_ROWS)) {
// Plain travel left over from the gesture before (what was left once its debt was repaid) is
// not dropped by the new opener: it counts toward the page, so a gesture made of notches pages
// once per `perPage` rows of travel, as it did before the first notch paged at once.
const carried = pending * dir > 0 ? pending : 0;
if (size >= PAGE_KEY_MIN_START_ROWS && size < perPage && (carried + lines) * dir < perPage) {
// Pre-pay one page; the skipped travel is owed back (pending carries the opposite sign).
return {
state: { pending: lines - dir * perPage, lastAt: now, dir, prepaid: true, touchId },
state: {
pending: carried + lines - dir * perPage,
lastAt: now,
dir,
prepaid: true,
notch: size >= PAGE_KEY_IMMEDIATE_MAX_ROWS,
touchId,
},
keys: dir < 0 ? KEY_PAGE_UP : KEY_PAGE_DOWN,
};
}
}
const step = pageKeysForTravel(pending, lines, rows);
// A debt carries the sign opposite to its direction; once the travel reaches
// or crosses zero it is repaid, and what is left is plain travel (so the
// one-second debt lifetime no longer applies).
// or crosses zero it is repaid, and what is left is plain travel (so neither
// debt lifetime, PAGE_KEY_DEBT_MS or PAGE_KEY_NOTCH_DEBT_MS, applies any more).
if (prepaid && step.pending * dir >= 0) prepaid = false;
return { state: { pending: step.pending, lastAt: now, dir, prepaid, touchId }, keys: step.keys };
return {
state: { pending: step.pending, lastAt: now, dir, prepaid, notch: prepaid && !!prev.notch, touchId },
keys: step.keys,
};
}
const TUI_PROMPT_DEFAULT_ROWS_FROM_BOTTOM = 4;
@@ -461,6 +489,7 @@
PAGE_KEY_MIN_START_ROWS,
PAGE_KEY_IMMEDIATE_MAX_ROWS,
PAGE_KEY_DEBT_MS,
PAGE_KEY_NOTCH_DEBT_MS,
TUI_PROMPT_DEFAULT_ROWS_FROM_BOTTOM,
MOBILE_KEYBOARD_DISMISS_EXEMPT_SELECTOR,
MOBILE_KEYBOARD_DISMISS_TAP_SLOP,
@@ -6081,17 +6110,19 @@ Object.assign(CodemanApp.prototype, {
* excluded on purpose: it is the explicit "give me local scrollback" gesture
* and must keep that meaning.
*
* The FIRST event of a trackpad-sized gesture (opening under
* PAGE_KEY_IMMEDIATE_MAX_ROWS) pages at once. Waiting for half a screen of
* The FIRST event of a gesture (any opening event under one page: a trackpad
* flick, or a mouse notch) pages at once. Waiting for half a screen of
* travel (19 rows, ~475px on a 38-row pane) meant an ordinary trackpad flick
* never sent a key at all. That page is pre-paid: the travel it skipped is
* owed back by the rest of the gesture, so the rate stays one page per
* `perPage` rows. A new touch contact (`ev.touchId`), a pause outside a contact
* (PAGE_KEY_GESTURE_GAP_MS, or PAGE_KEY_DEBT_MS while a same-direction page is
* still owed), a reversal after the gap or a tab switch starts a new gesture;
* (PAGE_KEY_GESTURE_GAP_MS; while a same-direction page is still owed,
* PAGE_KEY_DEBT_MS, or PAGE_KEY_NOTCH_DEBT_MS when a notch-sized opener pre-paid
* it), a reversal after the gap or a tab switch starts a new gesture;
* one contact is never split by time, and a quick reversal never pre-pays. A
* larger opening event is a wheel notch and accumulates as before, so slow
* notches still page once per `perPage` rows.
* mouse-wheel notch pages at once too, and its debt (PAGE_KEY_NOTCH_DEBT_MS) is repaid by the
* notches after it, so a mouse pages once per `perPage` rows of travel with the
* first page on the first notch, not the fifth.
*
* Consumed WITHOUT paging: a pinch (`ctrlKey`, how Chrome reports a trackpad
* pinch), a mostly horizontal swipe (both deltas present, |deltaX| > |deltaY|;