mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
Merge pull request #577: fix(tiles): the wheel scrolls Claude's fullscreen transcript in a tile, and Shift+wheel scrolls local history
This commit is contained in:
commit
3f4af2aa2e
8 files changed
+454
-38
No files matched your search
@@ -11,14 +11,20 @@
|
||||
*
|
||||
* Deliberately plainer than the primary pane (this.terminal/this._ws in
|
||||
* terminal-ui.js): no local-echo overlay, no CJK IME textarea, no touch/mobile
|
||||
* handlers (a swipe on a touch screen pages nothing), no keyboard accessory
|
||||
* bar, and no SGR wheel forwarding to Claude's fullscreen renderer
|
||||
* (docs/tile-grid-plan.md follow-up 4). Built for wide screens; see
|
||||
* docs/split-pane-sessions-plan.md and docs/tile-grid-plan.md.
|
||||
* handlers (a swipe on a touch screen pages nothing), and no keyboard
|
||||
* accessory bar. Built for wide screens; see docs/split-pane-sessions-plan.md
|
||||
* and docs/tile-grid-plan.md.
|
||||
*
|
||||
* 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):
|
||||
* - SGR wheel forwarding (_maybeForwardWheelToCli): Claude's fullscreen
|
||||
* renderer scrolls its own transcript on SGR wheel reports, while this
|
||||
* xterm holds only replayed repaint frames, so the wheel goes to the CLI
|
||||
* as reports at the pointer's cell in this pane, through the primary
|
||||
* pane's forwarding gate and its encoding (sgrWheelReports). Shift+wheel
|
||||
* scrolls the local scrollback itself (_maybeScrollLocalOnShift), as the
|
||||
* primary pane does, since xterm turns it into a horizontal no-op.
|
||||
* - 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
|
||||
@@ -43,7 +49,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/wheelDeltaWholeLines/sgrWheelReports/pageKeysForTravel, app._shouldForwardWheelToApp/_localScrollbackIsHollow/_terminalViewportAtBottom/_clientPointToCell/_handleDesktopTerminalClick)
|
||||
* @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
|
||||
*/
|
||||
@@ -201,6 +207,9 @@
|
||||
// Hollow-buffer paging (_maybePageCliTranscript): wheel travel short of a
|
||||
// whole page, carried to the next wheel event.
|
||||
this._pageKeyPending = 0;
|
||||
// Shift+wheel travel short of a whole line, carried to the next wheel
|
||||
// event (_maybeScrollLocalOnShift).
|
||||
this._shiftScrollPending = 0;
|
||||
// Page keys waiting for the 40 ms flush, and its timer (_queueScrollBytes).
|
||||
this._scrollBytes = '';
|
||||
this._scrollFlushTimer = null;
|
||||
@@ -1027,16 +1036,18 @@
|
||||
|
||||
// Capture phase, because xterm's own wheel handler stopPropagation()s every
|
||||
// event it consumes, so a bubbling listener here would never see the wheel
|
||||
// while the pane still has scrollback to scroll. Not passive: the one route
|
||||
// this pane takes over, paging a hollow buffer's CLI transcript
|
||||
// (_maybePageCliTranscript), is consumed right here (preventDefault plus
|
||||
// while the pane still has scrollback to scroll. Not passive: the three
|
||||
// routes this pane takes over, forwarding the wheel to Claude's fullscreen
|
||||
// renderer (_maybeForwardWheelToCli), paging a hollow buffer's CLI
|
||||
// transcript (_maybePageCliTranscript) and Shift+wheel's local scrollback
|
||||
// (_maybeScrollLocalOnShift), are consumed right here (preventDefault plus
|
||||
// stopPropagation in the capture phase, the primary pane's technique), so
|
||||
// xterm's viewport, a descendant, never sees it. Every other wheel is left
|
||||
// xterm's viewport, a descendant, never sees them. Every other wheel is left
|
||||
// to xterm, which keeps doing the scrolling, and only observed for the
|
||||
// shell history pull.
|
||||
_installWheelListener() {
|
||||
this._onWheel = (ev) => {
|
||||
if (this._maybePageCliTranscript(ev)) {
|
||||
if (this._maybeForwardWheelToCli(ev) || this._maybePageCliTranscript(ev) || this._maybeScrollLocalOnShift(ev)) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
@@ -1046,6 +1057,71 @@
|
||||
this.mountEl.addEventListener('wheel', this._onWheel, { capture: true, passive: false });
|
||||
}
|
||||
|
||||
// SGR wheel forwarding, the twin of the primary pane's capture-phase wheel
|
||||
// handler and _forwardScrollToApp (terminal-ui.js; keep them in step).
|
||||
// Claude's fullscreen renderer (claude 2.1.187+ while its mouse tracking is
|
||||
// on, cliMouseTracking) scrolls its own transcript on SGR wheel reports,
|
||||
// while this xterm holds only Codeman's replayed repaint frames (tmux keeps
|
||||
// no history for such a pane). Left to xterm, the wheel dragged those stale
|
||||
// frames, Claude's pinned input box with them, up the tile, or scrolled
|
||||
// nothing at all. The 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 reports go to this
|
||||
// pane's session through its own coalescer. Returns true when the wheel
|
||||
// belongs to the CLI: a gesture with no whole line or no measurable cell is
|
||||
// consumed too, as in the primary pane, so xterm never scrolls the stale
|
||||
// frames under a forwarding session. Shift fails the gate, so Shift+wheel
|
||||
// still scrolls the local scrollback (_maybeScrollLocalOnShift).
|
||||
_maybeForwardWheelToCli(ev) {
|
||||
if (this._destroyed || !this.terminal || !ev) return false;
|
||||
const app = global.app;
|
||||
const input = global.CodemanTerminalInput;
|
||||
if (!app?._shouldForwardWheelToApp || !input?.sgrWheelReports || !input.wheelDeltaWholeLines) return false;
|
||||
// xterm's own encoder forwards the wheel while the CLI's tracking reaches
|
||||
// it, and its alt-scroll owns the alternate buffer, as in the primary pane.
|
||||
const tracking = this.terminal.modes?.mouseTrackingMode;
|
||||
if (tracking && tracking !== 'none') return false;
|
||||
if (this.terminal.buffer?.active?.type === 'alternate') return false;
|
||||
if (!app._shouldForwardWheelToApp(ev, { terminal: this.terminal, sessionId: this.sessionId })) return false;
|
||||
// SGR coordinates address the live screen, so a report from a scrolled-up
|
||||
// viewport would hit-test another row: snap home first (_forwardScrollToApp).
|
||||
if (!app._terminalViewportAtBottom?.(this.terminal)) this.terminal.scrollToBottom?.();
|
||||
const lines = input.wheelDeltaWholeLines(ev, this.terminal.rows);
|
||||
const pos = app._clientPointToCell?.(ev.clientX, ev.clientY, this.terminal);
|
||||
const bytes = input.sgrWheelReports(lines, pos);
|
||||
if (bytes) this._queueScrollBytes(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Shift+wheel scrolls this xterm's local scrollback, the explicit "local
|
||||
// history" gesture, here as in the primary pane (whose capture-phase wheel
|
||||
// handler scrolls with terminal.scrollLines() for the same reason). Left to
|
||||
// xterm it was dead off macOS: Chrome on Windows sends Shift+wheel as a
|
||||
// HORIZONTAL wheel (deltaX), and xterm's own scroller turns a Shift+vertical
|
||||
// wheel into a horizontal one, so the viewport never moved. Reads the
|
||||
// dominant axis under Shift (wheelDeltaLines), keeps the sub-line remainder
|
||||
// for the next event (a trackpad's small deltas), and on the way up still
|
||||
// asks a shell pane for more history. Returns true when the wheel was
|
||||
// consumed here.
|
||||
_maybeScrollLocalOnShift(ev) {
|
||||
if (this._destroyed || !this.terminal || !ev?.shiftKey) return false;
|
||||
const input = global.CodemanTerminalInput;
|
||||
if (!input?.wheelDeltaLines) return false;
|
||||
// xterm's own encoder forwards the wheel while the CLI's tracking reaches
|
||||
// it, and its alt-scroll owns the alternate buffer, as in the primary pane.
|
||||
const tracking = this.terminal.modes?.mouseTrackingMode;
|
||||
if (tracking && tracking !== 'none') return false;
|
||||
if (this.terminal.buffer?.active?.type === 'alternate') return false;
|
||||
const total = this._shiftScrollPending + input.wheelDeltaLines(ev, this.terminal.rows);
|
||||
const lines = Math.trunc(total);
|
||||
this._shiftScrollPending = total - lines;
|
||||
if (lines) {
|
||||
this.terminal.scrollLines(lines);
|
||||
if (lines < 0) this._maybeLoadMoreHistory();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// A plain left-click reported to the CLI, the primary pane's desktop click
|
||||
// (terminal-ui.js _handleDesktopTerminalClick) aimed at this pane. The
|
||||
// server strips the mouse DECSETs of some modes (opencode's since #555, so a
|
||||
@@ -1087,9 +1163,9 @@
|
||||
const tracking = this.terminal.modes?.mouseTrackingMode;
|
||||
if (tracking && tracking !== 'none') return false;
|
||||
const target = { terminal: this.terminal, sessionId: this.sessionId };
|
||||
// The primary pane would forward this wheel to Claude's fullscreen
|
||||
// renderer as SGR reports. Tiles do not do that yet (docs/tile-grid-plan.md
|
||||
// follow-up 4), so the wheel stays with xterm, as before.
|
||||
// A wheel for Claude's fullscreen renderer was forwarded as SGR reports
|
||||
// before this ran (_maybeForwardWheelToCli); the gate is repeated so a
|
||||
// forwarding session is never paged.
|
||||
if (app._shouldForwardWheelToApp?.(ev, target)) return false;
|
||||
if (!app._localScrollbackIsHollow?.({ ...target, localRows: this._localRows() })) return false;
|
||||
// Only from the live screen. The one gate the primary pane never needs: a
|
||||
@@ -1111,8 +1187,9 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
// Coalesces the page keys into one send per 40 ms, bounded at 512 bytes so a
|
||||
// fling cannot build a backlog that keeps paging after it stops. A narrow
|
||||
// Coalesces the scroll bytes (SGR wheel reports and page keys alike) into
|
||||
// one send per 40 ms, bounded at 512 bytes so a fling cannot build a backlog
|
||||
// that keeps scrolling after it stops. A narrow
|
||||
// twin of the primary pane's _queueScrollBytes / _flushWheelSgrQueue
|
||||
// (terminal-ui.js; keep the two in step), which flushes to the active
|
||||
// session only. Sent ephemeral (no seq, never persisted) to THIS pane's
|
||||
|
||||
@@ -107,6 +107,32 @@
|
||||
: delta / 25; // DOM_DELTA_PIXEL (Chrome/WebKit, and every trackpad)
|
||||
}
|
||||
|
||||
// The same travel rounded to whole lines for the SGR wheel reports: a pure
|
||||
// horizontal swipe is 0 (nothing to send), and anything else moves at least
|
||||
// one line, so the small pixel deltas of a precision touchpad still scroll.
|
||||
// The body of the primary pane's _wheelScrollLines.
|
||||
function wheelDeltaWholeLines(ev, rows) {
|
||||
const lines = wheelDeltaLines(ev, rows);
|
||||
if (!lines) return 0;
|
||||
return Math.round(lines) || (lines > 0 ? 1 : -1);
|
||||
}
|
||||
|
||||
// Ticks one gesture batch may report: Claude applies its own scroll-speed
|
||||
// multiplier and acceleration on top, so a bigger batch only overshoots.
|
||||
const SGR_WHEEL_MAX_TICKS = 5;
|
||||
|
||||
// Whole wheel lines → SGR wheel reports at a 1-based cell `pos` ({ col, row },
|
||||
// live-screen relative): button 64 per line up, 65 per line down, capped at
|
||||
// SGR_WHEEL_MAX_TICKS. '' when there is nothing to send. The encoding of the
|
||||
// primary pane's _sendSyntheticSgrWheel, pure so a TerminalTile forwards
|
||||
// byte-identical reports to its own session.
|
||||
function sgrWheelReports(lines, pos) {
|
||||
if (!lines || !pos) return '';
|
||||
const btn = lines < 0 ? 64 : 65;
|
||||
const ticks = Math.min(Math.abs(lines), SGR_WHEEL_MAX_TICKS);
|
||||
return `\x1b[<${btn};${pos.col};${pos.row}M`.repeat(ticks);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -260,6 +286,9 @@
|
||||
PAGE_KEY_SCREEN_FRACTION,
|
||||
PAGE_KEY_MAX_PER_BATCH,
|
||||
wheelDeltaLines,
|
||||
wheelDeltaWholeLines,
|
||||
SGR_WHEEL_MAX_TICKS,
|
||||
sgrWheelReports,
|
||||
pageKeysForTravel,
|
||||
TUI_PROMPT_DEFAULT_ROWS_FROM_BOTTOM,
|
||||
MOBILE_KEYBOARD_DISMISS_EXEMPT_SELECTOR,
|
||||
@@ -5563,9 +5592,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
// the ±1 fallback — one line per notch, versus 4-5 for Chrome's ~110px. In
|
||||
// Claude mode the same value also capped the forwarded SGR report at one tick.
|
||||
_wheelScrollLines(ev) {
|
||||
const lines = this._wheelScrollLinesFloat(ev);
|
||||
if (!lines) return 0; // pure horizontal swipe: don't fall through to -1
|
||||
return Math.round(lines) || (lines > 0 ? 1 : -1);
|
||||
// Pure horizontal swipe: 0, never the ±1 fallback (wheelDeltaWholeLines).
|
||||
return window.CodemanTerminalInput.wheelDeltaWholeLines(ev, this.terminal?.rows);
|
||||
},
|
||||
|
||||
/** Unrounded variant for the smooth local-scroll path, which accumulates
|
||||
@@ -5639,13 +5667,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
// scroll-speed multiplier and acceleration on top), and the queue is bounded
|
||||
// so a wild scroll can't build a backlog that keeps scrolling after the finger
|
||||
// stops. Flushed via _sendInputEphemeral — loss-tolerant, off the durable queue.
|
||||
// The encoding is the pure CodemanTerminalInput.sgrWheelReports, which a
|
||||
// TerminalTile calls with its own cell (TerminalTile._maybeForwardWheelToCli).
|
||||
_sendSyntheticSgrWheel(clientX, clientY, lines) {
|
||||
if (!this.activeSessionId || !lines) return;
|
||||
const pos = this._clientPointToCell(clientX, clientY);
|
||||
if (!pos) return;
|
||||
const btn = lines < 0 ? 64 : 65;
|
||||
const ticks = Math.min(Math.abs(lines), 5);
|
||||
this._queueScrollBytes(`\x1b[<${btn};${pos.col};${pos.row}M`.repeat(ticks));
|
||||
this._queueScrollBytes(window.CodemanTerminalInput.sgrWheelReports(lines, pos));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user