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
+31
View File
@@ -62,6 +62,37 @@ describe("pty-attach", () => {
assert.throws(() => ptyAttach.validateRunId("agenthub-lane-42-0"), /EBADRUNID/);
});
it("leaves copy-mode before the first keystroke after a wheel, and only then", () => {
const calls = [];
tmux.__setExecImpl((args) => {
calls.push(args);
return "";
});
const ws = makeFakeWs();
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
const leaves = () => calls.filter((c) => c[0] === "copy-mode").length;
const send = (t) => ws.__emitter.emit("message", Buffer.from(t), false);
send("a");
assert.equal(leaves(), 0, "no scroll yet: typing spawns no tmux");
send("\x1b[<64;10;5M\x1b[<64;10;5M");
send("\x1b[I");
assert.equal(leaves(), 0, "a wheel or a focus report is not typing");
send("b");
assert.deepEqual(calls.filter((c) => c[0] === "copy-mode")[0], [
"copy-mode",
"-q",
"-t",
"=agenthub-lane-1:",
]);
send("c");
assert.equal(leaves(), 1, "once per scroll");
assert.deepEqual(
fakePty.__writes.filter((w) => typeof w === "string"),
["a", "\x1b[<64;10;5M\x1b[<64;10;5M", "\x1b[I", "b", "c"]
);
});
it("turns tmux mouse mode on for the session it attaches to", () => {
const calls = [];
tmux.__setExecImpl((args) => {