diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 0bc5ffc5a..ab7d108e0 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixed - Fixed SIXEL image rendering stripping a horizontal slice from images when the cell height was not a multiple of 6. SIXEL encodes in 6-pixel vertical bands; a non-multiple-of-6 height was padded, causing the terminal to allocate an extra row that the TUI did not reserve — the next line of content then overwrote the bottom of the image. The encode height is now rounded down to the largest multiple of 6 within the requested row budget, eliminating padding without exceeding the caller's height cap, and the width is scaled by the same ratio to preserve the image aspect ratio. +- 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 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(); }