@oh-my-pi/pi-tui 17.4.2 → 18.0.1

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.
@@ -18,9 +18,39 @@ export interface RenderScheduler {
18
18
  export interface TUIOptions {
19
19
  renderScheduler?: RenderScheduler;
20
20
  }
21
+ /** Physical terminal dimensions supplied to a frame provider. */
22
+ export interface ViewportSize {
23
+ readonly columns: number;
24
+ readonly rows: number;
25
+ }
26
+ /** Immutable finalized rows offered until the terminal accepts this identifier. */
27
+ export interface HistoryBatch {
28
+ readonly id: number;
29
+ readonly rows: readonly string[];
30
+ }
31
+ /** One history append and the complete mutable viewport for a terminal frame. */
32
+ export interface TerminalFramePlan {
33
+ readonly history?: HistoryBatch;
34
+ readonly viewport: readonly string[];
35
+ }
36
+ /** Produces bounded terminal frames and retires acknowledged history batches. */
37
+ export interface TerminalFrameProvider {
38
+ renderFrame(viewport: ViewportSize): TerminalFramePlan;
39
+ acknowledgeHistory(id: number): void;
40
+ /** Full semantic viewport used only on the transient resize buffer. */
41
+ renderResizeFrame?(viewport: ViewportSize): readonly string[];
42
+ /** Re-offer finalized history after a display reset or resize replay. */
43
+ resetHistory?(): void;
44
+ }
21
45
  export interface TUIStartOptions {
22
46
  /** Clear saved native scrollback before the first paint. */
23
47
  clearScrollback?: boolean;
48
+ /**
49
+ * Paint without owning stdin: the terminal stays in cooked mode (kernel
50
+ * echo + line editing at the hardware cursor) until {@link TUI.enableInput}
51
+ * switches to raw input and replays the kernel-buffered keystrokes.
52
+ */
53
+ deferInput?: boolean;
24
54
  }
25
55
  /**
26
56
  * Component interface - all components must implement this
@@ -75,109 +105,6 @@ export interface OverlayFocusOwner {
75
105
  /** Returns true when `component` is a focus target inside this overlay. */
76
106
  ownsOverlayFocusTarget(component: Component): boolean;
77
107
  }
