From 315d9a46f8401b2dbc9e90b75ea636b7b2eda574 Mon Sep 17 00:00:00 2001 From: skift Date: Sat, 30 May 2026 11:46:42 +0800 Subject: [PATCH 1/2] fix(cli): restore mouse wheel scrolling for TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default historyScrollMode from 'native' to 'app' so CardStream captures wheel events in alt-screen terminals where native scrollback cannot scroll TUI content. - Apple Terminal and classic Windows console (conhost) stay on 'native' to avoid renderer crashes / missing wheel forwarding. - --no-mouse / mouseTracking:false now force 'native' as a hard override before the resolver runs. - Decode modified SGR wheel button codes (68/69/80 etc.) that some terminals (Windows Terminal previews) send with modifier bits set — the old exact-match on 64/65 silently dropped them. Fixes #2260 (scroll wheel portion) --- src/cli/commands/chat.tsx | 21 ++++++++++++--------- src/cli/ui/history-scroll-mode.ts | 12 +++++++++++- src/cli/ui/stdin-reader.ts | 13 +++++++++++-- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/cli/commands/chat.tsx b/src/cli/commands/chat.tsx index 7bb36c3e8..278bccf05 100644 --- a/src/cli/commands/chat.tsx +++ b/src/cli/commands/chat.tsx @@ -314,11 +314,14 @@ export async function chatCommand(opts: ChatOptions): Promise { const mcpSpecs = [...requestedSpecs]; const mcpServers: McpServerSummary[] = []; const cfg = readConfig(); - const historyScrollMode = resolveHistoryScrollMode({ - configured: loadHistoryScrollMode(), - env: process.env, - platform: process.platform, - }); + const historyScrollMode = + opts.noMouse || cfg.mouseTracking === false + ? "native" + : resolveHistoryScrollMode({ + configured: loadHistoryScrollMode(), + env: process.env, + platform: process.platform, + }); const startupInfoHints: string[] = []; const hasAnyMcp = normalizeMcpConfig(cfg).length > 0 || mcpSpecs.length > 0; if (cfg.setupCompleted === true && !hasAnyMcp) { @@ -424,10 +427,10 @@ export async function chatCommand(opts: ChatOptions): Promise { // path so N cards don't accumulate N native stdout listeners. installResizeBroadcaster(); - // Wheel scrolling. Opt-out via `mouseTracking: false` for users who - // prefer native drag-select copy (Shift+drag still selects with mouse - // mode on in most terminals). exit hooks cover hard kills so the - // sequence doesn't leak into the parent shell. + // Wheel scrolling. Opt-out via `--no-mouse` / `mouseTracking: false` also + // forces native history mode so the terminal, not CardStream, owns wheel + // movement. Exit hooks cover hard kills so the sequence doesn't leak into + // the parent shell. if (!opts.noMouse && cfg.mouseTracking !== false) { enableMouseMode(historyScrollMode); process.once("exit", disableMouseMode); diff --git a/src/cli/ui/history-scroll-mode.ts b/src/cli/ui/history-scroll-mode.ts index b7af60b3c..18ffbaa66 100644 --- a/src/cli/ui/history-scroll-mode.ts +++ b/src/cli/ui/history-scroll-mode.ts @@ -15,11 +15,21 @@ export function resolveHistoryScrollMode({ }: ResolveHistoryScrollModeInput = {}): ResolvedHistoryScrollMode { if (configured === "native") return "native"; if (configured === "app") return "app"; + // Apple Terminal has native renderer crashes when it receives + // private mouse-mode toggles — keep it on native so the terminal + // handles scrollback without any escape sequences. + if ((env.TERM_PROGRAM ?? "").toLowerCase() === "apple_terminal") return "native"; if (isKnownJumpProneTerminal(env)) return "app"; + // Classic Windows console (conhost) doesn't advertise TERM_PROGRAM + // and its alt-screen buffer doesn't forward wheel events — native + // scrollback is the safer default there. if (platform === "win32" && env.TERM_PROGRAM === undefined && env.MSYSTEM === undefined) { return "native"; } - return "native"; + // Default to app-managed scroll for all other terminals so the mouse + // wheel feeds into CardStream's scroll logic. Native scrollback + // cannot scroll TUI alt-screen content on most terminals. + return "app"; } function isKnownJumpProneTerminal(env: NodeJS.ProcessEnv | Record) { diff --git a/src/cli/ui/stdin-reader.ts b/src/cli/ui/stdin-reader.ts index e4cb70e53..ef7ad9229 100644 --- a/src/cli/ui/stdin-reader.ts +++ b/src/cli/ui/stdin-reader.ts @@ -131,8 +131,17 @@ function decodeSgrMouseBody(body: string): KeyEvent | null { if (!Number.isFinite(btn) || !Number.isFinite(col) || !Number.isFinite(row)) return null; const tail = m[4]!; if (tail === "m") return { input: "", mouseRelease: true, mouseRow: row, mouseCol: col }; - if (btn === 64) return { input: "", mouseScrollUp: true, mouseRow: row, mouseCol: col }; - if (btn === 65) return { input: "", mouseScrollDown: true, mouseRow: row, mouseCol: col }; + // SGR encodes wheel events with bit 6 set (btn & 64). + // Modifier keys add bits 4/8/16 (e.g. 68 = wheel-up+Ctrl, 80 = wheel-up+Shift). + // Masking with bit 6 is stricter than `btn >= 64` — it rejects theoretical + // non-wheel codes that happen to fall >= 64. The wheel direction is in bit 0: + // 0 = up, 1 = down. Issue #2260: some terminals (especially Windows Terminal + // previews) send modified codes 68/69/80 etc. that the old exact-match missed. + if (btn & 64) { + const wheelDir = btn & 1; + if (wheelDir === 0) return { input: "", mouseScrollUp: true, mouseRow: row, mouseCol: col }; + return { input: "", mouseScrollDown: true, mouseRow: row, mouseCol: col }; + } if (btn === 0) return { input: "", mouseClick: true, mouseRow: row, mouseCol: col }; if (btn === 32) return { input: "", mouseDrag: true, mouseRow: row, mouseCol: col }; return null; From b9b9df67d278950a9d8756c8b88cd2446bf60284 Mon Sep 17 00:00:00 2001 From: skift Date: Sat, 30 May 2026 12:02:05 +0800 Subject: [PATCH 2/2] fix(test): update scroll-mode test for new app-managed default The auto-mode fall-through now returns 'app' instead of 'native', so the test for unknown terminals must expect 'app'. Added two new test cases: - Apple Terminal stays on 'native' (renderer crash avoidance) - Classic Windows console (no TERM_PROGRAM) stays on 'native' --- tests/chat-scroll-wheel.test.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/chat-scroll-wheel.test.ts b/tests/chat-scroll-wheel.test.ts index 57f7846ba..d5fff623c 100644 --- a/tests/chat-scroll-wheel.test.ts +++ b/tests/chat-scroll-wheel.test.ts @@ -94,13 +94,33 @@ describe("history scroll mode resolution", () => { ); }); - it("keeps native scrollback for unknown terminals in auto mode", () => { + it("defaults to app-managed scroll for unknown terminals in auto mode", () => { expect( resolveHistoryScrollMode({ configured: "auto", env: { TERM: "xterm-256color" }, platform: "linux", }), + ).toBe("app"); + }); + + it("keeps native scrollback for Apple Terminal to avoid renderer crashes", () => { + expect( + resolveHistoryScrollMode({ + configured: "auto", + env: { TERM_PROGRAM: "Apple_Terminal" }, + platform: "darwin", + }), + ).toBe("native"); + }); + + it("keeps native scrollback for classic Windows console (no TERM_PROGRAM)", () => { + expect( + resolveHistoryScrollMode({ + configured: "auto", + env: {}, + platform: "win32", + }), ).toBe("native"); }); });