@oh-my-pi/pi-tui 18.5.0 → 18.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.5.1] - 2026-10-03
6
+
7
+ ### Added
8
+
9
+ - Added native terminal support for submitting explicit composer prompts atomically, preserving displaced drafts and attachments for local recall.
10
+ - Added progress percentages to subagent entries in the agent tree, task, and wait views.
11
+ - Added a `Rebuilding…` indicator for lengthy tmux resize redraws without flashing it for quick updates.
12
+ - Added `writeTerminalSequence()` for sending supported terminal escape sequences through the active terminal output stream, with stdout fallback when no terminal is active.
13
+
14
+ ### Changed
15
+
16
+ - Clipboard payloads are now represented by their length in `PI_TUI_WRITE_LOG` rather than by their contents.
17
+
18
+ ### Fixed
19
+
20
+ - Fixed pending clipboard-image attachments being bypassed when subsequent input or native prompts were submitted before loading completed.
21
+ - Improved narrow `/models` layouts by retaining the scope sidebar while simplifying and truncating model rows as needed.
22
+ - Fixed Markdown rendering with themes that do not define their own symbol set; the active theme's symbols are now used as a fallback.
23
+ - Fixed model role assignment showing models that are unavailable for the selected provider.
24
+ - Improved tmux resize, zoom, and rebuild behavior, including smoother coalesced redraws, reduced visual artifacts and extra output, correct transcript restoration after rapid size changes, and preservation of history when panes are resized.
25
+ - Fixed multiline paste on Windows inserting escape-code fragments instead of line breaks, and prevented bracketed-paste markers from appearing in the composer.
26
+ - Fixed terminal notifications occasionally corrupting the screen with stray escape-code fragments during streaming output.
27
+ - Added rendering and notification compatibility for Monstar terminals, including Kitty graphics, hyperlinks, synchronized output, styled underlines, progress keepalives, and Monstar-focused desktop notifications.
28
+ - Fixed Agent Hub transcripts crashing when assistant messages do not include usage or cost data.
29
+ - Fixed idle terminal activity notifications while preserving bracketed-paste recovery during input and rendering.
30
+ - Fixed diff blocks in ask questions losing their layout; additions and removals now retain separate diff highlighting.
31
+ - Fixed duplicate ASCII Plan, Prewalk, and Goal labels in the status line.
32
+ - Improved Markdown rendering for long and streamed messages, including display math, fenced code, reference links, lists, whitespace, emphasis, and line breaks, so completed content no longer shows raw markup, misplaced blank lines, or broken block layout.
33
+
5
34
  ## [18.5.0] - 2026-10-03
6
35
 
7
36
  ### Breaking Changes
