@oh-my-pi/pi-tui 18.8.6 → 18.8.7
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 +18 -16
- package/README.md +7 -5
- package/dist/types/chat/assistant-message.d.ts +1 -0
- package/dist/types/chat/compaction-summary-message.d.ts +1 -0
- package/dist/types/chat/user-message.d.ts +2 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/disclosure.d.ts +1 -0
- package/dist/types/components/image.d.ts +1 -0
- package/dist/types/components/key-value-list.d.ts +1 -0
- package/dist/types/components/layout/row.d.ts +1 -0
- package/dist/types/components/layout/stack.d.ts +1 -0
- package/dist/types/components/markdown.d.ts +1 -0
- package/dist/types/components/scroll-view.d.ts +1 -0
- package/dist/types/components/section.d.ts +1 -0
- package/dist/types/components/table.d.ts +1 -0
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/components/truncated-text.d.ts +1 -0
- package/dist/types/native/blobs.d.ts +3 -1
- package/dist/types/overlays/session-selector.d.ts +1 -1
- package/dist/types/render/output-pane.d.ts +1 -0
- package/dist/types/render/render-utils.d.ts +1 -0
- package/dist/types/render/tool-card.d.ts +6 -0
- package/dist/types/render/width-aware-text.d.ts +2 -0
- package/dist/types/terminal.d.ts +10 -2
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +11 -0
- package/package.json +9 -9
- package/src/chat/advisor-message.ts +11 -0
- package/src/chat/assistant-message.ts +6 -0
- package/src/chat/compaction-summary-message.ts +8 -0
- package/src/chat/read-tool-group.ts +6 -4
- package/src/chat/skill-message.ts +6 -0
- package/src/chat/table-chart.ts +17 -20
- package/src/chat/tool-execution.ts +18 -0
- package/src/chat/user-message.ts +14 -0
- package/src/chrome/transcript-container.ts +32 -1
- package/src/components/box.ts +5 -0
- package/src/components/disclosure.ts +8 -0
- package/src/components/image.ts +7 -5
- package/src/components/key-value-list.ts +4 -0
- package/src/components/layout/row.ts +7 -0
- package/src/components/layout/stack.ts +7 -0
- package/src/components/markdown.ts +8 -0
- package/src/components/scroll-view.ts +5 -0
- package/src/components/section.ts +5 -0
- package/src/components/table.ts +4 -0
- package/src/components/text.ts +7 -0
- package/src/components/truncated-text.ts +5 -0
- package/src/native/backend.ts +16 -15
- package/src/native/blobs.ts +43 -10
- package/src/native/reconcile.ts +4 -0
- package/src/overlays/session-selector.ts +29 -19
- package/src/overlays/snapcompact-shape-preview.ts +2 -3
- package/src/prompt/attachment-chips.ts +13 -10
- package/src/render/output-pane.ts +6 -0
- package/src/render/render-utils.ts +11 -0
- package/src/render/tool-card.ts +15 -0
- package/src/render/width-aware-text.ts +5 -0
- package/src/terminal.ts +24 -3
- package/src/tools/bash.ts +10 -11
- package/src/tools/edit.ts +142 -120
- package/src/tools/fetch.ts +5 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/mcp.ts +15 -2
- package/src/tools/read.ts +7 -5
- package/src/tools/write.ts +70 -56
- package/src/tools/yield.ts +225 -0
- package/src/tui.ts +25 -0
package/src/tools/write.ts
CHANGED
|
@@ -637,40 +637,49 @@ export const writeToolRenderer = {
|
|
|
637
637
|
// back to the normalizing stringify.
|
|
638
638
|
const content = typeof args.content === "string" ? args.content : normalizeDisplayText(args.content);
|
|
639
639
|
const streamingCache = createRenderedStringCache();
|
|
640
|
-
return framedToolCard(
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
640
|
+
return framedToolCard(
|
|
641
|
+
uiTheme,
|
|
642
|
+
() => {
|
|
643
|
+
const body = content
|
|
644
|
+
? formatStreamingContent(
|
|
645
|
+
content,
|
|
646
|
+
Boolean(options?.expanded),
|
|
647
|
+
lang,
|
|
648
|
+
uiTheme,
|
|
649
|
+
options?.spinnerFrame,
|
|
650
|
+
streamingCache,
|
|
651
|
+
// `options` is the ToolExecutionComponent's persistent
|
|
652
|
+
// render-state object — a stable identity across reveal ticks
|
|
653
|
+
// that keys the incremental preview state. `argsComplete`
|
|
654
|
+
// flushes the trailing line through the highlighter once.
|
|
655
|
+
options,
|
|
656
|
+
options?.argsComplete,
|
|
657
|
+
)
|
|
658
|
+
: "";
|
|
659
|
+
const bodyLines = body ? body.split("\n") : [];
|
|
660
|
+
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
661
|
+
return {
|
|
662
|
+
header,
|
|
663
|
+
sections: bodyLines.length > 0 ? [{ content: bodyLines }] : [],
|
|
664
|
+
phase: "pending",
|
|
665
|
+
borderColor: "borderMuted",
|
|
666
|
+
};
|
|
667
|
+
},
|
|
668
|
+
// The highlighted body in `streamingCache` does not depend on width, and re-highlighting every committed
|
|
669
|
+
// card on each resize replay costs seconds; only the incremental highlighter state goes.
|
|
670
|
+
{ onReleaseRenderCaches: () => delete options?.[writeStreamingPreviewStateKey] },
|
|
671
|
+
);
|
|
666
672
|
},
|
|
667
673
|
|
|
668
674
|
renderResult(
|
|
669
675
|
result: WriteResult,
|
|
670
|
-
options: RenderResultOptions & { renderContext?: WriteRenderContext },
|
|
676
|
+
options: RenderResultOptions & WriteStreamingPreviewStateCarrier & { renderContext?: WriteRenderContext },
|
|
671
677
|
uiTheme: Theme,
|
|
672
678
|
args?: WriteRenderArgs,
|
|
673
679
|
): Component {
|
|
680
|
+
// Write merges call and result, so this builder runs once per display rebuild after the result arrives and the
|
|
681
|
+
// call card never renders again: its incremental highlighter state is dead from here on.
|
|
682
|
+
delete options[writeStreamingPreviewStateKey];
|
|
674
683
|
const cardPath =
|
|
675
684
|
typeof args?.path === "string" ? args.path : typeof args?.file_path === "string" ? args.file_path : "";
|
|
676
685
|
const routed = writeUrlCard(cardPath, result.details);
|
|
@@ -732,36 +741,41 @@ export const writeToolRenderer = {
|
|
|
732
741
|
const diagnostics = result.details?.diagnostics;
|
|
733
742
|
|
|
734
743
|
const previewCache = createRenderedStringCache();
|
|
735
|
-
return framedToolCard(
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>
|
|
748
|
-
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
749
|
-
);
|
|
750
|
-
if (diagText.trim()) {
|
|
751
|
-
const diagLines = diagText.split("\n");
|
|
752
|
-
const firstNonEmpty = diagLines.findIndex(line => line.trim());
|
|
753
|
-
if (firstNonEmpty >= 0) body += `\n${diagLines.slice(firstNonEmpty).join("\n")}`;
|
|
744
|
+
return framedToolCard(
|
|
745
|
+
uiTheme,
|
|
746
|
+
() => {
|
|
747
|
+
const { expanded } = options;
|
|
748
|
+
let body = renderContentPreview(fileContent, expanded, lang, uiTheme, previewCache);
|
|
749
|
+
if (isPartial && progressText) {
|
|
750
|
+
const safeProgressText = truncateToWidth(
|
|
751
|
+
replaceTabs(progressText),
|
|
752
|
+
TRUNCATE_LENGTHS.LINE,
|
|
753
|
+
Ellipsis.Unicode,
|
|
754
|
+
);
|
|
755
|
+
body = `${uiTheme.fg("muted", safeProgressText)}${body ? `\n${body}` : ""}`;
|
|
754
756
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
757
|
+
if (!isPartial && diagnostics) {
|
|
758
|
+
const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>
|
|
759
|
+
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
760
|
+
);
|
|
761
|
+
if (diagText.trim()) {
|
|
762
|
+
const diagLines = diagText.split("\n");
|
|
763
|
+
const firstNonEmpty = diagLines.findIndex(line => line.trim());
|
|
764
|
+
if (firstNonEmpty >= 0) body += `\n${diagLines.slice(firstNonEmpty).join("\n")}`;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const bodyLines = body.split("\n");
|
|
768
|
+
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
769
|
+
return {
|
|
770
|
+
header,
|
|
771
|
+
sections: bodyLines.length > 0 ? [{ content: bodyLines }] : [],
|
|
772
|
+
phase: isPartial ? "partial" : "success",
|
|
773
|
+
borderColor: "borderMuted",
|
|
774
|
+
};
|
|
775
|
+
},
|
|
776
|
+
// No release hook: `previewCache` holds the width-independent highlighted preview, which resize replay
|
|
777
|
+
// would otherwise re-highlight for every committed card.
|
|
778
|
+
);
|
|
765
779
|
},
|
|
766
780
|
describeCall(
|
|
767
781
|
args: WriteRenderArgs,
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript renderer for the subagent `yield` tool ("Submit Result"). Its
|
|
3
|
+
* reply ("Result submitted.") is for the model; the card shows what was
|
|
4
|
+
* submitted: the section labels or workpool item in the head, the data as a
|
|
5
|
+
* JSON tree (Markdown when it is a string), and the reported or rejected error.
|
|
6
|
+
*/
|
|
7
|
+
import { formatCount, isRecord, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { compact, md } from "../native/describe";
|
|
9
|
+
import { OwnerMemo } from "../native/memo";
|
|
10
|
+
import type { NativeChild } from "../native/node";
|
|
11
|
+
import { formatOutputPaneLines, styleToolOutputLine } from "../render/output-pane";
|
|
12
|
+
import { formatExpandHint, sanitizeDisplayLines, truncateToWidth } from "../render/render-utils";
|
|
13
|
+
import type { StatusLineOptions } from "../render/status-line";
|
|
14
|
+
import { plainToolCard, type ToolCardPhase } from "../render/tool-card";
|
|
15
|
+
import type { Theme } from "../theme/theme";
|
|
16
|
+
import type { Component } from "../tui";
|
|
17
|
+
import { wrapTextWithAnsi } from "../utils";
|
|
18
|
+
import {
|
|
19
|
+
describeJsonTree,
|
|
20
|
+
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
21
|
+
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
22
|
+
JSON_TREE_MAX_LINES_COLLAPSED,
|
|
23
|
+
JSON_TREE_MAX_LINES_EXPANDED,
|
|
24
|
+
JSON_TREE_SCALAR_LEN_COLLAPSED,
|
|
25
|
+
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
26
|
+
renderJsonTreeLines,
|
|
27
|
+
} from "./json-tree";
|
|
28
|
+
import { errorText, noteText, resultText } from "./native-view";
|
|
29
|
+
import type {
|
|
30
|
+
NativeToolView,
|
|
31
|
+
RenderResultContextOptions,
|
|
32
|
+
RenderResultOptions,
|
|
33
|
+
ToolRenderer,
|
|
34
|
+
ToolRenderResult,
|
|
35
|
+
} from "./renderer";
|
|
36
|
+
import type { YieldItem } from "./task";
|
|
37
|
+
|
|
38
|
+
const TITLE = "Submit Result";
|
|
39
|
+
|
|
40
|
+
/** Collapsed rows of string data (a report) in the ANSI card. */
|
|
41
|
+
const TEXT_ROWS_COLLAPSED = 4;
|
|
42
|
+
|
|
43
|
+
/** `yield` arguments as sent; strict-mode providers send omitted optionals as `null`. */
|
|
44
|
+
interface YieldRenderArgs {
|
|
45
|
+
type?: string | string[] | null;
|
|
46
|
+
data?: unknown;
|
|
47
|
+
error?: string | null;
|
|
48
|
+
/** 1-based workpool item number. */
|
|
49
|
+
key?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One `yield` call as its card shows it, read from the args and, once settled, the result. */
|
|
53
|
+
interface Submission {
|
|
54
|
+
/** Section labels or workpool item the call fills; absent for a whole result. */
|
|
55
|
+
target?: string;
|
|
56
|
+
/** The accepted (normalized) data once settled, else the data as sent. */
|
|
57
|
+
data: unknown;
|
|
58
|
+
/** The failure the call reports, or why the tool rejected it. */
|
|
59
|
+
error?: string;
|
|
60
|
+
settled: boolean;
|
|
61
|
+
/** Accepted although it failed the output schema. */
|
|
62
|
+
overridden: boolean;
|
|
63
|
+
/** Where a settled call without data takes its result from. */
|
|
64
|
+
note?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readSubmission(
|
|
68
|
+
args: YieldRenderArgs | undefined,
|
|
69
|
+
result: ToolRenderResult<YieldItem> | undefined,
|
|
70
|
+
): Submission {
|
|
71
|
+
const details = result?.isError ? undefined : result?.details;
|
|
72
|
+
const type = details?.type ?? args?.type ?? undefined;
|
|
73
|
+
const incremental = Array.isArray(type) && type.length > 0;
|
|
74
|
+
const data = details?.data !== undefined ? details.data : (args?.data ?? undefined);
|
|
75
|
+
let error = args?.error || undefined;
|
|
76
|
+
if (result?.isError) error = resultText(result) || `${TITLE} failed`;
|
|
77
|
+
else if (result && details?.status === "aborted") error = details.error || resultText(result) || "Aborted";
|
|
78
|
+
const settled = result !== undefined;
|
|
79
|
+
return {
|
|
80
|
+
target: typeof args?.key === "number" ? `item ${args.key}` : incremental ? type.join(", ") : undefined,
|
|
81
|
+
data,
|
|
82
|
+
error,
|
|
83
|
+
settled,
|
|
84
|
+
overridden: details?.schemaOverridden === true,
|
|
85
|
+
note:
|
|
86
|
+
settled && error === undefined && data === undefined
|
|
87
|
+
? incremental
|
|
88
|
+
? "Takes the last message as this section"
|
|
89
|
+
: "Ends with the sections submitted earlier, or the last message"
|
|
90
|
+
: undefined,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Head fact for container data (`13 fields`, `4 items`). */
|
|
95
|
+
function sizeMeta(data: unknown): string | undefined {
|
|
96
|
+
if (Array.isArray(data)) return formatCount("item", data.length);
|
|
97
|
+
if (isRecord(data)) return formatCount("field", Object.keys(data).length);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function describeData(data: unknown, streaming: boolean): NativeChild | undefined {
|
|
102
|
+
if (data === undefined) return undefined;
|
|
103
|
+
if (typeof data === "string") return data.trim() ? md(data, { stream: streaming }) : noteText("(empty)");
|
|
104
|
+
// Open to the depth cap: the data is the card's whole point, and the
|
|
105
|
+
// collapsed card's preview clamp already bounds what shows.
|
|
106
|
+
return describeJsonTree(data, { hiddenRootKeys: [], openDepth: JSON_TREE_MAX_DEPTH_EXPANDED });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function describeSubmission(s: Submission, streaming: boolean): NativeToolView {
|
|
110
|
+
const size = sizeMeta(s.data);
|
|
111
|
+
return {
|
|
112
|
+
tool: {
|
|
113
|
+
title: TITLE,
|
|
114
|
+
target: s.target,
|
|
115
|
+
targetKind: s.target ? "text" : undefined,
|
|
116
|
+
meta: size ? [size] : undefined,
|
|
117
|
+
badges: s.overridden ? [{ text: "schema overridden", tone: "warning" }] : undefined,
|
|
118
|
+
},
|
|
119
|
+
tone: s.settled && s.error !== undefined ? "error" : undefined,
|
|
120
|
+
body: compact([
|
|
121
|
+
s.error !== undefined ? errorText(s.error) : undefined,
|
|
122
|
+
describeData(s.data, streaming),
|
|
123
|
+
s.note ? noteText(s.note, "dim") : undefined,
|
|
124
|
+
]),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function dataLines(data: unknown, expanded: boolean, width: number, theme: Theme): string[] {
|
|
129
|
+
if (data === undefined) return [];
|
|
130
|
+
if (typeof data === "string") {
|
|
131
|
+
return [
|
|
132
|
+
...formatOutputPaneLines(
|
|
133
|
+
{
|
|
134
|
+
lines: sanitizeText(data).trimEnd().split("\n"),
|
|
135
|
+
expanded,
|
|
136
|
+
collapsedMaxLines: TEXT_ROWS_COLLAPSED,
|
|
137
|
+
styleLine: line => truncateToWidth(styleToolOutputLine(line, theme), width),
|
|
138
|
+
},
|
|
139
|
+
theme,
|
|
140
|
+
).lines,
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
const tree = renderJsonTreeLines(data, theme, {
|
|
144
|
+
maxDepth: expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
145
|
+
maxLines: expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED,
|
|
146
|
+
maxScalarLen: expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED,
|
|
147
|
+
sanitizeText,
|
|
148
|
+
hiddenRootKeys: [],
|
|
149
|
+
});
|
|
150
|
+
if (tree.truncated) tree.lines.push(expanded ? theme.fg("dim", "…") : formatExpandHint(theme, false, true));
|
|
151
|
+
return tree.lines;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderSubmission(s: Submission, options: RenderResultOptions, theme: Theme): Component {
|
|
155
|
+
// The body is spinner-invariant; rebuild it only on expansion or width change.
|
|
156
|
+
let memo: { expanded: boolean; width: number; lines: readonly string[] } | undefined;
|
|
157
|
+
return plainToolCard(
|
|
158
|
+
theme,
|
|
159
|
+
({ contentWidth }) => {
|
|
160
|
+
const running = options.spinnerFrame !== undefined;
|
|
161
|
+
const failed = s.settled && s.error !== undefined;
|
|
162
|
+
const size = sizeMeta(s.data);
|
|
163
|
+
const status: StatusLineOptions = {
|
|
164
|
+
icon: !s.settled ? (running ? "running" : "pending") : failed ? "error" : "done",
|
|
165
|
+
spinnerFrame: options.spinnerFrame,
|
|
166
|
+
title: TITLE,
|
|
167
|
+
description: s.target,
|
|
168
|
+
badge: s.overridden ? { label: "schema overridden", color: "warning" } : undefined,
|
|
169
|
+
meta: size ? [size] : undefined,
|
|
170
|
+
};
|
|
171
|
+
const phase: ToolCardPhase = !s.settled ? (running ? "running" : "partial") : failed ? "error" : "success";
|
|
172
|
+
if (memo === undefined || memo.expanded !== options.expanded || memo.width !== contentWidth) {
|
|
173
|
+
const lines: string[] = [];
|
|
174
|
+
// Wrapped in full: a schema rejection names every offending field.
|
|
175
|
+
if (s.error !== undefined) {
|
|
176
|
+
for (const line of sanitizeDisplayLines(s.error)) {
|
|
177
|
+
lines.push(...wrapTextWithAnsi(theme.fg("error", line), Math.max(1, contentWidth)));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
lines.push(...dataLines(s.data, options.expanded, contentWidth, theme));
|
|
181
|
+
if (s.note) lines.push(theme.fg("dim", s.note));
|
|
182
|
+
memo = { expanded: options.expanded, width: contentWidth, lines };
|
|
183
|
+
}
|
|
184
|
+
return { status, phase, body: memo.lines };
|
|
185
|
+
},
|
|
186
|
+
{ paddingX: 1, paddingY: 1, ignoreTight: true, onInvalidate: () => (memo = undefined) },
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const callMemo = new OwnerMemo<NativeToolView>();
|
|
191
|
+
const resultMemo = new OwnerMemo<NativeToolView>();
|
|
192
|
+
|
|
193
|
+
/** Shows a `yield` call's submitted data instead of its model-facing acknowledgement. */
|
|
194
|
+
export const yieldToolRenderer = {
|
|
195
|
+
renderCall(args: YieldRenderArgs, options: RenderResultOptions, theme: Theme): Component {
|
|
196
|
+
return renderSubmission(readSubmission(args, undefined), options, theme);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
renderResult(
|
|
200
|
+
result: ToolRenderResult<YieldItem>,
|
|
201
|
+
options: RenderResultContextOptions,
|
|
202
|
+
theme: Theme,
|
|
203
|
+
args?: YieldRenderArgs,
|
|
204
|
+
): Component {
|
|
205
|
+
return renderSubmission(readSubmission(args, result), options, theme);
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
describeCall(args: YieldRenderArgs, options: RenderResultOptions): NativeToolView {
|
|
209
|
+
const streaming = options.argsComplete === false;
|
|
210
|
+
return callMemo.get(args, [streaming], () => describeSubmission(readSubmission(args, undefined), streaming));
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
describeResult(
|
|
214
|
+
result: ToolRenderResult<YieldItem>,
|
|
215
|
+
_options: RenderResultContextOptions,
|
|
216
|
+
args?: YieldRenderArgs,
|
|
217
|
+
): NativeToolView {
|
|
218
|
+
return resultMemo.get(result.content, [args, result.details, result.isError], () =>
|
|
219
|
+
describeSubmission(readSubmission(args, result), false),
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
mergeCallAndResult: true,
|
|
224
|
+
animatedPendingPreview: true,
|
|
225
|
+
} satisfies ToolRenderer<YieldRenderArgs, YieldItem>;
|
package/src/tui.ts
CHANGED
|
@@ -314,6 +314,16 @@ export interface Component {
|
|
|
314
314
|
* Called when theme changes or when component needs to re-render from scratch.
|
|
315
315
|
*/
|
|
316
316
|
invalidate?(): void;
|
|
317
|
+
/**
|
|
318
|
+
* Optional hook to drop memoized render output (rows, parse and wrap state)
|
|
319
|
+
* that the next `render()` can rebuild from state the component keeps.
|
|
320
|
+
* Unlike {@link invalidate}, it must not rebuild anything eagerly: no
|
|
321
|
+
* renderer or extension callbacks, no image conversions, and no child
|
|
322
|
+
* replacement or disposal. The next render must return the same rows it
|
|
323
|
+
* would have returned without the release. Components without it are
|
|
324
|
+
* simply not released.
|
|
325
|
+
*/
|
|
326
|
+
releaseRenderCaches?(): void;
|
|
317
327
|
/**
|
|
318
328
|
* Optional hook to set whether this component ignores tight layout mode.
|
|
319
329
|
*/
|
|
@@ -544,6 +554,7 @@ export class Container implements Component {
|
|
|
544
554
|
clear(): void {
|
|
545
555
|
this.children = [];
|
|
546
556
|
this.#memoLines = undefined;
|
|
557
|
+
this.#memoChildLines = [];
|
|
547
558
|
}
|
|
548
559
|
|
|
549
560
|
/** Dispose every child, then detach it from this container. */
|
|
@@ -553,12 +564,23 @@ export class Container implements Component {
|
|
|
553
564
|
}
|
|
554
565
|
|
|
555
566
|
invalidate(): void {
|
|
567
|
+
// The per-child refs pin every row the children last rendered; dropping
|
|
568
|
+
// only the concatenation would keep all of them reachable.
|
|
556
569
|
this.#memoLines = undefined;
|
|
570
|
+
this.#memoChildLines = [];
|
|
557
571
|
for (const child of this.children) {
|
|
558
572
|
child.invalidate?.();
|
|
559
573
|
}
|
|
560
574
|
}
|
|
561
575
|
|
|
576
|
+
releaseRenderCaches(): void {
|
|
577
|
+
this.#memoLines = undefined;
|
|
578
|
+
this.#memoChildLines = [];
|
|
579
|
+
for (const child of this.children) {
|
|
580
|
+
child.releaseRenderCaches?.();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
562
584
|
/**
|
|
563
585
|
* Propagate teardown to children. Call when the container's children are
|
|
564
586
|
* being permanently discarded (not when they are detached for reuse — use
|
|
@@ -2246,6 +2268,9 @@ export class TUI extends Container {
|
|
|
2246
2268
|
this.terminal.enableInput?.();
|
|
2247
2269
|
this.#querySixelSupport();
|
|
2248
2270
|
this.#queryCellSize();
|
|
2271
|
+
// The probes went out over the painted frame; a terminal that could not
|
|
2272
|
+
// parse one left its bytes on the cursor row (see Terminal.enableInput).
|
|
2273
|
+
this.requestRender(true);
|
|
2249
2274
|
}
|
|
2250
2275
|
|
|
2251
2276
|
addStartListener(listener: StartListener): () => void {
|