Skip to content
Open
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
21 changes: 12 additions & 9 deletions src/cli/commands/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,14 @@ export async function chatCommand(opts: ChatOptions): Promise<void> {
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) {
Expand Down Expand Up @@ -424,10 +427,10 @@ export async function chatCommand(opts: ChatOptions): Promise<void> {
// 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);
Expand Down
12 changes: 11 additions & 1 deletion src/cli/ui/history-scroll-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update stale auto-mode test expectation

Changing the auto-mode fall-through to "app" leaves tests/chat-scroll-wheel.test.ts:97-104 asserting that an unknown xterm/linux environment still resolves to "native"; any CI path that runs npm test/npm run verify will fail on that assertion even though this is now the intended default. Please update the test to match the new default or keep the old behavior for that environment.

Useful? React with 👍 / 👎.

}

function isKnownJumpProneTerminal(env: NodeJS.ProcessEnv | Record<string, string | undefined>) {
Expand Down
13 changes: 11 additions & 2 deletions src/cli/ui/stdin-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 21 additions & 1 deletion tests/chat-scroll-wheel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});