From c46089681f86c2297b1c8f3b21409e04e8b3fceb Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 15 Jul 2026 14:05:33 +0000 Subject: [PATCH] fix(tui): suppressed osc 99 capability probe under tmux The Kitty OSC 99 desktop-notification capability probe (`p=?`) was gated only on `notifyProtocol === Osc99` plus a non-test runtime, with no multiplexer gate. Inside tmux the probe fired wrapped in passthrough; tmux forwards the query to the outer terminal but cannot route the reply back to the sending pane, so kitty's capability list leaked into the pane as text and its bytes switched panes. Gate `#shouldQueryOsc99Support()` behind `isInsideTerminalMultiplexer` so the probe never runs under tmux/screen. Rich notifications keep falling back to the single-line OSC 99 form until confirmed, and delivery still uses the passthrough/BEL path (#3395). Drop the now-dead passthrough wrap in `#queryOsc99Support()`. Fixes #5582 --- packages/tui/CHANGELOG.md | 4 ++++ packages/tui/src/terminal.ts | 22 +++++++++++++--------- packages/tui/test/notifications.test.ts | 12 +++++++----- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 82b5c0715..4076268d5 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -8,6 +8,10 @@ - Display LaTeX renders multi-letter script words (`N_{turns}`) as raised/lowered blocks instead of ragged per-character Unicode sub/superscript glyphs; single letters and digits keep the compact Unicode forms. - Added opt-in `Editor.setImeSafeCursorLayout()` protection for macOS IME preedit while retaining the compact bordered layout by default ([#5563](https://github.com/can1357/oh-my-pi/issues/5563)). +### Fixed + +- Fixed the Kitty OSC 99 desktop-notification capability probe (`p=?`) firing under tmux/screen, where the multiplexer forwards the passthrough query to the outer terminal but cannot route the reply back to the sending pane — the capability list leaked into the pane as text and its bytes switched panes ([#5582](https://github.com/can1357/oh-my-pi/issues/5582)). The probe is now suppressed inside a multiplexer (rich notifications fall back to the single-line OSC 99 form until confirmed), mirroring the graphics-probe fix in #5381. + ## [16.5.2] - 2026-07-14 ### Fixed diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index 2ad11d612..695757cb9 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -12,12 +12,12 @@ import { import { setKittyProtocolActive } from "./keys"; import { StdinBuffer } from "./stdin-buffer"; import { + isInsideTerminalMultiplexer, isInsideTmux, NotifyProtocol, setCellDimensions, setOsc99Supported, TERMINAL, - wrapTmuxPassthrough, } from "./terminal-capabilities"; import { type HangulCompatibilityJamoWidth, setHangulCompatibilityJamoWidth } from "./utils"; @@ -1042,6 +1042,15 @@ export class ProcessTerminal implements Terminal { #shouldQueryOsc99Support(): boolean { if (TERMINAL.notifyProtocol !== NotifyProtocol.Osc99) return false; + // Never probe inside a terminal multiplexer. tmux/screen forward the + // passthrough-wrapped `p=?` query to the outer terminal, but cannot route + // the capability reply back to the pane that sent it (tmux/tmux#4386, + // tmux/tmux#3964), so the reply leaks into the pane as literal text and + // its bytes perturb input (issue #5582 — the notification sibling of the + // graphics-probe leak #5381). Rich notifications fall back to the + // single-line OSC 99 form until confirmation, and delivery still uses the + // passthrough/BEL path (#3395). + if (isInsideTerminalMultiplexer($env)) return false; return !isBunTestRuntime() || $env.PI_TUI_OSC99_PROBE === "1"; } @@ -1055,14 +1064,9 @@ export class ProcessTerminal implements Terminal { const id = `omp-probe-${nextOsc99ProbeId++}`; this.#osc99PendingId = id; this.#da1SentinelOwners.push({ kind: "osc99Probe", id }); - // Wrap the probe under tmux so terminals behind `allow-passthrough on` - // can still respond (mirroring how `TerminalInfo.sendNotification` - // wraps notification deliveries). Without it the probe is swallowed - // inside tmux even when the outer terminal speaks OSC 99, and rich - // notifications stay permanently downgraded to the single-line fallback. - const probe = `\x1b]99;i=${id}:p=?;\x1b\\`; - const sequence = isInsideTmux() ? wrapTmuxPassthrough(probe) : probe; - this.#safeWrite(`${sequence}\x1b[c`); + // The probe never runs under a multiplexer (see #shouldQueryOsc99Support), + // so it is always sent directly to the terminal. + this.#safeWrite(`\x1b]99;i=${id}:p=?;\x1b\\\x1b[c`); } #handleOsc99CapabilityResponse(metaRaw: string, payload: string): boolean { diff --git a/packages/tui/test/notifications.test.ts b/packages/tui/test/notifications.test.ts index 2c5245445..b6da7151b 100644 --- a/packages/tui/test/notifications.test.ts +++ b/packages/tui/test/notifications.test.ts @@ -291,16 +291,18 @@ describe("terminal notifications", () => { expect(writes).toEqual(["\x1b]99;;ping\x1b\\\x07"]); }); - it("under tmux, the OSC 99 capability probe is wrapped in DCS passthrough", () => { + it("under tmux, the OSC 99 capability probe is suppressed (reply cannot route back to the pane)", () => { Bun.env.PI_TUI_OSC99_PROBE = "1"; Bun.env.TMUX = "/tmp/tmux-1000/default,1234,0"; mutableTerminal.notifyProtocol = NotifyProtocol.Osc99; const { terminal, writes } = setupProcessTerminal(); try { - const probe = writes.find( - w => w.startsWith("\x1bPtmux;\x1b\x1b]99;i=omp-probe-") && w.endsWith("\x1b\x1b\\\x1b\\\x1b[c"), - ); - expect(probe).toBeDefined(); + // tmux forwards the passthrough probe to the outer terminal but cannot + // route the `p=?` reply back to the sending pane, so the reply would + // leak into the pane as text (#5582). The probe must not fire at all. + const probe = writes.find(w => w.includes("]99;i=omp-probe-") && w.includes(":p=?")); + expect(probe).toBeUndefined(); + expect(isOsc99Supported()).toBe(false); } finally { terminal.stop(); }