@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 +66 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/editor.d.ts +2 -0
- package/dist/types/components/loader.d.ts +8 -1
- package/dist/types/components/scroll-view.d.ts +22 -0
- package/dist/types/kitty-graphics.d.ts +16 -31
- package/dist/types/terminal-capabilities.d.ts +70 -3
- package/dist/types/tui.d.ts +56 -13
- package/dist/types/utils.d.ts +5 -2
- package/package.json +3 -3
- package/src/components/box.ts +6 -0
- package/src/components/editor.ts +5 -0
- package/src/components/loader.ts +27 -17
- package/src/components/markdown.ts +14 -12
- package/src/components/scroll-view.ts +62 -1
- package/src/components/text.ts +3 -0
- package/src/index.ts +1 -1
- package/src/keybindings.ts +69 -11
- package/src/kitty-graphics.ts +2 -101
- package/src/terminal-capabilities.ts +193 -87
- package/src/terminal.ts +3 -61
- package/src/tui.ts +460 -123
- package/src/utils.ts +92 -60
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:
|
|
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
|
-
/**
|
|
55
|
-
|
|
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
|
-
|
|
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`
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
90
|
-
*
|
|
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
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
|
219
|
-
*
|
|
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
|
}
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
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.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.
|
|
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
|
},
|
package/src/components/box.ts
CHANGED
|
@@ -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
|
package/src/components/editor.ts
CHANGED
|
@@ -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();
|
package/src/components/loader.ts
CHANGED
|
@@ -3,21 +3,20 @@ import { sliceByColumn, visibleWidth } from "../utils";
|
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Loader component
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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 =
|
|
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:
|
|
31
|
-
private messageColorFn:
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
},
|
|
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
|
}
|