@linxiraos/pi-tui 1.1.5 → 1.1.7

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +7 -2
  2. package/dist/types/components/box.d.ts +2 -0
  3. package/dist/types/components/cancellable-loader.d.ts +2 -0
  4. package/dist/types/components/composer/band.d.ts +2 -0
  5. package/dist/types/components/composer/index.d.ts +1 -0
  6. package/dist/types/components/composer/registry.d.ts +7 -0
  7. package/dist/types/components/composer/types.d.ts +9 -3
  8. package/dist/types/components/editor.d.ts +6 -0
  9. package/dist/types/components/image.d.ts +2 -0
  10. package/dist/types/components/input.d.ts +2 -0
  11. package/dist/types/components/loader.d.ts +8 -0
  12. package/dist/types/components/markdown.d.ts +2 -2
  13. package/dist/types/components/scroll-view.d.ts +2 -0
  14. package/dist/types/components/select-list.d.ts +2 -0
  15. package/dist/types/components/settings-list.d.ts +4 -0
  16. package/dist/types/components/spacer.d.ts +2 -0
  17. package/dist/types/components/tab-bar.d.ts +2 -0
  18. package/dist/types/components/text.d.ts +2 -0
  19. package/dist/types/components/truncated-text.d.ts +2 -0
  20. package/dist/types/debug-server.d.ts +27 -0
  21. package/dist/types/index.d.ts +1 -0
  22. package/dist/types/latex-to-unicode.d.ts +4 -13
  23. package/dist/types/terminal-capabilities.d.ts +26 -0
  24. package/dist/types/terminal.d.ts +11 -0
  25. package/dist/types/tui.d.ts +30 -0
  26. package/package.json +3 -3
  27. package/src/autocomplete.ts +50 -8
  28. package/src/components/box.ts +10 -0
  29. package/src/components/cancellable-loader.ts +7 -0
  30. package/src/components/composer/band.ts +42 -0
  31. package/src/components/composer/field.ts +1 -0
  32. package/src/components/composer/index.ts +1 -0
  33. package/src/components/composer/rail.ts +1 -0
  34. package/src/components/composer/registry.ts +12 -0
  35. package/src/components/composer/types.ts +18 -3
  36. package/src/components/editor.ts +63 -11
  37. package/src/components/image.ts +11 -0
  38. package/src/components/input.ts +13 -0
  39. package/src/components/loader.ts +40 -7
  40. package/src/components/markdown.ts +60 -75
  41. package/src/components/scroll-view.ts +12 -0
  42. package/src/components/select-list.ts +13 -0
  43. package/src/components/settings-list.ts +19 -0
  44. package/src/components/spacer.ts +4 -0
  45. package/src/components/tab-bar.ts +11 -0
  46. package/src/components/text.ts +11 -0
  47. package/src/components/truncated-text.ts +15 -1
  48. package/src/debug-server.ts +422 -0
  49. package/src/index.ts +2 -0
  50. package/src/latex-to-unicode.ts +416 -230
  51. package/src/terminal-capabilities.ts +66 -9
  52. package/src/terminal.ts +22 -2
  53. package/src/tui.ts +133 -6
  54. package/src/utils.ts +10 -1
package/CHANGELOG.md CHANGED
@@ -2,9 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [1.1.5] - 2026-08-26
5
+ ## [1.1.7] - 2026-09-01
6
6
 
7
- - 同步上游 OMP v18.0.5 / v18.0.6:append-only transcript 声明与稳定行 API、markdown 渲染重构。
7
+ - 同步上游 OMP v18.0.11(`b8ce33a58911c26bed1d84f0db9a5e2e727c49a2`)。
8
+
9
+ ## [1.1.6] - 2026-08-30
10
+
11
+ - 同步上游 OMP v18.0.10(`33cc6b9a043a`)。
12
+ - 同步上游 OMP v18.0.9(`cc14e04f075d`)。
8
13
 
