@oh-my-pi/pi-tui 18.4.10 → 18.4.11

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,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.4.11] - 2026-10-02
6
+
7
+ ### Added
8
+
9
+ - Added subagent completion percentages to the agent tree, task, and wait views.
10
+
11
+ ### Changed
12
+
13
+ - Updated notebook evaluation cells in native hosts to use directional gutter indicators for inputs and outputs, with a progress indicator while a cell is running.
14
+
15
+ ### Fixed
16
+
17
+ - Fixed multiline paste on Windows so pasted line breaks are inserted correctly instead of terminal escape sequences.
18
+
5
19
  ## [18.4.10] - 2026-10-02
6
20
 
7
21
  ### Added
@@ -27,6 +27,8 @@ export interface AgentTreeRowOptions {
27
27
  description?: string;
28
28
  preview?: string;
29
29
  stats?: AgentStats;
30
+ /** The agent's own completion estimate (0–100); shown only while live. */
31
+ completionPercent?: number;
30
32
  durationMs?: number;
31
33
  }
32
34
  /** Render an agent row while reserving its identifier and required status badges. */
@@ -255,6 +255,8 @@ export interface AgentProgress {
255
255
  resolvedModelRoute?: string;
256
256
  /** True when a live advisor was attached to this run's session, not merely enabled in settings. */
257
257
  advisor?: boolean;
258
+ /** The agent's latest self-estimate of task completion (0–100), from the periodic `task.completionProbeMs` side request. */
259
+ completionPercent?: number;
258
260
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
259
261
  extractedToolData?: Record<string, unknown[]>;
260
262
  /**
@@ -27,6 +27,8 @@ export interface JobSnapshot {
27
27
  resolvedThinkingLevel?: ConfiguredThinkingLevel;
28
28
  /** True when the task progress reports an attached live advisor. */
29
29
  advisor?: boolean;
30
+ /** The task agent's latest self-estimated completion (0–100); see `AgentProgress.completionPercent`. */
31
+ completionPercent?: number;
30
32
  resultText?: string;
31
33
  errorText?: string;
32
34
  /** Source-output metadata retained for per-job warnings and persisted row rendering. */
@@ -28,6 +28,15 @@ interface WriteRenderArgs {
28
28
  file_path?: unknown;
29
29
  content?: unknown;
30
30
  }
31
+ /** A write tool result as the renderer receives it. */
32
+ interface WriteResult {
33
+ content: Array<{
34
+ type: string;
35
+ text?: string;
36
+ }>;
37
+ details?: WriteToolDetails;
38
+ isError?: boolean;
39
+ }
31
40
  declare const writeStreamingPreviewStateKey: unique symbol;
32
41
  /**
33
42
  * Per-component state for incrementally rendering a streamed write.
@@ -66,27 +75,13 @@ export declare const writeToolRenderer: {
66
75
  renderCall(args: WriteRenderArgs, options: RenderResultOptions & WriteStreamingPreviewStateCarrier & {
67
76
  renderContext?: WriteRenderContext;
68
77
  }, uiTheme: Theme): Component | undefined;
69
- renderResult(result: {
70
- content: Array<{
71
- type: string;
72
- text?: string;
73
- }>;
74
- details?: WriteToolDetails;
75
- isError?: boolean;
76
- }, options: RenderResultOptions & {
78
+ renderResult(result: WriteResult, options: RenderResultOptions & {
77
79
  renderContext?: WriteRenderContext;
78
80
  }, uiTheme: Theme, args?: WriteRenderArgs): Component;
79
81
  describeCall(args: WriteRenderArgs, options: RenderResultOptions & {
80
82
  renderContext?: WriteRenderContext;
81
83
  }): NativeToolView | undefined;
82
- describeResult(result: {
83
- content: Array<{
84
- type: string;
85
- text?: string;
86
- }>;
87
- details?: WriteToolDetails;
88
- isError?: boolean;
89
- }, options: RenderResultOptions & {
84
+ describeResult(result: WriteResult, options: RenderResultOptions & {
90
85
  renderContext?: WriteRenderContext;
91
86
  }, args?: WriteRenderArgs): NativeToolView | undefined;
92
87
  mergeCallAndResult: true;
@@ -6,8 +6,11 @@
6
6
  * deliver Shift+Enter as a bare `\r` (indistinguishable from Enter) and
7
7
  * Ctrl+Enter as `\n`. Enabling `CSI ? 9001 h` makes the console host serving
8
8
  * this process report every key as `CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _`, which
9
- * carries the full modifier state. Paste text, mouse reports and terminal
10
- * replies keep arriving as plain VT, so only key records need translating.
9
+ * carries the full modifier state. Mouse reports and terminal replies keep
10
+ * arriving as plain VT. Pasted text mostly does too, but the console host
11
+ * re-encodes some pasted characters (line breaks) as key records inside the
12
+ * bracketed paste; {@link Win32InputModeDecoder.decodePaste} turns those back
13
+ * into text.
11
14
  *
12
15
  * Unmodified and legacy-expressible keys translate to the bytes a legacy
13
16
  * terminal sends; chords legacy encoding loses (modified Enter/Tab/Escape,
@@ -23,4 +26,12 @@ export declare class Win32InputModeDecoder {
23
26
  * events it produces — empty for key releases and bare modifier presses.
24
27
  */
25
28
  decode(data: string): string[] | undefined;
29
+ /**
30
+ * Replace the key records embedded in bracketed paste `content` with the
31
+ * text they type, so a pasted line break stays a line break instead of
32
+ * acting as Enter. Key-downs yield their character (`\r` for Enter);
33
+ * releases and keys without text yield nothing. Text that merely looks like
34
+ * a record without ESC is left alone.
35
+ */
36
+ decodePaste(content: string): string;
26
37
  }
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.4.10",
4
+ "version": "18.4.11",
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.4.10",
45
- "@oh-my-pi/pi-agent-core": "18.4.10",
46
- "@oh-my-pi/pi-ai": "18.4.10",
47
- "@oh-my-pi/pi-catalog": "18.4.10",
48
- "@oh-my-pi/pi-natives": "18.4.10",
49
- "@oh-my-pi/pi-utils": "18.4.10",
50
- "@oh-my-pi/pi-wire": "18.4.10",
51
- "@oh-my-pi/snapcompact": "18.4.10"
44
+ "@oh-my-pi/omptype": "18.4.11",
45
+ "@oh-my-pi/pi-agent-core": "18.4.11",
46
+ "@oh-my-pi/pi-ai": "18.4.11",
47
+ "@oh-my-pi/pi-catalog": "18.4.11",
48
+ "@oh-my-pi/pi-natives": "18.4.11",
49
+ "@oh-my-pi/pi-utils": "18.4.11",
50
+ "@oh-my-pi/pi-wire": "18.4.11",
51
+ "@oh-my-pi/snapcompact": "18.4.11"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
package/src/terminal.ts CHANGED
@@ -1615,10 +1615,12 @@ export class ProcessTerminal implements Terminal {
1615
1615
  // Re-wrap paste content with bracketed paste markers for existing editor
1616
1616
  // handling. An Enter that shared the paste's stdin read rides along so
1617
1617
  // paste and submit reach the component focused right now, not one the
1618
- // paste itself is about to open.
1618
+ // paste itself is about to open. Under win32-input-mode the console host
1619
+ // encodes pasted line breaks as key records; decode them as text (#14065).
1619
1620
  this.#stdinBuffer.on("paste", (content: string, enter?: string) => {
1620
1621
  if (this.#inputHandler) {
1621
- this.#inputHandler(`\x1b[200~${content}\x1b[201~${enter ?? ""}`);
1622
+ const text = this.#win32InputDecoder?.decodePaste(content) ?? content;
1623
+ this.#inputHandler(`\x1b[200~${text}\x1b[201~${enter ?? ""}`);
1622
1624
  }
1623
1625
  });
