Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/web/public/terminal-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -1168,7 +1168,7 @@ Object.assign(CodemanApp.prototype, {
// Momentum phase — convert pixel velocity to lines
const lines = Math.round(velocity / cellHeight());
if (lines !== 0) {
if (this._shouldForwardWheelToApp({ shiftKey: false })) {
if (this._shouldForwardTouchToApp()) {
// Flick momentum keeps feeding the CLI's transcript from the last
// touch point; the 40ms coalescer batches the per-frame reports.
this._forwardScrollToApp(touchLastX, touchLastY, lines);
Expand Down Expand Up @@ -1347,7 +1347,7 @@ Object.assign(CodemanApp.prototype, {
const ch = cellHeight();
const lines = Math.trunc(pixelAccum / ch);
if (lines !== 0) {
if (this._shouldForwardWheelToApp({ shiftKey: false })) {
if (this._shouldForwardTouchToApp()) {
this._logScrollRouting('forward-sgr');
this._forwardScrollToApp(touchLastX, touchLastY, lines);
} else if (!this._maybePageCliTranscript({ shiftKey: false, touchId: touchContactId }, lines)) {
Expand Down Expand Up @@ -5833,6 +5833,18 @@ Object.assign(CodemanApp.prototype, {
// `target` ({ terminal, sessionId }) asks the question for a TerminalTile:
// its own terminal's tracking mode and its own session, never the active one.
// Either field left out means the primary pane's.
// xterm encodes native wheel events when mouse tracking is enabled, but
// our touch listeners consume finger drags themselves. Forward those
// explicitly: local scrollLines() cannot move an alternate-screen buffer.
_shouldForwardTouchToApp() {
if (this.loadAppSettingsFromStorage?.()?.terminalWheelLocalScrollback) return false;
const mode = this.terminal?.modes?.mouseTrackingMode;
// The existing synthetic sender uses SGR; tracking alone does not imply
// that encoding (legacy/X10 applications must not receive SGR bytes).
const sgr = this.terminal?._core?.coreMouseService?.activeEncoding === 'SGR';
return !!(mode && mode !== 'none' && mode !== 'x10' && sgr) || this._shouldForwardWheelToApp({ shiftKey: false });
},

_shouldForwardWheelToApp(ev, target = {}) {
if (ev.shiftKey) return false;
// Opt-out (App Settings → Input → "Wheel scrolls local history"): pin the
Expand Down
35 changes: 35 additions & 0 deletions test/terminal-scroll-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,3 +877,38 @@ describe('scroll routing diagnostic (issue #205 round 2)', () => {
expect(logs[0]).toContain('cliVersion=unknown');
});
});

describe('touch scrolling when xterm owns native mouse tracking', () => {
it.each(['vt200', 'drag', 'any'])('forwards touch for %s tracking even for a shell session', (mode) => {
const { app } = hollowApp({ mode: 'shell' });
app.terminal.modes.mouseTrackingMode = mode;
app.terminal._core = { coreMouseService: { activeEncoding: 'SGR' } };
app.terminal.buffer.active.type = 'alternate';
expect(app._shouldForwardWheelToApp({ shiftKey: false })).toBe(false);
expect(app._shouldForwardTouchToApp()).toBe(true);
});

it('does not send SGR to a legacy-protocol or X10 application', () => {
const { app } = hollowApp({ mode: 'shell' });
app.terminal.modes.mouseTrackingMode = 'vt200';
app.terminal._core = { coreMouseService: { activeEncoding: 'DEFAULT' } };
expect(app._shouldForwardTouchToApp()).toBe(false);
app.terminal._core.coreMouseService.activeEncoding = 'SGR';
app.terminal.modes.mouseTrackingMode = 'x10';
expect(app._shouldForwardTouchToApp()).toBe(false);
});

it('keeps the explicit local-history preference for touch', () => {
const { app } = hollowApp({ mode: 'shell' });
app.terminal.modes.mouseTrackingMode = 'any';
app.loadAppSettingsFromStorage = () => ({ terminalWheelLocalScrollback: true });
expect(app._shouldForwardTouchToApp()).toBe(false);
});

it('uses normal CLI routing when xterm mouse tracking is off', () => {
const { app } = hollowApp({ cliVersion: '2.1.187', cliMouseTracking: true });
expect(app._shouldForwardTouchToApp()).toBe(true);
app.sessions.get('sess-1').cliMouseTracking = false;
expect(app._shouldForwardTouchToApp()).toBe(false);
});
});
54 changes: 54 additions & 0 deletions test/terminal-touch-paging.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,58 @@ describe('touch paging of a hollow Codex pane in a real browser', () => {
expect(second.keys).toBe(UP + UP);
await touch('touchend', x, y0 + Math.ceil(cell));
}, 60000);
it.each([390, 820])('forwards drag and momentum with native mouse tracking at %ipx', async (width) => {
await page.setViewportSize({ width, height: 900 });
await page.evaluate(async (id) => {
const app = (window as any).app;
app.sessions.set(id, { id, name: id, mode: 'shell', status: 'idle' });
app.activeSessionId = id;
app.loadAppSettingsFromStorage = () => ({ terminalWheelLocalScrollback: false });
app._wheelSgrQueue = '';
const sent: string[] = [];
app._sendInputEphemeral = (_id: string, data: string) => sent.push(data);
(window as any).__mouseTouchProbe = { sent };
await new Promise<void>((resolve) => app.terminal.write('\x1b[?1049h\x1b[?1003h\x1b[?1006h', resolve));
}, PROBE_ID);
const point = await page.evaluate(() => {
const app = (window as any).app;
const r = document.getElementById('terminalContainer')!.getBoundingClientRect();
return {
x: r.left + 30,
y: r.top + 200,
mode: app.terminal.modes.mouseTrackingMode,
buffer: app.terminal.buffer.active.type,
};
});
expect(point.mode).toBe('any');
expect(point.buffer).toBe('alternate');
await touch('touchstart', point.x, point.y);
await touch('touchmove', point.x, point.y - 60);
const drag = await page.evaluate(() => {
(window as any).app._flushWheelSgrQueue();
return (window as any).__mouseTouchProbe.sent.join('');
});
expect(drag).toMatch(/\x1b\[<65;\d+;\d+M/);
await touch('touchend', point.x, point.y - 60);
await page.waitForTimeout(150);
const momentum = await page.evaluate(() => {
(window as any).app._flushWheelSgrQueue();
return (window as any).__mouseTouchProbe.sent.join('');
});
expect(momentum.length).toBeGreaterThan(drag.length);
// Stop momentum before changing the requested mouse encoding.
await touch('touchstart', point.x, point.y);
await touch('touchcancel', point.x, point.y);
await page.evaluate(async () => {
const app = (window as any).app;
app._wheelSgrQueue = '';
(window as any).__mouseTouchProbe.sent.length = 0;
await new Promise<void>((resolve) => app.terminal.write('\x1b[?1006l', resolve));
});
await touch('touchstart', point.x, point.y);
await touch('touchmove', point.x, point.y - 60);
await page.waitForTimeout(60);
expect(await page.evaluate(() => (window as any).__mouseTouchProbe.sent.join(''))).toBe('');
await touch('touchcancel', point.x, point.y);
});
});
Loading