@@ -2988,14 +3017,4 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
2988
3017
  - Crash in `visibleWidth()` and grapheme iteration when encountering undefined code points ([#372](https://github.com/badlogic/pi-mono/pull/372) by [@HACKE-RC](https://github.com/HACKE-RC))
2989
3018
  - ZWJ emoji sequences (rainbow flag, family, etc.) now render with correct width instead of being split into multiple characters ([#369](https://github.com/badlogic/pi-mono/pull/369) by [@nathyong](https://github.com/nathyong))
2990
3019
 
2991
- ## [0.29.0] - 2025-12-25
2992
-
2993
- ### Added
2994
-
2995
- - **Auto-space before pasted file paths**: When pasting a file path (starting with `/`, `~`, or `.`) and the cursor is after a word character, a space is automatically prepended for better readability. Useful when dragging screenshots from macOS. ([#307](https://github.com/badlogic/pi-mono/pull/307) by [@mitsuhiko](https://github.com/mitsuhiko))
2996
- - **Word navigation for Input component**: Added Ctrl+Left/Right and Alt+Left/Right support for word-by-word cursor movement. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
2997
- - **Full Unicode input**: Input component now accepts Unicode characters beyond ASCII. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
2998
-
2999
- ### Fixed
3000
-
3001
- - **Readline-style Ctrl+W**: Now skips trailing whitespace before deleting the preceding word, matching standard readline behavior. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
3020
+ Older entries are archived in [packages/tui/CHANGELOG.md@bac7e83b5b0e](https://github.com/can1357/oh-my-pi/blob/bac7e83b5b0eb86c909c17830a6666efc359578b/packages/tui/CHANGELOG.md).
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Ownership of stdout by a started {@link ProcessTerminal}, kept in a leaf
3
+ * module so writers that `terminal.ts` itself imports (terminal capabilities,
4
+ * notifications) can route through the active terminal without an import cycle.
5
+ */
6
+ import type { ProcessTerminal } from "./terminal.js";
7
+ /** The started terminal that owns stdout in this thread, if any. */
8
+ export declare function getActiveTerminal(): ProcessTerminal | null;
9
+ /** Record the terminal that owns stdout (`ProcessTerminal.start`), or release it with `null` (`stop`). */
10
+ export declare function setActiveTerminal(terminal: ProcessTerminal | null): void;
11
+ /** Observe stdout errors through the shared listener. Returns the unregister callback. */
12
+ export declare function registerStdoutErrorHandler(handler: (err: Error) => void): () => void;
13
+ /**
14
+ * Hand `data` to the active terminal's ordered output path. Returns false when
15
+ * no terminal in this thread has started. On the main thread that means the
16
+ * caller owns stdout; a worker thread never sees the main thread's terminal, so
17
+ * there `false` is not permission to write. Prefer {@link writeTerminalSequence}.
18
+ */
19
+ export declare function writeThroughActiveTerminal(data: string): boolean;
20
+ /**
21
+ * Write an out-of-band escape sequence (window title, OSC 52 clipboard,
22
+ * notification). While a TUI owns stdout, its frames go through an off-thread
23
+ * pump in chunks that ignore escape boundaries; a direct `process.stdout.write`
24
+ * can land between two chunks and split a frame's escape sequence, and the host
25
+ * terminal then prints its tail as text. So the sequence takes the active
26
+ * terminal's ordered output path, and goes straight to stdout, best-effort, only
27
+ * when no terminal owns it. A worker thread cannot reach the main thread's
28
+ * terminal and would write around its pump, so it writes nothing.
29
+ */
30
+ export declare function writeTerminalSequence(data: string): void;
@@ -1,8 +1,10 @@
1
1
  import type { DescribeContext, NativeNode } from "../native/node.js";
2
2
  import type { SymbolTheme } from "../symbols.js";
3
3
  import type { Component } from "../tui.js";
4
- /** @internal exported for tests — the grown-line-start block-kind gate. */
4
+ /** @internal exported for tests — the grown-line block-kind gate. */
5
5
  export declare function fastLineStartHazard(grownLine: string): boolean;
6
+ /** @internal exported for tests — the windowed lexer's first-probe window size. */
7
+ export declare const LEX_WINDOW_BYTES: number;
6
8
  /** A hyperlink as the renderer sees it: inline `[text](href)`, `<autolink>`, bare GFM URL, or reference link. */
7
9
  export interface MarkdownLink {
8
10
  /** Flattened visible label with whitespace collapsed to one row; falls back to `href` when empty. */
@@ -82,7 +84,12 @@ export interface MarkdownTheme {
82
84
  * Return null to fall back to fenced code rendering.
83
85
  */
84
86
  resolveMermaidAscii?: (source: string, maxWidth?: number) => string | null;
85
- symbols: SymbolTheme;
87
+ /**
88
+ * Glyphs for quote borders, rules, tables and color swatches. Optional so themes
89
+ * built to the upstream pi-tui `MarkdownTheme` shape (which has no `symbols`)
90
+ * still render; omitted symbols fall back to the active theme's set.
91
+ */
92
+ symbols?: SymbolTheme;
86
93
  }
87
94
  export declare class Markdown implements Component {
88
95
  #private;
@@ -101,8 +108,10 @@ export declare class Markdown implements Component {
101
108
  describe(_cx: DescribeContext): NativeNode;
102
109
  /**
103
110
  * Width-independent source prefix of the last render ending at a frozen
104
- * Markdown block boundary. Only meaningful while streaming (transient
105
- * render cache on); grows monotonically under append-only `setText`.
111
+ * Markdown block boundary that no append can move: it stops in front of a
112
+ * display-math opener whose block an append could still close. Only
113
+ * meaningful while streaming (transient render cache on); grows
114
+ * monotonically under append-only `setText`.
106
115
  */
107
116
  getLastRenderStableText(): string;
108
117
  get transientRenderCache(): boolean;
@@ -29,6 +29,7 @@ export declare function encodeTspJson(verb: TspVerb, value: unknown, params?: Ts
29
29
  * tells the terminal that omp applies its `edit` events (TSP §8.5), so it may keep a
30
30
  * native selection in omp's editors; without it, every key stays omp's. `"undo"`
31
31
  * says omp applies `undo` events, so the terminal may turn ⌃Z in a field into one.
32
+ * `"send"` accepts an explicit prompt for a live composer without simulating keys.
32
33
  */
33
34
  export declare function encodeTspHelloQuery(version?: string): string;
34
35
  /** One decoded APC message: verb, parameters and raw body. */
@@ -135,6 +135,12 @@ export type NativeUiEvent = {
135
135
  | {
136
136
  readonly type: "undo";
137
137
  readonly key: string;
138
+ }
139
+ /** Submit an explicit prompt through the composer's normal path, preserving the previous draft for recall. */
140
+ | {
141
+ readonly type: "send";
142
+ readonly key: string;
143
+ readonly text: string;
138
144
  };
139
145
  /**
140
146
  * A primitive edit the terminal made over its own text selection (cut,
@@ -61,7 +61,10 @@ export declare class HubFrame {
61
61
  constructor(title: string, sidebarBounds: {
62
62
  min: number;
63
63
  max: number;
64
- }, renderSidebar: (width: number, rows: number) => string[], renderBody: (width: number, height: number | undefined) => readonly string[]);
64
+ }, renderSidebar: (width: number, rows: number) => string[], renderBody: (width: number, height: number | undefined) => readonly string[], options?: {
65
+ bodyMinWidth?: number;
66
+ preserveSidebar?: boolean;
67
+ });
65
68
  /** Invalidate the composed layout. */
66
69
  invalidate(): void;
67
70
  /** Measure labels and annotations within this hub's sidebar width bounds. */
@@ -237,7 +237,8 @@ export declare class CustomEditor extends Editor {
237
237
  * Clicks on the composer's controls take the same paths as their keys: ⇧⇥,
238
238
  * ⏎ and Esc; the viewing header's links (`focus:<id>`) go to the host,
239
239
  * the status facts' clicks (`status.*`) to their source. Selection edits
240
- * go to the buffer.
240
+ * go to the buffer; `send` submits its own prompt after saving the old draft
241
+ * for recall and waiting for in-flight clipboard work.
241
242
  */
242
243
  handleNativeEvent(event: NativeUiEvent): void;
243
244
  onEscape?: () => void;
@@ -11,7 +11,7 @@ export declare enum NotifyProtocol {
11
11
  Osc99 = "\u001B]99;;",
12
12
  Osc9 = "\u001B]9;"
13
13
  }
14
- export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "warp" | "orca" | "otty" | "rio" | "tern" | "base" | "trueColor";
14
+ export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "warp" | "orca" | "otty" | "rio" | "tern" | "monstar" | "base" | "trueColor";
15
15
  /** Terminal capability details used for rendering and protocol selection. */
16
16
  export declare class TerminalInfo {
17
17
  readonly id: TerminalId;
@@ -1,4 +1,5 @@
1
1
  import { type TspHello } from "./native/encode.js";
2
+ export { writeTerminalSequence, writeThroughActiveTerminal } from "./active-terminal.js";
2
3
  /**
3
4
  * Split `data` into chunks whose encoded UTF-8 byte length is no greater than
4
5
  * `maxChunkBytes`, preferring a line boundary (`\n`) as the cut point so
@@ -69,16 +70,6 @@ export declare class StdoutStallWatchdog {
69
70
  }
70
71
  /** Record alternate-screen state (called by the TUI on `?1049h`/`?1049l` writes). */
71
72
  export declare function setAltScreenActive(active: boolean): void;
72
- /**
73
- * Route an out-of-band escape sequence (e.g. an OSC title update) through the
74
- * active terminal's output path. While a TUI owns stdout, frame paints go
75
- * through the off-thread write pump and can split across multiple write(2)
76
- * calls; a direct main-thread `process.stdout.write` can land between two of
77
- * them — mid escape sequence — and the host terminal then prints the payload
78
- * as literal text at the cursor position. Returns false when no terminal has
79
- * started, in which case the caller owns stdout and may write directly.
80
- */
81
- export declare function writeThroughActiveTerminal(data: string): boolean;
82
73
  /**
83
74
  * Emergency terminal restore - call this from signal/crash handlers
84
75
  * Resets terminal state without requiring access to the ProcessTerminal instance
@@ -4,8 +4,13 @@ import { type OutputMeta } from "./output-meta.js";
4
4
  import type { NativeToolView, RenderResultContextOptions, RenderResultOptions } from "./renderer.js";
5
5
  /** Default collapsed shell output preview height. */
6
6
  export declare const BASH_DEFAULT_PREVIEW_LINES = 10;
7
- /** LLM-facing footer appended when a tool call becomes a background job. */
8
- export declare function formatBackgroundNotice(jobId: string): string;
7
+ /**
8
+ * LLM-facing footer appended when a tool call becomes a background job. It states the job's kill
9
+ * deadline (`timeoutSec`, `undefined` when disabled) so the model knows the job will die at it before
10
+ * it waits on the result. The deadline counts the job's whole run time, not time left from now: an
11
+ * auto-backgrounded call has already spent its foreground wait.
12
+ */
13
+ export declare function formatBackgroundNotice(jobId: string, timeoutSec: number | undefined): string;
9
14
  /** Shell execution metadata used by transcript rendering. */
10
15
  export interface BashToolDetails {
11
16
  meta?: OutputMeta;
@@ -35,3 +35,15 @@ export declare class Win32InputModeDecoder {
35
35
  */
36
36
  decodePaste(content: string): string;
37
37
  }
38
+ /**
39
+ * Restores paste delimiters that a Windows console host emitted as individual
40
+ * win32-input-mode key records before StdinBuffer searches for the delimiters.
41
+ * Non-matching records remain untouched for normal key and paste decoding.
42
+ */
43
+ export declare class Win32PasteMarkerNormalizer {
44
+ #private;
45
+ constructor(onInput: (data: string) => void);
46
+ process(data: string): void;
47
+ /** Release an incomplete marker as its original key records. */
48
+ flush(): void;
49
+ }
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.5.0",
4
+ "version": "18.5.1",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -41,14 +41,14 @@
41
41
  "gen:glyphs": "bun scripts/gen-glyphs.ts"
42
42
  },
43
43
  "dependencies": {
44
- "@oh-my-pi/omptype": "18.5.0",
45
- "@oh-my-pi/pi-agent-core": "18.5.0",
46
- "@oh-my-pi/pi-ai": "18.5.0",
47
- "@oh-my-pi/pi-catalog": "18.5.0",
48
- "@oh-my-pi/pi-natives": "18.5.0",
49
- "@oh-my-pi/pi-utils": "18.5.0",
50
- "@oh-my-pi/pi-wire": "18.5.0",
51
- "@oh-my-pi/snapcompact": "18.5.0"
44
+ "@oh-my-pi/omptype": "18.5.1",
45
+ "@oh-my-pi/pi-agent-core": "18.5.1",
46
+ "@oh-my-pi/pi-ai": "18.5.1",
47
+ "@oh-my-pi/pi-catalog": "18.5.1",
48
+ "@oh-my-pi/pi-natives": "18.5.1",
49
+ "@oh-my-pi/pi-utils": "18.5.1",
50
+ "@oh-my-pi/pi-wire": "18.5.1",
51
+ "@oh-my-pi/snapcompact": "18.5.1"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Ownership of stdout by a started {@link ProcessTerminal}, kept in a leaf
3
+ * module so writers that `terminal.ts` itself imports (terminal capabilities,
4
+ * notifications) can route through the active terminal without an import cycle.
5
+ */
6
+ import type { ProcessTerminal } from "./terminal";
7
+
8
+ // The started terminal that owns stdout in this thread; also consulted by the
9
+ // emergency restore on crash.
10
+ let activeTerminal: ProcessTerminal | null = null;
11
+
12
+ /** The started terminal that owns stdout in this thread, if any. */
13
+ export function getActiveTerminal(): ProcessTerminal | null {
14
+ return activeTerminal;
15
+ }
16
+
17
+ /** Record the terminal that owns stdout (`ProcessTerminal.start`), or release it with `null` (`stop`). */
18
+ export function setActiveTerminal(terminal: ProcessTerminal | null): void {
19
+ activeTerminal = terminal;
20
+ }
21
+
22
+ const stdoutErrorHandlers = new Set<(err: Error) => void>();
23
+ let stdoutErrorListenerInstalled = false;
24
+
25
+ function onStdoutError(err: Error): void {
26
+ for (const handler of stdoutErrorHandlers) handler(err);
27
+ }
28
+
29
+ /**
30
+ * Install the one shared `process.stdout` `error` listener. Its presence alone
31
+ * keeps a stdout error (EPIPE on a closed pipe, EIO on a revoked PTY) from
32
+ * crashing the process as an unhandled `error` event.
33
+ */
34
+ function installStdoutErrorListener(): void {
35
+ if (stdoutErrorListenerInstalled) return;
36
+ process.stdout.on("error", onStdoutError);
37
+ stdoutErrorListenerInstalled = true;
38
+ }
39
+
40
+ /** Observe stdout errors through the shared listener. Returns the unregister callback. */
41
+ export function registerStdoutErrorHandler(handler: (err: Error) => void): () => void {
42
+ stdoutErrorHandlers.add(handler);
43
+ installStdoutErrorListener();
44
+ return () => {
45
+ stdoutErrorHandlers.delete(handler);
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Hand `data` to the active terminal's ordered output path. Returns false when
51
+ * no terminal in this thread has started. On the main thread that means the
52
+ * caller owns stdout; a worker thread never sees the main thread's terminal, so
53
+ * there `false` is not permission to write. Prefer {@link writeTerminalSequence}.
54
+ */
55
+ export function writeThroughActiveTerminal(data: string): boolean {
56
+ if (!activeTerminal) return false;
57
+ activeTerminal.write(data);
58
+ return true;
59
+ }
60
+
61
+ /**
62
+ * Write an out-of-band escape sequence (window title, OSC 52 clipboard,
63
+ * notification). While a TUI owns stdout, its frames go through an off-thread
64
+ * pump in chunks that ignore escape boundaries; a direct `process.stdout.write`
65
+ * can land between two chunks and split a frame's escape sequence, and the host
66
+ * terminal then prints its tail as text. So the sequence takes the active
67
+ * terminal's ordered output path, and goes straight to stdout, best-effort, only
68
+ * when no terminal owns it. A worker thread cannot reach the main thread's
69
+ * terminal and would write around its pump, so it writes nothing.
70
+ */
71
+ export function writeTerminalSequence(data: string): void {
72
+ if (!Bun.isMainThread) return;
73
+ if (writeThroughActiveTerminal(data)) return;
74
+ installStdoutErrorListener();
75
+ try {
76
+ process.stdout.write(data);
77
+ } catch {
78
+ // Best-effort: a closed stdout must not fail the caller.
79
+ }
80
+ }
@@ -85,10 +85,17 @@ export class BracketedPasteHandler {
85
85
  * buffer); omitted when still buffering.
86
86
  */
87
87
  process(data: string): PasteResult {
88
- if (data.includes(PASTE_START)) {
88
+ const start = data.indexOf(PASTE_START);
89
+ const end = data.indexOf(PASTE_END);
90
+ if (start !== -1 && (end === -1 || start < end)) {
91
+ if (!this.#active) this.#buffer = "";
89
92
  this.#active = true;
90
- this.#buffer = "";
91
- data = data.replace(PASTE_START, "");
93
+ // Only strip nested starts before this paste's first end. Later starts
94
+ // belong to the next paste in `remaining`.
95
+ data =
96
+ end === -1
97
+ ? data.replaceAll(PASTE_START, "")
98
+ : data.slice(0, end).replaceAll(PASTE_START, "") + data.slice(end);
92
99
  }
93
100
 
94
101
  if (!this.#active) return { handled: false };