@oh-my-pi/pi-tui 18.4.10 → 18.4.12
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 +20 -0
- package/dist/types/components/editor.d.ts +1 -1
- package/dist/types/components/input.d.ts +1 -1
- package/dist/types/native/encode.d.ts +2 -1
- package/dist/types/native/node.d.ts +6 -1
- package/dist/types/tools/agent-tree.d.ts +2 -0
- package/dist/types/tools/task.d.ts +2 -0
- package/dist/types/tools/wait.d.ts +2 -0
- package/dist/types/tools/write.d.ts +11 -16
- package/dist/types/windows-input-mode.d.ts +13 -2
- package/package.json +9 -9
- package/src/apps/git/sidebar.ts +2 -2
- package/src/components/editor.ts +2 -1
- package/src/components/input.ts +2 -1
- package/src/native/backend.ts +5 -1
- package/src/native/encode.ts +4 -2
- package/src/native/node.ts +3 -1
- package/src/terminal.ts +4 -2
- package/src/tools/agent-tree.ts +5 -0
- package/src/tools/eval.ts +14 -5
- package/src/tools/task.ts +4 -0
- package/src/tools/wait.ts +17 -3
- package/src/tools/write.ts +39 -4
- package/src/windows-input-mode.ts +26 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.4.12] - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Changed Ctrl+Z in Tern's native composer to undo the last edit (TSP `undo` event) instead of suspending
|
|
10
|
+
|
|
11
|
+
## [18.4.11] - 2026-10-02
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added subagent completion percentages to the agent tree, task, and wait views.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed multiline paste on Windows so pasted line breaks are inserted correctly instead of terminal escape sequences.
|
|
24
|
+
|
|
5
25
|
## [18.4.10] - 2026-10-02
|
|
6
26
|
|
|
7
27
|
### Added
|
|
@@ -296,7 +296,7 @@ export declare class Editor implements Component, Focusable {
|
|
|
296
296
|
submit(): void;
|
|
297
297
|
/** Insert text at the current cursor position */
|
|
298
298
|
insertText(text: string): void;
|
|
299
|
-
/** Terminal-side selection edits on the `editor` node (see {@link applyHostEdit}). */
|
|
299
|
+
/** Terminal-side selection edits and undo on the `editor` node (see {@link applyHostEdit}). */
|
|
300
300
|
handleNativeEvent(event: NativeUiEvent): void;
|
|
301
301
|
/**
|
|
302
302
|
* Apply an edit the terminal made over its own selection (TSP `edit`):
|
|
@@ -39,7 +39,7 @@ export declare class Input implements Component, Focusable {
|
|
|
39
39
|
/** Apply terminal paste semantics to text from non-bracketed paste transports
|
|
40
40
|
* (e.g. kitty's OSC 5522 enhanced clipboard read). Mirrors `Editor.pasteText`. */
|
|
41
41
|
pasteText(text: string): void;
|
|
42
|
-
/** Terminal-side selection edits on the `input` node (see {@link applyHostEdit}). */
|
|
42
|
+
/** Terminal-side selection edits and undo on the `input` node (see {@link applyHostEdit}). */
|
|
43
43
|
handleNativeEvent(event: NativeUiEvent): void;
|
|
44
44
|
/**
|
|
45
45
|
* Apply an edit the terminal made over its own selection (TSP `edit`):
|
|
@@ -27,7 +27,8 @@ export declare function encodeTspJson(verb: TspVerb, value: unknown, params?: Ts
|
|
|
27
27
|
/**
|
|
28
28
|
* The `hello` query; callers follow it with a DA1 sentinel. `features: ["edit"]`
|
|
29
29
|
* tells the terminal that omp applies its `edit` events (TSP §8.5), so it may keep a
|
|
30
|
-
* native selection in omp's editors; without it, every key stays omp's.
|
|
30
|
+
* native selection in omp's editors; without it, every key stays omp's. `"undo"`
|
|
31
|
+
* says omp applies `undo` events, so the terminal may turn ⌃Z in a field into one.
|
|
31
32
|
*/
|
|
32
33
|
export declare function encodeTspHelloQuery(version?: string): string;
|
|
33
34
|
/** One decoded APC message: verb, parameters and raw body. */
|
|
@@ -117,7 +117,12 @@ export type NativeUiEvent = {
|
|
|
117
117
|
| ({
|
|
118
118
|
readonly type: "edit";
|
|
119
119
|
readonly key: string;
|
|
120
|
-
} & NativeTextEdit)
|
|
120
|
+
} & NativeTextEdit)
|
|
121
|
+
/** Undo the last change to an `editor`/`input` node's text (the terminal's ⌃Z); a no-op with no history. */
|
|
122
|
+
| {
|
|
123
|
+
readonly type: "undo";
|
|
124
|
+
readonly key: string;
|
|
125
|
+
};
|
|
121
126
|
/**
|
|
122
127
|
* A primitive edit the terminal made over its own text selection (cut,
|
|
123
128
|
* typing, Backspace/Delete or paste over it, or collapsing it). Offsets are
|
|
@@ -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.
|
|
10
|
-
*
|
|
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.
|
|
4
|
+
"version": "18.4.12",
|
|
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.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.4.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.4.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.4.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.4.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.4.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.4.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.4.
|
|
44
|
+
"@oh-my-pi/omptype": "18.4.12",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.4.12",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.4.12",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.4.12",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.4.12",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.4.12",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.4.12",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.4.12"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/apps/git/sidebar.ts
CHANGED
|
@@ -1007,7 +1007,7 @@ export class Sidebar {
|
|
|
1007
1007
|
* text field being edited. False when not the sidebar's event.
|
|
1008
1008
|
*/
|
|
1009
1009
|
handleNativeEvent(event: NativeUiEvent): boolean {
|
|
1010
|
-
if (event.type === "edit") {
|
|
1010
|
+
if (event.type === "edit" || event.type === "undo") {
|
|
1011
1011
|
const editing = this.editing ? this.selected?.kind : undefined;
|
|
1012
1012
|
const field =
|
|
1013
1013
|
editing === "summary"
|
|
@@ -1017,7 +1017,7 @@ export class Sidebar {
|
|
|
1017
1017
|
: editing === "description"
|
|
1018
1018
|
? this.description
|
|
1019
1019
|
: undefined;
|
|
1020
|
-
field?.
|
|
1020
|
+
field?.handleNativeEvent(event);
|
|
1021
1021
|
return field !== undefined;
|
|
1022
1022
|
}
|
|
1023
1023
|
if (event.type === "select" || event.type === "activate") {
|
package/src/components/editor.ts
CHANGED
|
@@ -2862,9 +2862,10 @@ export class Editor implements Component, Focusable {
|
|
|
2862
2862
|
this.#insertTextAtCursor(text);
|
|
2863
2863
|
}
|
|
2864
2864
|
|
|
2865
|
-
/** Terminal-side selection edits on the `editor` node (see {@link applyHostEdit}). */
|
|
2865
|
+
/** Terminal-side selection edits and undo on the `editor` node (see {@link applyHostEdit}). */
|
|
2866
2866
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
2867
2867
|
if (event.type === "edit") this.applyHostEdit(event);
|
|
2868
|
+
else if (event.type === "undo") this.#applyUndo();
|
|
2868
2869
|
}
|
|
2869
2870
|
|
|
2870
2871
|
/**
|
package/src/components/input.ts
CHANGED
|
@@ -284,9 +284,10 @@ export class Input implements Component, Focusable {
|
|
|
284
284
|
this.#handlePaste(text);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
/** Terminal-side selection edits on the `input` node (see {@link applyHostEdit}). */
|
|
287
|
+
/** Terminal-side selection edits and undo on the `input` node (see {@link applyHostEdit}). */
|
|
288
288
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
289
289
|
if (event.type === "edit") this.applyHostEdit(event);
|
|
290
|
+
else if (event.type === "undo") this.#undo();
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
/**
|
package/src/native/backend.ts
CHANGED
|
@@ -671,6 +671,7 @@ export class NativeBackend {
|
|
|
671
671
|
case "action":
|
|
672
672
|
case "change":
|
|
673
673
|
case "edit":
|
|
674
|
+
case "undo":
|
|
674
675
|
this.#routeUiEvent(event);
|
|
675
676
|
return;
|
|
676
677
|
case "focus": {
|
|
@@ -687,7 +688,7 @@ export class NativeBackend {
|
|
|
687
688
|
}
|
|
688
689
|
|
|
689
690
|
#routeUiEvent(
|
|
690
|
-
event: Extract<TspEvent, { ev: "toggle" | "select" | "activate" | "action" | "change" | "edit" }>,
|
|
691
|
+
event: Extract<TspEvent, { ev: "toggle" | "select" | "activate" | "action" | "change" | "edit" | "undo" }>,
|
|
691
692
|
): void {
|
|
692
693
|
const reconciler = this.#surfaceFor(event.sf)?.reconciler;
|
|
693
694
|
const target = reconciler?.target(event.id);
|
|
@@ -721,6 +722,9 @@ export class NativeBackend {
|
|
|
721
722
|
ui = { type: "edit", key: target.keypath, from, to, text, cursor, len };
|
|
722
723
|
break;
|
|
723
724
|
}
|
|
725
|
+
case "undo":
|
|
726
|
+
ui = { type: "undo", key: target.keypath };
|
|
727
|
+
break;
|
|
724
728
|
}
|
|
725
729
|
target.component.handleNativeEvent(ui);
|
|
726
730
|
this.#host.requestRender();
|
package/src/native/encode.ts
CHANGED
|
@@ -109,10 +109,11 @@ export function encodeTspJson(verb: TspVerb, value: unknown, params?: TspParams,
|
|
|
109
109
|
/**
|
|
110
110
|
* The `hello` query; callers follow it with a DA1 sentinel. `features: ["edit"]`
|
|
111
111
|
* tells the terminal that omp applies its `edit` events (TSP §8.5), so it may keep a
|
|
112
|
-
* native selection in omp's editors; without it, every key stays omp's.
|
|
112
|
+
* native selection in omp's editors; without it, every key stays omp's. `"undo"`
|
|
113
|
+
* says omp applies `undo` events, so the terminal may turn ⌃Z in a field into one.
|
|
113
114
|
*/
|
|
114
115
|
export function encodeTspHelloQuery(version?: string): string {
|
|
115
|
-
return encodeTspJson("q", { q: "hello", v: [TSP_VERSION], app: "omp", features: ["edit"], ver: version });
|
|
116
|
+
return encodeTspJson("q", { q: "hello", v: [TSP_VERSION], app: "omp", features: ["edit", "undo"], ver: version });
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
/** One decoded APC message: verb, parameters and raw body. */
|
|
@@ -172,6 +173,7 @@ const EVENT_REQUIRED: Readonly<Record<string, Readonly<Record<string, "string" |
|
|
|
172
173
|
// `value` varies by control (boolean, number, string, string[], null): the handler checks it.
|
|
173
174
|
change: { id: "string", item: "string" },
|
|
174
175
|
edit: { id: "string", from: "number", to: "number", text: "string", cursor: "number", len: "number" },
|
|
176
|
+
undo: { id: "string" },
|
|
175
177
|
focus: { id: "string" },
|
|
176
178
|
error: { msg: "string" },
|
|
177
179
|
gone: { ids: "array" },
|
package/src/native/node.ts
CHANGED
|
@@ -113,7 +113,9 @@ export type NativeUiEvent =
|
|
|
113
113
|
readonly value: boolean | number | string | readonly string[] | null;
|
|
114
114
|
}
|
|
115
115
|
/** An edit over the terminal's own selection in an `editor`/`input` node (see {@link NativeTextEdit}). */
|
|
116
|
-
| ({ readonly type: "edit"; readonly key: string } & NativeTextEdit)
|
|
116
|
+
| ({ readonly type: "edit"; readonly key: string } & NativeTextEdit)
|
|
117
|
+
/** Undo the last change to an `editor`/`input` node's text (the terminal's ⌃Z); a no-op with no history. */
|
|
118
|
+
| { readonly type: "undo"; readonly key: string };
|
|
117
119
|
|
|
118
120
|
/**
|
|
119
121
|
* A primitive edit the terminal made over its own text selection (cut,
|
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.#
|
|
1622
|
+
const text = this.#win32InputDecoder?.decodePaste(content) ?? content;
|
|
1623
|
+
this.#inputHandler(`\x1b[200~${text}\x1b[201~${enter ?? ""}`);
|
|
1622
1624
|
}
|
|
1623
1625
|
});
|
|
1624
1626
|
|
package/src/tools/agent-tree.ts
CHANGED
|
@@ -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
|
|
693
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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" }),
|
package/src/tools/write.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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.
|
|
10
|
-
*
|
|
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) {
|