fix(terminal): only forward scroll to Claude while it tracks the mouse

Claude 2.1.280 renders inline by default: no alt screen, no mouse tracking, transcript in real scrollback. The version-only gate still sent every wheel tick and touch swipe as SGR reports, which Claude ignores, so scrolling a Claude session was dead while codex (routed locally) worked. Gate forwarding on the server-recorded cliMouseTracking flag, which fullscreen mode (CLAUDE_CODE_NO_FLICKER=1) sets.
This commit is contained in:
JD
2026-09-26 15:49:58 -04:00
parent 45ea2e1d32
commit 1da2fa2529
6 changed files with 38 additions and 9 deletions
+22 -3
View File
@@ -585,7 +585,7 @@ describe('terminal touch tap mouse guard', () => {
it('wheel: forwards to the app for verified sessions without Shift, at ANY scroll position', () => {
const { app } = loadTerminalUiHarness();
app.activeSessionId = 'sess-1';
app.sessions = new Map([['sess-1', { mode: 'claude', cliVersion: '2.1.187' }]]);
app.sessions = new Map([['sess-1', { mode: 'claude', cliVersion: '2.1.187', cliMouseTracking: true }]]);
app.terminal = {
modes: { mouseTrackingMode: 'none' },
buffer: { active: { viewportY: 50, baseY: 50 } },
@@ -638,7 +638,7 @@ describe('terminal touch tap mouse guard', () => {
buffer: { active: { viewportY: 50, baseY: 50 } },
};
const withVersion = (cliVersion?: string) => {
app.sessions = new Map([['sess-1', { mode: 'claude', cliVersion }]]);
app.sessions = new Map([['sess-1', { mode: 'claude', cliVersion, cliMouseTracking: true }]]);
return app._shouldForwardWheelToApp({ shiftKey: false });
};
@@ -650,6 +650,25 @@ describe('terminal touch tap mouse guard', () => {
expect(withVersion('garbage')).toBe(false); // unparseable → assume older
});
it('wheel: inline claude (no mouse tracking) keeps the local wheel', () => {
// Claude 2.1.280's default inline renderer never enables mouse tracking and
// keeps its transcript in real scrollback, so SGR wheel reports are ignored.
// Forwarding there made every swipe dead on iOS Safari while codex scrolled.
const { app } = loadTerminalUiHarness();
app.activeSessionId = 'sess-1';
app.terminal = {
modes: { mouseTrackingMode: 'none' },
buffer: { active: { viewportY: 50, baseY: 50 } },
};
app.sessions = new Map([['sess-1', { mode: 'claude', cliVersion: '2.1.280' }]]);
expect(app._shouldForwardWheelToApp({ shiftKey: false })).toBe(false);
app.sessions = new Map([['sess-1', { mode: 'claude', cliVersion: '2.1.280', cliMouseTracking: false }]]);
expect(app._shouldForwardWheelToApp({ shiftKey: false })).toBe(false);
// Fullscreen (CLAUDE_CODE_NO_FLICKER=1) turns tracking on → forwarding resumes.
app.sessions = new Map([['sess-1', { mode: 'claude', cliVersion: '2.1.280', cliMouseTracking: true }]]);
expect(app._shouldForwardWheelToApp({ shiftKey: false })).toBe(true);
});
it('wheel: only claude forwards — codex and gemini keep the local wheel', () => {
const { app } = loadTerminalUiHarness();
app.activeSessionId = 'sess-1';
@@ -674,7 +693,7 @@ describe('terminal touch tap mouse guard', () => {
it('wheel: the local-scrollback opt-out pins the plain wheel to local scrollback (issue #154)', () => {
const { app } = loadTerminalUiHarness();
app.activeSessionId = 'sess-1';
app.sessions = new Map([['sess-1', { mode: 'claude', cliVersion: '2.1.187' }]]);
app.sessions = new Map([['sess-1', { mode: 'claude', cliVersion: '2.1.187', cliMouseTracking: true }]]);
app.terminal = {
modes: { mouseTrackingMode: 'none' },
buffer: { active: { viewportY: 50, baseY: 50 } },