@oh-my-pi/pi-tui 17.0.7 → 17.0.8

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,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.0.8] - 2026-07-22
6
+
7
+ ### Fixed
8
+
9
+ - Fixed an issue where OSC 8 hyperlinks with inline markup corrupted Markdown table column widths.
10
+ - Prevented duplicate autocomplete suggestions for slash commands and their matching aliases.
11
+ - Fixed terminal background color detection during screen refreshes (Ctrl+L) inside tmux when passthrough is enabled.
12
+
5
13
  ## [17.0.6] - 2026-07-20
6
14
 
7
15
  ### Fixed
@@ -105,11 +105,11 @@ export declare class ProcessTerminal implements Terminal {
105
105
  onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
106
106
  /**
107
107
  * Re-query the terminal background via a single OSC 11 probe. Reuses the
108
- * startup query path — same DA1-sentinel FIFO, pending/queued gating, parsing,
109
- * dedup, and appearance callbacks so a light/dark switch is picked up
110
- * without a restart on terminals lacking end-to-end Mode 2031 notifications.
111
- * Bounded to one probe per call; no timers are armed. Suppressed while headless
112
- * or after the terminal is torn down.
108
+ * startup DA1-sentinel FIFO, pending/queued gating, parsing, dedup, and
109
+ * appearance callbacks. Inside tmux, only this explicit path wraps the query
110
+ * and sentinel together for passthrough to the outer terminal; startup and
111
+ * Mode 2031 probes remain direct. Bounded to one probe per call; no timers are
112
+ * armed. Suppressed while headless or after the terminal is torn down.
113
113
  */
114
114
  refreshAppearance(): void;
115
115
  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.0.7",
4
+ "version": "17.0.8",
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.0.7",
41
- "@oh-my-pi/pi-utils": "17.0.7",
40
+ "@oh-my-pi/pi-natives": "17.0.8",
41
+ "@oh-my-pi/pi-utils": "17.0.8",
42
42
  "lru-cache": "11.5.2",
43
43
  "marked": "^18.0.6"
44
44
  },
@@ -307,7 +307,7 @@ function buildSlashCommandCompletions(commands: CommandEntry[], lowerPrefix: str
307
307
  }
308
308
  return fullDescMemo;
309
309
  };
310
- const candidates: Array<AutocompleteItem & { score: number }> = [];
310
+ let best: (AutocompleteItem & { score: number }) | undefined;
311
311
 
312
312
  const isSkillCommand = name.startsWith("skill:");
313
313
  const nameScore =
@@ -318,30 +318,30 @@ function buildSlashCommandCompletions(commands: CommandEntry[], lowerPrefix: str
318
318
  const primaryScore = Math.max(nameScore, descScore);
319
319
  if (primaryScore > 0) {
320
320
  const fullDesc = resolveFullDesc();
321
- candidates.push({
321
+ best = {
322
322
  value: name,
323
323
  label: "name" in cmd ? cmd.name : cmd.label,
324
324
  score: primaryScore,
325
325
  ...(fullDesc && { description: fullDesc }),
326
- });
326
+ };
327
327
  }
328
328
 
329
329
  if (lowerPrefix.length > 0) {
330
330
  for (const alias of getCommandAliases(cmd)) {
331
331
  if (alias === name) continue;
332
332
  const aliasScore = scoreCommandTextMatch(lowerPrefix, alias.toLowerCase());
333
- if (aliasScore === 0) continue;
333
+ if (aliasScore === 0 || (best && aliasScore <= best.score)) continue;
334
334
  const fullDesc = resolveFullDesc();
335
- candidates.push({
335
+ best = {
336
336
  value: alias,
337
337
  label: alias,
338
338
  score: aliasScore,
339
339
  ...(fullDesc && { description: fullDesc }),
340
- });
340
+ };
341
341
  }
342
342
  }
343
343
 
344
- return candidates;
344
+ return best ? [best] : [];
345
345
  })
346
346
  .sort((a, b) => b.score - a.score)
347
347
  .map(({ score: _, ...rest }) => rest);
