@oh-my-pi/pi-tui 16.3.3 → 16.3.5

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.3.5] - 2026-07-04
6
+
7
+ ### Fixed
8
+
9
+ - Fixed the modifyOtherKeys keyboard fallback enabling on unknown SSH terminals, avoiding broken Shift input in iOS SSH clients such as Redock ([#4325](https://github.com/can1357/oh-my-pi/issues/4325)).
10
+
5
11
  ## [16.3.3] - 2026-07-02
6
12
 
7
13
  ### Fixed
@@ -105,11 +105,6 @@ export interface OverlayFocusOwner {
105
105
  * dropped row or an audit re-anchor spray. Provisional live blocks (collapsing
106
106
  * tool/edit previews whose head is a throwaway tail window) omit it. Defaults to
107
107
  * `commitSafeEnd ?? liveRegionStart` when absent.
108
- * `getNativeScrollbackOfferSafeEnd` optionally reports the deepest prefix row
109
- * that may physically enter native scrollback while still remaining audited.
110
- * This is for finalized lower siblings under a live block: the rows may scroll
111
- * off, but a later live-block insertion above them must trigger repair instead
112
- * of becoming durable audit-exempt history.
113
108
  *
114
109
  * When several root children report a seam in the same frame, the topmost
115
110
  * one (and its commit-safe / snapshot-safe extension) defines the boundary:
@@ -120,7 +115,6 @@ export interface NativeScrollbackLiveRegion {
120
115
  getNativeScrollbackLiveRegionStart(): number | undefined;
121
116
  getNativeScrollbackCommitSafeEnd?(): number | undefined;
122
117
  getNativeScrollbackSnapshotSafeEnd?(): number | undefined;
123
- getNativeScrollbackOfferSafeEnd?(): number | undefined;
124
118
  }
125
119
  export interface NativeScrollbackCommittedRows {
126
120
  setNativeScrollbackCommittedRows(rows: number): 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": "16.3.3",
4
+ "version": "16.3.5",
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.3",
41
- "@oh-my-pi/pi-utils": "16.3.3",
40
+ "@oh-my-pi/pi-natives": "16.3.5",
41
+ "@oh-my-pi/pi-utils": "16.3.5",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.5"
44
44
  },
package/src/tui.ts CHANGED
@@ -212,11 +212,6 @@ export interface OverlayFocusOwner {
212
212
  * dropped row or an audit re-anchor spray. Provisional live blocks (collapsing
213
213
  * tool/edit previews whose head is a throwaway tail window) omit it. Defaults to
214
214
  * `commitSafeEnd ?? liveRegionStart` when absent.
215
- * `getNativeScrollbackOfferSafeEnd` optionally reports the deepest prefix row
216
- * that may physically enter native scrollback while still remaining audited.
217
- * This is for finalized lower siblings under a live block: the rows may scroll
218
- * off, but a later live-block insertion above them must trigger repair instead
219
- * of becoming durable audit-exempt history.
220
215
  *
221
216
  * When several root children report a seam in the same frame, the topmost
222
217
  * one (and its commit-safe / snapshot-safe extension) defines the boundary:
@@ -227,7 +222,6 @@ export interface NativeScrollbackLiveRegion {
227
222
  getNativeScrollbackLiveRegionStart(): number | undefined;
228
223
  getNativeScrollbackCommitSafeEnd?(): number | undefined;
229
224
  getNativeScrollbackSnapshotSafeEnd?(): number | undefined;
230
- getNativeScrollbackOfferSafeEnd?(): number | undefined;
231
225
  }
232
226
 
233
227
  export interface NativeScrollbackCommittedRows {
@@ -257,10 +251,6 @@ function getNativeScrollbackSnapshotSafeEnd(component: Component): number | unde
257
251
  return (component as Component & Partial<NativeScrollbackLiveRegion>).getNativeScrollbackSnapshotSafeEnd?.();
258
252
  }
259
253
 
260
- function getNativeScrollbackOfferSafeEnd(component: Component): number | undefined {
261
- return (component as Component & Partial<NativeScrollbackLiveRegion>).getNativeScrollbackOfferSafeEnd?.();
262
- }
263
-
264
254
  /**
265
255
  * Opt-in stability report for components that mutate their returned render
266
256
  * array in place across frames (instead of returning a fresh array per
@@ -638,7 +628,6 @@ interface FrameSegment {
638
628
  liveLocalStart?: number;
639
629
  commitLocalEnd?: number;
640
630
  snapshotLocalEnd?: number;
641
- offerLocalEnd?: number;
642
631
  }
643
632
 
644
633
  /** Depth-first identity search through `Container`-shaped children. */
@@ -1020,27 +1009,23 @@ export class TUI extends Container {
1020
1009
  // #auditCommittedPrefix). Holds references to component-cached strings, so
1021
1010
  // the audit is a pointer walk in the common case.
1022
1011
  #committedPrefix: string[] = [];
1023
- // The committed prefix [0, committedRows) splits into four zones by three
1024
- // monotone marks auditRows ≤ durableRows ≤ offerRows ≤ committedRows:
1012
+ // The committed prefix [0, committedRows) splits into three audit zones by
1013
+ // two monotone marks auditRows ≤ durableRows ≤ committedRows:
1025
1014
  // [0, auditRows) BYTE-STABLE — audited (re-anchor on any shift).
1026
1015
  // [auditRows, durableRows) DURABLE snapshot — exempt: rows may drift in
1027
1016
  // place (a streaming table widening) without re-anchoring, so their
1028
1017
  // expected drift never sprays duplicate snapshots.
1029
- // [durableRows, offerRows) OFFERED — audited: rows were allowed into
1030
- // native scrollback while a live block above could still shift them.
1031
- // A mismatch here requires a destructive replay, not a duplicate tail.
1032
- // [offerRows, committedRows) FORCED-overflow — audited: rows committed
1018
+ // [durableRows, committedRows) FORCED-overflow — audited: rows committed
1033
1019
  // only because they scrolled above the window under a commit-unstable
1034
1020
  // barrier; auditing them re-anchors (duplication, never loss) when the
1035
1021
  // barrier later shifts/finalizes/removes, instead of stranding a stale
1036
1022
  // prefix that silently drops the rows beneath it.
1037
- // Marks re-base on a wholesale re-slice (full paint / shrink / geometry) and
1038
- // otherwise advance per the persistence rules in #updateCommittedAuditRows.
1023
+ // Both marks re-base on a wholesale re-slice (full paint / shrink / geometry)
1024
+ // and otherwise advance per the persistence rules in #updateCommittedAuditRows.
1039
1025
  // #auditCommittedPrefix audits [0, committedRows) skipping the exempt window
1040
1026
  // [auditRows, durableRows).
1041
1027
  #committedPrefixAuditRows = 0;
1042
1028
  #committedPrefixDurableRows = 0;
1043
- #committedPrefixOfferRows = 0;
1044
1029
  // Frame row currently mapped to screen row 0. Monotonic between full
1045
1030
  // paints: a shrink never re-exposes scrolled-off rows (they cannot be
1046
1031
  // un-scrolled without rewriting history); live rows repaint at fixed
@@ -1051,7 +1036,6 @@ export class TUI extends Container {
1051
1036
  #nativeScrollbackLiveRegionStart: number | undefined;
1052
1037
  #nativeScrollbackCommitSafeEnd: number | undefined;
1053
1038
  #nativeScrollbackSnapshotSafeEnd: number | undefined;
1054
- #nativeScrollbackOfferSafeEnd: number | undefined;
1055
1039
  #fullRedrawCount = 0;
1056
1040
  // Caps how many inline images render as live graphics; older ones fall back
1057
1041
  // to text via a purge + full redraw. Cap is configured by the host app.
@@ -1171,7 +1155,6 @@ export class TUI extends Container {
1171
1155
  this.#nativeScrollbackLiveRegionStart = undefined;
1172
1156
  this.#nativeScrollbackCommitSafeEnd = undefined;
1173
1157
  this.#nativeScrollbackSnapshotSafeEnd = undefined;
1174
- this.#nativeScrollbackOfferSafeEnd = undefined;
1175
1158
  const children = this.children;
1176
1159
  const previousSegments = this.#frameSegments;
1177
1160
  const segments: FrameSegment[] = new Array(children.length);
@@ -1194,14 +1177,12 @@ export class TUI extends Container {
1194
1177
  let liveLocalStart: number | undefined;
1195
1178
  let commitLocalEnd: number | undefined;
1196
1179
  let snapshotLocalEnd: number | undefined;
1197
- let offerLocalEnd: number | undefined;
1198
1180
  let reported: number | undefined;
1199
1181
  if (reuse) {
1200
1182
  childLines = previous.lines;
1201
1183
  liveLocalStart = previous.liveLocalStart;
1202
1184
  commitLocalEnd = previous.commitLocalEnd;
1203
1185
  snapshotLocalEnd = previous.snapshotLocalEnd;
1204
- offerLocalEnd = previous.offerLocalEnd;
1205
1186
  } else {
1206
1187
  // Feed the engine's committed-row claim (from the previous frame's
1207
1188
  // emit) before rendering so the child can skip re-deriving blocks
@@ -1230,13 +1211,6 @@ export class TUI extends Container {
1230
1211
  ? Math.max(snapshotFloor, Math.min(childLines.length, Math.trunc(snapshotSafeEnd)))
1231
1212
  : childLines.length;
1232
1213
  }
1233
- const offerSafeEnd = getNativeScrollbackOfferSafeEnd(child);
1234
- if (offerSafeEnd !== undefined) {
1235
- const offerFloor = snapshotLocalEnd ?? commitLocalEnd ?? liveLocalStart;
1236
- offerLocalEnd = Number.isFinite(offerSafeEnd)
1237
- ? Math.max(offerFloor, Math.min(childLines.length, Math.trunc(offerSafeEnd)))
1238
- : childLines.length;
1239
- }
1240
1214
  }
1241
1215
  // Consume the stability report unconditionally for implementers:
1242
1216
  // reading re-bases the component's baseline to the state this
@@ -1260,9 +1234,6 @@ export class TUI extends Container {
1260
1234
  if (snapshotLocalEnd !== undefined) {
1261
1235
  this.#nativeScrollbackSnapshotSafeEnd = offset + snapshotLocalEnd;
1262
1236
  }
1263
- if (offerLocalEnd !== undefined) {
1264
- this.#nativeScrollbackOfferSafeEnd = offset + offerLocalEnd;
1265
- }
1266
1237
  }
1267
1238
  if (chainStable) {
1268
1239
  if (previous !== undefined && previous.component === child && previous.start === offset) {
@@ -1293,7 +1264,6 @@ export class TUI extends Container {
1293
1264
  liveLocalStart,
1294
1265
  commitLocalEnd,
1295
1266
  snapshotLocalEnd,
1296
- offerLocalEnd,
1297
1267
  };
1298
1268
  offset += childLines.length;
1299
1269
  }
@@ -2694,7 +2664,6 @@ export class TUI extends Container {
2694
2664
  const liveRegionStart = this.#nativeScrollbackLiveRegionStart;
2695
2665
  const commitSafeEnd = this.#nativeScrollbackCommitSafeEnd;
2696
2666
  const snapshotSafeEnd = this.#nativeScrollbackSnapshotSafeEnd;
2697
- const offerSafeEnd = this.#nativeScrollbackOfferSafeEnd;
2698
2667
 
2699
2668
  // Commit boundaries (also used by the window/commit math in section 3),
2700
2669
  // hoisted above the audit gate because the resync needs byteStableBoundary
@@ -2702,27 +2671,22 @@ export class TUI extends Container {
2702
2671
  // The commit floor is windowTop in every non-frozen path (see chunkTo), so
2703
2672
  // whatever scrolls above the window is committed — never committed nowhere
2704
2673
  // AND painted nowhere (the loss bug). The boundaries no longer gate the
2705
- // commit; they define audit and repair spans. byteStableBoundary: rows below
2674
+ // commit; they define the audit-exempt span. byteStableBoundary: rows below
2706
2675
  // it are byte-stable (never re-layout), audited. durableBoundary: rows in
2707
2676
  // [byteStableBoundary, durableBoundary) are durable — permanent on scroll-off
2708
2677
  // but may drift in place (a streaming table re-aligning), committed
2709
- // audit-EXEMPT. offerBoundary: rows in [durableBoundary, offerBoundary)
2710
- // were explicitly allowed to enter native scrollback while remaining
2711
- // audited; if they later shift, the stale physical history is repaired by a
2712
- // destructive replay instead of duplicate recommit. Rows at/beyond
2713
- // offerBoundary committed only because they scrolled above the window (a
2714
- // commit-unstable barrier over a long tail) are forced-overflow rows:
2715
- // audited, so a later shift/finalize/removal re-anchors (duplication, never
2716
- // loss) instead of stranding a stale prefix. Built on the finalized prefix
2717
- // (live-region start); the whole frame when the root reports no seam (shell
2718
- // semantics: whatever scrolls is final).
2678
+ // audit-EXEMPT. Rows at/beyond durableBoundary committed only because they
2679
+ // scrolled above the window (a commit-unstable barrier over a long tail) are
2680
+ // forced-overflow rows: audited, so a later shift/finalize/removal re-anchors
2681
+ // (duplication, never loss) instead of stranding a stale prefix. Built on the
2682
+ // finalized prefix (live-region start); the whole frame when the root reports
2683
+ // no seam (shell semantics: whatever scrolls is final).
2719
2684
  const frameLength = rawFrame.length;
2720
2685
  const byteStableBoundary = Math.max(0, Math.min(frameLength, commitSafeEnd ?? liveRegionStart ?? frameLength));
2721
2686
  const durableBoundary = Math.max(
2722
2687
  byteStableBoundary,
2723
2688
  Math.min(frameLength, snapshotSafeEnd ?? byteStableBoundary),
2724
2689
  );
2725
- const offerBoundary = Math.max(durableBoundary, Math.min(frameLength, offerSafeEnd ?? durableBoundary));
2726
2690
 
2727
2691
  // 2. Transition state captured before any emitter runs.
2728
2692
  const prevWindowTop = this.#windowTopRow;
@@ -2755,28 +2719,18 @@ export class TUI extends Container {
2755
2719
  let committedRowsResynced = false;
2756
2720
  // Audit covers [0, auditRows) and the forced suffix [durableRows,
2757
2721
  // committedRows); the durable middle [auditRows, durableRows) is exempt
2758
- // (in-place drift). Three reasons to run the audit this frame:
2722
+ // (in-place drift). Two reasons to run the audit this frame:
2759
2723
  // - the stable prefix does not cover every audited row (auditUpper); or
2760
2724
  // - a forced-overflow row this frame became durable/permanent
2761
- // (committedPrefixDurableRows < min(committed, durableBoundary)): the
2762
- // barrier above it finalized, so its committed bytes must be re-checked
2763
- // even though the stable prefix says nothing moved — a stale committed
2764
- // copy there would silently drop the row; or
2765
- // - a forced-overflow row this frame joined the offered zone
2766
- // (committedPrefixOfferRows < min(committed, offerBoundary)): a
2767
- // finalized sibling under a live block asks for destructive-replay
2768
- // repair, so a single-row shift there must be caught even if
2769
- // tail-sample tolerance would otherwise skip it.
2770
- // The hard scan in findCommittedPrefixResync covers [durableRows,
2771
- // hardAuditEnd) in full (no tail-sample miss).
2725
+ // (committedPrefixDurableRows < hardAuditEnd): the barrier above it
2726
+ // finalized, so its committed bytes must be re-checked even though the
2727
+ // stable prefix says nothing moved — a stale committed copy there would
2728
+ // silently drop the row. The hard scan in findCommittedPrefixResync
2729
+ // covers [durableRows, hardAuditEnd) in full (no tail-sample miss).
2772
2730
  const auditUpper =
2773
2731
  this.#committedPrefixDurableRows < this.#committedRows ? this.#committedRows : this.#committedPrefixAuditRows;
2774
- const durableHardEnd = Math.min(this.#committedRows, durableBoundary);
2775
- const offerHardEnd = Math.min(this.#committedRows, offerBoundary);
2776
- const hardAuditEnd = Math.max(durableHardEnd, offerHardEnd);
2777
- const needHardAudit =
2778
- this.#committedPrefixDurableRows < durableHardEnd || this.#committedPrefixOfferRows < offerHardEnd;
2779
- let repairOfferedScrollback = false;
2732
+ const hardAuditEnd = Math.min(this.#committedRows, durableBoundary);
2733
+ const needHardAudit = this.#committedPrefixDurableRows < hardAuditEnd;
2780
2734
  const auditRan =
2781
2735
  this.#hasEverRendered &&
2782
2736
  !geometryChanged &&
@@ -2784,21 +2738,14 @@ export class TUI extends Container {
2784
2738
  (this.#renderStablePrefixRows < auditUpper || needHardAudit);
2785
2739
  if (auditRan) {
2786
2740
  const committedRowsBeforeAudit = this.#committedRows;
2787
- const offeredRowsBeforeAudit = this.#committedPrefixOfferRows;
2788
- const durableRowsBeforeAudit = this.#committedPrefixDurableRows;
2789
- this.#auditCommittedPrefix(rawFrame, hardAuditEnd);
2741
+ this.#auditCommittedPrefix(rawFrame, durableBoundary);
2790
2742
  committedRowsResynced = this.#committedRows !== committedRowsBeforeAudit;
2791
- repairOfferedScrollback =
2792
- offeredRowsBeforeAudit > durableRowsBeforeAudit &&
2793
- committedRowsResynced &&
2794
- this.#committedRows < offeredRowsBeforeAudit;
2795
2743
  }
2796
2744
  // Committed-prefix state this frame's commit math extends from (post-audit).
2797
2745
  // Drives the audit-rows / durable-rows caps recomputed after the emit.
2798
2746
  const preCommitRows = this.#committedRows;
2799
2747
  const preCommitAuditRows = this.#committedPrefixAuditRows;
2800
2748
  const preCommitDurableRows = this.#committedPrefixDurableRows;
2801
- const preCommitOfferRows = this.#committedPrefixOfferRows;
2802
2749
 
2803
2750
  // 3. Window and commit math (lengths only; content prepared below).
2804
2751
  let hasVisibleOverlay = false;
@@ -2816,7 +2763,7 @@ export class TUI extends Container {
2816
2763
  // place, because an ED3 rewrap is unsafe (pane scrollback / alt-screen
2817
2764
  // feedback loop), so committed history keeps its old wrap.
2818
2765
  const firstPaint = !this.#hasEverRendered;
2819
- const replaceRequested = this.#clearScrollbackOnNextRender || repairOfferedScrollback;
2766
+ const replaceRequested = this.#clearScrollbackOnNextRender;
2820
2767
  const geometryRebuild = geometryChanged && !resizeRepaintsInPlace();
2821
2768
  const fullPaint = firstPaint || replaceRequested || geometryRebuild;
2822
2769
  let windowTop: number;
@@ -2928,10 +2875,8 @@ export class TUI extends Container {
2928
2875
  preCommitRows,
2929
2876
  preCommitAuditRows,
2930
2877
  preCommitDurableRows,
2931
- preCommitOfferRows,
2932
2878
  byteStableBoundary,
2933
2879
  durableBoundary,
2934
- offerBoundary,
2935
2880
  false,
2936
2881
  );
2937
2882
  this.#clearScrollbackOnNextRender = false;
@@ -2959,10 +2904,8 @@ export class TUI extends Container {
2959
2904
  preCommitRows,
2960
2905
  preCommitAuditRows,
2961
2906
  preCommitDurableRows,
2962
- preCommitOfferRows,
2963
2907
  byteStableBoundary,
2964
2908
  durableBoundary,
2965
- offerBoundary,
2966
2909
  auditRan,
2967
2910
  );
2968
2911
  }
@@ -2989,7 +2932,6 @@ export class TUI extends Container {
2989
2932
  this.#committedRows = resyncTo;
2990
2933
  this.#committedPrefixAuditRows = Math.min(this.#committedPrefixAuditRows, resyncTo);
2991
2934
  this.#committedPrefixDurableRows = Math.min(this.#committedPrefixDurableRows, resyncTo);
2992
- this.#committedPrefixOfferRows = Math.min(this.#committedPrefixOfferRows, resyncTo);
2993
2935
  prefix.length = resyncTo;
2994
2936
  if ($flag("PI_DEBUG_REDRAW")) {
2995
2937
  const msg = `[${new Date().toISOString()}] commit resync: committed prefix diverged at row ${resyncTo}; recommitting\n`;
@@ -3002,24 +2944,21 @@ export class TUI extends Container {
3002
2944
  * #committedPrefixAuditRows field doc for the three audit zones).
3003
2945
  *
3004
2946
  * auditRows tracks the byte-stable boundary; durableRows the durable snapshot
3005
- * boundary; offerRows the deepest explicit audited-offer boundary. A wholesale
3006
- * re-slice (full paint / shrink / geometry) re-bases each mark from the
3007
- * current frame. An incremental extend keeps a mark once a row past it has
3008
- * committed (mark < committed): a later RISE in a boundary (a table finalizing)
3009
- * must neither pull already-committed stale snapshots back under the
3010
- * byte-stable cap nor retroactively exempt forced-overflow rows already
3011
- * audited. durableRows is floored at auditRows; offerRows is floored at
3012
- * durableRows.
2947
+ * boundary. A wholesale re-slice (full paint / shrink / geometry) re-bases
2948
+ * each mark from the current frame (min(committed, boundary)). An incremental
2949
+ * extend keeps a mark once a row past it has committed (mark < committed): a
2950
+ * later RISE in a boundary (a table finalizing) must neither pull
2951
+ * already-committed stale snapshots back under the byte-stable cap nor
2952
+ * retroactively exempt forced-overflow rows already audited. durableRows is
2953
+ * floored at auditRows so the exempt window can never invert.
3013
2954
  */
3014
2955
  #updateCommittedAuditRows(
3015
2956
  resliced: boolean,
3016
2957
  preCommittedRows: number,
3017
2958
  preAuditRows: number,
3018
2959
  preDurableRows: number,
3019
- preOfferRows: number,
3020
2960
  byteStableBoundary: number,
3021
2961
  durableBoundary: number,
3022
- offerBoundary: number,
3023
2962
  hardAudited: boolean,
3024
2963
  ): void {
3025
2964
  const committed = this.#committedRows;
@@ -3036,18 +2975,8 @@ export class TUI extends Container {
3036
2975
  resliced || preDurableRows >= preCommittedRows || hardAudited
3037
2976
  ? Math.min(committed, durableBoundary)
3038
2977
  : Math.min(preDurableRows, committed);
3039
- // offerRows may EXTEND to include forced-overflow rows within a
3040
- // newly-visible offerBoundary — unlike auditRows/durableRows, retroactive
3041
- // promotion is safe: both offered and forced-overflow zones stay
3042
- // audited; offered only switches divergence repair from tolerant recommit
3043
- // to destructive replay (stronger, not weaker). A dropped offerBoundary
3044
- // still keeps the durability rule via `preOfferRows` (never demote
3045
- // already-offered rows to forced-overflow).
3046
- const offerCap = Math.min(committed, offerBoundary);
3047
- const offerRows = resliced ? offerCap : Math.max(Math.min(preOfferRows, committed), offerCap);
3048
2978
  this.#committedPrefixAuditRows = auditRows;
3049
2979
  this.#committedPrefixDurableRows = Math.max(auditRows, durableRows);
3050
- this.#committedPrefixOfferRows = Math.max(this.#committedPrefixDurableRows, offerRows);
3051
2980
  }
3052
2981
 
3053
2982
  /**