@oh-my-pi/pi-tui 16.3.12 → 16.3.14

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,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.3.14] - 2026-07-09
6
+
7
+ ### Fixed
8
+
9
+ - Fixed race condition where scrollback rows could be incorrectly retracted between render frames
10
+
11
+ ## [16.3.13] - 2026-07-09
12
+
13
+ ### Fixed
14
+
15
+ - Fixed late terminal appearance subscribers missing the already-detected OSC 11 light/dark result, so theme auto-detection picks up the terminal appearance even when the response arrives before the UI subscribes ([#4731](https://github.com/can1357/oh-my-pi/issues/4731)).
16
+ - Fixed slash command Tab completion reopening the file autocomplete drawer after accepting no-argument commands ([#4808](https://github.com/can1357/oh-my-pi/issues/4808)).
17
+
5
18
  ## [16.3.12] - 2026-07-08
6
19
 
7
20
  ### Fixed
@@ -59,6 +59,8 @@ export interface Terminal {
59
59
  * Register a callback for terminal appearance (dark/light) changes.
60
60
  * Detection uses OSC 11 background color query with Mode 2031 as a change trigger.
61
61
  * Fires when the detected appearance changes, including the initial detection.
62
+ * Subscribers registered after detection are invoked immediately with the
63
+ * already-detected appearance so late subscribers never miss it.
62
64
  */
63
65
  onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
64
66
  /** The last detected terminal appearance, or undefined if not yet known. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "16.3.12",
4
+ "version": "16.3.14",
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": "16.3.12",
41
- "@oh-my-pi/pi-utils": "16.3.12",
40
+ "@oh-my-pi/pi-natives": "16.3.14",
41
+ "@oh-my-pi/pi-utils": "16.3.14",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.5"
44
44
  },
@@ -437,6 +437,9 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
437
437
  const argumentText = commandText.slice(spaceIndex + 1); // Text after space
438
438
 
439
439
  const command = this.#commands.find(cmd => commandMatchesNameOrAlias(cmd, commandName));
440
+ if (command && "allowArgs" in command && command.allowArgs === false && !/\S/.test(argumentText)) {
441
+ return null;
442
+ }
440
443
  if (command && (!("allowArgs" in command) || command.allowArgs !== false)) {
441
444
  if (!("getArgumentCompletions" in command) || !command.getArgumentCompletions) {
442
445
  return null; // No argument completion for this command
package/src/terminal.ts CHANGED
@@ -382,6 +382,8 @@ export interface Terminal {
382
382
  * Register a callback for terminal appearance (dark/light) changes.
383
383
  * Detection uses OSC 11 background color query with Mode 2031 as a change trigger.
384
384
  * Fires when the detected appearance changes, including the initial detection.
385
+ * Subscribers registered after detection are invoked immediately with the
386
+ * already-detected appearance so late subscribers never miss it.
385
387
  */
386
388
  onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
387
389
  /** The last detected terminal appearance, or undefined if not yet known. */
@@ -516,6 +518,17 @@ export class ProcessTerminal implements Terminal {
516
518
 
517
519
  onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void {
518
520
  this.#appearanceCallbacks.push(callback);
521
+ // Replay an already-detected appearance: the startup OSC 11 response can
522
+ // arrive before consumers (e.g. the theme bridge) subscribe, and the
523
+ // dedup in #handleOsc11Response would otherwise suppress the value for
524
+ // them forever (#4731).
525
+ if (this.#appearance) {
526
+ try {
527
+ callback(this.#appearance);
528
+ } catch {
529
+ /* ignore callback errors */
530
+ }
531
+ }
519
532
  }
520
533
 
521
534
  onPrivateModeReport(callback: (mode: number, supported: boolean) => void): void {
@@ -704,7 +717,10 @@ export class ProcessTerminal implements Terminal {
704
717
  const decrpmResponsePattern = /^\x1b\[\?(\d+);(\d+)\$y$/;
705
718
 
706
719
  // In-band resize report (DEC mode 2048): \x1b[48;rows;cols;yPixels;xPixels t
707
- const inBandResizePattern = /^\x1b\[48;(\d+);(\d+);(\d+);(\d+)t$/;
720
+ // Any field may carry `:`-separated subparameters, which clients MUST
721
+ // ignore per spec (#4748): capture the leading digits of each field and
722
+ // skip the subparameter tail instead of dropping the whole report.
723
+ const inBandResizePattern = /^\x1b\[48;(\d+)(?::[\d:]*)?;(\d+)(?::[\d:]*)?;(\d+)(?::[\d:]*)?;(\d+)(?::[\d:]*)?t$/;
708
724
 
709
725
  this.#stdinBuffer.on("data", (sequence: string) => {
710
726
  // Fast path for plain-text bytes: every escape-probe regex below
@@ -776,7 +792,7 @@ export class ProcessTerminal implements Terminal {
776
792
  // reassembled sequence that turns out not to be a resize report (e.g. a
777
793
  // split kitty `\x1b[48;…u` for a digit key) is forwarded to the input
778
794
  // handler rather than dropped.
779
- const inBandResizePartialPattern = /^\x1b\[4[\d;]*$/;
795
+ const inBandResizePartialPattern = /^\x1b\[4[\d;:]*$/;
780
796
  const isInBandResizePartial = this.#inBandResizeActive && inBandResizePartialPattern.test(sequence);
781
797
  if (this.#inBandResizeBuffer && sequence.startsWith("\x1b")) {
782
798
  // A new escape interrupted the partial; the stale partial is
@@ -1185,9 +1201,9 @@ export class ProcessTerminal implements Terminal {
1185
1201
  * `rows` before the `resize` event fires, so they are authoritative for the
1186
1202
  * new cell geometry. A cached DEC 2048 report can be stale: the matching
1187
1203
  * post-resize report may be dropped (split across stdin reads past the flush
1188
- * window) or carry `:`-subparameters the parser skips, leaving the getters
1189
- * pinned to the old size — which freezes the rendered width because the
1190
- * renderer reflows against {@link columns}/{@link rows}, not the live OS
1204
+ * window, or interrupted by another escape mid-reassembly), leaving the
1205
+ * getters pinned to the old size — which freezes the rendered width because
1206
+ * the renderer reflows against {@link columns}/{@link rows}, not the live OS
1191
1207
  * value. Drop a cached dimension that disagrees with the live OS value; the
1192
1208
  * terminal's next valid in-band report re-seeds pixel sizing.
1193
1209
  */
package/src/tui.ts CHANGED
@@ -1137,8 +1137,14 @@ export class TUI extends Container {
1137
1137
  // Feed the engine's committed-row claim (from the previous frame's
1138
1138
  // emit) before rendering so the child can skip re-deriving blocks
1139
1139
  // that already live in immutable native scrollback. Reused segments
1140
- // skip this: they never call render(), so the signal is moot.
1141
- setNativeScrollbackCommittedRows(child, Math.max(0, this.#committedRows - offset));
1140
+ // skip this: they never call render(), so the signal is moot. The
1141
+ // claim is in the previous frame's coordinates and never exceeds
1142
+ // the rows the child actually contributed there — history that
1143
+ // advanced into LATER root children must not read as this child's
1144
+ // own future rows being pre-committed.
1145
+ const prevRows = previous !== undefined && previous.component === child ? previous.rowCount : 0;
1146
+ const prevStart = previous !== undefined && previous.component === child ? previous.start : offset;
1147
+ setNativeScrollbackCommittedRows(child, Math.min(prevRows, Math.max(0, this.#committedRows - prevStart)));
1142
1148
  childLines = child.render(width);
1143
1149
  const liveRegionStart = getNativeScrollbackLiveRegionStart(child);
1144
1150
  if (liveRegionStart !== undefined) {
@@ -2802,6 +2808,7 @@ export class TUI extends Container {
2802
2808
  this.#committedPrefixAuditRows = Math.min(chunkTo, finalBoundary);
2803
2809
  this.#clearScrollbackOnNextRender = false;
2804
2810
  this.#hasEverRendered = true;
2811
+ this.#publishCommittedRows();
2805
2812
  if (!firstPaint && frameLength > height) this.#armPostFullPaintSettle();
2806
2813
  return;
2807
2814
  }
@@ -2829,6 +2836,7 @@ export class TUI extends Container {
2829
2836
  } else {
2830
2837
  this.#committedPrefixAuditRows = Math.min(preAuditRows, this.#committedRows);
2831
2838
  }
2839
+ this.#publishCommittedRows();
2832
2840
  }
2833
2841
 
2834
2842
  /**
@@ -2853,6 +2861,25 @@ export class TUI extends Container {
2853
2861
  }
2854
2862
  }
2855
2863
 
2864
+ /**
2865
+ * Push the post-emit committed-row count to root children that implement
2866
+ * {@link NativeScrollbackCommittedRows}. Compose feeds the same signal
2867
+ * before each child render (see {@link render}), but guards that run
2868
+ * BETWEEN frames — e.g. a controller consulting the transcript's
2869
+ * committed boundary to decide whether a displaceable block may still be
2870
+ * retracted — would otherwise observe a count one frame stale and retract
2871
+ * rows that just entered immutable native scrollback, stranding an
2872
+ * orphaned copy above the repainted block.
2873
+ */
2874
+ #publishCommittedRows(): void {
2875
+ for (const segment of this.#frameSegments) {
2876
+ setNativeScrollbackCommittedRows(
2877
+ segment.component,
2878
+ Math.min(segment.rowCount, Math.max(0, this.#committedRows - segment.start)),
2879
+ );
2880
+ }
2881
+ }
2882
+
2856
2883
  /**
2857
2884
  * Prepare the composed frame for emission, in place. Rows below
2858
2885
  * `#preparedValidRows` are already prepared against the current frame (the