fix(terminal): strip opencode's mouse DECSETs so a drag selects text again

opencode's TUI enables mouse tracking. tmux runs with `mouse off`, so it passes
the PANE's DECSETs straight through to the tmux client, and the browser's xterm
obeyed them: `mouseTrackingMode` flipped to 'any' (measured 62 none / 18 any over
16s) and xterm then reported DRAGS to the TUI instead of selecting locally.

In that state marking text produced no selection at all, so copy-on-select
silently did nothing (5/5 dead drags while `any`), and the obvious fallback —
Ctrl+C — is opencode's `app_exit`, which ended the session. Both were hit here.

opencode needs the middle strip: alt-screen toggles AND mouse DECSETs, but NOT
`3J` (a TUI is not a `clear` consumer). That is `altScreen: 'strip-mux-and-mouse'`
+ `isMuxMouseStripMode`, applied to the live stream (session.ts) and the replay
of a stored buffer, now the exported `stripReplayBuffer()` (session-routes.ts).

The browser's mouse-report gate keeps no mode list any more:
`_shouldReportMouseToCli()` reads only `cliMouseTracking`. The server sets that
flag solely in the mouse-strip branch (`_recordStrippedMouseMode`, one caller),
so it can only be true for a mode whose DECSETs are stripped, and whichever modes
the registry strips, the browser follows. Clicks still reach opencode through the
hand-encoded SGR tap it gates.

The `altScreen` JSDoc gets the decision table its three independent choices need
(alt-screen / `3J` / mouse DECSETs), written from the predicates, including that
`preserve` and `strip-mux-only` take the same runtime row. The table is pinned for
every stock CLI, with and without tmux, on both the live strip and the replay
strip, plus the published flag (test/claude-scrollback-strip.test.ts), so the two
halves cannot drift and a mis-ordered replay branch fails.

Docs and comments that still said opencode keeps its mouse reporting or gets the
narrow strip are updated (CLAUDE.md, architecture-invariants, scrollback and
copy-shortcut plans, session.ts, terminal-ui.js).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Randalix
2026-10-08 14:55:02 +02:00
co-authored by Claude Opus 5.5
parent 5a0018fc86
commit 5ba729fcbb
12 changed files with 318 additions and 85 deletions
+24 -5
View File
@@ -366,14 +366,33 @@ describe('terminal touch tap mouse guard', () => {
expect(sent).toEqual(['\x1b[<0;1;24M\x1b[<0;1;24m']);
});
it('never hand-reports for a shell session, even with tracking somehow set', () => {
// Shell DECSETs are NOT stripped (narrow strip), so xterm's own encoder owns
// the mouse there and a second, hand-encoded report would double-report.
it('follows the server flag alone, with no mode list of its own', () => {
// A shell's DECSETs are not stripped, so xterm's own encoder owns the mouse there
// and a hand-encoded report would double-report. That is kept by the SERVER never
// setting the flag for a non-stripping mode (pinned in claude-scrollback-strip.test.ts),
// not by a mode check here: the browser reads only the flag.
const { app } = loadTerminalUiHarness();
app.activeSessionId = 'sess-1';
app.sessions = new Map([['sess-1', { mode: 'shell', cliMouseTracking: true }]]);
app.sessions = new Map([['sess-1', { mode: 'shell' }]]);
expect(app._shouldReportMouseToCli()).toBe(false);
app.sessions = new Map([['sess-1', { mode: 'some-future-cli', cliMouseTracking: true }]]);
expect(app._shouldReportMouseToCli()).toBe(true);
});
it('hand-reports for opencode, whose DECSETs the server now strips', () => {
// opencode's TUI enables mouse tracking, tmux passes the DECSETs through, and
// xterm used to report DRAGS to the TUI instead of selecting — so marking text
// copied nothing. The server strips them now (isMuxMouseStripMode), which makes
// the hand-encoded tap the only way a click still reaches opencode.
const { app } = loadTerminalUiHarness();
app.activeSessionId = 'sess-1';
app.sessions = new Map([['sess-1', { mode: 'opencode' }]]);
expect(app._shouldReportMouseToCli()).toBe(false);
app.sessions = new Map([['sess-1', { mode: 'opencode', cliMouseTracking: true }]]);
expect(app._shouldReportMouseToCli()).toBe(true);
});
it('hand-reports only while the CLI actually has mouse tracking on', () => {