Merge PR #6204: fix(tui): route explicit appearance refresh through tmux (@roboomp)
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
|
||||
- Fixed idle Loader animations on WSL repeatedly entering render scheduling after an expired ConPTY post-paint settle window instead of resuming direct component writes ([#6024](https://github.com/can1357/oh-my-pi/issues/6024)).
|
||||
- Prevented canonical slash commands and their matching aliases from appearing as duplicate autocomplete rows ([#6131](https://github.com/can1357/oh-my-pi/issues/6131)).
|
||||
- Fixed Ctrl+L appearance refreshes inside tmux reading tmux's stale OSC 11 background cache instead of querying the outer terminal when passthrough is enabled ([#6066](https://github.com/can1357/oh-my-pi/issues/6066)).
|
||||
|
||||
## [17.0.5] - 2026-07-18
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
setOsc99Supported,
|
||||
TERMINAL,
|
||||
} from "./terminal-capabilities";
|
||||
import { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
|
||||
import { type HangulCompatibilityJamoWidth, setHangulCompatibilityJamoWidth } from "./utils";
|
||||
|
||||
const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
|
||||
@@ -456,6 +457,7 @@ function parseOsc99KeyValues(section: string): Map<string, string> {
|
||||
return values;
|
||||
}
|
||||
const XTERM_SCROLL_TO_BOTTOM_MODES = [1010, 1011] as const;
|
||||
type Osc11QueryRoute = "direct" | "tmux";
|
||||
|
||||
function isXtermScrollToBottomMode(mode: number): boolean {
|
||||
return mode === 1010 || mode === 1011;
|
||||
@@ -509,7 +511,7 @@ export class ProcessTerminal implements Terminal {
|
||||
#appearanceCallbacks: Array<(appearance: TerminalAppearance) => void> = [];
|
||||
#appearance: TerminalAppearance | undefined;
|
||||
#osc11Pending = false;
|
||||
#osc11QueryQueued = false;
|
||||
#osc11QueuedRoute?: Osc11QueryRoute;
|
||||
#osc11ResponseBuffer = "";
|
||||
#osc99PendingId: string | undefined;
|
||||
#osc99ResponseBuffer = "";
|
||||
@@ -572,15 +574,15 @@ export class ProcessTerminal implements Terminal {
|
||||
|
||||
/**
|
||||
* Re-query the terminal background via a single OSC 11 probe. Reuses the
|
||||
* startup query path — same DA1-sentinel FIFO, pending/queued gating, parsing,
|
||||
* dedup, and appearance callbacks — so a light/dark switch is picked up
|
||||
* without a restart on terminals lacking end-to-end Mode 2031 notifications.
|
||||
* Bounded to one probe per call; no timers are armed. Suppressed while headless
|
||||
* or after the terminal is torn down.
|
||||
* startup DA1-sentinel FIFO, pending/queued gating, parsing, dedup, and
|
||||
* appearance callbacks. Inside tmux, only this explicit path wraps the query
|
||||
* and sentinel together for passthrough to the outer terminal; startup and
|
||||
* Mode 2031 probes remain direct. Bounded to one probe per call; no timers are
|
||||
* armed. Suppressed while headless or after the terminal is torn down.
|
||||
*/
|
||||
refreshAppearance(): void {
|
||||
if (this.#headless || this.#dead) return;
|
||||
this.#queryBackgroundColor();
|
||||
this.#queryBackgroundColor(isInsideTmux() ? "tmux" : "direct");
|
||||
}
|
||||
|
||||
onPrivateModeReport(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void {
|
||||
@@ -917,13 +919,14 @@ export class ProcessTerminal implements Terminal {
|
||||
}
|
||||
// Start a queued OSC 11 query once the prior cycle is fully drained.
|
||||
if (
|
||||
this.#osc11QueryQueued &&
|
||||
this.#osc11QueuedRoute !== undefined &&
|
||||
!this.#osc11Pending &&
|
||||
!this.#da1SentinelOwners.some(o => o.kind === "osc11") &&
|
||||
!this.#dead
|
||||
) {
|
||||
this.#osc11QueryQueued = false;
|
||||
this.#startOsc11Query();
|
||||
const route = this.#osc11QueuedRoute;
|
||||
this.#osc11QueuedRoute = undefined;
|
||||
this.#startOsc11Query(route);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1058,23 +1061,28 @@ export class ProcessTerminal implements Terminal {
|
||||
* DA1 avoids indefinite hangs: if DA1 response arrives before OSC 11,
|
||||
* the terminal does not support OSC 11.
|
||||
*/
|
||||
#queryBackgroundColor(): void {
|
||||
#queryBackgroundColor(route: Osc11QueryRoute = "direct"): void {
|
||||
if (this.#dead) return;
|
||||
// Queue if an OSC 11 query is in flight or its DA1 sentinel hasn't been
|
||||
// consumed yet. Starting a new query while a DA1 is outstanding would
|
||||
// increment the sentinel counter, and the old DA1 arrival would then
|
||||
// prematurely clear the new query's pending state.
|
||||
// prematurely clear the new query's pending state. Preserve a requested
|
||||
// tmux passthrough route when coalescing direct and explicit queries.
|
||||
if (this.#osc11Pending || this.#da1SentinelOwners.some(o => o.kind === "osc11")) {
|
||||
this.#osc11QueryQueued = true;
|
||||
if (this.#osc11QueuedRoute !== "tmux") this.#osc11QueuedRoute = route;
|
||||
return;
|
||||
}
|
||||
this.#startOsc11Query();
|
||||
this.#startOsc11Query(route);
|
||||
}
|
||||
|
||||
#startOsc11Query(): void {
|
||||
#startOsc11Query(route: Osc11QueryRoute): void {
|
||||
this.#osc11Pending = true;
|
||||
this.#osc11ResponseBuffer = "";
|
||||
this.#da1SentinelOwners.push({ kind: "osc11" });
|
||||
if (route === "tmux") {
|
||||
this.#safeWrite(wrapTmuxPassthrough("\x1b]11;?\x07\x1b[c"));
|
||||
return;
|
||||
}
|
||||
this.#safeWrite("\x1b]11;?\x07"); // OSC 11 query (BEL terminated)
|
||||
this.#safeWrite("\x1b[c"); // DA1 sentinel
|
||||
}
|
||||
@@ -1397,7 +1405,7 @@ export class ProcessTerminal implements Terminal {
|
||||
this.#appearanceCallbacks = [];
|
||||
this.#osc11Pending = false;
|
||||
this.#clearWindowsTerminalAppearancePoll();
|
||||
this.#osc11QueryQueued = false;
|
||||
this.#osc11QueuedRoute = undefined;
|
||||
this.#osc11ResponseBuffer = "";
|
||||
this.#osc99PendingId = undefined;
|
||||
this.#osc99ResponseBuffer = "";
|
||||
|
||||
@@ -19,6 +19,7 @@ const originalWslDistroName = Bun.env.WSL_DISTRO_NAME;
|
||||
const originalWslInterop = Bun.env.WSL_INTEROP;
|
||||
const originalWtSession = Bun.env.WT_SESSION;
|
||||
const originalTermProgram = Bun.env.TERM_PROGRAM;
|
||||
const originalTmux = Bun.env.TMUX;
|
||||
|
||||
// These suites drive the real ProcessTerminal start()/probe pipeline, so they
|
||||
// opt out of the test-default headless suppression and restore it per case.
|
||||
@@ -60,6 +61,7 @@ describe("ProcessTerminal OSC 11 appearance detection", () => {
|
||||
restoreEnv("WSL_DISTRO_NAME", originalWslDistroName);
|
||||
restoreEnv("WT_SESSION", originalWtSession);
|
||||
restoreEnv("TERM_PROGRAM", originalTermProgram);
|
||||
restoreEnv("TMUX", originalTmux);
|
||||
});
|
||||
|
||||
function setupTerminal() {
|
||||
@@ -282,6 +284,22 @@ describe("ProcessTerminal OSC 11 appearance detection", () => {
|
||||
terminal.stop();
|
||||
});
|
||||
|
||||
it("passes an explicit appearance refresh through tmux without changing the startup probe", () => {
|
||||
Bun.env.TMUX = "/tmp/tmux-1000/default,1234,0";
|
||||
const { terminal, writes } = setupTerminal();
|
||||
|
||||
expect(writes).toContain("\x1b]11;?\x07");
|
||||
expect(writes).toContain("\x1b[c");
|
||||
|
||||
process.stdin.emit("data", "\x1b]11;rgb:ffff/ffff/ffff\x07");
|
||||
for (let i = 0; i < 7; i++) process.stdin.emit("data", "\x1b[?1;2c");
|
||||
terminal.refreshAppearance?.();
|
||||
|
||||
expect(writes).toContain("\x1bPtmux;\x1b\x1b]11;?\x07\x1b\x1b[c\x1b\\");
|
||||
|
||||
terminal.stop();
|
||||
});
|
||||
|
||||
it("refreshAppearance() re-evaluates a changed background through the callback pipeline", () => {
|
||||
const { terminal } = setupTerminal();
|
||||
const appearances: string[] = [];
|
||||
|
||||
Reference in New Issue
Block a user