@oh-my-pi/pi-tui 18.9.1 → 18.9.2

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,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.9.2] - 2026-10-11
6
+
7
+ ### Added
8
+
9
+ - Added native TSP rendering support for Tern terminal sessions running inside tmux, with automatic session detection and safe title-setting behavior.
10
+
11
+ ### Fixed
12
+
13
+ - Improved responsiveness when inserting assistant messages after tool calls in long transcripts.
14
+
5
15
  ## [18.9.1] - 2026-10-11
6
16
 
7
17
  ### Fixed
@@ -63,6 +63,11 @@ export interface TranscriptViewportSpan {
63
63
  export declare class TranscriptContainer extends Container {
64
64
  #private;
65
65
  addChild(component: Component): void;
66
+ /**
67
+ * `#entries` mirrors `children`; move both together or the next `#syncEntries`
68
+ * pays a full O(n) rebuild.
69
+ */
70
+ insertChildAt(component: Component, index: number): void;
66
71
  /** Removes `component` if {@link canRemoveBlock} allows it; otherwise does nothing. */
67
72
  removeChild(component: Component): void;
68
73
  clear(): void;
@@ -1,6 +1,16 @@
1
1
  export { wrapTmuxPassthrough } from "./multiplexers/tmux.js";
2
2
  /** Pass a control sequence through tmux, leaving direct-terminal output unchanged. */
3
3
  export declare function wrapTmuxPassthroughIfNeeded(payload: string, env?: NodeJS.ProcessEnv): string;
4
+ /**
5
+ * Whether the terminal hosting this tmux session draws TSP programs natively:
6
+ * it sets the session user option `@tern-tsp` while it watches the session.
7
+ */
8
+ export declare function tmuxSessionSpeaksTsp(env?: NodeJS.ProcessEnv): boolean;
9
+ /**
10
+ * Stop (`on` false) or restore (`on` true) tmux turning this pane's APC
11
+ * strings into its title, which would otherwise swallow every TSP message.
12
+ */
13
+ export declare function setTmuxPaneApcTitles(on: boolean, env?: NodeJS.ProcessEnv): void;
4
14
  /**
5
15
  * Resolve the terminal emulator name recorded for this tmux client.
6
16
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "18.9.1",
4
+ "version": "18.9.2",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -41,14 +41,14 @@
41
41
  "gen:glyphs": "bun scripts/gen-glyphs.ts"
42
42
  },
43
43
  "dependencies": {
44
- "@oh-my-pi/omptype": "18.9.1",
45
- "@oh-my-pi/pi-agent-core": "18.9.1",
46
- "@oh-my-pi/pi-ai": "18.9.1",
47
- "@oh-my-pi/pi-catalog": "18.9.1",
48
- "@oh-my-pi/pi-natives": "18.9.1",
49
- "@oh-my-pi/pi-utils": "18.9.1",
50
- "@oh-my-pi/pi-wire": "18.9.1",
51
- "@oh-my-pi/snapcompact": "18.9.1"
44
+ "@oh-my-pi/omptype": "18.9.2",
45
+ "@oh-my-pi/pi-agent-core": "18.9.2",
46
+ "@oh-my-pi/pi-ai": "18.9.2",
47
+ "@oh-my-pi/pi-catalog": "18.9.2",
48
+ "@oh-my-pi/pi-natives": "18.9.2",
49
+ "@oh-my-pi/pi-utils": "18.9.2",
50
+ "@oh-my-pi/pi-wire": "18.9.2",
51
+ "@oh-my-pi/snapcompact": "18.9.2"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
@@ -227,11 +227,31 @@ export class TranscriptContainer extends Container {
227
227
  emitted: 0,
228
228
  stableFrozen: false,
229
229
  });
230
- // Callers may splice a just-added block into place (insert after an
231
- // anchor); re-check the whole list once instead of trusting positions.
230
+ // `children` is public and may be edited externally; re-check the whole
231
+ // list once instead of trusting positions. Insert-after-anchor callers
232
+ // use insertChildAt, which keeps both arrays in lockstep.
232
233
  this.#entriesUnverified = true;
233
234
  }
234
235
 
236
+ /**
237
+ * `#entries` mirrors `children`; move both together or the next `#syncEntries`
238
+ * pays a full O(n) rebuild.
239
+ */
240
+ insertChildAt(component: Component, index: number): void {
241
+ this.addChild(component);
242
+ const from = this.children.length - 1;
243
+ if (index >= from) {
244
+ this.#entriesUnverified = false;
245
+ return;
246
+ }
247
+ const [moved] = this.children.splice(from, 1);
248
+ if (moved) this.children.splice(index, 0, moved);
249
+ const [entry] = this.#entries.splice(this.#entries.length - 1, 1);
250
+ if (entry) this.#entries.splice(index, 0, entry);
251
+ // Both arrays moved in lockstep, so the live-tail check applies again.
252
+ this.#entriesUnverified = false;
253
+ }
254
+
235
255
  /** Removes `component` if {@link canRemoveBlock} allows it; otherwise does nothing. */
236
256
  override removeChild(component: Component): void {
237
257
  const index = this.#removableIndex(component);
package/src/terminal.ts CHANGED
@@ -28,8 +28,10 @@ import {
28
28
  setTerminalGlyphProtocol,
29
29
  TERMINAL,
30
30
  } from "./terminal-capabilities";
31
+ import { tmuxMultiplexer } from "./multiplexers/tmux";
31
32
  import type { TerminalMultiplexerBackgroundColorCache } from "./multiplexers/types";
32
33
  import { terminalMultiplexerSessions } from "./terminal-multiplexer";
34
+ import { setTmuxPaneApcTitles, tmuxSessionSpeaksTsp } from "./tmux";
33
35
  import { setHangulCompatibilityJamoWidth } from "./utils";
34
36
  import { translateWindowsAltGrSequence } from "./windows-altgr";
35
37
  import { Win32InputModeDecoder, Win32PasteMarkerNormalizer } from "./windows-input-mode";
@@ -50,6 +52,9 @@ const WIN32_RELAYED_ESCAPE_TIMEOUT_MS = 75;
50
52
  */
51
53
  const TSP_OSC_PREFIX = `\x1b]877;${TSP_PREFIX.slice(2)}`;
52
54
 
55
+ /** How long a TSP hello waits for its reply inside tmux, which answers DA1 itself. */
56
+ const TSP_TMUX_HELLO_TIMEOUT_MS = 1000;
57
+
53
58
  /**
54
59
  * The TSP message a reassembled input string holds, in APC form, or
55
60
  * `undefined` while its terminator has not arrived. An OSC 877 message may end
@@ -874,6 +879,10 @@ export class ProcessTerminal implements Terminal {
874
879
  #tspPending = false;
875
880
  #tspResult: TspHello | null | undefined;
876
881
  #tspCallbacks: TspHelloHandler[] = [];
882
+ /** Hello deadline inside tmux, where no DA1 sentinel can end the probe. */
883
+ #tspTimeout: NodeJS.Timeout | undefined;
884
+ /** This tmux pane's APC titles are off for TSP and must be restored. */
885
+ #tspTmuxPaneGated = false;
877
886
  #tspReplyBuffer = "";
878
887
  #privateCsiResponseBuffer = "";
879
888
  #da1SentinelOwners: Da1SentinelOwner[] = [];
@@ -1831,6 +1840,9 @@ export class ProcessTerminal implements Terminal {
1831
1840
  #shouldQueryTspSupport(): boolean {
1832
1841
  const override = $env.PI_TUI_NATIVE;
1833
1842
  if (override === "0") return false;
1843
+ // tmux makes APC bodies the pane title unless the hosting terminal asked
1844
+ // for TSP through `@tern-tsp`: no override may force the probe there.
1845
+ if (tmuxMultiplexer.isInside($env)) return tmuxSessionSpeaksTsp($env);
1834
1846
  if (override === "1") return true;
1835
1847
  // Multiplexers swallow APC, so the reply could never arrive.
1836
1848
  if (isInsideTerminalMultiplexer($env)) return false;
@@ -1848,10 +1860,30 @@ export class ProcessTerminal implements Terminal {
1848
1860
  return;
1849
1861
  }
1850
1862
  this.#tspPending = true;
1863
+ if (tmuxMultiplexer.isInside($env)) {
1864
+ // tmux answers DA1 itself, ahead of any reply: wait for the reply instead.
1865
+ this.#tspTmuxPaneGated = true;
1866
+ setTmuxPaneApcTitles(false, $env);
1867
+ this.#tspTimeout = setTimeout(() => {
1868
+ this.#tspTimeout = undefined;
1869
+ this.#resolveTspSupport(null);
1870
+ }, TSP_TMUX_HELLO_TIMEOUT_MS);
1871
+ this.#safeWrite(encodeTspHelloQuery());
1872
+ return;
1873
+ }
1851
1874
  this.#da1SentinelOwners.push({ kind: "tsp" });
1852
1875
  this.#safeWrite(`${encodeTspHelloQuery()}\x1b[c`);
1853
1876
  }
1854
1877
 
1878
+ /** Hand APC titles back to tmux once this pane stops speaking TSP. */
1879
+ #endTspTmuxPane(): void {
1880
+ clearTimeout(this.#tspTimeout);
1881
+ this.#tspTimeout = undefined;
1882
+ if (!this.#tspTmuxPaneGated) return;
1883
+ this.#tspTmuxPaneGated = false;
1884
+ setTmuxPaneApcTitles(true, $env);
1885
+ }
1886
+
1855
1887
  #handleTspMessage(sequence: string): void {
1856
1888
  const message = parseTspMessage(sequence);
1857
1889
  if (message?.verb === "r" && message.reply.r === "hello") {
@@ -1867,6 +1899,12 @@ export class ProcessTerminal implements Terminal {
1867
1899
  this.#tspPending = false;
1868
1900
  const result = hello !== null && hello.v === TSP_VERSION ? hello : null;
1869
1901
  if (hello !== null && result === null) logger.warn("TSP: unsupported protocol version", { v: hello.v });
1902
+ if (result === null) {
1903
+ this.#endTspTmuxPane();
1904
+ } else {
1905
+ clearTimeout(this.#tspTimeout);
1906
+ this.#tspTimeout = undefined;
1907
+ }
1870
1908
  this.#tspResult = result;
1871
1909
  for (const cb of this.#tspCallbacks) {
1872
1910
  try {
@@ -2255,6 +2293,7 @@ export class ProcessTerminal implements Terminal {
2255
2293
  this.#sixelSupportCallbacks = [];
2256
2294
  setTerminalGlyphProtocol(false);
2257
2295
  this.#tspPending = false;
2296
+ this.#endTspTmuxPane();
2258
2297
  this.#tspResult = undefined;
2259
2298
  this.#tspReplyBuffer = "";
2260
2299
  this.#tspCallbacks = [];
package/src/tmux.ts CHANGED
@@ -31,6 +31,49 @@ function queryTmuxClientTerminalName(env: NodeJS.ProcessEnv): string | null {
31
31
  }
32
32
  }
33
33
 
34
+ const TMUX_COMMAND_TIMEOUT_MS = 500;
35
+
36
+ /** Run a tmux command against the current server; its stdout, or null on failure. */
37
+ function runTmux(args: string[], env: NodeJS.ProcessEnv): string | null {
38
+ const tmux = $which("tmux", { PATH: env.PATH });
39
+ if (!tmux) return null;
40
+ try {
41
+ const result = Bun.spawnSync([tmux, ...args], {
42
+ env,
43
+ stdout: "pipe",
44
+ stderr: "ignore",
45
+ timeout: TMUX_COMMAND_TIMEOUT_MS,
46
+ killSignal: "SIGKILL",
47
+ });
48
+ return result.exitCode === 0 ? result.stdout.toString() : null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Whether the terminal hosting this tmux session draws TSP programs natively:
56
+ * it sets the session user option `@tern-tsp` while it watches the session.
57
+ */
58
+ export function tmuxSessionSpeaksTsp(env: NodeJS.ProcessEnv = Bun.env): boolean {
59
+ return (runTmux(["show-options", "-qv", "@tern-tsp"], env)?.trim() ?? "") !== "";
60
+ }
61
+
62
+ /**
63
+ * Stop (`on` false) or restore (`on` true) tmux turning this pane's APC
64
+ * strings into its title, which would otherwise swallow every TSP message.
65
+ */
66
+ export function setTmuxPaneApcTitles(on: boolean, env: NodeJS.ProcessEnv = Bun.env): void {
67
+ const pane = env.TMUX_PANE;
68
+ if (!pane) return;
69
+ runTmux(
70
+ on
71
+ ? ["set-option", "-pu", "-t", pane, "allow-set-title"]
72
+ : ["set-option", "-p", "-t", pane, "allow-set-title", "off"],
73
+ env,
74
+ );
75
+ }
76
+
34
77
  /**
35
78
  * Resolve the terminal emulator name recorded for this tmux client.
36
79
  *