@oh-my-pi/pi-tui 15.10.0 → 15.10.2

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,66 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.10.2] - 2026-06-08
6
+ ### Added
7
+
8
+ - Added exported `canonicalKeyId` and `addKeyAliases` keybinding helpers so consumers can share the same canonical shortcut matching semantics as `KeybindingsManager`.
9
+ - Added `super` modifier support to native key parsing/matching and bound `super+alt+backspace` / `super+alt+delete` (and `super+alt+d`) into the word-delete defaults so Ghostty's default macOS Option+Backspace wire (`ESC [127;11u` — kitty modifier 11 = super|alt) deletes a word instead of falling through to single-char delete ([#2064](https://github.com/can1357/oh-my-pi/issues/2064)).
10
+
11
+ ### Fixed
12
+
13
+ - Fixed focus-changing in-place menus leaving stale Working/menu rows and parking the hardware cursor in the old menu viewport on terminals without a scroll-position oracle.
14
+ - Fixed redundant terminal cursor updates so repeated renders that do not change the cursor row, column, or visibility no longer emit ANSI move/hide sequences
15
+ - Fixed repeated cursor updates during no-op re-renders by reusing the last known cursor state, preventing unnecessary cursor position changes and hide/show sequences
16
+ - Fixed the kitty keyboard progressive-enhancement probe to honor the `CSI ? <flags> u` reply even when the terminal answers the DA1 sentinel first. Previously the kitty reply was discarded once the DA1-driven `modifyOtherKeys` fallback engaged, so terminals like Superset/xterm-on-Electron stayed on the fallback and delivered Shift+Enter as a bare `\r` ([#2042](https://github.com/can1357/oh-my-pi/issues/2042)).
17
+ - Bounded TUI line fitting for oversized raw rows so ANSI-heavy subagent output and zero-width-heavy text cannot grow render buffers independently of the viewport or hide visible suffix text ([#2045](https://github.com/can1357/oh-my-pi/issues/2045)).
18
+ - Fixed tmux offscreen-shrink frames to skip repainting when the visible tail is unchanged, avoiding intermittent blank/refresh flashes in pane terminals ([#2046](https://github.com/can1357/oh-my-pi/issues/2046)).
19
+ - Fixed Windows ConPTY hosts (Windows Terminal, Tabby, Hyper, VS Code) parking the viewport at the top of a full paint after a `/resume` or any long-session repaint. `ProcessTerminal#safeWrite` now splits oversized writes into ≤ 8 KiB pieces at line boundaries on `win32` and inside WSL (where stdout still crosses ConPTY at the `wslhost` boundary) so each underlying `WriteFile` stays below the ~32 KiB threshold where ConPTY stops tracking the cursor; the data was always delivered, but the host UI's scroll position would not follow until any focus event forced a re-query. ([#2034](https://github.com/can1357/oh-my-pi/issues/2034))
20
+
21
+ ## [15.10.1] - 2026-06-07
22
+ ### Breaking Changes
23
+
24
+ - 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.
25
+ - 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.
26
+
27
+ ### Added
28
+
29
+ - Added `TUI.addStartListener()` so feature hooks can re-enable terminal modes after temporary stop/start cycles such as external-editor handoffs.
30
+ - Added `Editor.pasteText()` to apply terminal-style paste handling for text inserted from non-bracketed paste transports
31
+ - Added an optional `dispose()` lifecycle method to `Component` so components can release timers and subscriptions during permanent teardown
32
+ - Added `Container.dispose()` to propagate teardown to child components when a component tree is permanently discarded
33
+ - Added `Loader.dispose()` to stop the loader animation timer when the component is disposed
34
+ - 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.
35
+ - Added `ScrollView.handleScrollKey()` plus a `fastScrollLines` option so every scroll view gets shared navigation keys, including Shift+Arrow to scroll faster.
36
+ - 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.
37
+ - 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.
38
+
39
+ ### Changed
40
+
41
+ - 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`.
42
+ - Changed keybinding matching to precompute canonical key sets so each input sequence is parsed once per binding check instead of once per candidate key.
43
+ - Made `Component.invalidate()` optional so leaf components without render caches no longer need no-op invalidation hooks.
44
+ - `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()`.
45
+
46
+ ### Fixed
47
+
48
+ - Fixed `Loader` text updates to skip identical messages and preserve the rendered `Text` cache instead of invalidating it every timer tick.
49
+
50
+ - Fixed fullscreen overlay alt-frame rendering to reuse the current line-preparation path instead of calling removed fitting helpers.
51
+ - Reduced TUI render-path line fitting by deferring overlay base-frame fitting until an overlay rebuild and by reusing already-fitted lines in emitters.
52
+ - Reduced live-region pinned repaint output by diffing unchanged viewport rows when no sealed rows are being committed to native scrollback.
53
+ - Fixed no-append live-region pinned repaints to re-anchor the hardware cursor when the logical viewport shifts.
54
+ - Fixed keybinding matching so printable uppercase input preserves `Shift` for bindings such as `shift+a`.
55
+ - Optimized terminal image-line detection and Thai/Lao AM normalization checks to avoid hot-path regex scans and substring allocations.
56
+ - 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.
57
+ - 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.
58
+ - 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.
59
+ - Fixed terminal stop and restore cleanup to disable enhanced paste mode so it does not remain enabled after shutdown
60
+ - Removed the per-frame line-fit `Map` cache from the render timer path to avoid forcing JSC rope-string hashing during scheduled viewport repaints.
61
+ - 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
62
+ - Fixed cursor, padding, and line-fit behavior when strings contain tabs or OSC escapes by aligning `visibleWidth()` with the native text-width model
63
+ - 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)`).
64
+
5
65
  ## [15.10.0] - 2026-06-06
6
66
 
7
67
  ### Changed
@@ -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
  }
@@ -6,7 +6,7 @@ export declare class Text implements Component {
6
6
  #private;
7
7
  constructor(text?: string, paddingX?: number, paddingY?: number, customBgFn?: (text: string) => string);
8
8
  getText(): string;
9
- setText(text: string): void;
9
+ setText(text: string): boolean;
10
10
  setCustomBgFn(customBgFn?: (text: string) => string): void;
11
11
  invalidate(): void;
12
12
  render(width: number): string[];
@@ -102,11 +102,11 @@ export declare const TUI_KEYBINDINGS: {
102
102
  readonly description: "Delete character forward";
103
103
  };
104
104
  readonly "tui.editor.deleteWordBackward": {
105
- readonly defaultKeys: ["ctrl+w", "alt+backspace", "ctrl+backspace"];
105
+ readonly defaultKeys: ["ctrl+w", "alt+backspace", "ctrl+backspace", "super+alt+backspace"];
106
106
  readonly description: "Delete word backward";
107
107
  };
108
108
  readonly "tui.editor.deleteWordForward": {
109
- readonly defaultKeys: ["alt+delete", "alt+d"];
109
+ readonly defaultKeys: ["alt+delete", "alt+d", "super+alt+delete", "super+alt+d"];
110
110
  readonly description: "Delete word forward";
111
111
  };
112
112
  readonly "tui.editor.deleteToLineStart": {
@@ -174,6 +174,8 @@ export interface KeybindingConflict {
174
174
  key: KeyId;
175
175
  keybindings: string[];
176
176
  }
177
+ export declare function canonicalKeyId(key: string): string;
178
+ export declare function addKeyAliases(keys: Set<string>, key: KeyId): void;
177
179
  export declare class KeybindingsManager {
178
180
  #private;
179
181
  constructor(definitions: KeybindingDefinitions, userBindings?: KeybindingsConfig);
@@ -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,6 +66,20 @@ 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;
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;
54
83
  /**
55
84
  * Resolve an explicit user override for DEC 2026 synchronized output. Returns
56
85
  * `false` for an opt-out, `true` for a force-on, or `null` when the user has
@@ -96,7 +125,22 @@ export declare function shouldEnableSynchronizedOutputByDefault(env?: NodeJS.Pro
96
125
  */
97
126
  export declare function detectRectangularSgrSupport(terminalId: TerminalId, env?: NodeJS.ProcessEnv): boolean;
98
127
  export declare const TERMINAL_ID: TerminalId;
99
- 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;
100
144
  /**
101
145
  * Override terminal image protocol at runtime after capability probes complete.
102
146
  */
@@ -109,6 +153,8 @@ export declare function setTerminalImageProtocol(imageProtocol: ImageProtocol |
109
153
  export declare function setTerminalDeccara(enabled: boolean): void;
110
154
  /** Override screen-to-scrollback clear support for targeted renderer tests. */
111
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;
112
158
  /**
113
159
  * Enable/disable OSC 66 text-sizing at runtime. The coding-agent calls this from
114
160
  * the `tui.textSizing` setting (gated on the terminal's static `textSizing`
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Split `data` into chunks no larger than `maxChunkSize`, preferring a line
3
+ * boundary (`\n`) as the cut point so escape sequences (which never contain
4
+ * `\n`) stay intact. The TUI's full-paint buffers are line-structured
5
+ * (`buffer += "\r\n"` between rows), so a newline almost always exists within
6
+ * the window. The fallback for a buffer with no newline in range is a hard
7
+ * cut at `maxChunkSize`: the ConPTY viewport bug from a single oversized
8
+ * write is strictly worse than a one-frame escape-sequence glitch on a buffer
9
+ * the renderer effectively never produces.
10
+ *
11
+ * Exported for unit testing of the chunking contract; `#safeWrite` is the
12
+ * sole production caller.
13
+ */
14
+ export declare function chunkForConPTY(data: string, maxChunkSize?: number): string[];
1
15
  /**
2
16
  * Emergency terminal restore - call this from signal/crash handlers
3
17
  * Resets terminal state without requiring access to the ProcessTerminal instance
@@ -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
  /**
@@ -255,7 +281,8 @@ export declare class TUI extends Container {
255
281
  /** Check if there are any visible overlays */
256
282
  hasOverlay(): boolean;
257
283
  invalidate(): void;
258
- start(): void;
284
+ start(options?: TUIStartOptions): void;
285
+ addStartListener(listener: StartListener): () => void;
259
286
  addInputListener(listener: InputListener): () => void;
260
287
  removeInputListener(listener: InputListener): void;
261
288
  stop(): void;
@@ -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.10.0",
4
+ "version": "15.10.2",
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.10.0",
41
- "@oh-my-pi/pi-utils": "15.10.0",
40
+ "@oh-my-pi/pi-natives": "15.10.2",
41
+ "@oh-my-pi/pi-utils": "15.10.2",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.4"
44
44
  },
@@ -1109,12 +1109,18 @@ export class Editor implements Component, Focusable {
1109
1109
  else if (matchesKey(data, "ctrl+w")) {
1110
1110
  this.#deleteWordBackwards();
1111
1111
  }
1112
- // Option/Alt+Backspace - Delete word backwards
1113
- else if (matchesKey(data, "alt+backspace")) {
1112
+ // Option/Alt+Backspace - Delete word backwards.
1113
+ // Ghostty on macOS reports Option+Backspace as super+alt (kitty mod 11) — see #2064.
1114
+ else if (matchesKey(data, "alt+backspace") || matchesKey(data, "super+alt+backspace")) {
1114
1115
  this.#deleteWordBackwards();
1115
1116
  }
1116
- // Option/Alt+D - Delete word forwards
1117
- else if (matchesKey(data, "alt+d") || matchesKey(data, "alt+delete")) {
1117
+ // Option/Alt+D and Option+Delete - Delete word forwards. Same Ghostty quirk applies.
1118
+ else if (
1119
+ matchesKey(data, "alt+d") ||
1120
+ matchesKey(data, "alt+delete") ||
1121
+ matchesKey(data, "super+alt+d") ||
1122
+ matchesKey(data, "super+alt+delete")
1123
+ ) {
1118
1124
  this.#deleteWordForwards();
1119
1125
  }
1120
1126
  // Ctrl+Y - Yank from kill ring
@@ -1495,6 +1501,11 @@ export class Editor implements Component, Focusable {
1495
1501
  this.#insertTextAtCursor(text);
1496
1502
  }
1497
1503
 
1504
+ /** Apply terminal paste semantics to text from non-bracketed paste transports. */
1505
+ pasteText(text: string): void {
1506
+ this.#handlePaste(text);
1507
+ }
1508
+
1498
1509
  // All the editor methods from before...
1499
1510
  #insertCharacter(char: string): void {
1500
1511
  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,15 +73,23 @@ 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
  }
78
88
 
79
89
  #updateDisplay() {
80
90
  const frame = this.#frames[this.#currentFrame];
81
- this.setText(`${this.spinnerColorFn(frame)} ${this.messageColorFn(this.message)}`);
82
- if (this.#ui) {
91
+ const text = `${this.spinnerColorFn(frame)} ${this.messageColorFn(this.message)}`;
92
+ if (this.setText(text) && this.#ui) {
83
93
  this.#ui.requestRender();
84
94
  }
85
95
  }