9
14
  ## [1.1.3] - 2026-08-25
10
15
 
@@ -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
  }
@@ -0,0 +1,2 @@
1
+ import type { ComposerStyle } from "./types.js";
2
+ export declare const bandComposerStyle: ComposerStyle;
@@ -1,3 +1,4 @@
1
+ export * from "./band.js";
1
2
  export * from "./borderless.js";
2
3
  export * from "./box.js";
3
4
  export * from "./claude.js";
@@ -1,6 +1,13 @@
1
1
  import type { ComposerStyle, EditorBorderStyle } from "./types.js";
2
2
  /** Whether an id names a composer style shipped by pi-tui. */
3
3
  export declare function isBuiltinComposerStyle(id: string): boolean;
4
+ /**
5
+ * Whether a style paints its own row foreground.
6
+ *
7
+ * Extensions registered before `filledSurface` existed received undecorated
8
+ * row text, so an omitted flag remains filled for extension-owned ids.
9
+ */
10
+ export declare function isFilledComposerStyle(style: ComposerStyle): boolean;
4
11
  /**
5
12
  * Register one extension-owned composer style for this process.
6
13
  *
@@ -10,7 +10,7 @@ import type { SymbolTheme } from "../../symbols.js";
10
10
  /** Box-drawing glyph set used for composer chrome (the theme's `boxRound`). */
11
11
  export type ComposerBox = SymbolTheme["boxRound"];
12
12
  /** Built-in composer shape identifiers shipped by pi-tui. */
13
- export declare const BUILTIN_EDITOR_BORDER_STYLES: readonly ["box", "claude", "pi", "borderless", "rule", "field", "rail"];
13
+ export declare const BUILTIN_EDITOR_BORDER_STYLES: readonly ["box", "band", "claude", "pi", "borderless", "rule", "field", "rail"];
14
14
  /** Identifier for a built-in composer shape. */
15
15
  export type BuiltinEditorBorderStyle = (typeof BUILTIN_EDITOR_BORDER_STYLES)[number];
16
16
  /** Composer shape identifier; extensions may register additional strings. */
@@ -60,14 +60,20 @@ export interface ComposerRowContext extends ComposerChromeContext {
60
60
  }
