@oh-my-pi/pi-tui 18.0.8 → 18.0.10

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 (46) hide show
  1. package/CHANGELOG.md +26 -0
  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/types.d.ts +3 -3
  7. package/dist/types/components/editor.d.ts +4 -0
  8. package/dist/types/components/image.d.ts +2 -0
  9. package/dist/types/components/input.d.ts +2 -0
  10. package/dist/types/components/loader.d.ts +8 -0
  11. package/dist/types/components/markdown.d.ts +2 -0
  12. package/dist/types/components/scroll-view.d.ts +2 -0
  13. package/dist/types/components/select-list.d.ts +2 -0
  14. package/dist/types/components/settings-list.d.ts +4 -0
  15. package/dist/types/components/spacer.d.ts +2 -0
  16. package/dist/types/components/tab-bar.d.ts +2 -0
  17. package/dist/types/components/text.d.ts +2 -0
  18. package/dist/types/components/truncated-text.d.ts +2 -0
  19. package/dist/types/debug-server.d.ts +27 -0
  20. package/dist/types/index.d.ts +1 -0
  21. package/dist/types/latex-to-unicode.d.ts +4 -4
  22. package/dist/types/tui.d.ts +23 -0
  23. package/package.json +3 -3
  24. package/src/components/box.ts +10 -0
  25. package/src/components/cancellable-loader.ts +7 -0
  26. package/src/components/composer/band.ts +42 -0
  27. package/src/components/composer/index.ts +1 -0
  28. package/src/components/composer/registry.ts +2 -0
  29. package/src/components/composer/types.ts +12 -3
  30. package/src/components/editor.ts +56 -10
  31. package/src/components/image.ts +11 -0
  32. package/src/components/input.ts +13 -0
  33. package/src/components/loader.ts +29 -2
  34. package/src/components/markdown.ts +39 -10
  35. package/src/components/scroll-view.ts +12 -0
  36. package/src/components/select-list.ts +13 -0
  37. package/src/components/settings-list.ts +19 -0
  38. package/src/components/spacer.ts +4 -0
  39. package/src/components/tab-bar.ts +11 -0
  40. package/src/components/text.ts +11 -0
  41. package/src/components/truncated-text.ts +15 -1
  42. package/src/debug-server.ts +422 -0
  43. package/src/index.ts +2 -0
  44. package/src/latex-to-unicode.ts +401 -172
  45. package/src/tui.ts +105 -12
  46. package/src/utils.ts +10 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.0.10] - 2026-08-28
6
+
7
+ ### Added
8
+
9
+ - Press the Right Arrow at the end of a line to accept autocomplete suggestions, inline ghost-text completions, and spelling corrections, just like Tab.
10
+ - Added a smooth brand-color transition and an elapsed turn timer to the status line while the agent is working.
11
+ - Added a full-width “band” composer style for flush status lines.
12
+
13
+ ## [18.0.9] - 2026-08-28
14
+
15
+ ### Added
16
+
17
+ - Exported TuiDebugServer for programmatic headless control
18
+ - Added debug demonstration script to examples
19
+ - Added an `OMP_TUI_DEBUG` Unix socket for headless TUI driving and structured inspection.
20
+
21
+ ### Changed
22
+
23
+ - Inline hex colors now render with VS Code-style colored backgrounds and automatically selected black or white text for readability, alongside the color swatch.
24
+ - LaTeX text formatting commands such as \textbf, \textit, \textsl, and \emph now render as terminal bold or italic text.
25
+
26
+ ### Fixed
27
+
28
+ - Fixed inline color swatches rendering incorrectly inside highlighted lines.
29
+ - Fixed terminal resizing in tmux panes and Windows consoles duplicating the current in-progress turn in scrollback.
30
+
5
31
  ## [18.0.8] - 2026-08-27
6
32
 
7
33
  ### Added
