@oh-my-pi/pi-tui 15.8.3 → 15.9.0

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,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.9.0] - 2026-06-04
6
+
7
+ ### Added
8
+
9
+ - Added Kitty `CSI 22 J` screen-to-scrollback clears for non-destructive full paints, while keeping ED3 for destructive history/session rebuilds.
10
+ - Added Kitty OSC 99 rich notification formatting and startup capability probing.
11
+ - Added Kitty OSC 66 text-sized Markdown H1 headings (2x scale) plus native text-width support for OSC 66 spans. Off by default and gated to Kitty (the only terminal implementing OSC 66) via the `TERMINAL.textSizing` capability; hosts enable it through `setTextSizing`.
12
+ - Added Kitty Unicode placeholder image rendering (`U=1` + U+10EEEE with explicit row/column diacritics): inline images are drawn as real text cells that carry the image id in their foreground color, so they survive horizontal slicing, reflow, and overlapping draws instead of relying on cursor-positioned `a=p` placements. Enabled by default on Kitty-family terminals; opt out with `PI_NO_KITTY_PLACEHOLDERS=1`, and falls back to direct placement when a grid exceeds the diacritic table's addressable range.
13
+ - Added Kitty temp-file image transmission (`t=t`): on local sessions, decoded PNG bytes are written to a `tty-graphics-protocol` temp file and the path is sent instead of in-band base64, gated behind a startup `a=q,t=t` support probe. Controlled by `PI_KITTY_IMAGE_TRANSMISSION=direct|temp-file|auto`; disabled over SSH unless explicitly forced.
14
+ - Added DECRQM capability detection for DEC private modes 2026 (synchronized output) and 2048 (in-band resize). Synchronized-output paint wrappers are dropped when the terminal reports 2026 unsupported (preserving the `PI_NO_SYNC_OUTPUT` override), and DEC 2048 in-band resize is enabled when supported — reported geometry and cell pixel size are updated from `CSI 48 ; rows ; cols ; yPx ; xPx t` reports, with SIGWINCH and `CSI 16 t` kept as fallbacks.
15
+ - Added an injectable render scheduler for TUI tests, allowing deterministic render drains without patching global clocks or event-loop timing.
16
+ - Added `ImageBudget`, an inline-image cap that keeps only the most recent N images as live terminal graphics and demotes older ones to their text fallback. Once a new image pushes the count past the cap, the renderer hides the oldest via a full redraw plus an explicit Kitty graphics purge (`a=d,d=I`) — text-clear escapes (`CSI 2 J`/`CSI 3 J`) do not remove Kitty images. Configure the cap via `TUI#setMaxInlineImages` (`0` disables it).
17
+ - Changed Kitty inline images to a transmit-once + placement scheme: the base64 data is sent a single time (`a=t`) keyed by a stable image id, then every repaint emits only the tiny placement (`a=p,i=…,p=…`). Repaints — including full redraws — no longer re-send image data or stack duplicate placements, and the diff/line buffers and render caches hold short placement strings instead of multi-KB base64. The `ImageBudget` doubles as the transmit store (it tracks which ids are loaded and re-transmits after a purge frees the data). iTerm2/Sixel, which have no addressable image store, keep sending inline data as before.
18
+ - Added a renderer-level DECCARA rectangular-SGR optimizer that paints solid background panels/rows (Box/Text/Markdown fills, status bars, any full-width `theme.bg` row) as a single coalesced rectangle escape (`CSI 2*x` / `CSI Pt;Pl;Pb;Pr;<sgr>$r` / `CSI *x`) instead of emitting a full-width run of background-styled spaces on every visible row. It operates at emit time on the final ANSI strings — components are unchanged — and strips only trailing padding it can prove sits under a single non-default background span, coalescing vertically adjacent identical fills into one rectangle and falling back to the original bytes whenever the rectangle would not save bytes. Enabled only on Kitty, which implements the SGR-background extension (`docs/deccara.rst`); **Ghostty is intentionally excluded** because its `CSI $r` is unimplemented (ghostty-org/ghostty#632) and would drop the background entirely. Scrollback-bound rows and the append/scroll paths always keep the padded representation so native history preserves colored cells, and the `PI_NO_DECCARA` kill switch (plus tmux/screen/zellij detection) forces the fallback.
19
+ - Added `CMUX_SURFACE_ID` environment variable support to `getTerminalId()`, so cmux terminal surfaces get a stable identifier alongside kitty, tmux, macOS Terminal.app, and Windows Terminal — enabling per-surface session breadcrumbs for `omp -c` in cmux.
20
+
21
+ ### Changed
22
+
23
+ - Changed TUI tests to use Ghostty's VT engine (`ghostty-web`) instead of `@xterm/headless`.
24
+ - Changed the default inline-image live graphics budget from 3 to 8 images.
25
+
26
+ ### Fixed
27
+
28
+ - Fixed the DECCARA background-fill optimizer rejecting or repainting the wrong cells when a trailing fill crossed from default-background spaces into colored spaces.
29
+ - Fixed DEC private-mode reports with DECRPM status 3/4 being treated as unsupported, so permanent 2026/2048 reports stay recognized.
30
+ - Fixed OSC 66 text-sizing width and slicing edge cases, including ZWJ emoji payloads and partial slices through scaled spans.
31
+
32
+ - Fixed focused `Input` components following `TUI#setShowHardwareCursor`, so single-line prompts render either the terminal cursor or software cursor consistently with the editor.
33
+ - Fixed the DECCARA background-fill optimizer painting fills on the wrong rows ("split into unaligned halves") in the differential repaint path. When a diff grew the transcript past the viewport, writing the rewritten rows scrolled the terminal, but the absolute DECCARA rectangle coordinates were derived from the pre-scroll viewport top, so every fill landed `scrollAmount` rows too low while the relatively-positioned text settled correctly; rows scrolled into history were also shortened, dropping their background padding from native scrollback. Rectangles now target the post-scroll rows and only rows remaining in the final viewport are optimized.
34
+ - Fixed native scrollback desynchronization after terminal width or height changes reflowed overflowing content while the viewport was not at the bottom
35
+ - Fixed a notification chip (or any injected block) rendering on top of an actively streaming tool render on ED3-risk terminals (Ghostty/kitty/Alacritty/iTerm2). While a foreground tool streams, its header's elapsed-time counter ticks every frame; once output scrolls the header above the viewport top, each tick is an offscreen edit that — because the eager scrollback-rebuild opt-in is gated off on these terminals — repaints the viewport in place and advances the rendered line count without committing the new overflow to native history. `#scrollbackHighWater` then lagged the logical viewport top, so a later content shrink whose changes landed in the visible region slipped past the shrink-across-boundary guard and reached the differential emitter, which is anchored to `#maxLinesRendered - height`: it rewrote only the suffix, dropped the newly exposed top row, and left a blank at the bottom, drifting every row below the edit one line up so it painted over the rows above. Such shrinks now re-anchor the bottom of the viewport with a non-destructive repaint, and the foreground-streaming shrink-across-boundary case repaints the live tail instead of padding and pinning the pre-shrink viewport.
36
+ - Fixed a terminal resize during foreground-tool streaming on an unknown-viewport / ED3-risk host (Ghostty/kitty/Alacritty/iTerm2/WSL) leaving native scrollback permanently out of sync, so scrolling back after the turn showed missing rows. A pure geometry resize (no content change) takes the in-place viewport-repaint path, which — unlike a content-bearing resize that rebuilds via the geometry branch — never flagged native history. Because the prompt-submit checkpoint (`refreshNativeScrollbackIfDirty`) only rebuilds when scrollback is marked dirty on these hosts, the discrepancy was never reconciled. Overflowing geometry repaints whose viewport is not known to be at the bottom now mark scrollback dirty so the next checkpoint rebuilds an exact copy of the transcript.
37
+
5
38
  ## [15.8.2] - 2026-06-03
6
39
 
7
40
  ### Added
package/README.md CHANGED
@@ -538,7 +538,7 @@ interface Terminal {
538
538
  **Built-in implementations:**
539
539
 
540
540
  - `ProcessTerminal` - Uses `process.stdin/stdout`
541
- - `VirtualTerminal` - For testing (uses `@xterm/headless`)
541
+ - `VirtualTerminal` - For testing (uses ghostty-web)
542
542
 
543
543
  ## Utilities
544
544
 
@@ -7,6 +7,72 @@ export interface ImageOptions {
7
7
  maxWidthCells?: number;
8
8
  maxHeightCells?: number;
9
9
  filename?: string;
10
+ /** Shared budget that caps how many inline images render as live graphics. */
11
+ budget?: ImageBudget;
12
+ /**
13
+ * Stable identity for the underlying image (e.g. `toolCallId:index`). Lets the
14
+ * budget hand back the same graphics id across component re-creations so a
15
+ * repaint replaces the placement instead of stacking a duplicate.
16
+ */
17
+ imageKey?: string;
18
+ }
19
+ /** Default count of inline images kept as live graphics before older ones fall back to text. */
20
+ export declare const DEFAULT_MAX_INLINE_IMAGES = 8;
21
+ /**
22
+ * Bounds how many inline images render as live terminal graphics at once.
23
+ *
24
+ * Terminal graphics protocols — Kitty especially — keep every transmitted image
25
+ * in a per-terminal store and re-draw placements as content scrolls; text-clear
26
+ * escapes (`CSI 2 J` / `CSI 3 J`) do not remove them. Unbounded, a session that
27
+ * shows many images piles up placements plus store memory and leaves ghosts in
28
+ * scrollback.
29
+ *
30
+ * The budget keeps the most recent `cap` images live and demotes older ones to
31
+ * their text fallback. Demotion needs a full redraw (so off-screen rows are
32
+ * rewritten) plus an explicit graphics purge of the demoted ids — {@link Image}
33
+ * reports display order via {@link observe}, and the TUI drives the purge +
34
+ * redraw on the frame after a new image pushes the count past the cap.
35
+ *
36
+ * `cap <= 0` disables budgeting: every image stays a live graphic.
37
+ */
38
+ export declare class ImageBudget {
39
+ #private;
40
+ constructor(cap?: number, requestRender?: () => void);
41
+ get cap(): number;
42
+ get enabled(): boolean;
43
+ setRequestRender(requestRender: () => void): void;
44
+ setCap(cap: number): void;
45
+ /**
46
+ * Stable graphics id for a logical image. A non-empty `key` maps to the same
47
+ * id across re-creations (so repaints replace the placement); a missing key
48
+ * gets a fresh id every call.
49
+ */
50
+ acquireId(key?: string): number;
51
+ /** Begin a render pass. Called by the renderer before composing the frame. */
52
+ beginPass(): void;
53
+ /**
54
+ * Record an image in display order and report whether it must render its text
55
+ * fallback this frame. Called by every {@link Image} during render — including
56
+ * on a cache hit, so the image keeps its display-order slot.
57
+ */
58
+ observe(imageId: number): boolean;
59
+ /**
60
+ * End a render pass. Returns true when this frame must purge graphics and
61
+ * fully repaint to apply a stricter budget; read the ids via
62
+ * {@link takePurgeIds}.
63
+ */
64
+ endPass(): boolean;
65
+ /** Image ids to delete from the terminal this frame; clears the pending set. */
66
+ takePurgeIds(): readonly number[];
67
+ /** Whether `imageId`'s data still needs to be transmitted to the terminal. */
68
+ shouldTransmit(imageId: number): boolean;
69
+ /**
70
+ * Queue a one-time transmit for `imageId`. No-op if already transmitted, so a
71
+ * repeated call (e.g. a width-change re-render) never re-sends the data.
72
+ */
73
+ enqueueTransmit(imageId: number, sequence: string): void;
74
+ /** Transmit sequences to write before this frame's placements; clears the queue. */
75
+ takeTransmits(): readonly string[];
10
76
  }
11
77
  export declare class Image implements Component {
12
78
  #private;
@@ -10,6 +10,8 @@ export declare class Input implements Component, Focusable {
10
10
  focused: boolean;
11
11
  getValue(): string;
12
12
  setValue(value: string): void;
13
+ setUseTerminalCursor(useTerminalCursor: boolean): void;
14
+ getUseTerminalCursor(): boolean;
13
15
  handleInput(data: string): void;
14
16
  invalidate(): void;
15
17
  render(width: number): string[];
@@ -0,0 +1,49 @@
1
+ /** DECSACE — select the rectangle change extent so DECCARA fills a rectangle. */
2
+ export declare const DECSACE_RECT = "\u001B[2*x";
3
+ /** DECSACE — restore the default (stream) change extent. */
4
+ export declare const DECSACE_DEFAULT = "\u001B[*x";
5
+ /**
6
+ * Encode a single DECCARA rectangle. `top`/`bottom` are 1-based inclusive screen
7
+ * rows, `left`/`right` 1-based inclusive columns, `sgr` the raw SGR parameter
8
+ * list to apply (e.g. `48;2;10;20;30`, `48;5;4`, `41`).
9
+ */
10
+ export declare function encodeDeccara(top: number, left: number, bottom: number, right: number, sgr: string): string;
11
+ /** Where to cut a fillable line and the background to paint over the remainder. */
12
+ export interface BgFillAnalysis {
13
+ /** Byte index where droppable trailing background padding begins (0 = whole line). */
14
+ cut: number;
15
+ /** 0-based column where the trailing padding begins (DECCARA left = leftCol + 1). */
16
+ leftCol: number;
17
+ /** SGR parameter list of the background covering the trailing region. */
18
+ bg: string;
19
+ }
20
+ /**
21
+ * Decide whether `line` (a final, width-fit, reset-terminated ANSI string) is a
22
+ * full-width background fill whose trailing padding can be replaced by a DECCARA
23
+ * rectangle. Returns `null` unless it can *prove* the dropped bytes are literal
24
+ * trailing spaces under a single, constant, non-default background span (or the
25
+ * entire row is background-styled spaces).
26
+ *
27
+ * Conservative by construction: any OSC sequence (hyperlinks/images), any
28
+ * non-SGR CSI, a partial row, an inconsistent or default trailing background, or
29
+ * a malformed escape all yield `null` so the caller keeps the exact original.
30
+ */
31
+ export declare function analyzeBgFillLine(line: string, width: number): BgFillAnalysis | null;
32
+ /** Per-frame plan: the (possibly shortened) row strings and the DECCARA batch. */
33
+ export interface DeccaraPlan {
34
+ /** Row strings to write, parallel to the input. Optimized rows are shortened. */
35
+ texts: string[];
36
+ /** DECSACE-wrapped rectangle batch to emit after the rows, or `""` if none. */
37
+ sequence: string;
38
+ }
39
+ /**
40
+ * Plan DECCARA rectangles for a contiguous block of visible rows.
41
+ *
42
+ * `lines[k]` is the final ANSI string for screen row `firstScreenRow + k`
43
+ * (0-based). For each fillable row the trailing background padding is removed
44
+ * (the row's cells are cleared/erased by the caller, then repainted by the
45
+ * rectangle), and vertically adjacent rows with an identical left/right/bg span
46
+ * coalesce into one rectangle. Rectangles are emitted only when they save more
47
+ * bytes than they cost, so the result never exceeds the original byte count.
48
+ */
49
+ export declare function planDeccaraFills(lines: string[], width: number, firstScreenRow?: number): DeccaraPlan;
@@ -12,10 +12,12 @@ export * from "./components/spacer";
12
12
  export * from "./components/tab-bar";
13
13
  export * from "./components/text";
14
14
  export * from "./components/truncated-text";
15
+ export * from "./deccara";
15
16
  export type * from "./editor-component";
16
17
  export * from "./fuzzy";
17
18
  export * from "./keybindings";
18
19
  export * from "./keys";
20
+ export * from "./kitty-graphics";
19
21
  export * from "./stdin-buffer";
20
22
  export type * from "./symbols";
21
23
  export * from "./terminal";
@@ -0,0 +1,76 @@
1
+ /** Kitty Unicode placeholder base character (U+10EEEE, Plane 16 PUA). */
2
+ export declare const KITTY_PLACEHOLDER = "\uDBFB\uDEEE";
3
+ /** Largest row/column index expressible with the diacritic table (one cell each). */
4
+ export declare const KITTY_PLACEHOLDER_MAX_CELLS: number;
5
+ export type KittyTransmissionMedium = "direct" | "temp-file";
6
+ export interface KittyGraphicsFeatures {
7
+ /** Display images via Unicode placeholders instead of direct `a=p` placement. */
8
+ unicodePlaceholders: boolean;
9
+ /** How image data reaches the terminal: in-band base64 or a temp file. */
10
+ transmissionMedium: KittyTransmissionMedium;
11
+ }
12
+ export declare function getKittyGraphics(): Readonly<KittyGraphicsFeatures>;
13
+ export declare function setKittyGraphics(partial: Partial<KittyGraphicsFeatures>): void;
14
+ /**
15
+ * Whether temp-file transmission may be promoted at runtime: forced via env,
16
+ * disabled via env, otherwise auto (local sessions only — a temp file written
17
+ * locally is not readable by a terminal on the far side of an SSH link).
18
+ */
19
+ export declare function kittyTempFileAllowed(): boolean;
20
+ /** Whether a `columns`×`rows` placeholder grid fits within the diacritic table. */
21
+ export declare function kittyPlaceholdersFit(columns: number, rows: number): boolean;
22
+ /** True when the base64 payload is a PNG (kitty `f=100` / temp-file path only). */
23
+ export declare function isPngBase64(base64Data: string): boolean;
24
+ /**
25
+ * Virtual placement APC (`a=p,U=1`): tells the terminal that placeholder cells
26
+ * carrying image id `i` should display the transmitted image, scaled to fit the
27
+ * `c`×`r` cell box. Re-emitting with a stable `placementId` replaces in place.
28
+ */
29
+ export declare function encodeKittyVirtualPlacement(opts: {
30
+ imageId: number;
31
+ placementId?: number;
32
+ columns: number;
33
+ rows: number;
34
+ }): string;
35
+ /**
36
+ * Build the placeholder cell grid as one string per row. The image id is carried
37
+ * in each row's foreground color and the placement id (if any) in its underline
38
+ * color; every cell names its explicit row+column diacritic (robust to slicing,
39
+ * unlike left-inheritance). Returns exactly `rows` strings.
40
+ */
41
+ export declare function encodeKittyPlaceholderGrid(opts: {
42
+ imageId: number;
43
+ placementId?: number;
44
+ columns: number;
45
+ rows: number;
46
+ }): string[];
47
+ /**
48
+ * Full placeholder render: the virtual-placement APC prefixes line 0, and every
49
+ * line carries placeholder cells. Returns exactly `rows` lines (no cursor moves).
50
+ */
51
+ export declare function renderKittyPlaceholderLines(opts: {
52
+ imageId: number;
53
+ placementId?: number;
54
+ columns: number;
55
+ rows: number;
56
+ }): string[];
57
+ /**
58
+ * Transmit a PNG via a temp file (`t=t`): decode the base64 to bytes once, write
59
+ * them to a temp file, and send the base64-encoded file path as payload. Returns
60
+ * the APC string, or `null` on any failure (caller falls back to direct base64).
61
+ *
62
+ * Synchronous filesystem writes are mandated by the synchronous render pipeline
63
+ * (`Image.render` → `renderImage` are sync); there is no async seam here.
64
+ */
65
+ export declare function encodeKittyTempFileTransmit(base64Png: string, imageId: number): string | null;
66
+ /**
67
+ * Encode a temp-file support probe: write a tiny PNG to a temp file and ask the
68
+ * terminal to query it (`a=q,t=t`). A conforming terminal replies
69
+ * `ESC _ G i=<probeId>;OK ESC \`. Returns the query APC plus a `cleanup` that
70
+ * removes the probe file (best-effort; kitty self-deletes the magic-named file).
71
+ * Returns `null` if the temp file cannot be written.
72
+ */
73
+ export declare function encodeKittyTempFileProbe(probeId: number): {
74
+ sequence: string;
75
+ cleanup: () => void;
76
+ } | null;
@@ -17,10 +17,16 @@ export declare class TerminalInfo {
17
17
  readonly hyperlinks: boolean;
18
18
  readonly notifyProtocol: NotifyProtocol;
19
19
  readonly eagerEraseScrollbackRisk: boolean;
20
- constructor(id: TerminalId, imageProtocol: ImageProtocol | null, trueColor: boolean, hyperlinks: boolean, notifyProtocol?: NotifyProtocol, eagerEraseScrollbackRisk?: boolean);
20
+ readonly deccara: boolean;
21
+ readonly supportsScreenToScrollback: boolean;
22
+ /** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
23
+ readonly textSizing: boolean;
24
+ constructor(id: TerminalId, imageProtocol: ImageProtocol | null, trueColor: boolean, hyperlinks: boolean, notifyProtocol?: NotifyProtocol, eagerEraseScrollbackRisk?: boolean, deccara?: boolean, supportsScreenToScrollback?: boolean,
25
+ /** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
26
+ textSizing?: boolean);
21
27
  isImageLine(line: string): boolean;
22
- formatNotification(message: string): string;
23
- sendNotification(message: string): void;
28
+ formatNotification(message: string | TerminalNotification): string;
29
+ sendNotification(message: string | TerminalNotification): void;
24
30
  }
25
31
  export declare function isNotificationSuppressed(): boolean;
26
32
  /**
@@ -52,12 +58,47 @@ export declare function isWindowsTerminalPreviewSixelSupported(env?: NodeJS.Proc
52
58
  * Pure helper for tests and `TERMINAL` trait construction. See #1682 and #1719.
53
59
  */
54
60
  export declare function detectTerminalEagerEraseScrollbackRisk(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
61
+ /**
62
+ * Whether the terminal applies Kitty-style DECCARA rectangular SGR changes
63
+ * (`CSI Pt ; Pl ; Pb ; Pr ; <sgr> $ r`) extended to background color, so large
64
+ * filled regions can be painted as rectangles instead of background-padded
65
+ * strings on every row.
66
+ *
67
+ * Verified against terminal sources rather than terminfo, because a bare
68
+ * `Cara`/DECCARA terminfo capability does not imply the Kitty SGR-background
69
+ * extension:
70
+ * - Kitty implements it for *all* SGR attributes including background (see
71
+ * kitty `docs/deccara.rst` and the `test_deccara` parser test).
72
+ * - Ghostty does NOT: its `CSI $ r` dispatch falls through to an "unknown CSI"
73
+ * warning and DECCARA/DECSACE are tracked as unsupported
74
+ * (ghostty-org/ghostty#632). Enabling it there would silently drop panel
75
+ * backgrounds, so ghostty stays on the padded-string fallback.
76
+ *
77
+ * Disabled under tmux/screen/zellij multiplexers — screen-coordinate rectangle
78
+ * protocols are not safe to assume through a multiplexer — and via the
79
+ * `PI_NO_DECCARA` kill switch. Pure helper for tests and `TERMINAL` construction.
80
+ */
81
+ export declare function detectRectangularSgrSupport(terminalId: TerminalId, env?: NodeJS.ProcessEnv): boolean;
55
82
  export declare const TERMINAL_ID: TerminalId;
56
83
  export declare const TERMINAL: TerminalInfo;
57
84
  /**
58
85
  * Override terminal image protocol at runtime after capability probes complete.
59
86
  */
60
87
  export declare function setTerminalImageProtocol(imageProtocol: ImageProtocol | null): void;
88
+ /**
89
+ * Override DECCARA rectangular-SGR capability at runtime. Used by tests to
90
+ * exercise the optimizer and fallback paths deterministically — the default is
91
+ * resolved once at import and force-disabled under the test runtime.
92
+ */
93
+ export declare function setTerminalDeccara(enabled: boolean): void;
94
+ /** Override screen-to-scrollback clear support for targeted renderer tests. */
95
+ export declare function setTerminalScreenToScrollback(enabled: boolean): void;
96
+ /**
97
+ * Enable/disable OSC 66 text-sizing at runtime. The coding-agent calls this from
98
+ * the `tui.textSizing` setting (gated on the terminal's static `textSizing`
99
+ * capability); tests flip it directly to exercise the scaled-heading path.
100
+ */
101
+ export declare function setTerminalTextSizing(enabled: boolean): void;
61
102
  export declare function getTerminalInfo(terminalId: TerminalId): TerminalInfo;
62
103
  export interface CellDimensions {
63
104
  widthPx: number;
@@ -71,14 +112,53 @@ export interface ImageRenderOptions {
71
112
  maxWidthCells?: number;
72
113
  maxHeightCells?: number;
73
114
  preserveAspectRatio?: boolean;
115
+ /**
116
+ * Stable Kitty image id (`i=`). When set, the image is displayed via a
117
+ * transmit-once + placement scheme keyed off this id instead of re-sending the
118
+ * base64 each frame.
119
+ */
120
+ imageId?: number;
121
+ /** Stable Kitty placement id (`p=`); defaults to {@link imageId}. */
122
+ placementId?: number;
123
+ /** When true (Kitty + {@link imageId}), also return the one-time transmit sequence. */
124
+ includeTransmit?: boolean;
74
125
  }
75
126
  export declare function getCellDimensions(): CellDimensions;
76
127
  export declare function setCellDimensions(dims: CellDimensions): void;
128
+ /** Transmit-and-display (`a=T`) — the self-contained form used when no stable id is available. */
77
129
  export declare function encodeKitty(base64Data: string, options?: {
78
130
  columns?: number;
79
131
  rows?: number;
80
132
  imageId?: number;
81
133
  }): string;
134
+ /**
135
+ * Transmit image data only (`a=t`), keyed by `imageId`, without displaying it.
136
+ * Sent once per image; the data then persists in the terminal's store (it
137
+ * survives scroll-off and text clears for images with a non-zero id), so
138
+ * subsequent frames display it with the tiny {@link encodeKittyPlacement}
139
+ * sequence instead of re-sending the base64.
140
+ */
141
+ export declare function encodeKittyTransmit(base64Data: string, imageId: number): string;
142
+ /**
143
+ * Display a previously transmitted image (`a=p`) at the cursor. Carrying a
144
+ * stable `placementId` (`p=`) means re-emitting the sequence on a repaint
145
+ * *replaces* the existing placement (moving/resizing it without flicker) rather
146
+ * than stacking a duplicate.
147
+ */
148
+ export declare function encodeKittyPlacement(options: {
149
+ imageId: number;
150
+ placementId?: number;
151
+ columns?: number;
152
+ rows?: number;
153
+ }): string;
154
+ /**
155
+ * Kitty graphics delete command for a single image id. Uses `d=I` (capital)
156
+ * which removes the image and every one of its placements — on screen *and* in
157
+ * scrollback — and frees the backing data. `q=2` suppresses the terminal reply.
158
+ * Text-clearing escapes (`CSI 2 J` / `CSI 3 J`) do not remove Kitty graphics, so
159
+ * this is the only way to actually purge a placed image.
160
+ */
161
+ export declare function encodeKittyDeleteImage(imageId: number): string;
82
162
  export declare function encodeITerm2(base64Data: string, options?: {
83
163
  width?: number | string;
84
164
  height?: number | string;
@@ -93,7 +173,29 @@ export declare function getGifDimensions(base64Data: string): ImageDimensions |
93
173
  export declare function getWebpDimensions(base64Data: string): ImageDimensions | null;
94
174
  export declare function getImageDimensions(base64Data: string, mimeType: string): ImageDimensions | null;
95
175
  export declare function renderImage(base64Data: string, imageDimensions: ImageDimensions, options?: ImageRenderOptions): {
96
- sequence: string;
176
+ sequence?: string;
177
+ lines?: string[];
97
178
  rows: number;
179
+ transmit?: string;
98
180
  } | null;
99
181
  export declare function imageFallback(mimeType: string, dimensions?: ImageDimensions, filename?: string): string;
182
+ /**
183
+ * Structured terminal notification. Rich fields are honored only by OSC 99
184
+ * (Kitty) once support is confirmed; other protocols and the unconfirmed Kitty
185
+ * path collapse to a single `title: body` line.
186
+ */
187
+ export interface TerminalNotification {
188
+ title?: string;
189
+ body?: string;
190
+ id?: string;
191
+ type?: string | string[];
192
+ urgency?: "low" | "normal" | "critical";
193
+ iconName?: string;
194
+ sound?: "silent" | "system" | "info" | "warning" | "error" | "question";
195
+ actions?: "focus" | "report" | "focus-report" | "none";
196
+ expiresMs?: number;
197
+ }
198
+ /** Record the OSC 99 capability-probe result (called by ProcessTerminal). */
199
+ export declare function setOsc99Supported(supported: boolean): void;
200
+ /** True when OSC 99 structured notifications have been confirmed available. */
201
+ export declare function isOsc99Supported(): boolean;
@@ -66,6 +66,11 @@ export interface Terminal {
66
66
  onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
67
67
  /** The last detected terminal appearance, or undefined if not yet known. */
68
68
  get appearance(): TerminalAppearance | undefined;
69
+ /**
70
+ * Register a callback fired once per DEC private mode when its DECRQM support
71
+ * status resolves. Optional: only real terminals implement capability probing.
72
+ */
73
+ onPrivateModeReport?(callback: (mode: number, supported: boolean) => void): void;
69
74
  }
70
75
  /**
71
76
  * Real terminal using process.stdin/stdout
@@ -75,6 +80,7 @@ export declare class ProcessTerminal implements Terminal {
75
80
  get kittyProtocolActive(): boolean;
76
81
  get appearance(): TerminalAppearance | undefined;
77
82
  onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
83
+ onPrivateModeReport(callback: (mode: number, supported: boolean) => void): void;
78
84
  start(onInput: (data: string) => void, onResize: () => void): void;
79
85
  drainInput(maxMs?: number, idleMs?: number): Promise<void>;
80
86
  stop(): void;
@@ -1,3 +1,4 @@
1
+ import { ImageBudget } from "./components/image";
1
2
  import type { Terminal } from "./terminal";
2
3
  import { visibleWidth } from "./utils";
3
4
  type InputListenerResult = {
@@ -5,6 +6,17 @@ type InputListenerResult = {
5
6
  data?: string;
6
7
  } | undefined;
7
8
  type InputListener = (data: string) => InputListenerResult;
9
+ export interface RenderTimer {
10
+ cancel(): void;
11
+ }
12
+ export interface RenderScheduler {
13
+ now(): number;
14
+ scheduleImmediate(callback: () => void): void;
15
+ scheduleRender(callback: () => void, delayMs: number): RenderTimer;
16
+ }
17
+ export interface TUIOptions {
18
+ renderScheduler?: RenderScheduler;
19
+ }
8
20
  /**
9
21
  * Component interface - all components must implement this
10
22
  */
@@ -31,14 +43,21 @@ export interface Component {
31
43
  invalidate(): void;
32
44
  }
33
45
  /**
34
- * Interface for components that can receive focus and display a hardware cursor.
46
+ * Interface for components that can receive focus and display a cursor.
35
47
  * When focused, the component should emit CURSOR_MARKER at the cursor position
36
48
  * in its render output. TUI will find this marker and position the hardware
37
49
  * cursor there for proper IME candidate window positioning.
50
+ *
51
+ * Components that can switch between terminal-cursor and software-cursor
52
+ * rendering expose `setUseTerminalCursor`; TUI keeps that mode in sync with
53
+ * its resolved hardware-cursor preference whenever focus or the preference
54
+ * changes.
38
55
  */
39
56
  export interface Focusable {
40
57
  /** Set by TUI when focus changes. Component should emit CURSOR_MARKER when true. */
41
58
  focused: boolean;
59
+ /** Set by TUI when hardware cursor rendering is enabled or disabled. */
60
+ setUseTerminalCursor?(useTerminalCursor: boolean): void;
42
61
  }
43
62
  /** Options for scheduling a TUI render. */
44
63
  export interface RenderRequestOptions {
@@ -153,8 +172,16 @@ export declare class TUI extends Container {
153
172
  preFocus: Component | null;
154
173
  hidden: boolean;
155
174
  }[];
156
- constructor(terminal: Terminal, showHardwareCursor?: boolean);
175
+ constructor(terminal: Terminal, showHardwareCursor?: boolean, options?: TUIOptions);
157
176
  get fullRedraws(): number;
177
+ /** Shared budget that caps how many inline images render as live graphics. */
178
+ get imageBudget(): ImageBudget;
179
+ /**
180
+ * Set how many inline images stay live graphics before older ones fall back
181
+ * to text (`0` disables the cap). Older images are hidden via a graphics purge
182
+ * plus a full redraw on the frame after a new image exceeds the cap.
183
+ */
184
+ setMaxInlineImages(cap: number): void;
158
185
  getShowHardwareCursor(): boolean;
159
186
  setShowHardwareCursor(enabled: boolean): void;
160
187
  getClearOnShrink(): boolean;
@@ -164,6 +191,12 @@ export declare class TUI extends Container {
164
191
  * When false, empty rows remain (reduces redraws on slower terminals).
165
192
  */
166
193
  setClearOnShrink(enabled: boolean): void;
194
+ /**
195
+ * Whether DEC 2026 synchronized-output wrappers are currently emitted around
196
+ * paints. Starts from `PI_NO_SYNC_OUTPUT` and is force-disabled at runtime if
197
+ * the terminal reports mode 2026 unsupported via DECRQM.
198
+ */
199
+ get synchronizedOutput(): boolean;
167
200
  /**
168
201
  * When enabled, live render frames rebuild native scrollback on offscreen and
169
202
  * structural changes even when the viewport position is unobservable (POSIX,
@@ -1,6 +1,21 @@
1
1
  import { Ellipsis, type ExtractSegmentsResult, type SliceResult } from "@oh-my-pi/pi-natives";
2
2
  export { Ellipsis } from "@oh-my-pi/pi-natives";
3
3
  export { getDefaultTabWidth, getIndentation } from "@oh-my-pi/pi-utils";
4
+ export type TextSizingScale = 1 | 2 | 3;
5
+ export type TextSizingVerticalAlign = "top" | "bottom" | "center";
6
+ export type TextSizingHorizontalAlign = "left" | "right" | "center";
7
+ export interface TextSizingOptions {
8
+ scale?: TextSizingScale;
9
+ widthCells?: number;
10
+ verticalAlign?: TextSizingVerticalAlign;
11
+ horizontalAlign?: TextSizingHorizontalAlign;
12
+ }
13
+ /**
14
+ * Encode a plain-text span using Kitty's OSC 66 text-sizing protocol. The TUI
15
+ * emits only safe UTF-8 payloads and ST terminators so its ANSI parser and the
16
+ * terminal agree on span boundaries.
17
+ */
18
+ export declare function encodeTextSized(text: string, options?: TextSizingOptions): string;
4
19
  export declare function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult;
5
20
  export declare function truncateToWidth(text: string, maxWidth: number, ellipsisKind?: Ellipsis | null, pad?: boolean | null): string;
6
21
  export declare function wrapTextWithAnsi(text: string, width: number): string[];
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.8.3",
4
+ "version": "15.9.0",
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,14 +37,14 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "15.8.3",
41
- "@oh-my-pi/pi-utils": "15.8.3",
40
+ "@oh-my-pi/pi-natives": "15.9.0",
41
+ "@oh-my-pi/pi-utils": "15.9.0",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.4"
44
44
  },
45
45
  "devDependencies": {
46
46
  "chalk": "^5.6.2",
47
- "@xterm/headless": "^6.0.0"
47
+ "ghostty-web": "^0.4.0"
48
48
  },
49
49
  "engines": {
50
50
  "bun": ">=1.3.14"