Skip to content

Commit 3929e0c

Browse files
committed
fix(tui): skip Kitty query inside Zellij\n\nfixes badlogic#3163
1 parent 9c1e6ef commit 3929e0c

File tree

5 files changed

+98
-0
lines changed

5 files changed

+98
-0
lines changed

packages/coding-agent/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixed
66

7+
- Fixed Alt keybindings inside Zellij by skipping the Kitty keyboard protocol query there and enabling xterm `modifyOtherKeys` mode 2 directly ([#3163](https://github.com/badlogic/pi-mono/issues/3163))
78
- Fixed `/scoped-models` reordering to propagate into the `/model` scoped tab, preserving the user-defined scoped model order instead of re-sorting it ([#3217](https://github.com/badlogic/pi-mono/issues/3217))
89
- Fixed `session_shutdown` to fire on `SIGHUP` and `SIGTERM` in interactive, print, and RPC modes so extensions can run shutdown cleanup on those signal-driven exits ([#3212](https://github.com/badlogic/pi-mono/issues/3212))
910
- Fixed screenshot path parsing to handle lower case am/pm in macOS screenshot filenames ([#3194](https://github.com/badlogic/pi-mono/pull/3194) by [@jay-aye-see-kay](https://github.com/jay-aye-see-kay))

packages/coding-agent/docs/terminal-setup.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ If you want `Shift+Enter` to keep working in tmux via that remap, add `ctrl+j` t
3232
}
3333
```
3434

35+
## Zellij
36+
37+
Pi detects Zellij automatically and skips the Kitty keyboard protocol query there.
38+
Zellij currently forwards that query to the outer terminal but still sends Alt as legacy ESC-prefixed sequences, which can break Alt keybindings if applications enable Kitty mode.
39+
Pi uses xterm `modifyOtherKeys` mode 2 inside Zellij instead, so no extra Zellij config is required.
40+
3541
## WezTerm
3642

3743
Create `~/.wezterm.lua`:

packages/tui/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Fixed
6+
7+
- Fixed Alt keybindings inside Zellij by skipping the Kitty keyboard protocol query there and enabling xterm `modifyOtherKeys` mode 2 directly ([#3163](https://github.com/badlogic/pi-mono/issues/3163))
8+
59
## [0.67.2] - 2026-04-14
610

711
### Added

packages/tui/src/terminal.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,17 @@ export class ProcessTerminal implements Terminal {
184184
private queryAndEnableKittyProtocol(): void {
185185
this.setupStdinBuffer();
186186
process.stdin.on("data", this.stdinDataHandler!);
187+
188+
// Zellij forwards the Kitty query to the outer terminal, which can make
189+
// Pi enable its Kitty parser even though Zellij still sends Alt as
190+
// legacy ESC-prefixed sequences. Skip the Kitty query there and use
191+
// modifyOtherKeys directly instead.
192+
if (process.env.ZELLIJ) {
193+
process.stdout.write("\x1b[>4;2m");
194+
this._modifyOtherKeysActive = true;
195+
return;
196+
}
197+
187198
process.stdout.write("\x1b[?u");
188199
setTimeout(() => {
189200
if (!this._kittyProtocolActive && !this._modifyOtherKeysActive) {

packages/tui/test/terminal.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import assert from "node:assert";
2+
import { describe, it } from "node:test";
3+
import { ProcessTerminal } from "../src/terminal.js";
4+
5+
function withEnv(name: string, value: string | undefined, fn: () => void): void {
6+
const previous = process.env[name];
7+
if (value === undefined) delete process.env[name];
8+
else process.env[name] = value;
9+
try {
10+
fn();
11+
} finally {
12+
if (previous === undefined) delete process.env[name];
13+
else process.env[name] = previous;
14+
}
15+
}
16+
17+
describe("ProcessTerminal", () => {
18+
it("should skip the Kitty query inside Zellij and enable modifyOtherKeys immediately", () => {
19+
const terminal = new ProcessTerminal();
20+
const writes: string[] = [];
21+
const stdinOnCalls: Array<{ event: string | symbol; listener: (...args: unknown[]) => void }> = [];
22+
const stdinRemoveCalls: Array<{ event: string | symbol; listener: (...args: unknown[]) => void }> = [];
23+
const stdoutRemoveCalls: Array<{ event: string | symbol; listener: (...args: unknown[]) => void }> = [];
24+
25+
const originalStdoutWrite = process.stdout.write;
26+
const originalStdinOn = process.stdin.on;
27+
const originalStdinRemoveListener = process.stdin.removeListener;
28+
const originalStdinPause = process.stdin.pause;
29+
const originalStdoutRemoveListener = process.stdout.removeListener;
30+
31+
process.stdout.write = ((chunk: string | Uint8Array) => {
32+
writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
33+
return true;
34+
}) as typeof process.stdout.write;
35+
process.stdin.on = ((event: string | symbol, listener: (...args: unknown[]) => void) => {
36+
stdinOnCalls.push({ event, listener });
37+
return process.stdin;
38+
}) as typeof process.stdin.on;
39+
process.stdin.removeListener = ((event: string | symbol, listener: (...args: unknown[]) => void) => {
40+
stdinRemoveCalls.push({ event, listener });
41+
return process.stdin;
42+
}) as typeof process.stdin.removeListener;
43+
process.stdin.pause = (() => process.stdin) as typeof process.stdin.pause;
44+
process.stdout.removeListener = ((event: string | symbol, listener: (...args: unknown[]) => void) => {
45+
stdoutRemoveCalls.push({ event, listener });
46+
return process.stdout;
47+
}) as typeof process.stdout.removeListener;
48+
49+
try {
50+
withEnv("ZELLIJ", "1", () => {
51+
(
52+
terminal as unknown as {
53+
queryAndEnableKittyProtocol(): void;
54+
}
55+
).queryAndEnableKittyProtocol();
56+
});
57+
58+
assert.deepStrictEqual(writes, ["\x1b[>4;2m"]);
59+
assert.strictEqual(stdinOnCalls.length, 1);
60+
assert.strictEqual(stdinOnCalls[0]?.event, "data");
61+
62+
terminal.stop();
63+
64+
assert.deepStrictEqual(writes, ["\x1b[>4;2m", "\x1b[?2004l", "\x1b[>4;0m"]);
65+
assert.strictEqual(stdinRemoveCalls.length, 1);
66+
assert.strictEqual(stdinRemoveCalls[0]?.event, "data");
67+
assert.strictEqual(stdoutRemoveCalls.length, 0);
68+
} finally {
69+
process.stdout.write = originalStdoutWrite;
70+
process.stdin.on = originalStdinOn;
71+
process.stdin.removeListener = originalStdinRemoveListener;
72+
process.stdin.pause = originalStdinPause;
73+
process.stdout.removeListener = originalStdoutRemoveListener;
74+
}
75+
});
76+
});

0 commit comments

Comments
 (0)