Files
kandev/apps/web/components/task/terminal-buffer-reader.ts
T
João Salavisa 78609c005e feat(terminal): scroll mobile passthrough terminal scrollback via touch (#1046)
* feat(terminal): add touch-scroll helper for xterm scrollback

Pure helper that translates single-finger vertical drags into
`terminal.scrollLines(n)` calls. Handles threshold gating so taps still
reach xterm, ignores multi-touch, yields to horizontal-dominant drags.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(terminal): wire mobile touch scrollback into passthrough terminal

Adds an `enableTouchScroll` prop on PassthroughTerminal and a thin hook
that calls `attachTouchScroll` while the terminal is mounted. The two
mobile mount points (terminal panel + chat-panel passthrough embed) opt
in; desktop callers are unaffected. Fixes the bug where xterm.js's
canvas absorbed touch gestures and left the scrollback unreachable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(terminal): mobile e2e for passthrough touch-scroll

Pixel 5-emulated Playwright spec: produce 200 lines of output, simulate
a single-finger down-swipe, assert `terminal.buffer.active.viewportY`
moved into the scrollback, then swipe back to return. Extends the e2e
buffer reader to expose viewportY alongside `__xtermReadBuffer`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(terminal): relax mobile swipe-back assertion against late shell output

Replace the exact `.toBe(bottomViewportY)` check with
`.toBeGreaterThanOrEqual` so the test doesn't flake if shell echo
arrives between the snapshot and the post-swipe read. xterm clamps at
the buffer boundary, so overshooting back-to-bottom is impossible.

Addresses PR #1046 Claude review thread 3293258085.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(terminal): document useTouchScroll's isTerminalReady invariant

Adds a comment next to the eslint-disable explaining why the deps
array can safely omit terminalRef/xtermRef: useTerminalInit only flips
isTerminalReady false→true once per mount. A future refactor that
recreates the terminal mid-mount must add a new dep.

Addresses PR #1046 Claude review thread 3293258175.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(terminal): clear all exposed buffer-reader fields on dispose

clearBufferReader now nulls `__xtermGetFontFamily` and
`__xtermGetFontSize` alongside the buffer readers. The two predated
this PR and were never cleaned up; keeping the teardown symmetric with
exposeBufferReader prevents stale references if the container is ever
reused.

Addresses PR #1046 Claude review thread 3293274674.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(terminal): reset document.body between touch-scroll cases

Adds an afterEach to wipe jsdom's body so orphan divs from one test
don't bleed into the next. Avoids cross-test pollution if a future
case queries document.body for containers.

Addresses PR #1046 Claude review thread 3293274769.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 19:27:49 +01:00

33 lines
1.2 KiB
TypeScript

import type { Terminal } from "@xterm/xterm";
type TerminalContainerWithBuffer = HTMLDivElement & {
__xtermReadBuffer?: () => string;
__xtermReadViewportY?: () => number;
__xtermGetFontFamily?: () => string;
__xtermGetFontSize?: () => number;
};
/** Expose buffer reader on the container for e2e tests (xterm renders to canvas). */
export function exposeBufferReader(container: HTMLDivElement, terminal: Terminal) {
const c = container as TerminalContainerWithBuffer;
c.__xtermReadBuffer = () => {
const buf = terminal.buffer.active;
const lines: string[] = [];
for (let i = 0; i <= buf.baseY + buf.cursorY; i++) {
lines.push(buf.getLine(i)?.translateToString(true) ?? "");
}
return lines.join("\n");
};
c.__xtermReadViewportY = () => terminal.buffer.active.viewportY;
c.__xtermGetFontFamily = () => terminal.options.fontFamily ?? "";
c.__xtermGetFontSize = () => terminal.options.fontSize ?? 13;
}
export function clearBufferReader(container: HTMLDivElement) {
const c = container as TerminalContainerWithBuffer;
c.__xtermReadBuffer = undefined;
c.__xtermReadViewportY = undefined;
c.__xtermGetFontFamily = undefined;
c.__xtermGetFontSize = undefined;
}