diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 72562a0d..62fc3d78 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- 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)) - 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)) - 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)) - 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)) diff --git a/packages/coding-agent/docs/terminal-setup.md b/packages/coding-agent/docs/terminal-setup.md index 74b0df2a..fd80e231 100644 --- a/packages/coding-agent/docs/terminal-setup.md +++ b/packages/coding-agent/docs/terminal-setup.md @@ -32,6 +32,12 @@ If you want `Shift+Enter` to keep working in tmux via that remap, add `ctrl+j` t } ``` +## Zellij + +Pi detects Zellij automatically and skips the Kitty keyboard protocol query there. +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. +Pi uses xterm `modifyOtherKeys` mode 2 inside Zellij instead, so no extra Zellij config is required. + ## WezTerm Create `~/.wezterm.lua`: diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 8a634661..f542078b 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- 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)) + ## [0.67.2] - 2026-04-14 ### Added diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index d98d1954..1ef9b451 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -184,6 +184,17 @@ export class ProcessTerminal implements Terminal { private queryAndEnableKittyProtocol(): void { this.setupStdinBuffer(); process.stdin.on("data", this.stdinDataHandler!); + + // Zellij forwards the Kitty query to the outer terminal, which can make + // Pi enable its Kitty parser even though Zellij still sends Alt as + // legacy ESC-prefixed sequences. Skip the Kitty query there and use + // modifyOtherKeys directly instead. + if (process.env.ZELLIJ) { + process.stdout.write("\x1b[>4;2m"); + this._modifyOtherKeysActive = true; + return; + } + process.stdout.write("\x1b[?u"); setTimeout(() => { if (!this._kittyProtocolActive && !this._modifyOtherKeysActive) { diff --git a/packages/tui/test/terminal.test.ts b/packages/tui/test/terminal.test.ts new file mode 100644 index 00000000..77ecaf83 --- /dev/null +++ b/packages/tui/test/terminal.test.ts @@ -0,0 +1,76 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { ProcessTerminal } from "../src/terminal.js"; + +function withEnv(name: string, value: string | undefined, fn: () => void): void { + const previous = process.env[name]; + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + try { + fn(); + } finally { + if (previous === undefined) delete process.env[name]; + else process.env[name] = previous; + } +} + +describe("ProcessTerminal", () => { + it("should skip the Kitty query inside Zellij and enable modifyOtherKeys immediately", () => { + const terminal = new ProcessTerminal(); + const writes: string[] = []; + const stdinOnCalls: Array<{ event: string | symbol; listener: (...args: unknown[]) => void }> = []; + const stdinRemoveCalls: Array<{ event: string | symbol; listener: (...args: unknown[]) => void }> = []; + const stdoutRemoveCalls: Array<{ event: string | symbol; listener: (...args: unknown[]) => void }> = []; + + const originalStdoutWrite = process.stdout.write; + const originalStdinOn = process.stdin.on; + const originalStdinRemoveListener = process.stdin.removeListener; + const originalStdinPause = process.stdin.pause; + const originalStdoutRemoveListener = process.stdout.removeListener; + + process.stdout.write = ((chunk: string | Uint8Array) => { + writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8")); + return true; + }) as typeof process.stdout.write; + process.stdin.on = ((event: string | symbol, listener: (...args: unknown[]) => void) => { + stdinOnCalls.push({ event, listener }); + return process.stdin; + }) as typeof process.stdin.on; + process.stdin.removeListener = ((event: string | symbol, listener: (...args: unknown[]) => void) => { + stdinRemoveCalls.push({ event, listener }); + return process.stdin; + }) as typeof process.stdin.removeListener; + process.stdin.pause = (() => process.stdin) as typeof process.stdin.pause; + process.stdout.removeListener = ((event: string | symbol, listener: (...args: unknown[]) => void) => { + stdoutRemoveCalls.push({ event, listener }); + return process.stdout; + }) as typeof process.stdout.removeListener; + + try { + withEnv("ZELLIJ", "1", () => { + ( + terminal as unknown as { + queryAndEnableKittyProtocol(): void; + } + ).queryAndEnableKittyProtocol(); + }); + + assert.deepStrictEqual(writes, ["\x1b[>4;2m"]); + assert.strictEqual(stdinOnCalls.length, 1); + assert.strictEqual(stdinOnCalls[0]?.event, "data"); + + terminal.stop(); + + assert.deepStrictEqual(writes, ["\x1b[>4;2m", "\x1b[?2004l", "\x1b[>4;0m"]); + assert.strictEqual(stdinRemoveCalls.length, 1); + assert.strictEqual(stdinRemoveCalls[0]?.event, "data"); + assert.strictEqual(stdoutRemoveCalls.length, 0); + } finally { + process.stdout.write = originalStdoutWrite; + process.stdin.on = originalStdinOn; + process.stdin.removeListener = originalStdinRemoveListener; + process.stdin.pause = originalStdinPause; + process.stdout.removeListener = originalStdoutRemoveListener; + } + }); +});