@@ -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";
@@ -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. */
@@ -66,8 +66,8 @@ export interface ComposerStyle {
66
66
  /** Rows consumed by top+bottom chrome (drives maxHeight budgeting). */
67
67
  readonly verticalChrome: 0 | 1 | 2;
68
68
  /** 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";
69
+ * rendered as a flush soft-capped band above the input, docked onto a top rule, or detached into a standalone bottom bar. */
70
+ readonly statusAttachment: "top-border" | "top-band" | "top-rule-chip" | "none";
71
71
  /** Which segment groups the standalone bottom status bar shows. */
72
72
  readonly bottomBar: "none" | "left" | "full";
73
73
  /** Insert a blank spacer row between the editor and the standalone bottom
@@ -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;
@@ -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 {};
@@ -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
+ }
@@ -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
  /**
@@ -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
@@ -302,6 +310,21 @@ export declare class TUI extends Container {
302
310
  setFocus(component: Component | null): void;
303
311
  /** Component currently receiving keyboard input, if any. */
304
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;
305
328
  /**
306
329
  * Show an overlay component with configurable positioning and sizing.
307
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.8",
4
+ "version": "18.0.10",
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.8",
41
- "@oh-my-pi/pi-utils": "18.0.8"
40
+ "@oh-my-pi/pi-natives": "18.0.10",
41
+ "@oh-my-pi/pi-utils": "18.0.10"
42
42
  },
43
43
  "devDependencies": {
44
44
  "kitty-vt-wasm": "^0.2.0"
@@ -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
+ };
@@ -1,3 +1,4 @@
1
+ export * from "./band";
1
2
  export * from "./borderless";
2
3
  export * from "./box";
3
4
  export * from "./claude";
@@ -1,3 +1,4 @@
1
+ import { bandComposerStyle } from "./band";
1
2
  import { borderlessComposerStyle } from "./borderless";
2
3
  import { boxComposerStyle } from "./box";
3
4
  import { claudeComposerStyle } from "./claude";
@@ -9,6 +10,7 @@ import type { ComposerStyle, EditorBorderStyle } from "./types";
9
10
 
10
11
  const BUILTIN_COMPOSER_STYLES: Readonly<Record<string, ComposerStyle>> = {
11
12
  box: boxComposerStyle,
13
+ band: bandComposerStyle,
12
14
  claude: claudeComposerStyle,
13
15
  pi: piComposerStyle,
14
16
  borderless: borderlessComposerStyle,
@@ -12,7 +12,16 @@ import type { SymbolTheme } from "../../symbols";
12
12
  export type ComposerBox = SymbolTheme["boxRound"];
13
13
 
14
14
  /** Built-in composer shape identifiers shipped by pi-tui. */
15
- export const BUILTIN_EDITOR_BORDER_STYLES = ["box", "claude", "pi", "borderless", "rule", "field", "rail"] as const;
15
+ export const BUILTIN_EDITOR_BORDER_STYLES = [
16
+ "box",
17
+ "band",
18
+ "claude",
19
+ "pi",
20
+ "borderless",
21
+ "rule",
22
+ "field",
23
+ "rail",
24
+ ] as const;
16
25
 
17
26
  /** Identifier for a built-in composer shape. */
18
27
  export type BuiltinEditorBorderStyle = (typeof BUILTIN_EDITOR_BORDER_STYLES)[number];
@@ -74,8 +83,8 @@ export interface ComposerStyle {
74
83
  /** Rows consumed by top+bottom chrome (drives maxHeight budgeting). */
75
84
  readonly verticalChrome: 0 | 1 | 2;
76
85
  /** Where the host should attach the status bar: embedded in the top border,
77
- * docked onto a top rule, or detached into a standalone bottom bar. */
78
- readonly statusAttachment: "top-border" | "top-rule-chip" | "none";
86
+ * rendered as a flush soft-capped band above the input, docked onto a top rule, or detached into a standalone bottom bar. */
87
+ readonly statusAttachment: "top-border" | "top-band" | "top-rule-chip" | "none";
79
88
  /** Which segment groups the standalone bottom status bar shows. */
80
89
  readonly bottomBar: "none" | "left" | "full";
81
90
  /** Insert a blank spacer row between the editor and the standalone bottom
@@ -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;
@@ -1375,11 +1403,17 @@ export class Editor implements Component, Focusable {
1375
1403
  this.#cancelAutocomplete(true);
1376
1404
  return;
1377
1405
  }
1406
+ // Right arrow at end of line accepts the selection like Tab (fish-style).
1407
+ // Mid-line, right arrow keeps its cursor-movement role and falls through.
1408
+ const rightArrowAccepts =
1409
+ kb.matchesCanonical(canonical, "tui.editor.cursorRight") &&
1410
+ this.#state.cursorCol >= (this.#state.lines[this.#state.cursorLine] ?? "").length;
1378
1411
  if (
1379
1412
  this.#autocompleteState === "assist" &&
1380
1413
  (kb.matchesCanonical(canonical, "tui.input.submit") ||
1381
1414
  data === "\n" ||
1382
- kb.matchesCanonical(canonical, "tui.input.tab"))
1415
+ kb.matchesCanonical(canonical, "tui.input.tab") ||
1416
+ rightArrowAccepts)
1383
1417
  ) {
1384
1418
  this.#applySpellingSuggestion();
1385
1419
  return;
@@ -1392,7 +1426,8 @@ export class Editor implements Component, Focusable {
1392
1426
  kb.matchesCanonical(canonical, "tui.select.pageDown") ||
1393
1427
  kb.matchesCanonical(canonical, "tui.input.submit") ||
1394
1428
  data === "\n" ||
1395
- kb.matchesCanonical(canonical, "tui.input.tab")
1429
+ kb.matchesCanonical(canonical, "tui.input.tab") ||
1430
+ rightArrowAccepts
1396
1431
  ) {
1397
1432
  // Only pass navigation keys to the list, not Enter/Tab (we handle those directly)
1398
1433
  if (
@@ -1407,7 +1442,7 @@ export class Editor implements Component, Focusable {
1407
1442
  }
1408
1443
 
1409
1444
  // If Tab was pressed, always apply the selection
1410
- if (kb.matchesCanonical(canonical, "tui.input.tab")) {
1445
+ if (kb.matchesCanonical(canonical, "tui.input.tab") || rightArrowAccepts) {
1411
1446
  const selected = this.#autocompleteList.getSelectedItem();
1412
1447
  // Check for stale autocomplete state due to buffer edits since last refresh
1413
1448
  // (destructive keys or paste can outrun the debounced update).
@@ -1703,6 +1738,13 @@ export class Editor implements Component, Focusable {
1703
1738
  }
1704
1739
  } else if (kb.matchesCanonical(canonical, "tui.editor.cursorRight")) {
1705
1740
  // Right
1741
+ // At end of line, accept the inline ghost word completion (IME-style) like Tab.
1742
+ if (
1743
+ this.#state.cursorCol >= (this.#state.lines[this.#state.cursorLine] ?? "").length &&
1744
+ this.#acceptWordCompletion()
1745
+ ) {
1746
+ return;
1747
+ }
1706
1748
  this.#moveCursor(0, 1);
1707
1749
  } else if (kb.matchesCanonical(canonical, "tui.editor.cursorLeft")) {
1708
1750
  // Left
@@ -3407,13 +3449,7 @@ export class Editor implements Component, Focusable {
3407
3449
  }
3408
3450
 
3409
3451
  async #handleTabCompletion(): Promise<void> {
3410
- const wordCompletion = this.#getWordCompletion();
3411
- if (wordCompletion) {
3412
- const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
3413
- const after = currentLine.slice(this.#state.cursorCol);
3414
- this.#insertTextAtCursor(wordCompletion + (/^[\s.,;:!?"\])}]/.test(after) ? "" : " "));
3415
- return;
3416
- }
3452
+ if (this.#acceptWordCompletion()) return;
3417
3453
  if (!this.#autocompleteProvider) return;
3418
3454
 
3419
3455
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
@@ -3430,6 +3466,16 @@ export class Editor implements Component, Focusable {
3430
3466
  await this.#forceFileAutocomplete();
3431
3467
  }
3432
3468
  }
3469
+ /** Insert the inline ghost word completion at the cursor, if any. Shared by Tab and right-arrow accept. */
3470
+ #acceptWordCompletion(): boolean {
3471
+ const wordCompletion = this.#getWordCompletion();
3472
+ if (!wordCompletion) return false;
3473
+ const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
3474
+ const after = currentLine.slice(this.#state.cursorCol);
3475
+ this.#insertTextAtCursor(wordCompletion + (/^[\s.,;:!?"\])}]/.test(after) ? "" : " "));
3476
+ return true;
3477
+ }
3478
+
3433
3479
  async #showSpellingSuggestions(): Promise<void> {
3434
3480
  const cursorLine = this.#state.cursorLine;
3435
3481
  const cursorCol = this.#state.cursorCol;
@@ -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;
@@ -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;