fix(tmux): scrolling a pane no longer freezes typing

The wheel puts a pane into tmux's copy-mode, which took every typed key as
a scrollback command and painted a yellow [0/0] box in the corner. In
AgentHub's own panes (every other session on the server keeps tmux's
defaults, restored verbatim):

- the copy-mode position box is off (copy-mode-position-format "");
- the first keystroke after a mouse report leaves copy-mode
  (`copy-mode -q`, pty-attach) before it is written, once per scroll;
- copy-mode keys that open a tmux prompt (g, :, /, f, digits…) leave
  copy-mode and reach the program;
- the wheel scrolls 10 lines a notch instead of 5, and the first notch
  already scrolls instead of only entering copy-mode.
This commit is contained in:
2026-10-19 11:59:22 +07:00
parent e2d8d314f6
commit 0434ffc750
4 changed files with 214 additions and 7 deletions
+44 -3
View File
@@ -183,9 +183,18 @@ describe("tmux wrapper", () => {
calls = fake(true);
tmux.enableMouse("agenthub-lane-1");
assert.ok(calls.some((c) => c[0] === "set-option" && c[2] === "=agenthub-lane-1:"));
// tmux chains the four options into two spawns; psmux above got four.
// tmux chains the four options into two spawns, plus the copy-mode
// position box (a newer tmux only) in its own; psmux above got five.
const sets = calls.filter((c) => c[0] === "set-option");
assert.equal(sets.length, 2);
assert.equal(sets.length, 3);
assert.deepEqual(sets[2], [
"set-option",
"-w",
"-t",
"=agenthub-lane-1:",
"copy-mode-position-format",
"",
]);
assert.deepEqual(sets[0], [
"set-option",
"-t",
@@ -235,7 +244,39 @@ describe("tmux wrapper", () => {
...["set-option", "-t", "=agenthub-lane-1:", "pane-scrollbars-style"],
"fg=brightwhite,bg=black,width=1,pad=0",
]);
assert.equal(calls.length, 3);
// No yellow `[0/0]` box in the corner while the wheel's copy-mode is on.
assert.deepEqual(calls[3], [
...["set-option", "-w", "-t", "=agenthub-lane-1:", "copy-mode-position-format", ""],
]);
// Copy-mode keys that open a prompt ("(goto line)"…) leave copy-mode and
// reach the program in OUR sessions only; others keep tmux's default.
assert.deepEqual(calls[4].slice(0, 9), [
...["bind-key", "-T", "copy-mode", "g", "if-shell", "-F"],
"#{m:agenthub-lane-*,#{session_name}}",
"send-keys -X cancel ; send-keys -l -- 'g'",
'command-prompt -P -p "(goto line)" { send-keys -X goto-line -- "%%" }',
]);
assert.ok(calls[4].includes(":"), "vi's `:` goto-line is re-bound too");
// The wheel scrolls 10 lines a notch in OUR panes, 5 (tmux's) elsewhere.
assert.deepEqual(calls[5].slice(0, 9), [
...["bind-key", "-T", "root", "WheelUpPane", "if-shell", "-F"],
"#{||:#{alternate_on},#{pane_in_mode},#{mouse_any_flag}}",
"send-keys -M",
'if-shell -F "#{m:agenthub-lane-*,#{session_name}}" { copy-mode -e ; send-keys -X -N 10 scroll-up } { copy-mode -e }',
]);
assert.ok(calls[5].includes("select-pane ; send-keys -X -N 10 scroll-down"));
assert.ok(calls[5].includes("select-pane ; send-keys -X -N 5 scroll-down"));
assert.equal(calls.length, 6);
});
it("newSession still succeeds when tmux refuses the copy-mode bindings", () => {
tmux.__setExecImpl((args) => {
if (args[0] === "bind-key") throw new Error("unknown flag -F");
return "";
});
assert.doesNotThrow(() =>
tmux.newSession({ name: "agenthub-lane-1", cwd: "/tmp/repo", argv: ["claude"] })
);
});
it("newSession still succeeds on a tmux older than 3.7 (no pane-scrollbars)", () => {