78
- /**
79
- * Component seam for append-only native-scrollback commits. A component whose
80
- * rendered rows can still change reports, after each render, the local line
81
- * index where that mutable suffix begins. Rows above the boundary are declared
82
- * FINAL — byte-stable at the current width for the component's lifetime — and
83
- * commit to native scrollback as exact, audited content. Rows at/after the
84
- * boundary repaint in place inside the visible window; when they scroll above
85
- * the window top they normally commit as frozen visual snapshots.
86
- *
87
- * A viewport-pinned region opts out of those mutable snapshot commits. Its
88
- * offscreen mutable rows are virtually clipped until the boundary advances;
89
- * use this for fixed-height dashboards whose frames replace each other rather
90
- * than append. A root that reports no seam commits everything that scrolls as
91
- * final (shell semantics).
92
- *
93
- * When several root children report a seam in the same frame, the topmost one
94
- * defines the boundary and pinning policy: commits are prefix-only, so
95
- * everything below the first seam is already excluded.
96
- */
97
- export interface NativeScrollbackLiveRegion {
98
- getNativeScrollbackLiveRegionStart(): number | undefined;
99
- /** Keeps the mutable suffix viewport-local instead of recording frozen snapshots. */
100
- isNativeScrollbackLiveRegionPinned?(): boolean;
101
- /**
102
- * Local row where viewport pinning begins. When omitted, pinning (if
103
- * reported) starts at {@link getNativeScrollbackLiveRegionStart}. A nested
104
- * transcript uses this to keep an earlier unpinned live seam while still
105
- * capping commits at a later pinned dashboard (hub wait, todo snapshot).
106
- */
107
- getNativeScrollbackLiveRegionPinnedStart?(): number | undefined;
108
- }
109
- export interface NativeScrollbackCommittedRows {
110
- setNativeScrollbackCommittedRows(rows: number): void;
111
- }
112
- /**
113
- * Width-independent source boundary for multiplexer resize epochs. Capture
114
- * reads the last rendered source state; resolve maps that same logical boundary
115
- * into the most recent render's physical rows at its new width. The current
116
- * boundary identifies the source tail after updates queued during the resize.
117
- */
118
- export interface NativeScrollbackWidthEpoch {
119
- captureNativeScrollbackWidthEpoch(): unknown;
120
- resolveNativeScrollbackWidthEpoch(boundary: unknown): number | undefined;
121
- getNativeScrollbackWidthEpochRows(): number | undefined;
122
- /** False when updates can insert before captured trailing rows. */
123
- isNativeScrollbackWidthEpochAppendOnly?(boundary: unknown): boolean;
124
- /** Changes when child structure mutates independently of width reflow. */
125
- getNativeScrollbackWidthEpochRevision?(): number;
126
- }
127
- /**
128
- * A component that discards rows after they enter native scrollback implements
129
- * this hook so a destructive full replay can rehydrate its complete frame.
130
- */
131
- export interface NativeScrollbackReplay {
132
- prepareNativeScrollbackReplay(): void;
133
- }
134
- /**
135
- * Opt-in stability report for components that mutate their returned render
136
- * array in place across frames (instead of returning a fresh array per
137
- * change). The engine reads it right after the component's `render()` returns:
138
- * the report counts the leading rows of the just-returned array that are
139
- * byte-identical to the array state the reader last observed. The engine uses
140
- * it to reuse the composed frame's prefix — skipping marker extraction, line
141
- * preparation, and the committed-prefix audit for those rows.
142
- *
143
- * Contract:
144
- * - Reading CONSUMES the report: it re-bases the baseline to the current
145
- * array state. The accumulated count therefore covers every render since
146
- * the previous read, so out-of-band `render()` calls between engine frames
147
- * (an exporter walking the tree) can only lower the report, never inflate
148
- * it past what the engine actually has.
149
- * - An implementer that cannot prove stability for a frame must lower the
150
- * accumulated count to 0 for that render.
151
- * - Rows at or beyond the report may have been mutated in place; rows before
152
- * it must be the identical string values at the identical indices.
153
- */
154
- export interface RenderStablePrefix {
155
- getRenderStablePrefixRows(): number;
156
- }
157
- /**
158
- * Opt-in fast path for composing only the visible tail of a tall component
159
- * during a terminal resize. A drag emits a SIGWINCH burst, and the width
160
- * changes on every event: a full compose re-lays-out (and, for markdown,
161
- * re-lexes) the entire transcript per event — O(history) work that is
162
- * discarded the instant the next event arrives. While the resize is in flight
163
- * the engine paints only the viewport, so it asks each tall root child for at
164
- * most `maxRows` rows from the bottom of its render at `width` and skips
165
- * composing everything above the fold. The authoritative full paint replays
166
- * once the drag settles (see {@link TUI} resize handling).
167
- *
168
- * Contract:
169
- * - Returns the BOTTOM rows of the component's full render at `width`, in
170
- * top-to-bottom order, capped at `maxRows` (fewer when the component is
171
- * shorter). The rows MUST be byte-identical to the corresponding tail of
172
- * what `render(width)` would have returned, modulo a one-row separator at
173
- * the very top edge (a transient frame the settle paint overwrites).
174
- * - MUST NOT mutate any persistent full-compose state: the next `render()`
175
- * (the settle paint) has to reconcile exactly as if the tail render never
176
- * happened. Warming pure per-width render caches is fine and desirable.
177
- */
178
- export interface ViewportTailProvider {
179
- renderViewportTail(width: number, maxRows: number): readonly string[];
180
- }
181
108
  /**
182
109
  * Interface for components that can receive focus and display a cursor.
183
110
  * When focused, the component should emit CURSOR_MARKER at the cursor position
@@ -200,6 +127,13 @@ export interface RenderRequestOptions {
200
127
  /** Clear terminal scrollback for intentional transcript replacement. */
201
128
  clearScrollback?: boolean;
202
129
  }
130
+ /**
131
+ * Controls how a settled terminal resize refreshes native history.
132
+ *
133
+ * `append` replays the current transcript below retained history, `rebuild`
134
+ * clears history before replaying it, and `preserve` repaints only the viewport.
135
+ */
136
+ export type ResizeScrollbackMode = "append" | "rebuild" | "preserve";
203
137
  /** Type guard to check if a component implements Focusable */