1624
1626
 
@@ -60,6 +60,8 @@ export interface AgentTreeRowOptions {
60
60
  description?: string;
61
61
  preview?: string;
62
62
  stats?: AgentStats;
63
+ /** The agent's own completion estimate (0–100); shown only while live. */
64
+ completionPercent?: number;
63
65
  durationMs?: number;
64
66
  }
65
67
 
@@ -120,6 +122,9 @@ export function renderAgentTreeRow(
120
122
  }
121
123
  let line = `${lead}${modelLead}${title}${badges}`;
122
124
  if (options.preview) line += options.preview;
125
+ if (live && options.completionPercent !== undefined) {
126
+ line += `${theme.sep.dot}${theme.fg("accent", `${options.completionPercent}%`)}`;
127
+ }
123
128
  if (options.stats) line += formatAgentStatRun(options.stats, theme);
124
129
  if (options.durationMs !== undefined && options.durationMs > 0) {
125
130
  line += `${theme.sep.dot}${theme.fg("dim", formatDuration(options.durationMs))}`;
package/src/tools/eval.ts CHANGED
@@ -689,8 +689,9 @@ interface EvalCellSection {
689
689
  }
690
690
 
691
691
  /**
692
- * A notebook cell with separate input and output gutters. Only multi-cell
693
- * calls repeat titles; prompts show execution state, never invented counts.
692
+ * A notebook cell: a gutter mark beside the input (←, muted until the cell
693
+ * runs, omp's thinking starburst while it does) and beside its output (→).
694
+ * Only multi-cell calls repeat titles; marks never invent execution counts.
694
695
  */
695
696
  function evalCellSection(cell: EvalCellSection, index: number, total: number): NativeNode {
696
697
  let head: TspSpan[] | undefined;
@@ -701,7 +702,15 @@ function evalCellSection(cell: EvalCellSection, index: number, total: number): N
701
702
  if (cell.status === "error") head.push(span(" · failed", "error"));
702
703
  if (cell.durationMs !== undefined) head.push(span(` · ${(cell.durationMs / 1000).toFixed(2)}s`, "muted"));
703
704
  }
704
- const prompt = cell.status === "running" ? "In [*]:" : !cell.status || cell.status === "pending" ? "In [ ]:" : "In:";
705
+ const inputMark =
706
+ cell.status === "running"
707
+ ? node("spinner", { style: "starburst", role: "omp.tool.eval.prompt", aria: "Running" })
708
+ : node("icon", {
709
+ name: "arrow-left",
710
+ role: "omp.tool.eval.prompt",
711
+ aria: "Input",
712
+ tone: !cell.status || cell.status === "pending" ? "muted" : undefined,
713
+ });
705
714
  return node(
706
715
  "col",
707
716
  {
@@ -714,7 +723,7 @@ function evalCellSection(cell: EvalCellSection, index: number, total: number): N
714
723
  "row",
715
724
  { role: "omp.tool.eval.input", align: "start" },
716
725
  [
717
- text(prompt, { role: "omp.tool.eval.prompt", wrap: "none" }),
726
+ inputMark,
718
727
  keyed(codeNode(cell.code, { lang: languageForHighlighter(cell.language), numbers: false }), "code"),
719
728
  ],
720
729
  "input",
@@ -724,7 +733,7 @@ function evalCellSection(cell: EvalCellSection, index: number, total: number): N
724
733
  "row",
725
734
  { role: "omp.tool.eval.result", align: "start" },
726
735
  [
727
- text("Out:", { role: "omp.tool.eval.prompt", wrap: "none" }),
736
+ node("icon", { name: "arrow-right", role: "omp.tool.eval.prompt", aria: "Output" }),
728
737
  node("col", { role: "omp.tool.eval.outputs" }, cell.output),
729
738
  ],
730
739
  "output",
package/src/tools/task.ts CHANGED
@@ -715,6 +715,7 @@ function renderAgentProgress(
715
715
  ? ` ${theme.fg("muted", previewLine(sanitizeText(progress.assignment ?? progress.task), 40))}`
716
716
  : undefined,
717
717
  stats: progress.status === "running" || progress.status === "completed" ? progress : undefined,
718
+ completionPercent: progress.completionPercent,
718
719
  },
719
720
  theme,
720
721
  );
@@ -1837,6 +1838,7 @@ function describeProgressAgent(progress: AgentProgress, state: AgentDescribeStat
1837
1838
  requests: progress.requests || undefined,
1838
1839
  tokens: progress.tokens || undefined,
1839
1840
  ...contextStats(progress.contextTokens, progress.contextWindow),
1841
+ done: running && progress.completionPercent !== undefined ? progress.completionPercent / 100 : undefined,
1840
1842
  cost: progress.cost > 0 ? progress.cost : undefined,
1841
1843
  ...(running ? { age: progress.durationMs } : { took: progress.durationMs }),
1842
1844
  },
@@ -2312,6 +2314,8 @@ export interface AgentProgress {
2312
2314
  resolvedModelRoute?: string;
2313
2315
  /** True when a live advisor was attached to this run's session, not merely enabled in settings. */
2314
2316
  advisor?: boolean;
2317
+ /** The agent's latest self-estimate of task completion (0–100), from the periodic `task.completionProbeMs` side request. */
2318
+ completionPercent?: number;
2315
2319
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
2316
2320
  extractedToolData?: Record<string, unknown[]>;
2317
2321
  /**
package/src/tools/wait.ts CHANGED
@@ -61,6 +61,8 @@ export interface JobSnapshot {
61
61
  resolvedThinkingLevel?: ConfiguredThinkingLevel;
62
62
  /** True when the task progress reports an attached live advisor. */
63
63
  advisor?: boolean;
64
+ /** The task agent's latest self-estimated completion (0–100); see `AgentProgress.completionPercent`. */
65
+ completionPercent?: number;
64
66
  resultText?: string;
65
67
  errorText?: string;
66
68
  /** Source-output metadata retained for per-job warnings and persisted row rendering. */
@@ -309,7 +311,11 @@ function jobsRenderResult(
309
311
  job.status === "running" ? options.spinnerFrame : undefined,
310
312
  )}${job.exitCode === undefined ? "" : `${uiTheme.sep.dot}${uiTheme.fg(job.exitCode === 0 ? "muted" : "error", `exit ${job.exitCode}`)}`}`;
311
313
  const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
312
- const durationSuffix = `${uiTheme.sep.dot}${uiTheme.fg("dim", formatDuration(job.durationMs))}`;
314
+ const completion =
315
+ job.status === "running" && job.completionPercent !== undefined
316
+ ? `${uiTheme.sep.dot}${uiTheme.fg("accent", `${job.completionPercent}%`)}`
317
+ : "";
318
+ const durationSuffix = `${completion}${uiTheme.sep.dot}${uiTheme.fg("dim", formatDuration(job.durationMs))}`;
313
319
  const displayId = truncateToWidth(
314
320
  replaceTabs(job.id).replace(/\s+/g, " "),
315
321
  Math.max(0, rowWidth - visibleWidth(`${icon} ${typeBadge} ${durationSuffix}`)),
@@ -601,11 +607,19 @@ function describeJob(job: JobSnapshot, isPartial: boolean): NativeNode {
601
607
  { role: "omp.wait.job", tone },
602
608
  compact([
603
609
  row(
604
- [
610
+ compact([
605
611
  node("badge", { text: job.type, tone }),
606
612
  text(spans, { truncate: "end", grow: 1 }),
613
+ job.status === "running" && job.completionPercent !== undefined
614
+ ? node("progress", {
615
+ value: job.completionPercent / 100,
616
+ label: `${job.completionPercent}%`,
617
+ tone: "accent",
618
+ title: "Agent's own completion estimate",
619
+ })
620
+ : undefined,
607
621
  elapsed(job.durationMs, !running),
608
- ],
622
+ ]),
609
623
  { gap: "sm" },
610
624
  ),
611
625
  artifactError && text([span(formatArtifactErrorNotice(artifactError), "warning")], { wrap: "word" }),
@@ -32,7 +32,8 @@ import {
32
32
  } from "./proc-render";
33
33
  import type { TspTone } from "@oh-my-pi/pi-wire";
34
34
  import { code, compact, node, span } from "../native/describe";
35
- import type { NativeChild } from "../native/node";
35
+ import { registerNativeBlob } from "../native/blobs";
36
+ import type { NativeChild, NativeNode } from "../native/node";
36
37
  import { diagnosticsBadge, diagnosticsSection, displayPath, errorText, fileHref, resultText } from "./native-view";
37
38
  import { describeCfgWrite, renderCfgWrite, type CfgWriteDetails } from "./cfg-render";
38
39
  import type { FileDiagnosticsResult } from "./lsp";
@@ -90,6 +91,37 @@ interface WriteRenderArgs {
90
91
  const WRITE_PREVIEW_LINES = 6;
91
92
  /** Collapsed native write body: the first lines of the file (§7.3). */
92
93
  const NATIVE_WRITE_PREVIEW = { lines: 8 } as const;
94
+ /** Tallest rendered SVG preview, in lines; collapsed, the card shows just the picture. */
95
+ const NATIVE_SVG_PREVIEW_LINES = 16;
96
+
97
+ /** A write tool result as the renderer receives it. */
98
+ interface WriteResult {
99
+ content: Array<{ type: string; text?: string }>;
100
+ details?: WriteToolDetails;
101
+ isError?: boolean;
102
+ }
103
+
104
+ /** Cached SVG preview node on its result, so the content is encoded and hashed once per write. */
105
+ const kSvgPreview = Symbol("write.svgPreview");
106
+
107
+ interface TaggedWriteResult extends WriteResult {
108
+ [kSvgPreview]?: { content: string; node: NativeNode };
109
+ }
110
+
111
+ /** Native `image` node drawing written SVG `content`, cached on `result`. */
112
+ function describeSvgPreview(result: TaggedWriteResult, content: string, alt: string): NativeNode {
113
+ const cached = result[kSvgPreview];
114
+ if (cached?.content === content) return cached.node;
115
+ const blob = registerNativeBlob(new TextEncoder().encode(content), "image/svg+xml");
116
+ const preview = node("image", {
117
+ blob,
118
+ alt,
119
+ role: "omp.tool.write.image",
120
+ max: { h: `${NATIVE_SVG_PREVIEW_LINES}lines` },
121
+ });
122
+ result[kSvgPreview] = { content, node: preview };
123
+ return preview;
124
+ }
93
125
 
94
126
  function countLines(text: string): number {
95
127
  if (!text) return 0;
@@ -589,7 +621,7 @@ export const writeToolRenderer = {
589
621
  },
590
622
 
591
623
  renderResult(
592
- result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails; isError?: boolean },
624
+ result: WriteResult,
593
625
  options: RenderResultOptions & { renderContext?: WriteRenderContext },
594
626
  uiTheme: Theme,
595
627
  args?: WriteRenderArgs,
@@ -722,7 +754,7 @@ export const writeToolRenderer = {
722
754
  },
723
755
 
724
756
  describeResult(
725
- result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails; isError?: boolean },
757
+ result: WriteResult,
726
758
  options: RenderResultOptions & { renderContext?: WriteRenderContext },
727
759
  args?: WriteRenderArgs,
728
760
  ): NativeToolView | undefined {
@@ -744,10 +776,13 @@ export const writeToolRenderer = {
744
776
  });
745
777
  if (result.isError) return { tool, tone: "error", body: [errorText(resultText(result))] };
746
778
  const progressText = resultText(result);
779
+ // A finished SVG write leads with the drawing; its source follows below the collapsed clamp.
780
+ const svg = !isPartial && fileContent.trim().length > 0 && rawPath.toLowerCase().endsWith(".svg");
747
781
  const body = compact<NativeChild>([
748
782
  isPartial &&
749
783
  progressText.length > 0 &&
750
784
  node("text", { spans: [span(progressText, "muted")], truncate: "end" }),
785
+ svg && describeSvgPreview(result, fileContent, displayPath(rawPath)),
751
786
  fileContent.length > 0 &&
752
787
  code(fileContent, {
753
788
  lang: rawPath ? getLanguageFromPath(rawPath) : undefined,
@@ -755,7 +790,7 @@ export const writeToolRenderer = {
755
790
  }),
756
791
  diagnosticsSection(diagnostics),
757
792
  ]);
758
- return { tool, body, preview: NATIVE_WRITE_PREVIEW };
793
+ return { tool, body, preview: svg ? { lines: NATIVE_SVG_PREVIEW_LINES } : NATIVE_WRITE_PREVIEW };
759
794
  },
760
795
  mergeCallAndResult: true,
761
796
  // The collapsed pending preview follows the streaming edge with a tail
@@ -6,8 +6,11 @@
6
6
  * deliver Shift+Enter as a bare `\r` (indistinguishable from Enter) and
7
7
  * Ctrl+Enter as `\n`. Enabling `CSI ? 9001 h` makes the console host serving
8
8
  * this process report every key as `CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _`, which
9
- * carries the full modifier state. Paste text, mouse reports and terminal
10
- * replies keep arriving as plain VT, so only key records need translating.
9
+ * carries the full modifier state. Mouse reports and terminal replies keep
10
+ * arriving as plain VT. Pasted text mostly does too, but the console host
11
+ * re-encodes some pasted characters (line breaks) as key records inside the
12
+ * bracketed paste; {@link Win32InputModeDecoder.decodePaste} turns those back
13
+ * into text.
11
14
  *
12
15
  * Unmodified and legacy-expressible keys translate to the bytes a legacy
13
16
  * terminal sends; chords legacy encoding loses (modified Enter/Tab/Escape,
@@ -16,6 +19,7 @@
16
19
  */
17
20
 
18
21
  const W32IM_PATTERN = /^\x1b\[([\d;]*)_$/;
22
+ const W32IM_RECORDS = /\x1b\[[\d;]*_/g;
19
23
 
20
24
  const RIGHT_ALT_PRESSED = 0x0001;
21
25
  const LEFT_ALT_PRESSED = 0x0002;
@@ -189,6 +193,26 @@ export class Win32InputModeDecoder {
189
193
  return Array.from({ length: record.repeat }, () => encoded);
190
194
  }
191
195
 
196
+ /**
197
+ * Replace the key records embedded in bracketed paste `content` with the
198
+ * text they type, so a pasted line break stays a line break instead of
199
+ * acting as Enter. Key-downs yield their character (`\r` for Enter);
200
+ * releases and keys without text yield nothing. Text that merely looks like
201
+ * a record without ESC is left alone.
202
+ */
203
+ decodePaste(content: string): string {
204
+ if (!content.includes("\x1b[")) return content;
205
+ this.#pendingHighSurrogate = 0;
206
+ const decoded = content.replace(W32IM_RECORDS, match => {
207
+ const record = parseRecord(match);
208
+ if (!record) return match;
209
+ if (!record.down) return "";
210
+ return this.#emitText(record.uc, record.repeat).join("");
211
+ });
212
+ this.#pendingHighSurrogate = 0;
213
+ return decoded;
214
+ }
215
+
192
216
  #emitText(codeUnit: number, repeat = 1): string[] {
193
217
  if (codeUnit === 0) return [];
194
218
  if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {