mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(terminal): share the gesture-aware page-key math with tiles, add 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 (#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.
This commit is contained in:
1 parent
a4926e49f1
commit
d7198dd058
8 files changed
+386
-79
No files matched your search
@@ -19,13 +19,15 @@
|
||||
* What it does carry over from the primary pane, through the primary pane's
|
||||
* own code aimed at THIS pane (its terminal, its session, never the active
|
||||
* one):
|
||||
* - Hollow-buffer paging (#555): a CLI that draws in place (opencode on the
|
||||
* alternate screen, Claude's repaint mode) leaves the xterm no scrollback,
|
||||
* so the wheel pages the CLI's own transcript with PageUp/PageDown
|
||||
* (_maybePageCliTranscript) through the primary pane's gates, plus an
|
||||
* overflow-row discount for this pane's capture-before-resize load
|
||||
* (_localRows), and only while the viewport is on the live screen (a
|
||||
* wheel-down from those overflow rows is xterm's, and brings it home).
|
||||
* - Hollow-buffer paging (#555): a CLI that draws in place (opencode and codex
|
||||
* on the alternate screen, Claude's repaint mode) leaves the xterm no
|
||||
* scrollback, so the wheel pages the CLI's own transcript with
|
||||
* PageUp/PageDown (_maybePageCliTranscript) through the primary pane's gates
|
||||
* and its pure gesture rules (CodemanTerminalInput.pageKeysForGesture), plus
|
||||
* an overflow-row discount for this pane's capture-before-resize load
|
||||
* (_localRows), only while the viewport is on the live screen (a wheel-down
|
||||
* from those overflow rows is xterm's, and brings it home), and never while
|
||||
* THIS pane's session has a dialog open.
|
||||
* - The desktop click report: a plain left-click hand-encoded as SGR while
|
||||
* the session's CLI has mouse tracking on (cliMouseTracking), for the modes
|
||||
* whose mouse DECSETs the server strips (_installClickListener), sent
|
||||
@@ -43,7 +45,7 @@
|
||||
*
|
||||
* @dependency vendor/xterm.js, vendor/xterm-addon-fit.js
|
||||
* @dependency constants.js (window.CodemanTerminalFont, window.CodemanFetchDeadline, DEFAULT_SCROLLBACK, TERMINAL_TAIL_SIZE, TERMINAL_CHUNK_SIZE)
|
||||
* @dependency terminal-ui.js (codemanCurrentXtermTheme, codemanCurrentSkinIsLight, CodemanTerminalInput.shouldSuppressTerminalQueryResponse/isTerminalFocusOrMouseReport/wheelDeltaLines/pageKeysForTravel, app._shouldForwardWheelToApp/_localScrollbackIsHollow/_terminalViewportAtBottom/_handleDesktopTerminalClick)
|
||||
* @dependency terminal-ui.js (codemanCurrentXtermTheme, codemanCurrentSkinIsLight, CodemanTerminalInput.shouldSuppressTerminalQueryResponse/isTerminalFocusOrMouseReport/wheelDeltaLines/pageKeysForGesture, app._shouldForwardWheelToApp/_localScrollbackIsHollow/_terminalViewportAtBottom/_handleDesktopTerminalClick, app.tabAlerts)
|
||||
* @dependency terminal-keycode229-recovery.js (window.CodemanKeyCode229Recovery, optional: absent, xterm's own textarea handling stands)
|
||||
* @loadorder 7.4 of 16, loaded after terminal-ui.js and before terminal-split.js
|
||||
*/
|
||||
@@ -198,9 +200,10 @@
|
||||
// flag, app._linkHovered, belongs to its terminal alone).
|
||||
this._linkHovered = false;
|
||||
this._onFocusIn = null;
|
||||
// Hollow-buffer paging (_maybePageCliTranscript): wheel travel short of a
|
||||
// whole page, carried to the next wheel event.
|
||||
this._pageKeyPending = 0;
|
||||
// Hollow-buffer paging (_maybePageCliTranscript): the gesture in progress
|
||||
// (travel short of a whole page, its direction and last event time), the
|
||||
// state CodemanTerminalInput.pageKeysForGesture hands back.
|
||||
this._pageKeyGesture = null;
|
||||
// Page keys waiting for the 40 ms flush, and its timer (_queueScrollBytes).
|
||||
this._scrollBytes = '';
|
||||
this._scrollFlushTimer = null;
|
||||
@@ -1071,17 +1074,20 @@
|
||||
|
||||
// Hollow-buffer paging, the twin of the primary pane's
|
||||
// _maybePageCliTranscript (terminal-ui.js; keep the two in step). A CLI that
|
||||
// draws in place (opencode, on the alternate screen; Claude's repaint mode)
|
||||
// leaves this xterm no scrollback, so a wheel scrolled nothing; instead the
|
||||
// travel 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. Returns true when the wheel was consumed here.
|
||||
// draws in place (on the alternate screen, or Claude's repaint mode) leaves
|
||||
// this xterm no scrollback, so a wheel scrolled nothing; instead the travel
|
||||
// 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,
|
||||
// pinch and sideways-swipe consume) are the shared pure
|
||||
// CodemanTerminalInput.pageKeysForGesture, run on this tile's own state.
|
||||
// Returns true when the wheel was consumed here.
|
||||
_maybePageCliTranscript(ev) {
|
||||
if (this._destroyed || !this.terminal || !ev || ev.shiftKey) return false;
|
||||
const app = global.app;
|
||||
const input = global.CodemanTerminalInput;
|
||||
if (!app || !input?.pageKeysForTravel || !input.wheelDeltaLines) return false;
|
||||
if (!app || !input?.pageKeysForGesture || !input.wheelDeltaLines) return false;
|
||||
// xterm's own encoder forwards the wheel while the CLI's tracking reaches
|
||||
// it (a shell running htop), as in the primary pane.
|
||||
const tracking = this.terminal.modes?.mouseTrackingMode;
|
||||
@@ -1105,8 +1111,12 @@
|
||||
if (!app._terminalViewportAtBottom?.(this.terminal)) return false;
|
||||
const lines = input.wheelDeltaLines(ev, this.terminal.rows);
|
||||
if (!lines) return false;
|
||||
const step = input.pageKeysForTravel(this._pageKeyPending, lines, this.terminal.rows);
|
||||
this._pageKeyPending = step.pending;
|
||||
// An open dialog (a pending permission prompt or question) in THIS tile's
|
||||
// session, which is usually not the active one: a page key would move its
|
||||
// selector. Consumed, so xterm does not scroll the rows under it either.
|
||||
if (app.tabAlerts?.get(this.sessionId) === 'action') return true;
|
||||
const step = input.pageKeysForGesture(this._pageKeyGesture, ev, lines, this.terminal.rows, performance.now());
|
||||
this._pageKeyGesture = step.state;
|
||||
if (step.keys) this._queueScrollBytes(step.keys);
|
||||
return true;
|
||||
}
|
||||
@@ -1459,7 +1469,7 @@
|
||||
clearTimeout(this._scrollFlushTimer);
|
||||
this._scrollFlushTimer = null;
|
||||
this._scrollBytes = '';
|
||||
this._pageKeyPending = 0;
|
||||
this._pageKeyGesture = null;
|
||||
this._detachSocket();
|
||||
if (this._onFocusIn) {
|
||||
this.terminal?.textarea?.removeEventListener('focus', this._onFocusIn);
|
||||
|
||||
@@ -117,13 +117,17 @@
|
||||
: delta / 25; // DOM_DELTA_PIXEL (Chrome/WebKit, and every trackpad)
|
||||
}
|
||||
|
||||
// Rows of travel per page key for a terminal `rows` tall (half a screen).
|
||||
function pageKeyRows(rows) {
|
||||
return Math.max(2, Math.round((rows || 24) * PAGE_KEY_SCREEN_FRACTION));
|
||||
}
|
||||
|
||||
// Gesture travel → PageUp/PageDown keys for a terminal `rows` tall: adds
|
||||
// `lines` to the sub-page travel already `pending`, and returns the travel
|
||||
// left over plus the keys to send ('' below one page). The arithmetic of the
|
||||
// primary pane's _maybePageCliTranscript, pure so a TerminalTile (which keeps
|
||||
// its own pending travel) pages identically.
|
||||
// left over plus the keys to send ('' below one page). The plain accumulation
|
||||
// step of pageKeysForGesture below.
|
||||
function pageKeysForTravel(pending, lines, rows) {
|
||||
const perPage = Math.max(2, Math.round((rows || 24) * PAGE_KEY_SCREEN_FRACTION));
|
||||
const perPage = pageKeyRows(rows);
|
||||
const total = (pending || 0) + lines;
|
||||
const pages = Math.trunc(total / perPage);
|
||||
const keys = pages
|
||||
@@ -132,6 +136,51 @@
|
||||
return { pending: total - pages * perPage, keys };
|
||||
}
|
||||
|
||||
// One wheel or touch event of a hollow-pane gesture → the PageUp/PageDown keys
|
||||
// to send, for a terminal `rows` tall, at time `now` (ms). Pure: `state` is
|
||||
// the previous call's returned state (null or {} for a fresh pane), and the
|
||||
// caller keeps it, so the primary pane and every TerminalTile page with the
|
||||
// same gesture rules through their own state:
|
||||
// - a pinch (`ctrlKey`, Chrome's trackpad-pinch wheel) and a mostly
|
||||
// horizontal swipe (both deltas present, |deltaX| > |deltaY|) send nothing
|
||||
// and leave the state alone (the caller still consumes them);
|
||||
// - after PAGE_KEY_GESTURE_GAP_MS of quiet, or a direction change, a NEW
|
||||
// gesture starts, and a trackpad-sized opening event (at least
|
||||
// PAGE_KEY_MIN_START_ROWS, under PAGE_KEY_IMMEDIATE_MAX_ROWS) 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;
|
||||
// - everything else, a wheel notch included, 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 || {};
|
||||
if (ev?.ctrlKey) return { state: prev, keys: '' };
|
||||
const dx = Math.abs(ev?.deltaX || 0);
|
||||
const dy = Math.abs(ev?.deltaY || 0);
|
||||
if (dx && dy && dx > dy) return { state: prev, keys: '' };
|
||||
const perPage = pageKeyRows(rows);
|
||||
const dir = lines < 0 ? -1 : 1;
|
||||
let pending = prev.pending || 0;
|
||||
let prepaid = !!prev.prepaid;
|
||||
const gestureStart =
|
||||
typeof prev.lastAt !== 'number' || now - prev.lastAt > PAGE_KEY_GESTURE_GAP_MS || dir !== prev.dir;
|
||||
if (gestureStart) {
|
||||
if (prepaid) pending = 0;
|
||||
prepaid = false;
|
||||
const size = Math.abs(lines);
|
||||
if (size >= PAGE_KEY_MIN_START_ROWS && size < Math.min(perPage, PAGE_KEY_IMMEDIATE_MAX_ROWS)) {
|
||||
// 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 },
|
||||
keys: dir < 0 ? KEY_PAGE_UP : KEY_PAGE_DOWN,
|
||||
};
|
||||
}
|
||||
}
|
||||
const step = pageKeysForTravel(pending, lines, rows);
|
||||
return { state: { pending: step.pending, lastAt: now, dir, prepaid }, keys: step.keys };
|
||||
}
|
||||
|
||||
const TUI_PROMPT_DEFAULT_ROWS_FROM_BOTTOM = 4;
|
||||
// Composer navigation keys as xterm.js encodes user keystrokes: plain and
|
||||
// modified arrows (CSI A-D, CSI 1;mA-D, SS3 A-D), Home/End (CSI H/F, SS3
|
||||
@@ -271,6 +320,7 @@
|
||||
PAGE_KEY_MAX_PER_BATCH,
|
||||
wheelDeltaLines,
|
||||
pageKeysForTravel,
|
||||
pageKeysForGesture,
|
||||
PAGE_KEY_GESTURE_GAP_MS,
|
||||
PAGE_KEY_MIN_START_ROWS,
|
||||
PAGE_KEY_IMMEDIATE_MAX_ROWS,
|
||||
@@ -5760,54 +5810,34 @@ Object.assign(CodemanApp.prototype, {
|
||||
* @returns true when the gesture was consumed here (the caller must not also
|
||||
* scroll locally).
|
||||
*
|
||||
* The gesture rules above live in the pure CodemanTerminalInput.pageKeysForGesture
|
||||
* (top of this file); this method adds the gates and keeps the state.
|
||||
*
|
||||
* Twin: TerminalTile._maybePageCliTranscript (terminal-tile.js) pages a tile
|
||||
* through the same gates and the same pageKeysForTravel arithmetic; keep the
|
||||
* two in step. The tile adds one gate this pane cannot need, viewport at the
|
||||
* through the same gates and the same pageKeysForGesture, with its own state
|
||||
* and its OWN session's dialog check; keep the two in step. The tile adds one gate this pane cannot need, viewport at the
|
||||
* bottom: hollow here means baseY 0, so this viewport is always there, while a
|
||||
* tile is hollow with its own discounted rows still above the screen.
|
||||
*/
|
||||
_maybePageCliTranscript(ev, lines) {
|
||||
if (!lines || ev?.shiftKey || !this.activeSessionId) return false;
|
||||
if (!this._localScrollbackIsHollow()) return false;
|
||||
if (ev?.ctrlKey) return true; // pinch
|
||||
const dx = Math.abs(ev?.deltaX || 0);
|
||||
const dy = Math.abs(ev?.deltaY || 0);
|
||||
if (dx && dy && dx > dy) return true; // mostly horizontal swipe
|
||||
if (this.tabAlerts?.get(this.activeSessionId) === 'action') return true; // dialog up
|
||||
// Leftover travel belongs to the tab it was made on.
|
||||
// An open dialog in THIS pane's session (the active one): a page key would
|
||||
// move its selector. Consumed, so the buffer does not scroll under it either.
|
||||
if (this.tabAlerts?.get(this.activeSessionId) === 'action') return true;
|
||||
// Leftover travel and the gesture in progress belong to the tab they were made on.
|
||||
if (this._pageKeySession !== this.activeSessionId) {
|
||||
this._pageKeySession = this.activeSessionId;
|
||||
this._pageKeyPending = 0;
|
||||
this._pageKeyLastAt = undefined;
|
||||
this._pageKeyPrepaid = false;
|
||||
this._pageKeyGesture = null;
|
||||
}
|
||||
const tuning = window.CodemanTerminalInput;
|
||||
const perPage = Math.max(2, Math.round((this.terminal?.rows || 24) * tuning.PAGE_KEY_SCREEN_FRACTION));
|
||||
const now = performance.now();
|
||||
const dir = lines < 0 ? -1 : 1;
|
||||
const gestureStart =
|
||||
typeof this._pageKeyLastAt !== 'number' ||
|
||||
now - this._pageKeyLastAt > tuning.PAGE_KEY_GESTURE_GAP_MS ||
|
||||
dir !== this._pageKeyDir;
|
||||
this._pageKeyLastAt = now;
|
||||
this._pageKeyDir = dir;
|
||||
if (gestureStart) {
|
||||
// A debt left by an earlier pre-paid page dies with its gesture; plain
|
||||
// wheel travel keeps accumulating across notches.
|
||||
if (this._pageKeyPrepaid) this._pageKeyPending = 0;
|
||||
this._pageKeyPrepaid = false;
|
||||
const size = Math.abs(lines);
|
||||
if (size >= tuning.PAGE_KEY_MIN_START_ROWS && size < Math.min(perPage, tuning.PAGE_KEY_IMMEDIATE_MAX_ROWS)) {
|
||||
// Pre-pay one page; the skipped travel is owed back (pending carries the opposite sign).
|
||||
this._pageKeyPrepaid = true;
|
||||
this._pageKeyPending = lines - dir * perPage;
|
||||
this._queueScrollBytes(dir < 0 ? tuning.KEY_PAGE_UP : tuning.KEY_PAGE_DOWN);
|
||||
this._logScrollRouting('page-keys');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const step = tuning.pageKeysForTravel(this._pageKeyPending, lines, this.terminal?.rows);
|
||||
this._pageKeyPending = step.pending;
|
||||
const step = window.CodemanTerminalInput.pageKeysForGesture(
|
||||
this._pageKeyGesture,
|
||||
ev,
|
||||
lines,
|
||||
this.terminal?.rows,
|
||||
performance.now()
|
||||
);
|
||||
this._pageKeyGesture = step.state;
|
||||
if (step.keys) this._queueScrollBytes(step.keys);
|
||||
this._logScrollRouting('page-keys');
|
||||
return true;
|
||||
|
||||
Reference in new issue
Block a user