204
138
  export declare function isFocusable(component: Component | null): component is Component & Focusable;
205
139
  /**
@@ -283,7 +217,7 @@ export interface OverlayHandle {
283
217
  /**
284
218
  * Container - a component that contains other components
285
219
  */
286
- export declare class Container implements Component, NativeScrollbackCommittedRows, NativeScrollbackReplay, NativeScrollbackWidthEpoch {
220
+ export declare class Container implements Component {
287
221
  #private;
288
222
  children: Component[];
289
223
  setIgnoreTight(ignore: boolean): this;
@@ -299,21 +233,6 @@ export declare class Container implements Component, NativeScrollbackCommittedRo
299
233
  * {@link clear} for that). Idempotent per child via each child's own dispose.
300
234
  */
301
235
  dispose(): void;
302
- /**
303
- * Split the committed prefix from the container's most recently rendered
304
- * rows across its children. The memoized child arrays are the exact geometry
305
- * that produced that frame; when the child list was invalidated or rebuilt,
306
- * there is no safe old-to-new coordinate mapping, so propagation waits for
307
- * the next render/post-emit publication.
308
- */
309
- setNativeScrollbackCommittedRows(rows: number): void;
310
- /** Recursively discard layout locks that are meaningful only to the old tape. */
311
- prepareNativeScrollbackReplay(): void;
312
- captureNativeScrollbackWidthEpoch(): unknown;
313
- resolveNativeScrollbackWidthEpoch(boundary: unknown): number | undefined;
314
- getNativeScrollbackWidthEpochRows(): number | undefined;
315
- isNativeScrollbackWidthEpochAppendOnly(boundary: unknown): boolean;
316
- getNativeScrollbackWidthEpochRevision(): number;
317
236
  render(width: number): readonly string[];
318
237
  }
319
238
  /**
@@ -323,41 +242,6 @@ export declare class Container implements Component, NativeScrollbackCommittedRo
323
242
  * merges, so SGR-light lines incur only a single `indexOf` scan.
324
243
  */
325
244
  export declare function coalesceAdjacentSgr(line: string): string;
326
- /**
327
- * Decide whether `frame` still aligns with the committed prefix, and where to
328
- * re-anchor the commit index when it does not. Returns the resync row index,
329
- * or -1 when no resync is needed.
330
- *
331
- * Zones (verifiedTo ≤ finalTo ≤ prefix.length):
332
- * [0, verifiedTo) VERIFIED exact rows — sampled with tolerance.
333
- * [verifiedTo, finalTo) NEWLY-FINAL rows — frozen visual snapshots whose
334
- * source just became declared-final (the block finalized / a barrier
335
- * cleared). Hard-scanned in FULL with no tolerance: any content change
336
- * (a pending header settling, a preview replaced by its result, a tail
337
- * shifting up after a barrier removal) re-anchors so the engine can
338
- * erase-and-replay history with the final content exactly once (or, on
339
- * ED3-unsafe multiplexers, recommit it below the frozen snapshot —
340
- * duplication, never loss) instead of committing it nowhere and
341
- * painting it nowhere.
342
- * [finalTo, prefix.length) FROZEN visual snapshots of still-live rows —
343
- * exempt: their drift is expected (a collapsing preview, a ticking
344
- * progress tree) and must never spray re-anchors mid-run.
345
- *
346
- * The verified zone's sampled check exploits the asymmetry between the two
347
- * mutation classes: an in-place edit/restyle disturbs only the touched rows
348
- * (alignment below stays intact; the stale copy in history is the accepted
349
- * artifact), while an insertion/deletion shifts EVERY row below it. Up to 8
350
- * non-blank rows within the last 24 verified rows are compared SGR-stripped
351
- * (theme changes stay quiet), tolerating a SINGLE mismatch. The tolerance is
352
- * load-bearing for roots that report NO seam: an animated row already in
353
- * history would otherwise re-anchor on every glyph tick.
354
- *
355
- * Highly repetitive tails (identical filler rows) can mask a shift in the tail
356
- * sample, in which case the skipped rows are content-identical to the committed
357
- * ones — observationally harmless. Exported for the render-stress harness, whose
358
- * shadow commit ledger must mirror the engine's law exactly.
359
- */
360
- export declare function findCommittedPrefixResync(frame: readonly string[], prefix: readonly string[], verifiedTo?: number, finalTo?: number): number;
361
245
  /**
362
246
  * TUI - Main class for managing terminal UI with differential rendering
363
247
  */