@@ -20,6 +20,16 @@ import {
20
20
 
21
21
  const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
22
22
 
23
+ // Marked treats the backslash in an ST-terminated OSC 8 sequence (`ESC \\`) as
24
+ // Markdown punctuation when it is immediately followed by markup such as a
25
+ // codespan backtick. Normalize well-formed OSC 8 prefixes to the equivalent BEL
26
+ // terminator before lexing so the control sequence stays opaque to Markdown.
27
+ const OSC8_ST_PREFIX_REGEX = /(\x1b\]8;[^\x07\x1b]*)\x1b\\/g;
28
+
29
+ function normalizeOsc8Terminators(text: string): string {
30
+ return text.replace(OSC8_ST_PREFIX_REGEX, "$1\x07");
31
+ }
32
+
23
33
  // OSC 66 (Kitty text-sizing) heading spans are emitted as a single indivisible
24
34
  // unit by the H1 render path. Like image-protocol lines, they must bypass
25
35
  // ANSI wrapping and width padding: re-wrapping splits/normalizes the sized span
@@ -1097,7 +1107,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
1097
1107
  defaultTextStyle?: DefaultTextStyle,
1098
1108
  codeBlockIndent: number = 2,
1099
1109
  ) {
1100
- this.#text = text;
1110
+ this.#text = normalizeOsc8Terminators(text);
1101
1111
  this.#paddingX = paddingX;
1102
1112
  this.#paddingY = paddingY;
1103
1113
  this.#theme = theme;
@@ -1106,6 +1116,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
1106
1116
  }
1107
1117
 
