feat(ui): /mouse toggle — enable terminal text selection/copy by default

Mouse-wheel tracking (xterm ?1000h) was always on so the wheel could scroll
the transcript, but it also captures mouse events, which prevents the
terminal's native text selection/drag-to-copy — a frequent ask.

Now mouse tracking defaults to OFF, so selecting/dragging output to copy
works out of the box. PageUp/PageDown still scroll (they always did). Toggle
the wheel back on with '/mouse on' (re-captures mouse events, so selection
won't work while on); '/mouse off' restores selection. '/mouse' shows the
current state. Documented in the /help list and README.

Verified: typecheck clean, build 257.55 KB, 263 tests pass.
This commit is contained in:
kim
2026-08-21 13:48:41 +09:00
parent c8cc78e8f1
commit e3520b8eb1
2 changed files with 23 additions and 2 deletions
+1
View File
@@ -115,6 +115,7 @@ Note: even models with genuine native tool-calling support occasionally emit a t
/model <name> switch the model used for the current backend
/backend <name> switch backend (ollama | lmstudio), keeps current model
/mode <name> view or force tool-call mode (native | fallback)
/mouse [on|off] toggle mouse-wheel scroll (off by default so terminal text selection/copy works)
/perm [mode] cycle or set permission mode (default | plan | auto-edit | auto-accept)
/status show current model, backend, tool-call mode, and cwd
/dashboard show session stats: token I/O, elapsed/model time, turns, tool calls
+22 -2
View File
@@ -136,6 +136,11 @@ export function App({
const [inputValue, setInputValue] = useState("");
const [permission, setPermission] = useState<PendingPermission | null>(null);
const [exportPrompt, setExportPrompt] = useState<{ defaultName: string } | null>(null);
// Mouse-wheel tracking (xterm ?1000h) lets the wheel scroll the transcript, but it ALSO
// captures mouse events so the terminal can't select/drag text to copy it. Default off — copy/
// drag is more important than wheel scroll, and PageUp/PageDown already scroll. Toggle with
// `/mouse on|off`. When off the wheel does nothing in-app; the terminal's native selection works.
const [mouseMode, setMouseMode] = useState(false);
const [streamingText, setStreamingText] = useState<string | null>(null);
const [isThinking, setIsThinking] = useState(false);
const [permMode, setPermMode] = useState<PermissionMode>("default");
@@ -275,13 +280,13 @@ export function App({
// open would otherwise type them straight into the filename — simplest fix is to stop the
// terminal from sending them at all rather than filtering inside a dependency we don't control.
useEffect(() => {
if (!isRawModeSupported || phase !== "input" || exportPrompt) return;
if (!isRawModeSupported || phase !== "input" || exportPrompt || !mouseMode) return;
stdout.write("[?1000h[?1006h");
setRawMode(true);
return () => {
stdout.write("[?1006l[?1000l");
};
}, [phase, isRawModeSupported, stdout, setRawMode, exportPrompt]);
}, [phase, isRawModeSupported, stdout, setRawMode, exportPrompt, mouseMode]);
// A backgrounded bash job (see Ctrl+B below) can finish long after the turn that started it has
// ended — this is how its completion still reaches the transcript.
@@ -985,6 +990,21 @@ export function App({
}
return;
}
if (trimmed.startsWith("/mouse")) {
const arg = trimmed.slice("/mouse".length).trim().toLowerCase();
if (!arg) {
push({ kind: "notice", text: `Mouse wheel scroll: ${mouseMode ? "on" : "off"}. Use /mouse on|off.` });
} else if (arg === "on") {
setMouseMode(true);
push({ kind: "notice", text: "Mouse wheel scroll on (note: this captures mouse events, so terminal text selection/drag-to-copy won't work while on). Use /mouse off to copy text." });
} else if (arg === "off") {
setMouseMode(false);
push({ kind: "notice", text: "Mouse wheel scroll off — you can now select/drag terminal text to copy. Use PageUp/PageDown to scroll, or /mouse on to re-enable the wheel." });
} else {
push({ kind: "notice", text: `Unknown option "${arg}". Use /mouse on or /mouse off.`, isError: true });
}
return;
}
// UserPromptSubmit hooks see the raw text before plugin-command expansion or @mention
// resolution — a hook can block the message outright, or inject extra context (appended