61
61
  export interface ComposerStyle {
62
62
  readonly id: EditorBorderStyle;
63
+ /**
64
+ * True when rows paint their own foreground through `surfaceColor`.
65
+ * Built-ins default to transparent; registered extensions that omit this
66
+ * field retain the pre-field behavior and receive undecorated row text.
67
+ */
68
+ readonly filledSurface?: boolean;
63
69
  /** Content rows carry left/right border glyphs; drives the cursor-reserve
64
70
  * column, IME-safe layout, and the right-border scrollbar. */
65
71
  readonly sideBorders: boolean;
66
72
  /** Rows consumed by top+bottom chrome (drives maxHeight budgeting). */
67
73
  readonly verticalChrome: 0 | 1 | 2;
68
74
  /** Where the host should attach the status bar: embedded in the top border,
69
- * docked onto a top rule, or detached into a standalone bottom bar. */
70
- readonly statusAttachment: "top-border" | "top-rule-chip" | "none";
75
+ * rendered as a flush soft-capped band above the input, docked onto a top rule, or detached into a standalone bottom bar. */
76
+ readonly statusAttachment: "top-border" | "top-band" | "top-rule-chip" | "none";
71
77
  /** Which segment groups the standalone bottom status bar shows. */
72
78
  readonly bottomBar: "none" | "left" | "full";
73
79
  /** Insert a blank spacer row between the editor and the standalone bottom
@@ -10,6 +10,8 @@ export interface EditorTheme {
10
10
  accentColor?: (str: string) => string;
11
11
  /** Background fill used by filled composer styles. */
12
12
  surfaceColor?: (str: string) => string;
13
+ /** Foreground used when the composer shape leaves its text surface transparent. */
14
+ textColor?: (str: string) => string;
13
15
  selectList: SelectListTheme;
14
16
  symbols: SymbolTheme;
15
17
  editorPaddingX?: number;
@@ -92,6 +94,10 @@ export declare class Editor implements Component, Focusable {
92
94
  onAutocompleteCancel?: () => void;
93
95
  disableSubmit: boolean;
94
96
  constructor(theme: EditorTheme);
97
+ /** Return bounded editor content, cursor, and transient child state for debug inspection. */
98
+ debugState(): Record<string, unknown>;
99
+ /** Expose the active autocomplete list to the debug tree walker. */
100
+ get debugChildren(): readonly Component[];
95
101
  setTheme(theme: EditorTheme): void;
96
102
  setAutocompleteProvider(provider: AutocompleteProvider): void;
97
103
  /** 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;
@@ -1,5 +1,7 @@
1
1
  import type { TUI } from "../tui.js";
2
2
  import { Text } from "./text.js";
3
+ /** Milliseconds between spinner-frame advances; exported so time-derived spinners elsewhere tick at the Loader cadence. */
4
+ export declare const SPINNER_ADVANCE_MS = 80;
3
5
  type ColorFn = (str: string) => string;
4
6
  /**
5
7
  * Styles Loader message fragments without changing their visible text or width.
@@ -15,11 +17,17 @@ export declare class Loader extends Text {
15
17
  private messageColorFn;
16
18
  private message;
17
19
  constructor(ui: TUI, spinnerColorFn: ColorFn, messageColorFn: LoaderMessageColorFn, message?: string | (() => string), spinnerFrames?: string[]);
20
+ /** Return the current message and animation state for debug inspection. */
21
+ debugState(): Record<string, unknown>;
18
22
  render(width: number): readonly string[];
19
23
  start(): void;
20
24
  stop(): void;
21
25
  /** Lifecycle teardown: stop the animation timer. Idempotent. */
22
26
  dispose(): void;
27
+ /** Install a lazy right-docked suffix for the spinner row (e.g. a styled
28
+ * session title). Re-evaluated every paint; dropped when the row leaves
29
+ * less than a two-cell gap. */
30
+ setTrailer(trailer: (() => string | undefined) | undefined): void;
23
31
  setMessage(message: string): void;
24
32
  }
25
33
  export {};
@@ -1,8 +1,6 @@
1
1
  import type { SymbolTheme } from "../symbols.js";
2
2
  import type { Component } from "../tui.js";
3
3
  /** @internal exported for tests — must stay index-identical to the old regex scan. */
4
- export declare function mathStartIndex(src: string): number | undefined;
5
- /** @internal exported for tests — must stay index-identical to the old regex scan. */
6
4
  export declare function autolinkSchemeScanIndex(src: string): number | undefined;
7
5
  /** @internal exported for tests — must never return false for a src the built-in url regex matches. */
8
6
  export declare function urlTokenPossible(src: string): boolean;
@@ -81,6 +79,8 @@ export declare class Markdown implements Component {
81
79
  #private;
82
80
  setIgnoreTight(ignore: boolean): this;
83
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>;
84
84
  setText(text: string): boolean;
85
85
  invalidate(): void;
86
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
+ }
@@ -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 (`\mathbf`, `\mathbb`, …) whose single brace argument
3
- * restyles glyphs. Exported for the display block engine (`latex-block`), which
4
- * re-wraps inline runs inside these commands when their argument contains 2-D
5
- * layout (fractions, matrices) so styling survives box boundaries.
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
  /**
@@ -39,12 +39,3 @@ export declare function isBareMathEnvironment(env: string): boolean;
39
39
  * $10" is left untouched.
40
40
  */
41
41
  export declare function renderMathInText(text: string): string;
42
- /**
43
- * Index of the `$` that closes an inline math span opened at `open` (the index
44
- * of the opening `$`), or -1 when the run is not inline math. Applies pandoc's
45
- * anti-currency heuristics: the opener must not be followed by whitespace, the
46
- * closer must not be preceded by whitespace nor followed by a digit, `\$` is a
47
- * literal dollar, and the span may not span a newline. Shared by
48
- * `renderMathInText` and the markdown math tokenizer so the rule has one home.
49
- */
50
- export declare function inlineMathSpanEnd(text: string, open: number): number;
@@ -153,6 +153,23 @@ export declare function shouldEnableHyperlinksByDefault(env?: NodeJS.ProcessEnv,
153
153
  * without mutating `process.platform`.
154
154
  */
155
155
  export declare function resolveWarpImageProtocol(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): ImageProtocol | null;
156
+ /**
157
+ * Paseo (getpaseo/paseo) hardcodes `TERM_PROGRAM=kitty` into every PTY
158
+ * (`buildTerminalEnvironment`) while its xterm.js renderer implements neither
159
+ * Kitty graphics nor Unicode placeholders — trusting the advertisement turns
160
+ * image previews into literal PUA garbage (getpaseo/paseo#3850). Paseo
161
+ * injects `PASEO_TERMINAL_ID` into every terminal it hosts, which makes a
162
+ * reliable embedder signal.
163
+ */
164
+ export declare function isPaseoEmbedder(env?: NodeJS.ProcessEnv): boolean;
165
+ /**
166
+ * Resolve the image protocol for a non-forced runtime: static per-terminal
167
+ * support (with Warp's platform carve-out), then the multiplexer fallback,
168
+ * then the Paseo embedder carve-out. `isTTY` is injectable because the
169
+ * fallback only fires on a real TTY — a piped subprocess cannot exercise
170
+ * that path, so regression tests call this directly.
171
+ */
172
+ export declare function resolveImageProtocol(terminalId: TerminalId, env?: NodeJS.ProcessEnv, isTTY?: boolean): ImageProtocol | null;
156
173
  /** Resolve terminal identity from environment markers used by common emulators. */
157
174
  export declare function detectTerminalId(env?: NodeJS.ProcessEnv): TerminalId;
158
175
  export declare const TERMINAL_ID: TerminalId;
@@ -189,6 +206,15 @@ export declare function setTerminalScreenToScrollback(enabled: boolean): void;
189
206
  * capability); tests flip it directly to exercise the scaled-heading path.
190
207
  */
191
208
  export declare function setTerminalTextSizing(enabled: boolean): void;
209
+ /**
210
+ * Override OSC 8 hyperlink capability at runtime. The coding-agent calls this
211
+ * from the `tui.hyperlinks` setting so its resolved policy (`off`/`auto`/`always`)
212
+ * drives every renderer that gates on {@link TERMINAL}`.hyperlinks` — notably the
213
+ * Markdown component's `[text](url)`/bare-URL links — consistently with the
214
+ * path/resource links that already consult the setting directly. Tests flip it
215
+ * to exercise the OSC 8 and plain-text paths deterministically.
216
+ */
217
+ export declare function setTerminalHyperlinks(enabled: boolean): void;
192
218
  export declare function getTerminalInfo(terminalId: TerminalId, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): TerminalInfo;
193
219
  export interface CellDimensions {
194
220
  widthPx: number;
@@ -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;
@@ -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": "@linxiraos/pi-tui",
4
- "version": "1.1.5",
4
+ "version": "1.1.7",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://linxira-os.github.io/zeta/",
7
7
  "author": "Stencil Labs, Inc.",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@linxiraos/pi-natives": "1.1.5",
41
- "@linxiraos/pi-utils": "1.1.5"
40
+ "@linxiraos/pi-natives": "1.1.7",
41
+ "@linxiraos/pi-utils": "1.1.7"
42
42
  },
43
43
  "devDependencies": {
44
44
  "kitty-vt-wasm": "^0.2.0"
@@ -331,10 +331,19 @@ function buildSlashCommandCompletions(
331
331
  let best: (AutocompleteItem & { score: number; usage: number }) | undefined;
332
332
 
333
333
  const isSkillCommand = name.startsWith(SKILL_NAMESPACE);
334
+ // Skills are matched by their bare name as well as the full
335
+ // `skill:` name so a broken-out or mid-prompt skill ranks at
336
+ // prefix strength (`/batch` → `skill:batch`) instead of a weak
337
+ // full-name fuzzy hit.
334
338
  const nameScore =
335
339
  lowerPrefix.length === 0 && isSkillCommand
336
340
  ? 950
337
- : scoreCommandTextMatch(lowerPrefix, name.toLowerCase());
341
+ : isSkillCommand
342
+ ? Math.max(
343
+ scoreCommandTextMatch(lowerPrefix, name.toLowerCase()),
344
+ scoreCommandTextMatch(lowerPrefix, name.slice(SKILL_NAMESPACE.length).toLowerCase()),
345
+ )
346
+ : scoreCommandTextMatch(lowerPrefix, name.toLowerCase());
338
347
  const lowerDesc = staticDesc.toLowerCase();
339
348
  const descScore =
340
349
  lowerDesc && fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
@@ -391,23 +400,56 @@ function hasPromptTextBeforeSlash(
391
400
 
392
401
  export const SKILL_NAMESPACE = "skill:";
393
402
 
403
+ /**
404
+ * Match tier used to compare a skill's bare name against non-skill command
405
+ * names when deciding whether the skill may break out of the collapsed
406
+ * `skill:` group: exact (1000) > prefix (900) > anything weaker (0). Fuzzy
407
+ * hits deliberately map to 0 — a fuzzy skill match is never strong enough to
408
+ * mix skills into the command popup.
409
+ */
410
+ function skillBreakoutTier(lowerPrefix: string, lowerTarget: string): number {
411
+ if (lowerPrefix === lowerTarget) return 1000;
412
+ if (lowerTarget.startsWith(lowerPrefix)) return 900;
413
+ return 0;
414
+ }
415
+
394
416
  /**
395
417
  * Collapse `skill:*` commands into a single `/skill:` namespace row while the
396
- * typed prefix has not committed to the namespace. Until the prefix starts
397
- * with `skill:`, individual skills never list a lone group entry (shown only
398
- * while the prefix is still a prefix of `skill:`) keeps the `/` popup
399
- * readable. Accepting the group inserts `/skill:` without a trailing space so
400
- * the reopened popup expands to the individual skills.
418
+ * typed prefix has not committed to the namespace. A lone group entry (shown
419
+ * only while the prefix is still a prefix of `skill:`) keeps the `/` popup
420
+ * readable. A skill breaks out of the group only when its bare name matches
421
+ * the prefix at a strictly stronger tier than every non-skill command name
422
+ * and alias (`/batch` `skill:batch` while no command prefix-matches
423
+ * `batch`); a tie keeps the popup command-only, and fuzzy-only skill hits
424
+ * never surface. Accepting the group inserts `/skill:` without a trailing
425
+ * space so the reopened popup expands to the individual skills.
401
426
  */
402
427
  function collapseSkillNamespace(commands: CommandEntry[], lowerPrefix: string): CommandEntry[] {
403
428
  if (lowerPrefix.startsWith(SKILL_NAMESPACE)) return commands;
429
+ const approachesNamespace = SKILL_NAMESPACE.startsWith(lowerPrefix);
430
+ let commandTier = 0;
431
+ if (!approachesNamespace) {
432
+ for (const cmd of commands) {
433
+ const name = getCommandName(cmd);
434
+ if (!name || name.startsWith(SKILL_NAMESPACE)) continue;
435
+ commandTier = Math.max(commandTier, skillBreakoutTier(lowerPrefix, name.toLowerCase()));
436
+ for (const alias of getCommandAliases(cmd)) {
437
+ commandTier = Math.max(commandTier, skillBreakoutTier(lowerPrefix, alias.toLowerCase()));
438
+ }
439
+ if (commandTier === 1000) break;
440
+ }
441
+ }
404
442
  let skillCount = 0;
405
443
  let skillIcon: string | undefined;
406
444
  const rest = commands.filter(cmd => {
407
- if (!getCommandName(cmd)?.startsWith(SKILL_NAMESPACE)) return true;
445
+ const name = getCommandName(cmd);
446
+ if (!name?.startsWith(SKILL_NAMESPACE)) return true;
408
447
  skillCount += 1;
409
448
  skillIcon ??= cmd.icon;
410
- return false;
449
+ return (
450
+ !approachesNamespace &&
451
+ skillBreakoutTier(lowerPrefix, name.slice(SKILL_NAMESPACE.length).toLowerCase()) > commandTier
452
+ );
411
453
  });
412
454
  if (skillCount === 0) return commands;
413
455
  if (!SKILL_NAMESPACE.startsWith(lowerPrefix)) return rest;
@@ -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();
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Soft status band composer (the rust omp default): the full status line sits
3
+ * flush against the terminal's left edge as a filled powerline band with a
4
+ * soft opening cap — no frame, rules, or corners — above an unboxed prompt
5
+ * anchored by a single curved `╰─ ` cue.
6
+ */
7
+ import { truncateToWidth } from "../../utils";
8
+ import type { ComposerChromeContext, ComposerRowContext, ComposerStyle } from "./types";
9
+
10
+ export const bandComposerStyle: ComposerStyle = {
11
+ id: "band",
12
+ sideBorders: false,
13
+ verticalChrome: 1,
14
+ statusAttachment: "top-band",
15
+ bottomBar: "none",
16
+ bottomBarGap: false,
17
+ defaultPromptGutter: "╰─ ",
18
+
19
+ defaultPaddingX(): number {
20
+ return 0;
21
+ },
22
+
23
+ sideChromeWidth(): number {
24
+ return 0;
25
+ },
26
+
27
+ renderTop(ctx: ComposerChromeContext): string | undefined {
28
+ const { topBorder, width } = ctx;
29
+ if (!topBorder?.content) return undefined;
30
+ // The band builder already sizes its groups + gauge to the full width;
31
+ // truncation only guards against a stale provider during resize.
32
+ return topBorder.width > width ? truncateToWidth(topBorder.content, width) : topBorder.content;
33
+ },
34
+
35
+ renderRow(ctx: ComposerRowContext): string[] {
36
+ return [(ctx.gutter ? ctx.borderColor(ctx.gutter) : "") + ctx.text + ctx.pad];
37
+ },
38
+
39
+ renderBottom(): undefined {
40
+ return undefined;
41
+ },
42
+ };
@@ -11,6 +11,7 @@ const RIGHT_CAP = "▌";
11
11
  /** One-row filled field with accent caps. */
12
12
  export const fieldComposerStyle: ComposerStyle = {
13
13
  id: "field",
14
+ filledSurface: true,
14
15
  sideBorders: true,
15
16
  verticalChrome: 0,
16
17
  statusAttachment: "none",
@@ -1,3 +1,4 @@
1
+ export * from "./band";
1
2
  export * from "./borderless";
2
3
  export * from "./box";
3
4
  export * from "./claude";
@@ -10,6 +10,7 @@ const ACCENT_RAIL = "▎";
10
10
  /** Filled composer surface anchored by a single left accent rail. */
11
11
  export const railComposerStyle: ComposerStyle = {
12
12
  id: "rail",
13
+ filledSurface: true,
13
14
  sideBorders: true,
14
15
  verticalChrome: 0,
15
16
  statusAttachment: "none",