1108
1118
  setText(text: string): boolean {
1119
+ text = normalizeOsc8Terminators(text);
1109
1120
  // Equality guard: streaming re-emits identical text on ticks that carried
1110
1121
  // no delta (throttled provider frames, reconciled tool-execution updates).
1111
1122
  // Without this, the caller-side `#cachedLines` gets thrown away and the
@@ -2611,7 +2622,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
2611
2622
  export function renderInlineMarkdown(text: string, mdTheme: MarkdownTheme, baseColor?: (t: string) => string): string {
2612
2623
  // Guard against undefined/null during streaming — partial JSON can leave fields unpopulated.
2613
2624
  if (typeof text !== "string") return (baseColor ?? (t => t))(text != null ? String(text) : "");
2614
- const tokens = markdownParser.lexer(text);
2625
+ const tokens = markdownParser.lexer(normalizeOsc8Terminators(text));
2615
2626
  const applyText = baseColor ?? ((t: string) => t);
2616
2627
  let result = "";
2617
2628
  for (const token of tokens) {
package/src/terminal.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  setOsc99Supported,
19
19
  TERMINAL,
20
20
  } from "./terminal-capabilities";
21
+ import { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
21
22
  import { type HangulCompatibilityJamoWidth, setHangulCompatibilityJamoWidth } from "./utils";
22
23
 
23
24
  const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
@@ -456,6 +457,7 @@ function parseOsc99KeyValues(section: string): Map<string, string> {
456
457
  return values;
457
458
  }
458
459
  const XTERM_SCROLL_TO_BOTTOM_MODES = [1010, 1011] as const;
460
+ type Osc11QueryRoute = "direct" | "tmux";
459
461
 
460
462
  function isXtermScrollToBottomMode(mode: number): boolean {
461
463
  return mode === 1010 || mode === 1011;
@@ -509,7 +511,7 @@ export class ProcessTerminal implements Terminal {
509
511
  #appearanceCallbacks: Array<(appearance: TerminalAppearance) => void> = [];
510
512
  #appearance: TerminalAppearance | undefined;
511
513
  #osc11Pending = false;
512
- #osc11QueryQueued = false;
514
+ #osc11QueuedRoute?: Osc11QueryRoute;
513
515
  #osc11ResponseBuffer = "";
514
516
  #osc99PendingId: string | undefined;
515
517
  #osc99ResponseBuffer = "";
@@ -572,15 +574,15 @@ export class ProcessTerminal implements Terminal {
572
574
 
573
575
  /**
574
576
  * Re-query the terminal background via a single OSC 11 probe. Reuses the
575
- * startup query path — same DA1-sentinel FIFO, pending/queued gating, parsing,
576
- * dedup, and appearance callbacks so a light/dark switch is picked up
577
- * without a restart on terminals lacking end-to-end Mode 2031 notifications.
578
- * Bounded to one probe per call; no timers are armed. Suppressed while headless
579
- * or after the terminal is torn down.
577
+ * startup DA1-sentinel FIFO, pending/queued gating, parsing, dedup, and
578
+ * appearance callbacks. Inside tmux, only this explicit path wraps the query
579
+ * and sentinel together for passthrough to the outer terminal; startup and
580
+ * Mode 2031 probes remain direct. Bounded to one probe per call; no timers are
581
+ * armed. Suppressed while headless or after the terminal is torn down.
580
582
  */
581
583
  refreshAppearance(): void {
582
584
  if (this.#headless || this.#dead) return;
583
- this.#queryBackgroundColor();
585
+ this.#queryBackgroundColor(isInsideTmux() ? "tmux" : "direct");
584
586
  }
585
587
 
586
588
  onPrivateModeReport(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void {
@@ -917,13 +919,14 @@ export class ProcessTerminal implements Terminal {
917
919
  }
918
920
  // Start a queued OSC 11 query once the prior cycle is fully drained.
919
921
  if (
920
- this.#osc11QueryQueued &&
922
+ this.#osc11QueuedRoute !== undefined &&
921
923
  !this.#osc11Pending &&
922
924
  !this.#da1SentinelOwners.some(o => o.kind === "osc11") &&
923
925
  !this.#dead
924
926
  ) {
925
- this.#osc11QueryQueued = false;
926
- this.#startOsc11Query();
927
+ const route = this.#osc11QueuedRoute;
928
+ this.#osc11QueuedRoute = undefined;
929
+ this.#startOsc11Query(route);
927
930
  }
928
931
  break;
929
932
  }
@@ -1058,23 +1061,28 @@ export class ProcessTerminal implements Terminal {
1058
1061
  * DA1 avoids indefinite hangs: if DA1 response arrives before OSC 11,
1059
1062
  * the terminal does not support OSC 11.
1060
1063
  */
1061
- #queryBackgroundColor(): void {
1064
+ #queryBackgroundColor(route: Osc11QueryRoute = "direct"): void {
1062
1065
  if (this.#dead) return;
1063
1066
  // Queue if an OSC 11 query is in flight or its DA1 sentinel hasn't been
1064
1067
  // consumed yet. Starting a new query while a DA1 is outstanding would
1065
1068
  // increment the sentinel counter, and the old DA1 arrival would then
1066
- // prematurely clear the new query's pending state.
1069
+ // prematurely clear the new query's pending state. Preserve a requested
1070
+ // tmux passthrough route when coalescing direct and explicit queries.
1067
1071
  if (this.#osc11Pending || this.#da1SentinelOwners.some(o => o.kind === "osc11")) {
1068
- this.#osc11QueryQueued = true;
1072
+ if (this.#osc11QueuedRoute !== "tmux") this.#osc11QueuedRoute = route;
1069
1073
  return;
1070
1074
  }
1071
- this.#startOsc11Query();
1075
+ this.#startOsc11Query(route);
1072
1076
  }
1073
1077
 
1074
- #startOsc11Query(): void {
1078
+ #startOsc11Query(route: Osc11QueryRoute): void {
1075
1079
  this.#osc11Pending = true;
1076
1080
  this.#osc11ResponseBuffer = "";
1077
1081
  this.#da1SentinelOwners.push({ kind: "osc11" });
1082
+ if (route === "tmux") {
1083
+ this.#safeWrite(wrapTmuxPassthrough("\x1b]11;?\x07\x1b[c"));
1084
+ return;
1085
+ }
1078
1086
  this.#safeWrite("\x1b]11;?\x07"); // OSC 11 query (BEL terminated)
1079
1087
  this.#safeWrite("\x1b[c"); // DA1 sentinel
1080
1088
  }
@@ -1397,7 +1405,7 @@ export class ProcessTerminal implements Terminal {
1397
1405
  this.#appearanceCallbacks = [];
1398
1406
  this.#osc11Pending = false;
1399
1407
  this.#clearWindowsTerminalAppearancePoll();
1400
- this.#osc11QueryQueued = false;
1408
+ this.#osc11QueuedRoute = undefined;
1401
1409
  this.#osc11ResponseBuffer = "";
1402
1410
  this.#osc99PendingId = undefined;
1403
1411
  this.#osc99ResponseBuffer = "";