mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
fix(tiles): page a hollow tile only from the live screen, review follow-up
A tile counts as hollow when every row above its screen is its own overflow (baseY minus _overflowRows is 0), so unlike the primary pane, whose hollow buffer has baseY 0, its viewport can sit above the bottom while it is hollow: Shift+PageUp, a scrollbar drag or a wheel during the first replay leave it up there. _maybePageCliTranscript never looked at the viewport, so every wheel, wheel-down included, was turned into PageUp/PageDown and swallowed. xterm never scrolled back, the stale rows stayed on screen while the CLI paged out of view, and clicks were dropped too, because the click report refuses an off-bottom viewport. The tile now pages only while _terminalViewportAtBottom holds for its own terminal, checked before the pending travel is touched. Off the bottom the wheel stays with xterm, so a wheel-down brings the viewport home and paging resumes from there. The primary pane is unchanged: its hollow test already implies a viewport at the bottom, which the twin comment now says. Tests: a unit case for a tile hollow by the discount with its viewport above the bottom (no page key, no preventDefault, and no travel carried over once back home), and the real-browser case now scrolls a hollow tile up and proves a real wheel-down scrolls xterm home with no page key sent, then pages again. Both go red with the gate removed, and the unit case also with the gate moved below the pending-travel update. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -225,7 +225,7 @@ Further detail: the `<prefix>: <title>` form (`w3-myapp: fix the login redirect`
|
|||||||
|
|
||||||
**Wheel/touch forwarding is NOT gated on viewport-at-bottom** (#205, `terminal-ui.js:_shouldForwardWheelToApp`): for sessions verified to scroll their own transcript on SGR wheel reports (claude ≥ 2.1.187 while `cliMouseTracking` is true, i.e. fullscreen; version via the local/docker/remote `--version` probes), the plain wheel AND touch drags forward as coalesced SGR reports (`_forwardScrollToApp` → `_sendSyntheticSgrWheel`, 40ms batches, 5-tick cap, 512-byte queue bound). It used to gate on the viewport being at the bottom so both scrollbacks stayed reachable, but a repaint-mode CLI keeps NO terminal scrollback of its own — xterm's buffer holds only replayed repaint frames, so local scrolling drags the CLI's pinned prompt box up the screen over stale frames; and `scrollToLastNonEmptyLine()` routinely parked the viewport off-bottom, silently pinning the wheel to local. Forwarding now snaps the viewport home first (SGR coordinates address the LIVE screen — a report computed from a scrolled-up viewport would hit-test the wrong row). Local scrollback remains on Shift+wheel and the `terminalWheelLocalScrollback` opt-out (both also cover touch via the shared gate; touch has no Shift, so the setting is its only local pin). `_wheelScrollLines()` normalizes `deltaMode` (Firefox fires LINE deltas ≈3/notch — read as pixels that rounded to 0 and fell to the ±1 fallback, ~4× too slow; PAGE deltas scale by `terminal.rows`) while keeping the #154 Shift-axis trap (macOS trackpads put Shift+scroll magnitude on deltaX). Tests: `test/terminal-touch-tap.test.ts`.
|
**Wheel/touch forwarding is NOT gated on viewport-at-bottom** (#205, `terminal-ui.js:_shouldForwardWheelToApp`): for sessions verified to scroll their own transcript on SGR wheel reports (claude ≥ 2.1.187 while `cliMouseTracking` is true, i.e. fullscreen; version via the local/docker/remote `--version` probes), the plain wheel AND touch drags forward as coalesced SGR reports (`_forwardScrollToApp` → `_sendSyntheticSgrWheel`, 40ms batches, 5-tick cap, 512-byte queue bound). It used to gate on the viewport being at the bottom so both scrollbacks stayed reachable, but a repaint-mode CLI keeps NO terminal scrollback of its own — xterm's buffer holds only replayed repaint frames, so local scrolling drags the CLI's pinned prompt box up the screen over stale frames; and `scrollToLastNonEmptyLine()` routinely parked the viewport off-bottom, silently pinning the wheel to local. Forwarding now snaps the viewport home first (SGR coordinates address the LIVE screen — a report computed from a scrolled-up viewport would hit-test the wrong row). Local scrollback remains on Shift+wheel and the `terminalWheelLocalScrollback` opt-out (both also cover touch via the shared gate; touch has no Shift, so the setting is its only local pin). `_wheelScrollLines()` normalizes `deltaMode` (Firefox fires LINE deltas ≈3/notch — read as pixels that rounded to 0 and fell to the ±1 fallback, ~4× too slow; PAGE deltas scale by `terminal.rows`) while keeping the #154 Shift-axis trap (macOS trackpads put Shift+scroll magnitude on deltaX). Tests: `test/terminal-touch-tap.test.ts`.
|
||||||
|
|
||||||
**A false gate on a hollow pane must not mean a DEAD gesture** (#205 round 2, `_maybePageCliTranscript`): every way `_shouldForwardWheelToApp()` returns false leaves a repaint-mode pane scrolling a buffer that has nothing in it (`baseY === 0`) — the version probe came back empty, the CLI really is older than 2.1.187, the `cliMouseTracking` flag is unset (inline claude, or fullscreen right after a server restart), or the user turned on `terminalWheelLocalScrollback`. **opencode is the fifth case, and the gate is false there by design**: its TUI runs on the ALTERNATE SCREEN (1.18.31 measured: tmux `alternate_on=1`, `history_size=0`), so the buffer is hollow, and it IGNORES SGR wheel reports entirely (six `\x1b[<64;…M` reports against an idle pane left the capture byte-identical) while still paging its transcript on PageUp/PageDown (`messages_page_up/down`) — so paging is the only gesture that can reach it, and without it the wheel was silently dead in every opencode tab. The 1.12.0 retest reported exactly that shape for Claude: a wheel that did nothing at all while Fn+Up (PageUp) paged back through intact text, which is the proof that the CLI's own history and the PTY input path were both fine. So under the guard (`_localScrollbackIsHollow()` — `claude` or `opencode`, gate false, `baseY === 0`) wheel and touch travel is translated into coalesced `\x1b[5~` / `\x1b[6~` through the same 40ms queue as the SGR reports, at half a screen of travel per page key (the key jumps a whole screen; a 1:1 mapping was unusably slow with a discrete wheel). ⚠️ `shell`/`pi` own real terminal scrollback and are never paged, and codex/gemini/antigravity/grok/deepseek/omp page-key behaviour is unverified (`docs/scrollback-fix-plan.md`). ⚠️ Shift is excluded on purpose — it is the explicit "give me local scrollback" gesture and must keep that meaning. ⚠️ `terminalWheelLocalScrollback` is deliberately NOT scoped away from repaint-mode CLIs even though it is a footgun there: that would silently override an explicit user choice, so the fallback catches it instead. **Server-side counterpart**: `getClaudeCliVersion()` caches SUCCESS for the process lifetime but must never cache FAILURE — it used to, so one timed-out or PATH-starved probe at the first Claude session start disabled wheel-forwarding for every Claude session until the server restarted (a dead wheel on phone, tablet and laptop at once, the signature of a server-side cause). Failures now retry with a 1/2/4…15min backoff; the policy is the pure `resolveClaudeCliVersion()`. **Tiles** (a grid tile, the split's Pane B: `TerminalTile`) page the wheel through these same gates, called with the tile's own terminal and session (`_localScrollbackIsHollow(target)`, `_shouldForwardWheelToApp(ev, target)`) and the pure `CodemanTerminalInput.pageKeysForTravel`, so the mode list stays in terminal-ui.js alone. ⚠️ A tile's `baseY` is rarely 0 even when hollow: its first capture is taken at the PTY's previous, taller size and its row-shrinking fits push rows up, so it passes `localRows` with those rows discounted (`TerminalTile._localRows`). Tiles have no touch path and do not forward SGR wheel (tile-grid-plan follow-up 4). Tests: `test/terminal-scroll-routing.test.ts`, `test/terminal-tile-scroll.test.ts`, `test/claude-cli-version-cache.test.ts`.
|
**A false gate on a hollow pane must not mean a DEAD gesture** (#205 round 2, `_maybePageCliTranscript`): every way `_shouldForwardWheelToApp()` returns false leaves a repaint-mode pane scrolling a buffer that has nothing in it (`baseY === 0`) — the version probe came back empty, the CLI really is older than 2.1.187, the `cliMouseTracking` flag is unset (inline claude, or fullscreen right after a server restart), or the user turned on `terminalWheelLocalScrollback`. **opencode is the fifth case, and the gate is false there by design**: its TUI runs on the ALTERNATE SCREEN (1.18.31 measured: tmux `alternate_on=1`, `history_size=0`), so the buffer is hollow, and it IGNORES SGR wheel reports entirely (six `\x1b[<64;…M` reports against an idle pane left the capture byte-identical) while still paging its transcript on PageUp/PageDown (`messages_page_up/down`) — so paging is the only gesture that can reach it, and without it the wheel was silently dead in every opencode tab. The 1.12.0 retest reported exactly that shape for Claude: a wheel that did nothing at all while Fn+Up (PageUp) paged back through intact text, which is the proof that the CLI's own history and the PTY input path were both fine. So under the guard (`_localScrollbackIsHollow()` — `claude` or `opencode`, gate false, `baseY === 0`) wheel and touch travel is translated into coalesced `\x1b[5~` / `\x1b[6~` through the same 40ms queue as the SGR reports, at half a screen of travel per page key (the key jumps a whole screen; a 1:1 mapping was unusably slow with a discrete wheel). ⚠️ `shell`/`pi` own real terminal scrollback and are never paged, and codex/gemini/antigravity/grok/deepseek/omp page-key behaviour is unverified (`docs/scrollback-fix-plan.md`). ⚠️ Shift is excluded on purpose — it is the explicit "give me local scrollback" gesture and must keep that meaning. ⚠️ `terminalWheelLocalScrollback` is deliberately NOT scoped away from repaint-mode CLIs even though it is a footgun there: that would silently override an explicit user choice, so the fallback catches it instead. **Server-side counterpart**: `getClaudeCliVersion()` caches SUCCESS for the process lifetime but must never cache FAILURE — it used to, so one timed-out or PATH-starved probe at the first Claude session start disabled wheel-forwarding for every Claude session until the server restarted (a dead wheel on phone, tablet and laptop at once, the signature of a server-side cause). Failures now retry with a 1/2/4…15min backoff; the policy is the pure `resolveClaudeCliVersion()`. **Tiles** (a grid tile, the split's Pane B: `TerminalTile`) page the wheel through these same gates, called with the tile's own terminal and session (`_localScrollbackIsHollow(target)`, `_shouldForwardWheelToApp(ev, target)`) and the pure `CodemanTerminalInput.pageKeysForTravel`, so the mode list stays in terminal-ui.js alone. ⚠️ A tile's `baseY` is rarely 0 even when hollow: its first capture is taken at the PTY's previous, taller size and its row-shrinking fits push rows up, so it passes `localRows` with those rows discounted (`TerminalTile._localRows`). ⚠️ So a tile can be hollow with its viewport still up in those rows (Shift+PageUp, a scrollbar drag, a wheel during the first replay), and it pages only while `_terminalViewportAtBottom(tile.terminal)` holds: otherwise every wheel, wheel-down included, was paged and swallowed, the stale rows stayed on screen and the click report (which refuses an off-bottom viewport) went dead too. Off the bottom the wheel stays xterm's, so a wheel-down brings the viewport home. The primary pane needs no such gate: hollow there means `baseY === 0`, which is always at the bottom. Tiles have no touch path and do not forward SGR wheel (tile-grid-plan follow-up 4). Tests: `test/terminal-scroll-routing.test.ts`, `test/terminal-tile-scroll.test.ts`, `test/claude-cli-version-cache.test.ts`.
|
||||||
|
|
||||||
**Why the wheel went where it went is LOGGED** (`_logScrollRouting`): one console line per session per distinct decision — `[scroll] <id> → forward-sgr|page-keys|local-scrollback|repull-refused-downgrade (mode=…, cliVersion=…, localScrollbackOptOut=…, mouseTracking=…, localScrollbackRows=…)`. #205 ran two rounds of remote guesswork over questions this line answers directly; keep it when touching the routing.
|
**Why the wheel went where it went is LOGGED** (`_logScrollRouting`): one console line per session per distinct decision — `[scroll] <id> → forward-sgr|page-keys|local-scrollback|repull-refused-downgrade (mode=…, cliVersion=…, localScrollbackOptOut=…, mouseTracking=…, localScrollbackRows=…)`. #205 ran two rounds of remote guesswork over questions this line answers directly; keep it when touching the routing.
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
* so the wheel pages the CLI's own transcript with PageUp/PageDown
|
* so the wheel pages the CLI's own transcript with PageUp/PageDown
|
||||||
* (_maybePageCliTranscript) through the primary pane's gates, plus an
|
* (_maybePageCliTranscript) through the primary pane's gates, plus an
|
||||||
* overflow-row discount for this pane's capture-before-resize load
|
* overflow-row discount for this pane's capture-before-resize load
|
||||||
* (_localRows).
|
* (_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).
|
||||||
* - The desktop click report: a plain left-click hand-encoded as SGR while
|
* - 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
|
* the session's CLI has mouse tracking on (cliMouseTracking), for the modes
|
||||||
* whose mouse DECSETs the server strips (_installClickListener).
|
* whose mouse DECSETs the server strips (_installClickListener).
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
*
|
*
|
||||||
* @dependency vendor/xterm.js, vendor/xterm-addon-fit.js
|
* @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 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/_handleDesktopTerminalClick)
|
* @dependency terminal-ui.js (codemanCurrentXtermTheme, codemanCurrentSkinIsLight, CodemanTerminalInput.shouldSuppressTerminalQueryResponse/isTerminalFocusOrMouseReport/wheelDeltaLines/pageKeysForTravel, app._shouldForwardWheelToApp/_localScrollbackIsHollow/_terminalViewportAtBottom/_handleDesktopTerminalClick)
|
||||||
* @dependency terminal-keycode229-recovery.js (window.CodemanKeyCode229Recovery, optional: absent, xterm's own textarea handling stands)
|
* @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
|
* @loadorder 7.4 of 16, loaded after terminal-ui.js and before terminal-split.js
|
||||||
*/
|
*/
|
||||||
@@ -887,6 +888,17 @@
|
|||||||
// follow-up 4), so the wheel stays with xterm, as before.
|
// follow-up 4), so the wheel stays with xterm, as before.
|
||||||
if (app._shouldForwardWheelToApp?.(ev, target)) return false;
|
if (app._shouldForwardWheelToApp?.(ev, target)) return false;
|
||||||
if (!app._localScrollbackIsHollow?.({ ...target, localRows: this._localRows() })) 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
|
||||||
|
// primary hollow buffer has baseY 0, so its viewport is always at the
|
||||||
|
// bottom, while a tile's is hollow with its own overflow rows still above
|
||||||
|
// the screen, and Shift+PageUp, a scrollbar drag or a wheel during the
|
||||||
|
// first replay can leave the viewport up there. Paging from there would
|
||||||
|
// swallow every wheel (wheel-down included) and keep the stale rows on
|
||||||
|
// screen while the CLI pages out of view; left to xterm, a wheel-down
|
||||||
|
// brings the viewport home and paging resumes from there. The click
|
||||||
|
// report refuses an off-bottom viewport for the same reason
|
||||||
|
// (_terminalViewportAtBottom).
|
||||||
|
if (!app._terminalViewportAtBottom?.(this.terminal)) return false;
|
||||||
const lines = input.wheelDeltaLines(ev, this.terminal.rows);
|
const lines = input.wheelDeltaLines(ev, this.terminal.rows);
|
||||||
if (!lines) return false;
|
if (!lines) return false;
|
||||||
const step = input.pageKeysForTravel(this._pageKeyPending, lines, this.terminal.rows);
|
const step = input.pageKeysForTravel(this._pageKeyPending, lines, this.terminal.rows);
|
||||||
|
|||||||
@@ -5688,7 +5688,9 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
*
|
*
|
||||||
* Twin: TerminalTile._maybePageCliTranscript (terminal-tile.js) pages a tile
|
* Twin: TerminalTile._maybePageCliTranscript (terminal-tile.js) pages a tile
|
||||||
* through the same gates and the same pageKeysForTravel arithmetic; keep the
|
* through the same gates and the same pageKeysForTravel arithmetic; keep the
|
||||||
* two in step.
|
* 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) {
|
_maybePageCliTranscript(ev, lines) {
|
||||||
if (!lines || ev?.shiftKey || !this.activeSessionId) return false;
|
if (!lines || ev?.shiftKey || !this.activeSessionId) return false;
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
* screen, and a row-shrinking fit pushes more rows up, which is what the
|
* screen, and a row-shrinking fit pushes more rows up, which is what the
|
||||||
* tile's overflow discount (`_localRows`) counts. Output that scrolls real
|
* tile's overflow discount (`_localRows`) counts. Output that scrolls real
|
||||||
* lines is history, and the tile stops paging.
|
* lines is history, and the tile stops paging.
|
||||||
|
* - a viewport left up in those discounted rows gets its wheel back: a real
|
||||||
|
* wheel-down scrolls xterm home instead of being paged, and paging resumes
|
||||||
|
* from the live screen.
|
||||||
*
|
*
|
||||||
* The wheel is a real one (`page.mouse.wheel()`), and the last step proves it
|
* The wheel is a real one (`page.mouse.wheel()`), and the last step proves it
|
||||||
* reaches xterm: a wheel the tile does not page scrolls xterm's viewport, so
|
* reaches xterm: a wheel the tile does not page scrolls xterm's viewport, so
|
||||||
@@ -69,14 +72,15 @@ describe('TerminalTile wheel paging in a real browser', () => {
|
|||||||
} as Snap;
|
} as Snap;
|
||||||
});
|
});
|
||||||
|
|
||||||
/** A real wheel-up of a whole screen over the tile, then time for the 40 ms flush and a frame. */
|
/** A real wheel of `rows` lines over the tile (negative = up), then time for the 40 ms flush and a frame. */
|
||||||
async function wheelUp(rows: number) {
|
async function wheelBy(rows: number) {
|
||||||
await page.mouse.move(200, 60);
|
await page.mouse.move(200, 60);
|
||||||
await page.mouse.wheel(0, -rows * 25);
|
await page.mouse.wheel(0, rows * 25);
|
||||||
await page.waitForTimeout(150);
|
await page.waitForTimeout(150);
|
||||||
}
|
}
|
||||||
|
const wheelUp = (rows: number) => wheelBy(-rows);
|
||||||
|
|
||||||
it('pages a hollow tile, keeps xterm still, survives a shrink, and stops once real lines scroll', async () => {
|
it('pages a hollow tile, keeps xterm still, survives a shrink, returns an off-bottom wheel, and stops at real lines', async () => {
|
||||||
await page.evaluate(async (id) => {
|
await page.evaluate(async (id) => {
|
||||||
const w = window as any;
|
const w = window as any;
|
||||||
const app = w.app;
|
const app = w.app;
|
||||||
@@ -147,6 +151,29 @@ describe('TerminalTile wheel paging in a real browser', () => {
|
|||||||
expect(afterSecondWheel.sent.length).toBeGreaterThan(afterWheel.sent.length);
|
expect(afterSecondWheel.sent.length).toBeGreaterThan(afterWheel.sent.length);
|
||||||
expect(afterSecondWheel.viewportY).toBe(afterShrink.baseY);
|
expect(afterSecondWheel.viewportY).toBe(afterShrink.baseY);
|
||||||
|
|
||||||
|
// A viewport left up in those rows (Shift+PageUp, a scrollbar drag): the
|
||||||
|
// wheel is xterm's again, so a wheel-down really scrolls it home and sends
|
||||||
|
// no page key, and from the live screen the wheel pages once more.
|
||||||
|
await page.evaluate(() => (window as any).__tileProbe.tile.terminal.scrollLines(-5));
|
||||||
|
const scrolledUp = await snap();
|
||||||
|
expect(scrolledUp.viewportY).toBe(afterShrink.baseY - 5);
|
||||||
|
expect(scrolledUp.localRows).toBe(0);
|
||||||
|
// xterm scrolls a few lines per real wheel event, so wheel down until home
|
||||||
|
// (bounded), each step really moving it and none of them paged.
|
||||||
|
let backHome = scrolledUp;
|
||||||
|
for (let i = 0; i < 10 && backHome.viewportY < afterShrink.baseY; i++) {
|
||||||
|
const before = backHome.viewportY;
|
||||||
|
await wheelBy(afterShrink.rows);
|
||||||
|
backHome = await snap();
|
||||||
|
expect(backHome.viewportY).toBeGreaterThan(before);
|
||||||
|
}
|
||||||
|
expect(backHome.viewportY).toBe(afterShrink.baseY);
|
||||||
|
expect(backHome.sent.length).toBe(afterSecondWheel.sent.length);
|
||||||
|
await wheelUp(afterShrink.rows);
|
||||||
|
const pagedAgain = await snap();
|
||||||
|
expect(pagedAgain.sent.length).toBeGreaterThan(backHome.sent.length);
|
||||||
|
expect(pagedAgain.viewportY).toBe(afterShrink.baseY);
|
||||||
|
|
||||||
// Output that scrolled real lines is history: the wheel goes back to
|
// Output that scrolled real lines is history: the wheel goes back to
|
||||||
// xterm, which scrolls its own buffer, and no page key is sent.
|
// xterm, which scrolls its own buffer, and no page key is sent.
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
@@ -159,7 +186,7 @@ describe('TerminalTile wheel paging in a real browser', () => {
|
|||||||
expect(afterOutput.localRows).toBe(3);
|
expect(afterOutput.localRows).toBe(3);
|
||||||
await wheelUp(afterOutput.rows);
|
await wheelUp(afterOutput.rows);
|
||||||
const afterThirdWheel = await snap();
|
const afterThirdWheel = await snap();
|
||||||
expect(afterThirdWheel.sent.length).toBe(afterSecondWheel.sent.length);
|
expect(afterThirdWheel.sent.length).toBe(pagedAgain.sent.length);
|
||||||
expect(afterThirdWheel.viewportY).toBeLessThan(afterOutput.baseY);
|
expect(afterThirdWheel.viewportY).toBeLessThan(afterOutput.baseY);
|
||||||
} finally {
|
} finally {
|
||||||
await page.evaluate((id) => {
|
await page.evaluate((id) => {
|
||||||
|
|||||||
@@ -451,6 +451,38 @@ describe('rows the tile pushed above the screen itself are not history', () => {
|
|||||||
expect(flushed(ws)).toEqual([]);
|
expect(flushed(ws)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('leaves the wheel to xterm while the viewport sits in those rows, and pages again from the bottom', async () => {
|
||||||
|
// Hollow by the discount alone: baseY > 0, every row above the screen the
|
||||||
|
// tile's own. Shift+PageUp, a scrollbar drag or a wheel during the replay
|
||||||
|
// can leave the viewport up there; a primary hollow buffer (baseY 0) never
|
||||||
|
// can. Paging from there would swallow every wheel and keep the stale rows
|
||||||
|
// on screen, so xterm gets the wheel and a wheel-down brings it home.
|
||||||
|
serveCapture(lines(40), 40);
|
||||||
|
const { tile, ws, term, mount } = await connectTile(makeApp({ 's-tile': { mode: 'opencode' } }));
|
||||||
|
const baseY = term.buffer.active.baseY;
|
||||||
|
expect(baseY).toBe(16);
|
||||||
|
expect((tile as unknown as { _localRows(): number })._localRows()).toBe(0);
|
||||||
|
|
||||||
|
term.buffer.active.viewportY = baseY - 5;
|
||||||
|
for (const ev of [wheelLines(-12), wheelLines(12), wheelLines(-5), wheelLines(-5)]) {
|
||||||
|
mount.fire('wheel', ev);
|
||||||
|
expect(ev.preventDefault).not.toHaveBeenCalled();
|
||||||
|
expect(ev.stopPropagation).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
expect(flushed(ws)).toEqual([]);
|
||||||
|
|
||||||
|
// Back on the live screen, only travel made there counts: two quarter-screen
|
||||||
|
// wheels are one PageUp, with nothing carried over from the wheels xterm had
|
||||||
|
// (the gate sits before the pending travel is touched).
|
||||||
|
term.buffer.active.viewportY = baseY;
|
||||||
|
const first = wheelLines(-6);
|
||||||
|
mount.fire('wheel', first);
|
||||||
|
expect(first.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(flushed(ws)).toEqual([]);
|
||||||
|
mount.fire('wheel', wheelLines(-6));
|
||||||
|
expect(flushed(ws)).toEqual([{ t: 'i', d: PAGE_UP }]);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps paging when the PTY geometry report reflows the rows above the screen', async () => {
|
it('keeps paging when the PTY geometry report reflows the rows above the screen', async () => {
|
||||||
serveCapture(lines(40), 40);
|
serveCapture(lines(40), 40);
|
||||||
const { tile, ws, term, mount } = await connectTile(makeApp({ 's-tile': { mode: 'opencode' } }));
|
const { tile, ws, term, mount } = await connectTile(makeApp({ 's-tile': { mode: 'opencode' } }));
|
||||||
|
|||||||
Reference in New Issue
Block a user