@oh-my-pi/pi-tui 17.2.10 → 17.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.11] - 2026-08-07
6
+
7
+ ### Fixed
8
+
9
+ - Fixed an issue where Herdr panes lost native terminal scrollback during TUI transcript replacements or resize redraws.
10
+ - Fixed an issue inside tmux where explicit display resets retained stale light/dark palettes and leaked terminal capability bytes into the editor.
11
+
5
12
  ## [17.2.10] - 2026-08-06
6
13
 
7
14
  ### Fixed
@@ -101,12 +101,13 @@ export interface Terminal {
101
101
  */
102
102
  onAppearanceReport?(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): (() => void) | void;
103
103
  /**
104
- * Issue a single OSC 11 background-color re-query, driving the appearance
104
+ * Start a bounded OSC 11 background-color refresh cycle, driving appearance
105
105
  * callbacks through the same parse/dedup pipeline used at startup and on Mode
106
- * 2031 notifications. Bounded: one probe per call, no timers. Invoked on the
107
- * user's explicit display-reset gesture so terminals that cannot
108
- * deliver end-to-end Mode 2031 notifications still pick up a light/dark switch
109
- * without a restart.
106
+ * 2031 notifications. Direct terminals need one query; tmux needs a
107
+ * passthrough query to update its cache followed by one delayed direct cache
108
+ * read. Invoked on the user's explicit display-reset gesture so terminals
109
+ * without end-to-end Mode 2031 notifications pick up a light/dark switch
110
+ * without a restart. No periodic probes are armed.
110
111
  *
111
112
  * A caller-provided token must be propagated unchanged to callbacks and
112
113
  * returned when the request is accepted. This lets callers establish ownership
@@ -146,13 +147,14 @@ export declare class ProcessTerminal implements Terminal {
146
147
  onAppearanceChange(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): void;
147
148
  onAppearanceReport(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): () => void;
148
149
  /**
149
- * Re-query the terminal background via a single OSC 11 probe. Reuses the
150
- * startup DA1-sentinel FIFO, pending/queued gating, parsing, dedup, and
151
- * appearance callbacks. Inside tmux, only this explicit path wraps the query
152
- * and sentinel together for passthrough to the outer terminal; startup and
153
- * Mode 2031 probes remain direct. Bounded to one probe per call; no timers are
154
- * armed. Suppressed while inactive, headless, or after the terminal is torn
155
- * down.
150
+ * Re-query the terminal background through the startup DA1-sentinel FIFO,
151
+ * pending/queued gating, parsing, dedup, and appearance callbacks. Inside
152
+ * tmux, only this explicit path first passes an OSC 11 query to the outer
153
+ * terminal, waits briefly for tmux to consume the response into its cache,
154
+ * then reads that cache with a direct query. The outer query deliberately has
155
+ * no DA1 sentinel: multiplexers can decode a fragmented DA1 response as a key
156
+ * sequence and leak the remaining bytes into the editor. Startup and Mode 2031
157
+ * probes remain direct. Suppressed while inactive, headless, or after teardown.
156
158
  */
157
159
  refreshAppearance(requestToken?: TerminalAppearanceRequestToken): TerminalAppearanceRequestToken | void;
158
160
  onPrivateModeReport(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "17.2.10",
4
+ "version": "17.2.11",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "17.2.10",
41
- "@oh-my-pi/pi-utils": "17.2.10"
40
+ "@oh-my-pi/pi-natives": "17.2.11",
41
+ "@oh-my-pi/pi-utils": "17.2.11"
42
42
  },
43
43
  "devDependencies": {
44
44
  "ghostty-web": "^0.4.0"
@@ -187,12 +187,12 @@ export class TerminalInfo {
187
187
 
188
188
  /** Detect terminal multiplexers where scrollback clearing and height-change redraws are hostile. */
189
189
  export function isInsideTerminalMultiplexer(env: NodeJS.ProcessEnv = Bun.env): boolean {
190
- // TMUX/STY/ZELLIJ and CMUX workspace/surface/remote-transport markers are
191
- // authoritative session signals. TERM can also survive when those are
190
+ // TMUX/STY/ZELLIJ, Herdr, and CMUX workspace/surface/remote-transport
191
+ // markers are authoritative session signals. TERM can also survive when those are
192
192
  // stripped (`sudo` without -E, `su`, env-sanitizing launchers/ssh). Do not
193
193
  // use CMUX_SOCKET_PATH here: it is a CLI socket override and can be set
194
194
  // outside a CMUX terminal.
195
- if (env.TMUX || env.STY || env.ZELLIJ) return true;
195
+ if (env.TMUX || env.STY || env.ZELLIJ || env.HERDR_ENV === "1") return true;
196
196
  if (env.CMUX_WORKSPACE_ID || env.CMUX_SURFACE_ID || env.CMUX_REMOTE_TRANSPORT) return true;
197
197
  const term = env.TERM?.toLowerCase() ?? "";
198
198
  return term.startsWith("tmux") || term.startsWith("screen");
package/src/terminal.ts CHANGED
@@ -459,12 +459,13 @@ export interface Terminal {
459
459
  callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void,
460
460
  ): (() => void) | void;
461
461
  /**
462
- * Issue a single OSC 11 background-color re-query, driving the appearance
462
+ * Start a bounded OSC 11 background-color refresh cycle, driving appearance
463
463
  * callbacks through the same parse/dedup pipeline used at startup and on Mode
464
- * 2031 notifications. Bounded: one probe per call, no timers. Invoked on the
465
- * user's explicit display-reset gesture so terminals that cannot
466
- * deliver end-to-end Mode 2031 notifications still pick up a light/dark switch
467
- * without a restart.
464
+ * 2031 notifications. Direct terminals need one query; tmux needs a
465
+ * passthrough query to update its cache followed by one delayed direct cache
466
+ * read. Invoked on the user's explicit display-reset gesture so terminals
467
+ * without end-to-end Mode 2031 notifications pick up a light/dark switch
468
+ * without a restart. No periodic probes are armed.
468
469
  *
469
470
  * A caller-provided token must be propagated unchanged to callbacks and
470
471
  * returned when the request is accepted. This lets callers establish ownership
@@ -517,6 +518,7 @@ function parseOsc99KeyValues(section: string): Map<string, string> {
517
518
  }
518
519
  const XTERM_SCROLL_TO_BOTTOM_MODES = [1010, 1011] as const;
519
520
  type Osc11QueryRoute = "direct" | "tmux";
521
+ const TMUX_OSC11_CACHE_REFRESH_DELAY_MS = 100;
520
522
 
521
523
  function isXtermScrollToBottomMode(mode: number): boolean {
522
524
  return mode === 1010 || mode === 1011;
@@ -596,6 +598,7 @@ export class ProcessTerminal implements Terminal {
596
598
  #osc11QueuedQuery?: { route: Osc11QueryRoute; token?: TerminalAppearanceRequestToken };
597
599
  #nextAppearanceRequestToken = 1;
598
600
  #osc11ResponseBuffer = "";
601
+ #osc11TmuxRefreshTimer?: Timer;
599
602
  #osc99PendingId: string | undefined;
600
603
  #osc99ResponseBuffer = "";
601
604
  #osc99Capabilities = new Map<string, string>();
@@ -671,13 +674,14 @@ export class ProcessTerminal implements Terminal {
671
674
  }
672
675
 
673
676
  /**
674
- * Re-query the terminal background via a single OSC 11 probe. Reuses the
675
- * startup DA1-sentinel FIFO, pending/queued gating, parsing, dedup, and
676
- * appearance callbacks. Inside tmux, only this explicit path wraps the query
677
- * and sentinel together for passthrough to the outer terminal; startup and
678
- * Mode 2031 probes remain direct. Bounded to one probe per call; no timers are
679
- * armed. Suppressed while inactive, headless, or after the terminal is torn
680
- * down.
677
+ * Re-query the terminal background through the startup DA1-sentinel FIFO,
678
+ * pending/queued gating, parsing, dedup, and appearance callbacks. Inside
679
+ * tmux, only this explicit path first passes an OSC 11 query to the outer
680
+ * terminal, waits briefly for tmux to consume the response into its cache,
681
+ * then reads that cache with a direct query. The outer query deliberately has
682
+ * no DA1 sentinel: multiplexers can decode a fragmented DA1 response as a key
683
+ * sequence and leak the remaining bytes into the editor. Startup and Mode 2031
684
+ * probes remain direct. Suppressed while inactive, headless, or after teardown.
681
685
  */
682
686
  refreshAppearance(requestToken?: TerminalAppearanceRequestToken): TerminalAppearanceRequestToken | void {
683
687
  if (!this.#active || this.#headless || this.#dead) return;
@@ -1041,7 +1045,7 @@ export class ProcessTerminal implements Terminal {
1041
1045
  switch (owner.kind) {
1042
1046
  case "osc11": {
1043
1047
  if (this.#osc11Pending) {
1044
- // DA1 arrived before the OSC 11 reply: terminal does not support OSC 11.
1048
+ // DA1 arrived before OSC 11 response: terminal doesn't support OSC 11.
1045
1049
  this.#osc11Pending = false;
1046
1050
  this.#osc11ActiveToken = undefined;
1047
1051
  this.#osc11ResponseBuffer = "";
@@ -1221,11 +1225,20 @@ export class ProcessTerminal implements Terminal {
1221
1225
  this.#osc11Pending = true;
1222
1226
  this.#osc11ActiveToken = token;
1223
1227
  this.#osc11ResponseBuffer = "";
1224
- this.#da1SentinelOwners.push({ kind: "osc11" });
1225
1228
  if (route === "tmux") {
1226
- this.#safeWrite(wrapTmuxPassthrough("\x1b]11;?\x07\x1b[c"));
1229
+ this.#safeWrite(wrapTmuxPassthrough("\x1b]11;?\x07"));
1230
+ this.#osc11TmuxRefreshTimer = setTimeout(() => {
1231
+ this.#osc11TmuxRefreshTimer = undefined;
1232
+ if (this.#dead || !this.#osc11Pending) return;
1233
+ this.#startDirectOsc11Query();
1234
+ }, TMUX_OSC11_CACHE_REFRESH_DELAY_MS);
1227
1235
  return;
1228
1236
  }
1237
+ this.#startDirectOsc11Query();
1238
+ }
1239
+
1240
+ #startDirectOsc11Query(): void {
1241
+ this.#da1SentinelOwners.push({ kind: "osc11" });
1229
1242
  this.#safeWrite("\x1b]11;?\x07"); // OSC 11 query (BEL terminated)
1230
1243
  this.#safeWrite("\x1b[c"); // DA1 sentinel
1231
1244
  }
@@ -1562,6 +1575,10 @@ export class ProcessTerminal implements Terminal {
1562
1575
  clearTimeout(this.#mode2031DebounceTimer);
1563
1576
  this.#mode2031DebounceTimer = undefined;
1564
1577
  }
1578
+ if (this.#osc11TmuxRefreshTimer) {
1579
+ clearTimeout(this.#osc11TmuxRefreshTimer);
1580
+ this.#osc11TmuxRefreshTimer = undefined;
1581
+ }
1565
1582
  this.#appearanceCallbacks = [];
1566
1583
  this.#appearanceReportCallbacks = [];
1567
1584
  this.#osc11Pending = false;