@@ -373,19 +257,9 @@ export declare class TUI extends Container {
373
257
  hidden: boolean;
374
258
  }[];
375
259
  constructor(terminal: Terminal, showHardwareCursor?: boolean, options?: TUIOptions);
376
- captureNativeScrollbackWidthEpoch(): unknown;
377
- resolveNativeScrollbackWidthEpoch(boundary: unknown): number | undefined;
378
- getNativeScrollbackWidthEpochRows(): number | undefined;
379
- render(width: number): readonly string[];
260
+ /** Install the product-owned bounded frame provider. */
261
+ setFrameProvider(provider: TerminalFrameProvider | undefined): void;
380
262
  get fullRedraws(): number;
381
- /**
382
- * Transient viewport-only paints emitted by the non-multiplexer resize fast
383
- * path. These never touch native scrollback or the commit ledger, so they
384
- * are counted apart from {@link fullRedraws}.
385
- */
386
- get resizeViewportPaints(): number;
387
- /** Whether a non-multiplexer resize drag is currently in flight. */
388
- get resizeViewportActive(): boolean;
389
263
  /** Shared budget that caps how many inline images render as live graphics. */
390
264
  get imageBudget(): ImageBudget;
391
265
  /**
@@ -394,19 +268,12 @@ export declare class TUI extends Container {
394
268
  * plus a full redraw on the frame after a new image exceeds the cap.
395
269
  */
396
270
  setMaxInlineImages(cap: number): void;
271
+ /** Return how settled resizes refresh native scrollback. */
272
+ getResizeScrollback(): ResizeScrollbackMode;
273
+ /** Set how settled resizes refresh native scrollback. */
274
+ setResizeScrollback(mode: ResizeScrollbackMode): void;
397
275
  /** Delete every tracked Kitty image from the terminal graphics store. */
398
276
  clearInlineImages(): void;
399
- /**
400
- * Get whether scrollback divergence rebuild is enabled.
401
- */
402
- getScrollbackRebuild(): boolean;
403
- /**
404
- * Enable or disable scrollback divergence rebuild (default off).
405
- * When enabled, the engine will erase and replay the terminal's
406
- * scrollback (using ED3 / alt buffer / scrollback replay) to avoid
407
- * duplicate blocks when a block's final form replaces its live preview.
408
- */
409
- setScrollbackRebuild(enabled: boolean): void;
410
277
  getShowHardwareCursor(): boolean;
411
278
  setShowHardwareCursor(enabled: boolean): void;
412
279
  /**
@@ -430,58 +297,36 @@ export declare class TUI extends Container {
430
297
  hasOverlay(): boolean;
431
298
  invalidate(): void;
432
299
  start(options?: TUIStartOptions): void;
300
+ /**
301
+ * Take ownership of stdin after a `deferInput` start: raw mode, input
302
+ * handlers, and the response-eliciting capability probes start() skipped.
303
+ * Keystrokes typed in cooked mode meanwhile arrive through the normal input
304
+ * path. Idempotent; no-op when input was never deferred.
305
+ */
306
+ enableInput(): void;
433
307
  addStartListener(listener: StartListener): () => void;
434
308
  addInputListener(listener: InputListener): () => void;
435
309
  removeInputListener(listener: InputListener): void;
436
310
  stop(): void;
437
311
  /**
438
- * Force an immediate full replay of the current frame, including native
439
- * scrollback. This is the keyboard-accessible equivalent of the resize reset:
440
- * no queued diff frame or terminal scrollback probe can downgrade it to a
441
- * viewport-only repaint.
442
- *
443
- * Invalidates every component first so the replay reflects current state. A
444
- * geometry-driven reset thaws frozen scrollback snapshots implicitly (the new
445
- * width misses every cached snapshot), but a same-width reset would otherwise
446
- * replay stale snapshots — leaving host-frozen blocks (e.g. a transcript whose
447
- * committed rows are immutable on ED3-risk terminals) showing pre-mutation
448
- * content. Invalidation is the generic signal those containers use to retire
449
- * their snapshots, which is exactly what a user-driven display reset wants.
312
+ * Destructive user-gesture reset: invalidate every component, erase native
313
+ * history, then repaint from row zero. Reachable only from explicit gestures (session
314
+ * replace, /tree, an explicit clear) — never from ordinary rendering,
315
+ * animation, resize, or finalization.
450
316
  */
