@oh-my-pi/pi-tui 18.0.7 → 18.0.9
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 +29 -0
- package/dist/types/components/box.d.ts +2 -0
- package/dist/types/components/cancellable-loader.d.ts +2 -0
- package/dist/types/components/editor.d.ts +4 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/input.d.ts +2 -0
- package/dist/types/components/loader.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +2 -0
- package/dist/types/components/scroll-view.d.ts +2 -0
- package/dist/types/components/select-list.d.ts +2 -0
- package/dist/types/components/settings-list.d.ts +4 -0
- package/dist/types/components/spacer.d.ts +2 -0
- package/dist/types/components/tab-bar.d.ts +2 -0
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/components/truncated-text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/latex-to-unicode.d.ts +4 -4
- package/dist/types/terminal.d.ts +11 -0
- package/dist/types/tui.d.ts +30 -0
- package/package.json +3 -3
- package/src/components/box.ts +10 -0
- package/src/components/cancellable-loader.ts +7 -0
- package/src/components/editor.ts +28 -0
- package/src/components/image.ts +11 -0
- package/src/components/input.ts +13 -0
- package/src/components/loader.ts +21 -5
- package/src/components/markdown.ts +39 -10
- package/src/components/scroll-view.ts +12 -0
- package/src/components/select-list.ts +13 -0
- package/src/components/settings-list.ts +19 -0
- package/src/components/spacer.ts +4 -0
- package/src/components/tab-bar.ts +11 -0
- package/src/components/text.ts +11 -0
- package/src/components/truncated-text.ts +15 -1
- package/src/debug-server.ts +422 -0
- package/src/index.ts +2 -0
- package/src/latex-to-unicode.ts +401 -172
- package/src/terminal.ts +22 -2
- package/src/tui.ts +115 -3
- package/src/utils.ts +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.0.9] - 2026-08-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Exported TuiDebugServer for programmatic headless control
|
|
10
|
+
- Added debug demonstration script to examples
|
|
11
|
+
- Added an `OMP_TUI_DEBUG` Unix socket for headless TUI driving and structured inspection.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Inline hex colors now render with VS Code-style colored backgrounds and automatically selected black or white text for readability, alongside the color swatch.
|
|
16
|
+
- LaTeX text formatting commands such as \textbf, \textit, \textsl, and \emph now render as terminal bold or italic text.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed inline color swatches rendering incorrectly inside highlighted lines.
|
|
21
|
+
- Fixed terminal resizing in tmux panes and Windows consoles duplicating the current in-progress turn in scrollback.
|
|
22
|
+
|
|
23
|
+
## [18.0.8] - 2026-08-27
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `ProcessTerminal` accepts a `conpty` option to force ConPTY-hosted behavior on or off, keeping terminal tests hermetic on WSL where live env detection would otherwise flip kitty-keyboard flags and write chunking ([#9887](https://github.com/can1357/oh-my-pi/issues/9887)).
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Fixed pending-work animations repeatedly composing expensive frames without applying their full render cost to CPU backpressure.
|
|
32
|
+
- Fixed unfinished live viewport rows entering tmux pane history and duplicating streamed output ([#9780](https://github.com/can1357/oh-my-pi/issues/9780)).
|
|
33
|
+
|
|
5
34
|
## [18.0.7] - 2026-08-26
|
|
6
35
|
|
|
7
36
|
### Breaking Changes
|
|
@@ -19,6 +19,8 @@ export declare class Box implements Component {
|
|
|
19
19
|
children: Component[];
|
|
20
20
|
setIgnoreTight(ignore: boolean): this;
|
|
21
21
|
constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string, border?: BoxBorder);
|
|
22
|
+
/** Return container layout and child-count state for debug inspection. */
|
|
23
|
+
debugState(): Record<string, unknown>;
|
|
22
24
|
addChild(component: Component): void;
|
|
23
25
|
removeChild(component: Component): void;
|
|
24
26
|
clear(): void;
|
|
@@ -16,6 +16,8 @@ export declare class CancellableLoader extends Loader {
|
|
|
16
16
|
get signal(): AbortSignal;
|
|
17
17
|
/** Whether the loader was aborted */
|
|
18
18
|
get aborted(): boolean;
|
|
19
|
+
/** Return loader state including whether cancellation was requested. */
|
|
20
|
+
debugState(): Record<string, unknown>;
|
|
19
21
|
handleInput(data: string): void;
|
|
20
22
|
dispose(): void;
|
|
21
23
|
}
|
|
@@ -92,6 +92,10 @@ export declare class Editor implements Component, Focusable {
|
|
|
92
92
|
onAutocompleteCancel?: () => void;
|
|
93
93
|
disableSubmit: boolean;
|
|
94
94
|
constructor(theme: EditorTheme);
|
|
95
|
+
/** Return bounded editor content, cursor, and transient child state for debug inspection. */
|
|
96
|
+
debugState(): Record<string, unknown>;
|
|
97
|
+
/** Expose the active autocomplete list to the debug tree walker. */
|
|
98
|
+
get debugChildren(): readonly Component[];
|
|
95
99
|
setTheme(theme: EditorTheme): void;
|
|
96
100
|
setAutocompleteProvider(provider: AutocompleteProvider): void;
|
|
97
101
|
/** Install prose assistance without changing command/file autocomplete. */
|
|
@@ -157,6 +157,8 @@ export declare class ImageBudget {
|
|
|
157
157
|
export declare class Image implements Component {
|
|
158
158
|
#private;
|
|
159
159
|
constructor(base64Data: string, mimeType: string, theme: ImageTheme, options?: ImageOptions, dimensions?: ImageDimensions);
|
|
160
|
+
/** Return source metadata without exposing the encoded image buffer. */
|
|
161
|
+
debugState(): Record<string, unknown>;
|
|
160
162
|
invalidate(): void;
|
|
161
163
|
render(width: number): readonly string[];
|
|
162
164
|
}
|
|
@@ -13,6 +13,8 @@ export declare class Input implements Component, Focusable {
|
|
|
13
13
|
/** Focusable interface - set by TUI when focus changes */
|
|
14
14
|
focused: boolean;
|
|
15
15
|
getValue(): string;
|
|
16
|
+
/** Return bounded input content and cursor state for debug inspection. */
|
|
17
|
+
debugState(): Record<string, unknown>;
|
|
16
18
|
setValue(value: string): void;
|
|
17
19
|
setUseTerminalCursor(useTerminalCursor: boolean): void;
|
|
18
20
|
getUseTerminalCursor(): boolean;
|
|
@@ -15,6 +15,8 @@ export declare class Loader extends Text {
|
|
|
15
15
|
private messageColorFn;
|
|
16
16
|
private message;
|
|
17
17
|
constructor(ui: TUI, spinnerColorFn: ColorFn, messageColorFn: LoaderMessageColorFn, message?: string | (() => string), spinnerFrames?: string[]);
|
|
18
|
+
/** Return the current message and animation state for debug inspection. */
|
|
19
|
+
debugState(): Record<string, unknown>;
|
|
18
20
|
render(width: number): readonly string[];
|
|
19
21
|
start(): void;
|
|
20
22
|
stop(): void;
|
|
@@ -79,6 +79,8 @@ export declare class Markdown implements Component {
|
|
|
79
79
|
#private;
|
|
80
80
|
setIgnoreTight(ignore: boolean): this;
|
|
81
81
|
constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, codeBlockIndent?: number);
|
|
82
|
+
/** Return bounded source text and layout state for debug inspection. */
|
|
83
|
+
debugState(): Record<string, unknown>;
|
|
82
84
|
setText(text: string): boolean;
|
|
83
85
|
invalidate(): void;
|
|
84
86
|
/**
|
|
@@ -36,6 +36,8 @@ export interface ScrollViewOptions {
|
|
|
36
36
|
export declare class ScrollView implements Component {
|
|
37
37
|
#private;
|
|
38
38
|
constructor(lines: readonly string[], options: ScrollViewOptions);
|
|
39
|
+
/** Return viewport, content, and scroll position state for debug inspection. */
|
|
40
|
+
debugState(): Record<string, unknown>;
|
|
39
41
|
setLines(lines: readonly string[]): void;
|
|
40
42
|
setTotalRows(totalRows: number | undefined): void;
|
|
41
43
|
setHeight(height: number): void;
|
|
@@ -58,6 +58,8 @@ export declare class SelectList implements Component, MouseRoutable {
|
|
|
58
58
|
onCancel?: () => void;
|
|
59
59
|
onSelectionChange?: (item: SelectItem) => void;
|
|
60
60
|
constructor(items: ReadonlyArray<SelectItem>, maxVisible: number, theme: SelectListTheme, layout?: SelectListLayoutOptions);
|
|
61
|
+
/** Return item, selection, and filter state for debug inspection. */
|
|
62
|
+
debugState(): Record<string, unknown>;
|
|
61
63
|
/** Refit the visible row budget (hosts clamp the list to available height). */
|
|
62
64
|
setMaxVisible(rows: number): void;
|
|
63
65
|
setFilter(filter: string): void;
|
|
@@ -68,6 +68,10 @@ export declare class SettingsList implements Component {
|
|
|
68
68
|
/** Fired when the selected item changes (navigation, filtering, or setItems). */
|
|
69
69
|
onSelectionChange?: (item: SettingItem | undefined) => void;
|
|
70
70
|
constructor(items: SettingItem[], maxVisible: number, theme: SettingsListTheme, onChange: (id: string, newValue: string) => void, onCancel: () => void, options?: SettingsListOptions);
|
|
71
|
+
/** Return item, selection, filter, and submenu state for debug inspection. */
|
|
72
|
+
debugState(): Record<string, unknown>;
|
|
73
|
+
/** Expose the active submenu to the debug tree walker. */
|
|
74
|
+
get debugChildren(): readonly Component[];
|
|
71
75
|
/** The currently selected item, or undefined when empty or on a heading. */
|
|
72
76
|
getSelectedItem(): SettingItem | undefined;
|
|
73
77
|
/** Move selection to the item with `id`. Returns false when it is not visible. */
|
|
@@ -5,6 +5,8 @@ import type { Component } from "../tui.js";
|
|
|
5
5
|
export declare class Spacer implements Component {
|
|
6
6
|
#private;
|
|
7
7
|
constructor(lines?: number);
|
|
8
|
+
/** Return the spacer height for debug inspection. */
|
|
9
|
+
debugState(): Record<string, unknown>;
|
|
8
10
|
setLines(lines: number): void;
|
|
9
11
|
invalidate(): void;
|
|
10
12
|
render(_width: number): readonly string[];
|
|
@@ -45,6 +45,8 @@ export declare class TabBar implements Component {
|
|
|
45
45
|
/** Render the trailing "(tab to cycle)" hint. Disable when the host folds the hint into its own footer. */
|
|
46
46
|
showHint: boolean;
|
|
47
47
|
constructor(label: string, tabs: Tab[], theme: TabBarTheme, initialIndex?: number);
|
|
48
|
+
/** Return tab identities and active-tab state for debug inspection. */
|
|
49
|
+
debugState(): Record<string, unknown>;
|
|
48
50
|
/** Get the currently active tab */
|
|
49
51
|
getActiveTab(): Tab;
|
|
50
52
|
/** Get the index of the currently active tab */
|
|
@@ -12,6 +12,8 @@ export declare class Text implements Component {
|
|
|
12
12
|
#private;
|
|
13
13
|
setIgnoreTight(ignore: boolean): this;
|
|
14
14
|
constructor(text?: string, paddingX?: number, paddingY?: number, customBgFn?: (text: string) => string);
|
|
15
|
+
/** Return bounded text and layout state for debug inspection. */
|
|
16
|
+
debugState(): Record<string, unknown>;
|
|
15
17
|
getText(): string;
|
|
16
18
|
setText(text: string): boolean;
|
|
17
19
|
setCustomBgFn(customBgFn?: (text: string) => string): void;
|
|
@@ -5,6 +5,8 @@ import type { Component } from "../tui.js";
|
|
|
5
5
|
export declare class TruncatedText implements Component {
|
|
6
6
|
#private;
|
|
7
7
|
constructor(text: string, paddingX?: number, paddingY?: number);
|
|
8
|
+
/** Return bounded source text and the last-known truncation state. */
|
|
9
|
+
debugState(): Record<string, unknown>;
|
|
8
10
|
invalidate(): void;
|
|
9
11
|
render(width: number): readonly string[];
|
|
10
12
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type TUI } from "./tui.js";
|
|
2
|
+
export interface TuiDebugTreeNode {
|
|
3
|
+
kind: string;
|
|
4
|
+
id?: string;
|
|
5
|
+
rect?: [x: number, y: number, w: number, h: number];
|
|
6
|
+
focused?: boolean;
|
|
7
|
+
focusable?: boolean;
|
|
8
|
+
hidden?: boolean;
|
|
9
|
+
children?: TuiDebugTreeNode[];
|
|
10
|
+
}
|
|
11
|
+
export interface TuiDebugOverlayNode {
|
|
12
|
+
overlay: number;
|
|
13
|
+
band?: unknown;
|
|
14
|
+
hidden?: boolean;
|
|
15
|
+
root: TuiDebugTreeNode;
|
|
16
|
+
}
|
|
17
|
+
export interface TuiDebugTree {
|
|
18
|
+
root: TuiDebugTreeNode;
|
|
19
|
+
overlays: TuiDebugOverlayNode[];
|
|
20
|
+
}
|
|
21
|
+
/** NDJSON debug and input server enabled by the `OMP_TUI_DEBUG` socket path. */
|
|
22
|
+
export declare class TuiDebugServer {
|
|
23
|
+
#private;
|
|
24
|
+
constructor(tui: TUI, path: string);
|
|
25
|
+
start(): void;
|
|
26
|
+
stop(): void;
|
|
27
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./components/spacer.js";
|
|
|
14
14
|
export * from "./components/tab-bar.js";
|
|
15
15
|
export * from "./components/text.js";
|
|
16
16
|
export * from "./components/truncated-text.js";
|
|
17
|
+
export * from "./debug-server.js";
|
|
17
18
|
export * from "./deccara.js";
|
|
18
19
|
export * from "./desktop-notify.js";
|
|
19
20
|
export type * from "./editor-component.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Math font command names
|
|
3
|
-
*
|
|
4
|
-
* re-wraps inline runs inside these commands when their argument
|
|
5
|
-
*
|
|
2
|
+
* Math font and text-style command names whose single brace argument
|
|
3
|
+
* is rendered with the corresponding style. Exported for the display block
|
|
4
|
+
* engine, which re-wraps inline runs inside these commands when their argument
|
|
5
|
+
* contains 2-D layout so styling survives box boundaries.
|
|
6
6
|
*/
|
|
7
7
|
export declare const MATH_FONT_COMMANDS: ReadonlySet<string>;
|
|
8
8
|
/**
|
package/dist/types/terminal.d.ts
CHANGED
|
@@ -172,11 +172,22 @@ export interface Terminal {
|
|
|
172
172
|
* single predicate.
|
|
173
173
|
*/
|
|
174
174
|
export declare function isConPTYHosted(): boolean;
|
|
175
|
+
/** Construction-time overrides for {@link ProcessTerminal}. */
|
|
176
|
+
export interface ProcessTerminalOptions {
|
|
177
|
+
/**
|
|
178
|
+
* Force ConPTY-hosted behavior on or off. Defaults to live detection via
|
|
179
|
+
* {@link isConPTYHosted}. Tests set this so the kitty-flag and write-chunking
|
|
180
|
+
* paths stay hermetic regardless of the ambient WSL env (`WSL_DISTRO_NAME` /
|
|
181
|
+
* `WSL_INTEROP`) — the suite must behave identically on WSL and on CI.
|
|
182
|
+
*/
|
|
183
|
+
conpty?: boolean;
|
|
184
|
+
}
|
|
175
185
|
/**
|
|
176
186
|
* Real terminal using process.stdin/stdout
|
|
177
187
|
*/
|
|
178
188
|
export declare class ProcessTerminal implements Terminal {
|
|
179
189
|
#private;
|
|
190
|
+
constructor(options?: ProcessTerminalOptions);
|
|
180
191
|
get kittyProtocolActive(): boolean;
|
|
181
192
|
get kittyEnableSequence(): string | null;
|
|
182
193
|
get keyboardEnhancementEnterSequence(): string | null;
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -76,6 +76,14 @@ export interface TUIStartOptions {
|
|
|
76
76
|
* which leading rows survived.
|
|
77
77
|
*/
|
|
78
78
|
export interface Component {
|
|
79
|
+
/** Stable identifier surfaced in the debug tree as kind#id. */
|
|
80
|
+
debugId?: string;
|
|
81
|
+
/** Override for the tree node kind (default: constructor.name). */
|
|
82
|
+
debugKind?: string;
|
|
83
|
+
/** Widget state for the debug `values`/`tree` ops. JSON-serializable. */
|
|
84
|
+
debugState?(): Record<string, unknown>;
|
|
85
|
+
/** Children for the debug tree when not already exposed as a public `children` array. */
|
|
86
|
+
debugChildren?: readonly Component[];
|
|
79
87
|
/**
|
|
80
88
|
* Render the component to an array of physical rows at the given width.
|
|
81
89
|
* The result is component-owned and `readonly` to the caller; an unchanged
|
|
@@ -292,9 +300,31 @@ export declare class TUI extends Container {
|
|
|
292
300
|
* positive report, disabled on a negative one.
|
|
293
301
|
*/
|
|
294
302
|
get synchronizedOutput(): boolean;
|
|
303
|
+
/**
|
|
304
|
+
* Cost in milliseconds of the most recently completed frame.
|
|
305
|
+
*
|
|
306
|
+
* Animation components use this to apply proportional backpressure after
|
|
307
|
+
* their render request is asynchronously composed and written.
|
|
308
|
+
*/
|
|
309
|
+
get lastFrameCostMs(): number;
|
|
295
310
|
setFocus(component: Component | null): void;
|
|
296
311
|
/** Component currently receiving keyboard input, if any. */
|
|
297
312
|
getFocused(): Component | null;
|
|
313
|
+
/** Last viewport successfully written by the renderer, for debug inspection. */
|
|
314
|
+
getDebugPaint(): {
|
|
315
|
+
lines: readonly string[];
|
|
316
|
+
windowTop: number;
|
|
317
|
+
altScreen: boolean;
|
|
318
|
+
cursor?: {
|
|
319
|
+
x: number;
|
|
320
|
+
y: number;
|
|
321
|
+
visible?: boolean;
|
|
322
|
+
};
|
|
323
|
+
} | undefined;
|
|
324
|
+
/** Render the current root document at the live terminal width for debug inspection. */
|
|
325
|
+
getDebugDocument(): readonly string[];
|
|
326
|
+
/** Feed debug and test input through the same pipeline as terminal stdin. */
|
|
327
|
+
injectDebugInput(data: string): void;
|
|
298
328
|
/**
|
|
299
329
|
* Show an overlay component with configurable positioning and sizing.
|
|
300
330
|
* Returns a handle to control the overlay's visibility.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.0.
|
|
4
|
+
"version": "18.0.9",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.0.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.0.
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.0.9",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.0.9"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/components/box.ts
CHANGED
|
@@ -59,6 +59,16 @@ export class Box implements Component {
|
|
|
59
59
|
this.#bgFn = bgFn;
|
|
60
60
|
this.#border = border;
|
|
61
61
|
}
|
|
62
|
+
/** Return container layout and child-count state for debug inspection. */
|
|
63
|
+
debugState(): Record<string, unknown> {
|
|
64
|
+
return {
|
|
65
|
+
childCount: this.children.length,
|
|
66
|
+
paddingX: this.#paddingX,
|
|
67
|
+
paddingY: this.#paddingY,
|
|
68
|
+
bordered: this.#border !== undefined,
|
|
69
|
+
ignoreTight: this.#ignoreTight,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
62
72
|
|
|
63
73
|
addChild(component: Component): void {
|
|
64
74
|
this.children.push(component);
|
|
@@ -25,6 +25,13 @@ export class CancellableLoader extends Loader {
|
|
|
25
25
|
get aborted(): boolean {
|
|
26
26
|
return this.#abortController.signal.aborted;
|
|
27
27
|
}
|
|
28
|
+
/** Return loader state including whether cancellation was requested. */
|
|
29
|
+
override debugState(): Record<string, unknown> {
|
|
30
|
+
return {
|
|
31
|
+
...super.debugState(),
|
|
32
|
+
cancelled: this.aborted,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
28
35
|
|
|
29
36
|
handleInput(data: string): void {
|
|
30
37
|
const kb = getKeybindings();
|
package/src/components/editor.ts
CHANGED
|
@@ -595,6 +595,34 @@ export class Editor implements Component, Focusable {
|
|
|
595
595
|
this.#theme = theme;
|
|
596
596
|
this.borderColor = theme.borderColor;
|
|
597
597
|
}
|
|
598
|
+
/** Return bounded editor content, cursor, and transient child state for debug inspection. */
|
|
599
|
+
debugState(): Record<string, unknown> {
|
|
600
|
+
const lines = this.#state.lines;
|
|
601
|
+
let textLength = Math.max(0, lines.length - 1);
|
|
602
|
+
let textPreview = "";
|
|
603
|
+
for (let i = 0; i < lines.length; i++) {
|
|
604
|
+
const line = lines[i] ?? "";
|
|
605
|
+
textLength += line.length;
|
|
606
|
+
if (textPreview.length >= 120) continue;
|
|
607
|
+
if (i > 0) textPreview += "\n";
|
|
608
|
+
textPreview += line.slice(0, 120 - textPreview.length);
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
textPreview,
|
|
612
|
+
textLength,
|
|
613
|
+
previewTruncated: textLength > 120,
|
|
614
|
+
cursorLine: this.#state.cursorLine,
|
|
615
|
+
cursorCol: this.#state.cursorCol,
|
|
616
|
+
lineCount: lines.length,
|
|
617
|
+
selection: null,
|
|
618
|
+
placeholderActive: false,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Expose the active autocomplete list to the debug tree walker. */
|
|
623
|
+
get debugChildren(): readonly Component[] {
|
|
624
|
+
return this.#autocompleteList ? [this.#autocompleteList] : [];
|
|
625
|
+
}
|
|
598
626
|
|
|
599
627
|
setTheme(theme: EditorTheme): void {
|
|
600
628
|
this.#theme = theme;
|
package/src/components/image.ts
CHANGED
|
@@ -505,6 +505,17 @@ export class Image implements Component {
|
|
|
505
505
|
this.#budget = options.budget;
|
|
506
506
|
this.#imageId = options.budget ? options.budget.acquireId(options.imageKey) : undefined;
|
|
507
507
|
}
|
|
508
|
+
/** Return source metadata without exposing the encoded image buffer. */
|
|
509
|
+
debugState(): Record<string, unknown> {
|
|
510
|
+
return {
|
|
511
|
+
mimeType: this.#mimeType,
|
|
512
|
+
widthPx: this.#dimensions.widthPx,
|
|
513
|
+
heightPx: this.#dimensions.heightPx,
|
|
514
|
+
filename: this.#options.filename ?? null,
|
|
515
|
+
imageId: this.#imageId ?? null,
|
|
516
|
+
suppressed: this.#cachedSuppressed,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
508
519
|
|
|
509
520
|
invalidate(): void {
|
|
510
521
|
this.#cachedLines = undefined;
|
package/src/components/input.ts
CHANGED
|
@@ -51,6 +51,19 @@ export class Input implements Component, Focusable {
|
|
|
51
51
|
getValue(): string {
|
|
52
52
|
return this.#value;
|
|
53
53
|
}
|
|
54
|
+
/** Return bounded input content and cursor state for debug inspection. */
|
|
55
|
+
debugState(): Record<string, unknown> {
|
|
56
|
+
return {
|
|
57
|
+
textPreview: this.#value.slice(0, 120),
|
|
58
|
+
textLength: this.#value.length,
|
|
59
|
+
previewTruncated: this.#value.length > 120,
|
|
60
|
+
cursor: this.#cursor,
|
|
61
|
+
cursorLine: 0,
|
|
62
|
+
selection: null,
|
|
63
|
+
placeholderActive: false,
|
|
64
|
+
masked: this.mask,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
54
67
|
|
|
55
68
|
setValue(value: string): void {
|
|
56
69
|
this.#value = value;
|
package/src/components/loader.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Text } from "./text";
|
|
|
5
5
|
const RENDER_INTERVAL_MS = 1000 / 30;
|
|
6
6
|
const SPINNER_ADVANCE_MS = 80;
|
|
7
7
|
const RENDER_BACKPRESSURE_MULTIPLIER = 9;
|
|
8
|
+
const MAX_BACKPRESSURE_FRAME_COST_MS = 200;
|
|
8
9
|
|
|
9
10
|
type ColorFn = (str: string) => string;
|
|
10
11
|
|
|
@@ -53,6 +54,16 @@ export class Loader extends Text {
|
|
|
53
54
|
this.#layoutFrame = this.#layoutFrames[0];
|
|
54
55
|
this.start();
|
|
55
56
|
}
|
|
57
|
+
/** Return the current message and animation state for debug inspection. */
|
|
58
|
+
override debugState(): Record<string, unknown> {
|
|
59
|
+
const message = this.#resolveMessage();
|
|
60
|
+
return {
|
|
61
|
+
message: message.slice(0, 120),
|
|
62
|
+
messageLength: message.length,
|
|
63
|
+
running: this.#intervalId !== undefined,
|
|
64
|
+
frame: this.#currentFrame,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
56
67
|
|
|
57
68
|
override render(width: number): readonly string[] {
|
|
58
69
|
const source = super.render(width);
|
|
@@ -139,12 +150,17 @@ export class Loader extends Text {
|
|
|
139
150
|
this.#requestPaint();
|
|
140
151
|
}
|
|
141
152
|
|
|
142
|
-
const
|
|
153
|
+
const completedFrameCostMs = this.#ui?.lastFrameCostMs ?? 0;
|
|
154
|
+
const requestCostMs = performance.now() - startedAt;
|
|
143
155
|
if (this.#intervalId !== timer) return;
|
|
144
|
-
const cadenceDelayMs = Math.max(0, intervalMs -
|
|
145
|
-
// Idle for nine times the
|
|
146
|
-
// 10% CPU
|
|
147
|
-
const
|
|
156
|
+
const cadenceDelayMs = Math.max(0, intervalMs - requestCostMs);
|
|
157
|
+
// Idle for nine times the full frame cost to keep animation at or
|
|
158
|
+
// below 10% CPU even though requestComponentRender() only enqueues.
|
|
159
|
+
const boundedFrameCostMs = Math.min(
|
|
160
|
+
MAX_BACKPRESSURE_FRAME_COST_MS,
|
|
161
|
+
Math.max(completedFrameCostMs, requestCostMs),
|
|
162
|
+
);
|
|
163
|
+
const backpressureDelayMs = boundedFrameCostMs * RENDER_BACKPRESSURE_MULTIPLIER;
|
|
148
164
|
this.#scheduleTick(intervalMs, Math.max(cadenceDelayMs, backpressureDelayMs));
|
|
149
165
|
}, delayMs);
|
|
150
166
|
this.#intervalId = timer;
|
|
@@ -1539,12 +1539,28 @@ function classifyHexColor(hex: string, strict: boolean): boolean {
|
|
|
1539
1539
|
return true;
|
|
1540
1540
|
}
|
|
1541
1541
|
|
|
1542
|
-
/**
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1542
|
+
/** Black-or-white foreground legible on `#${hex}` used as a fill. VS Code's
|
|
1543
|
+
* rule (Color.isLighter): YIQ brightness (r·299 + g·587 + b·114)/1000 ≥ 128
|
|
1544
|
+
* → dark text, else light. */
|
|
1545
|
+
function swatchContrastFg(hex: string): string {
|
|
1546
|
+
const rgba = Bun.color(`#${hex}`, "{rgba}");
|
|
1547
|
+
if (!rgba) return "";
|
|
1548
|
+
const light = (rgba.r * 299 + rgba.g * 587 + rgba.b * 114) / 1000 >= 128;
|
|
1549
|
+
if (TERMINAL.trueColor) return light ? "\x1b[38;2;0;0;0m" : "\x1b[38;2;255;255;255m";
|
|
1550
|
+
return light ? "\x1b[38;5;16m" : "\x1b[38;5;231m";
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/** Chip glyph + `text` (the `#hex` mention) painted onto the color itself:
|
|
1554
|
+
* a fg-painted chip, then the token with the color as background and a
|
|
1555
|
+
* YIQ-contrast foreground (VS Code's color-picker rule). Returns "" when the
|
|
1556
|
+
* color can't be encoded. Foreground closes with \x1b[39m and background with
|
|
1557
|
+
* a standalone \x1b[49m so line-level backgrounds re-open themselves via
|
|
1558
|
+
* applyBackgroundToLine / Theme.bgFill. */
|
|
1559
|
+
function colorSwatch(hex: string, glyph: string, text: string): string {
|
|
1560
|
+
const fg = Bun.color(`#${hex}`, TERMINAL.trueColor ? "ansi-16m" : "ansi-256");
|
|
1561
|
+
if (!fg) return "";
|
|
1562
|
+
const bg = fg.replace("[38;", "[48;");
|
|
1563
|
+
return `${fg}${glyph}\x1b[39m ${bg}${swatchContrastFg(hex)}${text}\x1b[39m\x1b[49m`;
|
|
1548
1564
|
}
|
|
1549
1565
|
|
|
1550
1566
|
/**
|
|
@@ -1560,10 +1576,10 @@ function renderTextWithSwatches(text: string, applySegment: (t: string) => strin
|
|
|
1560
1576
|
const match = HEX_COLOR_REGEX.exec(text);
|
|
1561
1577
|
if (match === null) break;
|
|
1562
1578
|
if (!classifyHexColor(match[1], true)) continue;
|
|
1563
|
-
const swatch = colorSwatch(match[1], glyph);
|
|
1579
|
+
const swatch = colorSwatch(match[1], glyph, match[0]);
|
|
1564
1580
|
if (!swatch) continue;
|
|
1565
1581
|
if (match.index > last) result += applySegment(text.slice(last, match.index));
|
|
1566
|
-
result += swatch
|
|
1582
|
+
result += swatch;
|
|
1567
1583
|
last = match.index + match[0].length;
|
|
1568
1584
|
}
|
|
1569
1585
|
if (last === 0) return applySegment(text);
|
|
@@ -1575,7 +1591,7 @@ function renderTextWithSwatches(text: string, applySegment: (t: string) => strin
|
|
|
1575
1591
|
function codespanSwatch(code: string, glyph: string): string {
|
|
1576
1592
|
const match = HEX_COLOR_EXACT_REGEX.exec(code.trim());
|
|
1577
1593
|
if (!match || !classifyHexColor(match[1], false)) return "";
|
|
1578
|
-
return colorSwatch(match[1], glyph);
|
|
1594
|
+
return colorSwatch(match[1], glyph, match[0]);
|
|
1579
1595
|
}
|
|
1580
1596
|
|
|
1581
1597
|
interface RenderSignature {
|
|
@@ -1757,6 +1773,18 @@ export class Markdown implements Component {
|
|
|
1757
1773
|
this.#defaultTextStyle = defaultTextStyle;
|
|
1758
1774
|
this.#codeBlockIndent = Math.max(0, Math.floor(codeBlockIndent));
|
|
1759
1775
|
}
|
|
1776
|
+
/** Return bounded source text and layout state for debug inspection. */
|
|
1777
|
+
debugState(): Record<string, unknown> {
|
|
1778
|
+
return {
|
|
1779
|
+
textPreview: this.#text.slice(0, 120),
|
|
1780
|
+
textLength: this.#text.length,
|
|
1781
|
+
previewTruncated: this.#text.length > 120,
|
|
1782
|
+
paddingX: this.#paddingX,
|
|
1783
|
+
paddingY: this.#paddingY,
|
|
1784
|
+
codeBlockIndent: this.#codeBlockIndent,
|
|
1785
|
+
ignoreTight: this.#ignoreTight,
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1760
1788
|
|
|
1761
1789
|
setText(text: string): boolean {
|
|
1762
1790
|
// Identical re-emit (throttled tick): fully normalized already.
|
|
@@ -3118,7 +3146,8 @@ export class Markdown implements Component {
|
|
|
3118
3146
|
|
|
3119
3147
|
case "codespan": {
|
|
3120
3148
|
markHtmlItemWhenContent(token.text);
|
|
3121
|
-
|
|
3149
|
+
const painted = codespanSwatch(token.text, swatchGlyph);
|
|
3150
|
+
result += (painted || this.#theme.code(token.text)) + stylePrefix;
|
|
3122
3151
|
break;
|
|
3123
3152
|
}
|
|
3124
3153
|
|
|
@@ -79,6 +79,18 @@ export class ScrollView implements Component {
|
|
|
79
79
|
this.#fastScrollLines = Math.max(1, Math.trunc(options.fastScrollLines ?? 5));
|
|
80
80
|
this.#clampScrollOffset();
|
|
81
81
|
}
|
|
82
|
+
/** Return viewport, content, and scroll position state for debug inspection. */
|
|
83
|
+
debugState(): Record<string, unknown> {
|
|
84
|
+
const rowCount = this.#totalRows ?? this.#lines.length;
|
|
85
|
+
return {
|
|
86
|
+
scrollOffset: this.#scrollOffset,
|
|
87
|
+
maxScrollOffset: this.getMaxScrollOffset(),
|
|
88
|
+
height: this.#height,
|
|
89
|
+
rowCount,
|
|
90
|
+
bufferedLineCount: this.#lines.length,
|
|
91
|
+
scrollbar: this.#scrollbar,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
82
94
|
|
|
83
95
|
setLines(lines: readonly string[]): void {
|
|
84
96
|
this.#lines = [...lines];
|
|
@@ -116,6 +116,19 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
116
116
|
this.#maxVisible = Math.max(1, Math.trunc(maxVisible));
|
|
117
117
|
this.#filteredItems = items;
|
|
118
118
|
}
|
|
119
|
+
/** Return item, selection, and filter state for debug inspection. */
|
|
120
|
+
debugState(): Record<string, unknown> {
|
|
121
|
+
const selected = this.#filteredItems[this.#selectedIndex];
|
|
122
|
+
return {
|
|
123
|
+
itemCount: this.items.length,
|
|
124
|
+
filteredItemCount: this.#filteredItems.length,
|
|
125
|
+
selectedIndex: this.#filteredItems.length > 0 ? this.#selectedIndex : -1,
|
|
126
|
+
selectedItemId: selected?.value ?? null,
|
|
127
|
+
selectedItemLabel: selected?.label ?? null,
|
|
128
|
+
filterText: this.#filterQuery,
|
|
129
|
+
maxVisible: this.#maxVisible,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
119
132
|
|
|
120
133
|
/** Refit the visible row budget (hosts clamp the list to available height). */
|
|
121
134
|
setMaxVisible(rows: number): void {
|
|
@@ -142,6 +142,25 @@ export class SettingsList implements Component {
|
|
|
142
142
|
this.#selectedIndex = this.#firstSelectableIndex();
|
|
143
143
|
this.#lastNotifiedSelectionId = this.getSelectedItem()?.id;
|
|
144
144
|
}
|
|
145
|
+
/** Return item, selection, filter, and submenu state for debug inspection. */
|
|
146
|
+
debugState(): Record<string, unknown> {
|
|
147
|
+
const selected = this.getSelectedItem();
|
|
148
|
+
return {
|
|
149
|
+
itemCount: this.#items.length,
|
|
150
|
+
filteredItemCount: this.#filteredItems.length,
|
|
151
|
+
selectedIndex: this.#filteredItems.length > 0 ? this.#selectedIndex : -1,
|
|
152
|
+
selectedItemId: selected?.id ?? null,
|
|
153
|
+
selectedItemLabel: selected?.label ?? null,
|
|
154
|
+
filterText: this.#filterQuery,
|
|
155
|
+
sectionFocused: this.#sectionFocus,
|
|
156
|
+
submenuOpen: this.#submenuComponent !== null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Expose the active submenu to the debug tree walker. */
|
|
161
|
+
get debugChildren(): readonly Component[] {
|
|
162
|
+
return this.#submenuComponent ? [this.#submenuComponent] : [];
|
|
163
|
+
}
|
|
145
164
|
|
|
146
165
|
/** The currently selected item, or undefined when empty or on a heading. */
|
|
147
166
|
getSelectedItem(): SettingItem | undefined {
|
package/src/components/spacer.ts
CHANGED
|
@@ -10,6 +10,10 @@ export class Spacer implements Component {
|
|
|
10
10
|
constructor(lines: number = 1) {
|
|
11
11
|
this.#lines = lines;
|
|
12
12
|
}
|
|
13
|
+
/** Return the spacer height for debug inspection. */
|
|
14
|
+
debugState(): Record<string, unknown> {
|
|
15
|
+
return { height: this.#lines };
|
|
16
|
+
}
|
|
13
17
|
|
|
14
18
|
setLines(lines: number): void {
|
|
15
19
|
if (lines === this.#lines) return;
|