@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 +14 -0
- 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/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,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.
|
|
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.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.
|
|
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.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.#
|
|
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) {
|