451
317
  resetDisplay(): void;
452
318
  requestRender(force?: boolean, options?: RenderRequestOptions): void;
453
319
  /**
454
- * Opt `component` into subtree-only renders when input leaves focus stable.
455
- *
456
- * The host must explicitly request renders for every sibling mutated by the
457
- * component's input callbacks. Components without this opt-in retain the
458
- * legacy full-root render after input.
320
+ * Paint a forced frame synchronously when startup must hand off an already
321
+ * visible component tree before further async initialization. Same as
322
+ * {@link requestRender} minus the `setImmediate` hop.
459
323
  */
460
- enableScopedInputRender(component: Component): void;
324
+ renderNow(options?: RenderRequestOptions): void;
461
325
  /**
462
326
  * Schedule a render on behalf of `component` after a self-contained change
463
- * (spinner frame, blink) that cannot have affected any other component.
464
- *
465
- * When every request since the last frame is component-scoped and the
466
- * frame is otherwise quiet — no resize or geometry change, no overlays, no
467
- * live inline images, no forced repaint, unchanged root child list — the
468
- * next compose re-renders only the root subtrees containing the requesting
469
- * components and reuses the previous frame's rows (and seam reports) for
470
- * every other root child, skipping the full component-tree walk that makes
471
- * long transcripts expensive to repaint at animation rate. Any concurrent
472
- * full request or unsafe condition downgrades the frame to a normal full
473
- * compose, so this is never less correct than `requestRender()` — only
474
- * cheaper.
475
- */
476
- requestComponentRender(component: Component): void;
477
- /**
478
- * Rewrite a quiet, visible component segment directly.
479
- *
480
- * Loader-style animation changes one already-positioned segment at a fixed
481
- * size. When the current frame geometry is still valid, rewrite just those
482
- * rows and update the diff baseline instead of scheduling a full render
483
- * cycle. Unsafe states fall back to `requestComponentRender()`, preserving
484
- * the ordinary renderer as the correctness path.
327
+ * (spinner frame, blink). Frames always compose the bounded viewport from
328
+ * scratch — retired blocks no longer render — so a scoped request is simply
329
+ * an ordinary render.
485
330
  */
486
- requestDirectWrite(component: Component): void;
331
+ requestComponentRender(_component: Component): void;
487
332
  }
@@ -42,8 +42,9 @@ export declare function getSegmenter(): Intl.Segmenter;
42
42
  * Visible width of a string in terminal columns, excluding ANSI/OSC escapes.
43
43
  *
44
44
  * `Bun.stringWidth` does the heavy lifting (UAX#11 width tables + ANSI/OSC
45
- * stripping); this adds the two corrections it omits — tabs (expanded to
46
- * `tabWidth` cells) and OSC 66 text-sizing payloads (scaled by `s=`).
45
+ * stripping); this adds the corrections it omits — tabs (expanded to
46
+ * `tabWidth` cells), OSC 66 text-sizing payloads (scaled by `s=`), and APC
47
+ * sequences (counted as printable by Bun, actually zero cells).
47
48
  */
48
49
  export declare function visibleWidth(str: string): number;
49
50
  /**
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.4.2",
4
+ "version": "18.0.1",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -37,11 +37,11 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "17.4.2",
41
- "@oh-my-pi/pi-utils": "17.4.2"
40
+ "@oh-my-pi/pi-natives": "18.0.1",
41
+ "@oh-my-pi/pi-utils": "18.0.1"
42
42
  },
43
43
  "devDependencies": {
44
- "ghostty-web": "^0.4.0"
44
+ "kitty-vt-wasm": "^0.2.0"
45
45
  },
46
46
  "engines": {
47
47
  "bun": ">=1.3.14"