@oh-my-pi/pi-tui 15.9.67 → 15.10.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,72 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.10.1] - 2026-06-07
6
+ ### Breaking Changes
7
+
8
+ - Removed Kitty temp-file image transmission, its startup support probe, the `PI_KITTY_IMAGE_TRANSMISSION` override, and the temp-file helper exports. Kitty/Ghostty image payloads now stay on in-band base64 before placeholder/direct placement, avoiding blank first renders from temp-file load races.
9
+ - Renamed `RenderRequestOptions.allowUnknownViewportMutation` → `allowUnknownViewportTransientRepaint`. The option only permits a transient live-viewport repaint (autocomplete/IME/focused-editor chrome) on hosts that cannot report viewport position; it never authorizes a settled transcript commit. The old name implied any offscreen mutation was safe to push into native scrollback, which led callers to emit duplicate transcript copies.
10
+
11
+ ### Added
12
+
13
+ - Added `TUI.addStartListener()` so feature hooks can re-enable terminal modes after temporary stop/start cycles such as external-editor handoffs.
14
+ - Added `Editor.pasteText()` to apply terminal-style paste handling for text inserted from non-bracketed paste transports
15
+ - Added an optional `dispose()` lifecycle method to `Component` so components can release timers and subscriptions during permanent teardown
16
+ - Added `Container.dispose()` to propagate teardown to child components when a component tree is permanently discarded
17
+ - Added `Loader.dispose()` to stop the loader animation timer when the component is disposed
18
+ - Added a `ScrollView` `ellipsis` option (defaults to `Ellipsis.Unicode`) so callers that pre-wrap content to width can pass `Ellipsis.Omit` and suppress the stray per-line `…` that lands on trailing padding.
19
+ - Added `ScrollView.handleScrollKey()` plus a `fastScrollLines` option so every scroll view gets shared navigation keys, including Shift+Arrow to scroll faster.
20
+ - Added `OverlayOptions.fullscreen`: while the topmost visible overlay sets it, the engine borrows the terminal's alternate screen buffer for the overlay's lifetime and paints only the modal there — no ED3, no transcript re-commit — so the transcript stays untouched on the normal screen and is not scrollable behind the modal. Mouse tracking (`?1000h`/`?1006h`) is enabled for the modal's lifetime and disabled on exit, so the rest of the app keeps the terminal's native text selection.
21
+ - Added the `submitPinsViewportToTail` terminal capability and `detectSubmitPinsViewportToTail()`: genuine local terminals where a submit keystroke scrolls the host to its tail reconcile deferred native scrollback at the prompt-submit checkpoint even when the viewport position is unprobeable (Ghostty/kitty/iTerm/WezTerm/Alacritty). Restores the pre-regression submit reconciliation without re-enabling it for Windows Terminal/ConPTY, SSH, or multiplexers, where a submit is not proof the host is at the tail.
22
+
23
+ ### Changed
24
+
25
+ - Changed static `Loader` messages to repaint only at the spinner's 80 ms cadence; time-dependent message colorizers can opt into 16 ms redraws with `animated: true`.
26
+ - Changed keybinding matching to precompute canonical key sets so each input sequence is parsed once per binding check instead of once per candidate key.
27
+ - Made `Component.invalidate()` optional so leaf components without render caches no longer need no-op invalidation hooks.
28
+ - `TERMINAL` is now a `RuntimeTerminal` whose post-construction capabilities (image protocol and the probe-driven flags) are writable, replacing the `as unknown as MutableTerminalInfo` cast pattern and the positional `withTerminalOverrides` rebuild with a prototype-preserving `clone()`.
29
+
30
+ ### Fixed
31
+
32
+ - Fixed `Loader` text updates to skip identical messages and preserve the rendered `Text` cache instead of invalidating it every timer tick.
33
+
34
+ - Fixed fullscreen overlay alt-frame rendering to reuse the current line-preparation path instead of calling removed fitting helpers.
35
+ - Reduced TUI render-path line fitting by deferring overlay base-frame fitting until an overlay rebuild and by reusing already-fitted lines in emitters.
36
+ - Reduced live-region pinned repaint output by diffing unchanged viewport rows when no sealed rows are being committed to native scrollback.
37
+ - Fixed no-append live-region pinned repaints to re-anchor the hardware cursor when the logical viewport shifts.
38
+ - Fixed keybinding matching so printable uppercase input preserves `Shift` for bindings such as `shift+a`.
39
+ - Optimized terminal image-line detection and Thai/Lao AM normalization checks to avoid hot-path regex scans and substring allocations.
40
+ - Fixed `Markdown.render()` cache hits returning the cache's mutable backing array, which let callers that append extra rows corrupt cached Markdown and duplicate those rows on every redraw.
41
+ - Fixed first-paint full replays for callers that intentionally replace terminal history by allowing `TUI.start({ clearScrollback: true })`, so they do not briefly append an entire initial frame before the first clean replay.
42
+ - Fixed ED3-risk streaming cap accounting to preserve the native scrollback high-water mark for rows that were already physically committed before transient frames were viewport-capped.
43
+ - Fixed terminal stop and restore cleanup to disable enhanced paste mode so it does not remain enabled after shutdown
44
+ - Removed the per-frame line-fit `Map` cache from the render timer path to avoid forcing JSC rope-string hashing during scheduled viewport repaints.
45
+ - Fixed `visibleWidth()` so terminal column measurements for ANSI and OSC text now match the native truncation/wrapping helpers, including OSC 66 text-sizing spans being counted at their scaled payload width
46
+ - Fixed cursor, padding, and line-fit behavior when strings contain tabs or OSC escapes by aligning `visibleWidth()` with the native text-width model
47
+ - Fixed the transcript — or a re-appearing prior view such as the welcome screen — duplicating itself on terminals without a scroll-position oracle (Ghostty/kitty/iTerm/WezTerm) when a foreground tool completes by rewriting a partly-committed block, or when the transcript is reset. A non-destructive viewport repaint no longer re-paints rows that are byte-identical to what is already committed to native scrollback into the active grid; the repaint anchor is clamped to the committed-and-unchanged prefix (`min(firstChanged, scrollbackHighWater)`).
48
+
49
+ ## [15.10.0] - 2026-06-06
50
+
51
+ ### Changed
52
+
53
+ - Reworked the DEC 2026 synchronized-output default policy: a positive DECRQM mode-2026 report now **enables** sync (previously a report could only disable it), so conservatively defaulted-off hosts that actually support it — current Zellij, tmux master, foot, contour, mintty — are upgraded at runtime. The static allowlist also covers Alacritty and the VS Code terminal, honors a `TERM_FEATURES` `Sy` advertisement and `WT_SESSION` (Windows Terminal / WSL), and no longer blanket-disables SSH (DEC 2026 passes through to the outer terminal). Risky multiplexers still start off and rely on the probe. Added `synchronizedOutputUserOverride()` as the shared opt-out/force resolver.
54
+
55
+ ### Fixed
56
+
57
+ - Fixed WSL/Windows Terminal row flicker while typing by repainting changed text rows before clearing only their stale suffix ([#2011](https://github.com/can1357/oh-my-pi/issues/2011)).
58
+ - Fixed terminals that support DEC 2026 still tearing/flickering because the renderer ignored a positive DECRQM capability report and kept synchronized output off — most visibly WSL + Windows Terminal, Alacritty (≥0.13), and the VS Code terminal (≥1.108), which were detected yet refused sync.
59
+
60
+ ## [15.9.69] - 2026-06-06
61
+
62
+ ### Added
63
+
64
+ - Added `TUI.resetDisplay()` to force an immediate full-frame replay, including native scrollback when the host can safely clear it.
65
+ - Added `setPaddingY` to `Box` so vertical padding can be updated programmatically after creation.
66
+
67
+ ### Fixed
68
+
69
+ - Fixed DECCARA background-fill optimization running when synchronized output is disabled, which could expose default-background gaps during rapidly updating tool-use panels ([#2000](https://github.com/can1357/oh-my-pi/issues/2000)).
70
+
5
71
  ## [15.9.67] - 2026-06-06
6
72
  ### Added
7
73
 
@@ -10,6 +10,7 @@ export declare class Box implements Component {
10
10
  removeChild(component: Component): void;
11
11
  clear(): void;
12
12
  setPaddingX(paddingX: number): void;
13
+ setPaddingY(paddingY: number): void;
13
14
  setBgFn(bgFn?: (text: string) => string): void;
14
15
  invalidate(): void;
15
16
  render(width: number): string[];
@@ -100,6 +100,8 @@ export declare class Editor implements Component, Focusable {
100
100
  setText(text: string): void;
101
101
  /** Insert text at the current cursor position */
102
102
  insertText(text: string): void;
103
+ /** Apply terminal paste semantics to text from non-bracketed paste transports. */
104
+ pasteText(text: string): void;
103
105
  isShowingAutocomplete(): boolean;
104
106
  }
105
107
  export {};
@@ -1,13 +1,20 @@
1
1
  import type { TUI } from "../tui";
2
2
  import { Text } from "./text";
3
+ type ColorFn = (str: string) => string;
4
+ export type LoaderMessageColorFn = ColorFn & {
5
+ readonly animated?: true;
6
+ };
3
7
  export declare class Loader extends Text {
4
8
  #private;
5
9
  private spinnerColorFn;
6
10
  private messageColorFn;
7
11
  private message;
8
- constructor(ui: TUI, spinnerColorFn: (str: string) => string, messageColorFn: (str: string) => string, message?: string, spinnerFrames?: string[]);
12
+ constructor(ui: TUI, spinnerColorFn: ColorFn, messageColorFn: LoaderMessageColorFn, message?: string, spinnerFrames?: string[]);
9
13
  render(width: number): string[];
10
14
  start(): void;
11
15
  stop(): void;
16
+ /** Lifecycle teardown: stop the animation timer. Idempotent. */
17
+ dispose(): void;
12
18
  setMessage(message: string): void;
13
19
  }
20
+ export {};
@@ -1,4 +1,5 @@
1
1
  import type { Component } from "../tui";
2
+ import { Ellipsis } from "../utils";
2
3
  type ScrollbarMode = "auto" | "always" | "never";
3
4
  export interface ScrollViewTheme {
4
5
  track?: (text: string) => string;
@@ -13,6 +14,18 @@ export interface ScrollViewOptions {
13
14
  theme?: ScrollViewTheme;
14
15
  trackChar?: string;
15
16
  thumbChar?: string;
17
+ /**
18
+ * Indicator appended when a row overflows `contentWidth`. Defaults to
19
+ * {@link Ellipsis.Unicode}. Pass {@link Ellipsis.Omit} when callers wrap
20
+ * lines to width themselves and only trailing padding can overflow (e.g.
21
+ * the plan-review overlay), so no stray `…` lands on every padded row.
22
+ */
23
+ ellipsis?: Ellipsis;
24
+ /**
25
+ * Rows moved per keystroke when {@link ScrollView.handleScrollKey} sees a
26
+ * Shift+Arrow (the "scroll faster" affordance). Defaults to 5.
27
+ */
28
+ fastScrollLines?: number;
16
29
  }
17
30
  /**
18
31
  * Fixed-height viewport over pre-rendered lines, with optional right-edge scrollbar.
@@ -34,6 +47,15 @@ export declare class ScrollView implements Component {
34
47
  page(delta: number): void;
35
48
  scrollToTop(): void;
36
49
  scrollToBottom(): void;
50
+ /**
51
+ * Apply a standard navigation key to the viewport. Shift+Arrow scrolls by
52
+ * {@link ScrollViewOptions.fastScrollLines} (the "scroll faster" affordance);
53
+ * plain Arrow by one line; PageUp/PageDown by a page; Home/End to the ends.
54
+ * Returns true when the key was consumed, so callers can fall through to
55
+ * their own (e.g. vim-style) bindings. Generic on purpose: every ScrollView
56
+ * consumer gets the same scroll keys, including Shift-to-go-faster.
57
+ */
58
+ handleScrollKey(data: string): boolean;
37
59
  invalidate(): void;
38
60
  render(width: number): string[];
39
61
  }
@@ -1,13 +1,26 @@
1
+ /**
2
+ * Kitty graphics: Unicode placeholder placement (`U=1` + U+10EEEE), with
3
+ * runtime feature state and env overrides.
4
+ *
5
+ * Unicode placeholders let a transmitted image be displayed by writing ordinary
6
+ * text cells — the placeholder char U+10EEEE plus row/column combining
7
+ * diacritics — instead of a cursor-positioned `a=p` direct placement. The image
8
+ * then participates in the normal text grid, so it survives horizontal slicing,
9
+ * reflow and overlapping draws (each cell names its own row+column, so a sliced
10
+ * row still maps to the correct sub-region). See kitty
11
+ * `docs/graphics-protocol.rst` "Unicode placeholders for relative placements".
12
+ *
13
+ * This module is intentionally free of `./terminal-capabilities` imports so the
14
+ * dependency stays one-way (capabilities → kitty-graphics) and no import cycle
15
+ * forms. Protocol gating (`imageProtocol === Kitty`) lives in the caller.
16
+ */
1
17
  /** Kitty Unicode placeholder base character (U+10EEEE, Plane 16 PUA). */
2
18
  export declare const KITTY_PLACEHOLDER = "\uDBFB\uDEEE";
3
19
  /** Largest row/column index expressible with the diacritic table (one cell each). */
4
20
  export declare const KITTY_PLACEHOLDER_MAX_CELLS: number;
5
- export type KittyTransmissionMedium = "direct" | "temp-file";
6
21
  export interface KittyGraphicsFeatures {
7
22
  /** Display images via Unicode placeholders instead of direct `a=p` placement. */
8
23
  unicodePlaceholders: boolean;
9
- /** How image data reaches the terminal: in-band base64 or a temp file. */
10
- transmissionMedium: KittyTransmissionMedium;
11
24
  }
12
25
  /**
13
26
  * Whether the detected terminal renders Kitty Unicode placeholders (`U=1` +
@@ -29,16 +42,8 @@ export interface KittyGraphicsFeatures {
29
42
  export declare function detectKittyUnicodePlaceholdersSupport(terminalId: string, env?: NodeJS.ProcessEnv): boolean;
30
43
  export declare function getKittyGraphics(): Readonly<KittyGraphicsFeatures>;
31
44
  export declare function setKittyGraphics(partial: Partial<KittyGraphicsFeatures>): void;
32
- /**
33
- * Whether temp-file transmission may be promoted at runtime: forced via env,
34
- * disabled via env, otherwise auto (local sessions only — a temp file written
35
- * locally is not readable by a terminal on the far side of an SSH link).
36
- */
37
- export declare function kittyTempFileAllowed(): boolean;
38
45
  /** Whether a `columns`×`rows` placeholder grid fits within the diacritic table. */
39
46
  export declare function kittyPlaceholdersFit(columns: number, rows: number): boolean;
40
- /** True when the base64 payload is a PNG (kitty `f=100` / temp-file path only). */
41
- export declare function isPngBase64(base64Data: string): boolean;
42
47
  /**
43
48
  * Virtual placement APC (`a=p,U=1`): tells the terminal that placeholder cells
44
49
  * carrying image id `i` should display the transmitted image, scaled to fit the
@@ -72,23 +77,3 @@ export declare function renderKittyPlaceholderLines(opts: {
72
77
  columns: number;
73
78
  rows: number;
74
79
  }): string[];
75
- /**
76
- * Transmit a PNG via a temp file (`t=t`): decode the base64 to bytes once, write
77
- * them to a temp file, and send the base64-encoded file path as payload. Returns
78
- * the APC string, or `null` on any failure (caller falls back to direct base64).
79
- *
80
- * Synchronous filesystem writes are mandated by the synchronous render pipeline
81
- * (`Image.render` → `renderImage` are sync); there is no async seam here.
82
- */
83
- export declare function encodeKittyTempFileTransmit(base64Png: string, imageId: number): string | null;
84
- /**
85
- * Encode a temp-file support probe: write a tiny PNG to a temp file and ask the
86
- * terminal to query it (`a=q,t=t`). A conforming terminal replies
87
- * `ESC _ G i=<probeId>;OK ESC \`. Returns the query APC plus a `cleanup` that
88
- * removes the probe file (best-effort; kitty self-deletes the magic-named file).
89
- * Returns `null` if the temp file cannot be written.
90
- */
91
- export declare function encodeKittyTempFileProbe(probeId: number): {
92
- sequence: string;
93
- cleanup: () => void;
94
- } | null;
@@ -24,6 +24,21 @@ export declare class TerminalInfo {
24
24
  constructor(id: TerminalId, imageProtocol: ImageProtocol | null, trueColor: boolean, hyperlinks: boolean, notifyProtocol?: NotifyProtocol, eagerEraseScrollbackRisk?: boolean, deccara?: boolean, supportsScreenToScrollback?: boolean,
25
25
  /** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
26
26
  textSizing?: boolean);
27
+ /**
28
+ * Whether a prompt-submit keystroke scrolls this host to its tail, so the
29
+ * native-scrollback reconciliation checkpoint may ED3-rebuild even when the
30
+ * viewport position is unprobeable. Assigned by the TERMINAL builder from
31
+ * {@link detectSubmitPinsViewportToTail}; readonly but tests opt in via the
32
+ * {@link setTerminalSubmitPinsViewportToTail} mutable-cast setter.
33
+ */
34
+ readonly submitPinsViewportToTail: boolean;
35
+ /**
36
+ * Mutable clone for the {@link TERMINAL} singleton: copies every field and
37
+ * keeps the prototype methods, so the builder and runtime setters flip
38
+ * runtime-resolved {@link RuntimeTerminal} capabilities in place instead of
39
+ * reconstructing positional constructor args.
40
+ */
41
+ clone(): RuntimeTerminal;
27
42
  isImageLine(line: string): boolean;
28
43
  formatNotification(message: string | TerminalNotification): string;
29
44
  sendNotification(message: string | TerminalNotification): void;
@@ -51,8 +66,43 @@ export declare function isWindowsTerminalPreviewSixelSupported(env?: NodeJS.Proc
51
66
  * outer host fronting WSL, where the same ED3 yank applies. See #1610/#1682/#1799.
52
67
  */
53
68
  export declare function detectTerminalEagerEraseScrollbackRisk(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
54
- /** Whether DEC 2026 synchronized-output wrappers should be enabled by default. */
55
- export declare function shouldEnableSynchronizedOutputByDefault(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, terminalId?: TerminalId): boolean;
69
+ /**
70
+ * Whether a prompt-submit keystroke scrolls this terminal to its tail, making the
71
+ * native-scrollback reconciliation checkpoint (`refreshNativeScrollbackIfDirty`)
72
+ * safe to ED3-rebuild even when the viewport position cannot be probed.
73
+ *
74
+ * True only for recognized genuine *local* terminals where typing into the prompt
75
+ * brings the host viewport to the bottom. False — the checkpoint keeps deferring
76
+ * until a positive at-tail probe — for hosts whose scrollback a keystroke does not
77
+ * move: Windows consoles/ConPTY, Windows Terminal (incl. WSL), SSH, multiplexers,
78
+ * and unrecognized profiles. This is the per-terminal counterpart to the blanket
79
+ * block from #1610/#1682/#1746: those hosts genuinely cannot treat a submit as
80
+ * proof of at-tail, but genuine local terminals can.
81
+ */
82
+ export declare function detectSubmitPinsViewportToTail(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
83
+ /**
84
+ * Resolve an explicit user override for DEC 2026 synchronized output. Returns
85
+ * `false` for an opt-out, `true` for a force-on, or `null` when the user has
86
+ * expressed no preference. Shared by the static default and the runtime DECRQM
87
+ * probe so both honor the same precedence — an opt-out beats a force-on.
88
+ */
89
+ export declare function synchronizedOutputUserOverride(env?: NodeJS.ProcessEnv): boolean | null;
90
+ /**
91
+ * Whether DEC 2026 synchronized-output wrappers should be enabled by default.
92
+ *
93
+ * Policy (highest precedence first):
94
+ * 1. Explicit user override (`PI_NO_SYNC_OUTPUT`/`PI_TUI_SYNC_OUTPUT=0` off,
95
+ * `PI_FORCE_SYNC_OUTPUT=1`/`PI_TUI_SYNC_OUTPUT=1` on).
96
+ * 2. Positive `TERM_FEATURES` advertisement (`Sy`) — survives SSH/mux wrapping.
97
+ * 3. Windows Terminal (1.24+) via `WT_SESSION`, on native win32 and the
98
+ * WSL/SSH-fronted host alike.
99
+ * 4. Known direct terminals with confirmed support. SSH does *not* disable —
100
+ * DEC 2026 passes through SSH when the outer terminal honors it.
101
+ * 5. Everything else starts off, including risky multiplexers; the runtime
102
+ * DECRQM probe upgrades any of them when the terminal actually reports
103
+ * `?2026` supported (current zellij, tmux master, foot, contour, mintty…).
104
+ */
105
+ export declare function shouldEnableSynchronizedOutputByDefault(env?: NodeJS.ProcessEnv, terminalId?: TerminalId): boolean;
56
106
  /**
57
107
  * Whether the terminal applies Kitty-style DECCARA rectangular SGR changes
58
108
  * (`CSI Pt ; Pl ; Pb ; Pr ; <sgr> $ r`) extended to background color, so large
@@ -75,7 +125,22 @@ export declare function shouldEnableSynchronizedOutputByDefault(env?: NodeJS.Pro
75
125
  */
76
126
  export declare function detectRectangularSgrSupport(terminalId: TerminalId, env?: NodeJS.ProcessEnv): boolean;
77
127
  export declare const TERMINAL_ID: TerminalId;
78
- export declare const TERMINAL: TerminalInfo;
128
+ /**
129
+ * The process-wide {@link TERMINAL} singleton: a {@link TerminalInfo} whose
130
+ * post-construction capabilities — the image protocol and the probe-driven
131
+ * flags — are writable, so the runtime setters and tests mutate them directly
132
+ * instead of through an unsound cast. Every other field stays readonly.
133
+ */
134
+ export interface RuntimeTerminal extends TerminalInfo {
135
+ imageProtocol: ImageProtocol | null;
136
+ hyperlinks: boolean;
137
+ eagerEraseScrollbackRisk: boolean;
138
+ deccara: boolean;
139
+ supportsScreenToScrollback: boolean;
140
+ textSizing: boolean;
141
+ submitPinsViewportToTail: boolean;
142
+ }
143
+ export declare const TERMINAL: RuntimeTerminal;
79
144
  /**
80
145
  * Override terminal image protocol at runtime after capability probes complete.
81
146
  */
@@ -88,6 +153,8 @@ export declare function setTerminalImageProtocol(imageProtocol: ImageProtocol |
88
153
  export declare function setTerminalDeccara(enabled: boolean): void;
89
154
  /** Override screen-to-scrollback clear support for targeted renderer tests. */
90
155
  export declare function setTerminalScreenToScrollback(enabled: boolean): void;
156
+ /** Override submit-pins-viewport-to-tail for checkpoint reconciliation tests. */
157
+ export declare function setTerminalSubmitPinsViewportToTail(enabled: boolean): void;
91
158
  /**
92
159
  * Enable/disable OSC 66 text-sizing at runtime. The coding-agent calls this from
93
160
  * the `tui.textSizing` setting (gated on the terminal's static `textSizing`
@@ -6,6 +6,7 @@ type InputListenerResult = {
6
6
  data?: string;
7
7
  } | undefined;
8
8
  type InputListener = (data: string) => InputListenerResult;
9
+ type StartListener = () => void;
9
10
  export interface RenderTimer {
10
11
  cancel(): void;
11
12
  }
@@ -17,6 +18,10 @@ export interface RenderScheduler {
17
18
  export interface TUIOptions {
18
19
  renderScheduler?: RenderScheduler;
19
20
  }
21
+ export interface TUIStartOptions {
22
+ /** Clear saved native scrollback before the first paint. */
23
+ clearScrollback?: boolean;
24
+ }
20
25
  /**
21
26
  * Component interface - all components must implement this
22
27
  */
@@ -37,10 +42,17 @@ export interface Component {
37
42
  */
38
43
  wantsKeyRelease?: boolean;
39
44
  /**
40
- * Invalidate any cached rendering state.
45
+ * Optional hook to invalidate any cached rendering state.
41
46
  * Called when theme changes or when component needs to re-render from scratch.
42
47
  */
43
- invalidate(): void;
48
+ invalidate?(): void;
49
+ /**
50
+ * Optional teardown. Called when the component is permanently removed from
51
+ * the live tree (e.g. a transcript reset). Release timers, intervals, and
52
+ * subscriptions here. Must be idempotent. Containers propagate dispose to
53
+ * their children; leaf components without resources may omit it.
54
+ */
55
+ dispose?(): void;
44
56
  }
45
57
  /**
46
58
  * Optional component seam for native-scrollback pinning. A component that
@@ -86,15 +98,14 @@ export interface RenderRequestOptions {
86
98
  /** Clear terminal scrollback for intentional transcript replacement. */
87
99
  clearScrollback?: boolean;
88
100
  /**
89
- * Bypass the unknown-Windows-viewport deferral for this render so the
90
- * caller's intentional live UI mutation reaches the terminal even when
91
- * `Terminal#isNativeViewportAtBottom()` cannot answer.
101
+ * Allow a transient live-viewport repaint when the terminal cannot report
102
+ * whether its native viewport is at the tail.
92
103
  *
93
- * Use only for renders driven by direct user interaction (autocomplete
94
- * updates, IME, etc.). Any background/offscreen transcript change that
95
- * coalesces into the same frame WILL also bypass the deferral and reach
96
- * native scrollback that is the trade-off, and the reason ordinary
97
- * `requestRender()` calls must continue to omit this flag.
104
+ * This is **not** a settled transcript commit and must not be used for tool
105
+ * completion, session replay, or other background/offscreen rewrites. On
106
+ * ED3-risk terminals it may deliberately choose a viewport repaint/deferred
107
+ * shrink without clearing native scrollback so autocomplete, IME, and focused
108
+ * editor chrome stay responsive without yanking a scrolled reader.
98
109
  */
99
110
  allowUnknownViewportMutation?: boolean;
100
111
  }
@@ -156,6 +167,15 @@ export interface OverlayOptions {
156
167
  * Called each render cycle with current terminal dimensions.
157
168
  */
158
169
  visible?: (termWidth: number, termHeight: number) => boolean;
170
+ /**
171
+ * Borrow the terminal's alternate screen buffer for this overlay's lifetime
172
+ * (vim/less idiom). While the topmost visible overlay sets this, the engine
173
+ * paints only the modal on the alt screen and emits no ED3 / scrollback
174
+ * bytes, so the transcript on the normal screen stays untouched and is not
175
+ * scrollable behind the modal. Defaults off — all other overlays are
176
+ * unchanged and still draw over the transcript on the normal screen.
177
+ */
178
+ fullscreen?: boolean;
159
179
  }
160
180
  /**
161
181
  * Handle returned by showOverlay for controlling the overlay
@@ -177,6 +197,12 @@ export declare class Container implements Component {
177
197
  removeChild(component: Component): void;
178
198
  clear(): void;
179
199
  invalidate(): void;
200
+ /**
201
+ * Propagate teardown to children. Call when the container's children are
202
+ * being permanently discarded (not when they are detached for reuse — use
203
+ * {@link clear} for that). Idempotent per child via each child's own dispose.
204
+ */
205
+ dispose(): void;
180
206
  render(width: number): string[];
181
207
  }
182
208
  /**
@@ -215,8 +241,9 @@ export declare class TUI extends Container {
215
241
  setClearOnShrink(enabled: boolean): void;
216
242
  /**
217
243
  * Whether DEC 2026 synchronized-output wrappers are currently emitted around
218
- * paints. Starts from conservative terminal/env detection and is force-disabled
219
- * at runtime if the terminal reports mode 2026 unsupported via DECRQM.
244
+ * paints. Starts from conservative terminal/env detection and is reconciled at
245
+ * runtime against the terminal's DECRQM mode-2026 report enabled on a
246
+ * positive report, disabled on a negative one.
220
247
  */
221
248
  get synchronizedOutput(): boolean;
222
249
  /**
@@ -254,7 +281,8 @@ export declare class TUI extends Container {
254
281
  /** Check if there are any visible overlays */
255
282
  hasOverlay(): boolean;
256
283
  invalidate(): void;
257
- start(): void;
284
+ start(options?: TUIStartOptions): void;
285
+ addStartListener(listener: StartListener): () => void;
258
286
  addInputListener(listener: InputListener): () => void;
259
287
  removeInputListener(listener: InputListener): void;
260
288
  stop(): void;
@@ -264,5 +292,20 @@ export declare class TUI extends Container {
264
292
  * at the terminal bottom, such as after submitting a new prompt.
265
293
  */
266
294
  refreshNativeScrollbackIfDirty(_options?: NativeScrollbackRefreshOptions): boolean;
295
+ /**
296
+ * Force an immediate full replay of the current frame, including native
297
+ * scrollback. This is the keyboard-accessible equivalent of the resize reset:
298
+ * no queued diff frame or terminal scrollback probe can downgrade it to a
299
+ * viewport-only repaint.
300
+ *
301
+ * Invalidates every component first so the replay reflects current state. A
302
+ * geometry-driven reset thaws frozen scrollback snapshots implicitly (the new
303
+ * width misses every cached snapshot), but a same-width reset would otherwise
304
+ * replay stale snapshots — leaving host-frozen blocks (e.g. a transcript whose
305
+ * committed rows are immutable on ED3-risk terminals) showing pre-mutation
306
+ * content. Invalidation is the generic signal those containers use to retire
307
+ * their snapshots, which is exactly what a user-driven display reset wants.
308
+ */
309
+ resetDisplay(): void;
267
310
  requestRender(force?: boolean, options?: RenderRequestOptions): void;
268
311
  }
@@ -33,9 +33,12 @@ export declare function padding(n: number): string;
33
33
  * Get the shared grapheme segmenter instance.
34
34
  */
35
35
  export declare function getSegmenter(): Intl.Segmenter;
36
- export declare function visibleWidthRaw(str: string): number;
37
36
  /**
38
- * Calculate the visible width of a string in terminal columns.
37
+ * Visible width of a string in terminal columns, excluding ANSI/OSC escapes.
38
+ *
39
+ * `Bun.stringWidth` does the heavy lifting (UAX#11 width tables + ANSI/OSC
40
+ * stripping); this adds the two corrections it omits — tabs (expanded to
41
+ * `tabWidth` cells) and OSC 66 text-sizing payloads (scaled by `s=`).
39
42
  */
40
43
  export declare function visibleWidth(str: string): number;
41
44
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "15.9.67",
4
+ "version": "15.10.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": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "15.9.67",
41
- "@oh-my-pi/pi-utils": "15.9.67",
40
+ "@oh-my-pi/pi-natives": "15.10.1",
41
+ "@oh-my-pi/pi-utils": "15.10.1",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.4"
44
44
  },
@@ -48,6 +48,12 @@ export class Box implements Component {
48
48
  this.#invalidateCache();
49
49
  }
50
50
 
51
+ setPaddingY(paddingY: number): void {
52
+ if (this.#paddingY === paddingY) return;
53
+ this.#paddingY = paddingY;
54
+ this.#invalidateCache();
55
+ }
56
+
51
57
  setBgFn(bgFn?: (text: string) => string): void {
52
58
  this.#bgFn = bgFn;
53
59
  // Don't invalidate here - we'll detect bgFn changes by sampling output
@@ -1495,6 +1495,11 @@ export class Editor implements Component, Focusable {
1495
1495
  this.#insertTextAtCursor(text);
1496
1496
  }
1497
1497
 
1498
+ /** Apply terminal paste semantics to text from non-bracketed paste transports. */
1499
+ pasteText(text: string): void {
1500
+ this.#handlePaste(text);
1501
+ }
1502
+
1498
1503
  // All the editor methods from before...
1499
1504
  #insertCharacter(char: string): void {
1500
1505
  this.#exitHistoryForEditing();
@@ -3,21 +3,20 @@ import { sliceByColumn, visibleWidth } from "../utils";
3
3
  import { Text } from "./text";
4
4
 
5
5
  /**
6
- * Loader component that drives display refresh at ~60fps so callers whose
7
- * message colorizer is time-dependent (e.g. shimmer/KITT) animate smoothly.
6
+ * Loader component. Spinner frames advance at `SPINNER_ADVANCE_MS`.
8
7
  *
9
- * Two cadences are interleaved on a single timer:
10
- * - **Render tick** (every `RENDER_INTERVAL_MS`) asks the TUI to redraw.
11
- * The TUI already throttles at 16ms (`MIN_RENDER_INTERVAL_MS`), so this
12
- * is the natural upper bound; static messageColorFns produce identical
13
- * output and the differ drops the no-op redraw at ~zero cost.
14
- * - **Spinner advance** (every `SPINNER_ADVANCE_MS`) → bumps the spinner
15
- * frame index. Decoupled from the render cadence so the spinner keeps
16
- * its classic ~12.5fps step pace regardless of shimmer state.
8
+ * Message colorizers that are time-dependent can opt into 30fps redraws by
9
+ * setting `animated` to `true` on the function object.
17
10
  */
18
- const RENDER_INTERVAL_MS = 16;
11
+ const RENDER_INTERVAL_MS = 1000 / 30;
19
12
  const SPINNER_ADVANCE_MS = 80;
20
13
 
14
+ type ColorFn = (str: string) => string;
15
+
16
+ export type LoaderMessageColorFn = ColorFn & {
17
+ readonly animated?: true;
18
+ };
19
+
21
20
  export class Loader extends Text {
22
21
  #frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
23
22
  #currentFrame = 0;
@@ -27,8 +26,8 @@ export class Loader extends Text {
27
26
 
28
27
  constructor(
29
28
  ui: TUI,
30
- private spinnerColorFn: (str: string) => string,
31
- private messageColorFn: (str: string) => string,
29
+ private spinnerColorFn: ColorFn,
30
+ private messageColorFn: LoaderMessageColorFn,
32
31
  private message: string = "Loading...",
33
32
  spinnerFrames?: string[],
34
33
  ) {
@@ -54,14 +53,17 @@ export class Loader extends Text {
54
53
  start() {
55
54
  this.#lastSpinnerTick = performance.now();
56
55
  this.#updateDisplay();
56
+ const intervalMs = this.messageColorFn.animated === true ? RENDER_INTERVAL_MS : SPINNER_ADVANCE_MS;
57
57
  this.#intervalId = setInterval(() => {
58
58
  const now = performance.now();
59
- if (now - this.#lastSpinnerTick >= SPINNER_ADVANCE_MS) {
60
- this.#currentFrame = (this.#currentFrame + 1) % this.#frames.length;
61
- this.#lastSpinnerTick = now;
59
+ const elapsed = now - this.#lastSpinnerTick;
60
+ if (elapsed >= SPINNER_ADVANCE_MS) {
61
+ const steps = Math.floor(elapsed / SPINNER_ADVANCE_MS);
62
+ this.#currentFrame = (this.#currentFrame + steps) % this.#frames.length;
63
+ this.#lastSpinnerTick += steps * SPINNER_ADVANCE_MS;
62
64
  }
63
65
  this.#updateDisplay();
64
- }, RENDER_INTERVAL_MS);
66
+ }, intervalMs);
65
67
  }
66
68
 
67
69
  stop() {
@@ -71,7 +73,15 @@ export class Loader extends Text {
71
73
  }
72
74
  }
73
75
 
76
+ /** Lifecycle teardown: stop the animation timer. Idempotent. */
77
+ dispose() {
78
+ this.stop();
79
+ }
80
+
74
81
  setMessage(message: string) {
82
+ if (message === this.message) {
83
+ return;
84
+ }
75
85
  this.message = message;
76
86
  this.#updateDisplay();
77
87
  }