@oh-my-pi/pi-coding-agent 3.20.0 → 3.21.0
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 +78 -8
- package/docs/custom-tools.md +3 -3
- package/docs/extensions.md +226 -220
- package/docs/hooks.md +2 -2
- package/docs/sdk.md +3 -3
- package/examples/custom-tools/README.md +2 -2
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/examples/extensions/README.md +76 -74
- package/examples/extensions/todo.ts +2 -5
- package/examples/hooks/custom-compaction.ts +1 -1
- package/examples/hooks/handoff.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/sdk/02-custom-model.ts +1 -1
- package/examples/sdk/12-full-control.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +5 -5
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/list-models.ts +1 -1
- package/src/core/agent-session.ts +13 -2
- package/src/core/auth-storage.ts +1 -1
- package/src/core/compaction/branch-summarization.ts +2 -2
- package/src/core/compaction/compaction.ts +2 -2
- package/src/core/compaction/utils.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +1 -1
- package/src/core/extensions/wrapper.ts +1 -1
- package/src/core/hooks/runner.ts +2 -2
- package/src/core/hooks/types.ts +1 -1
- package/src/core/messages.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/model-resolver.ts +1 -1
- package/src/core/sdk.ts +33 -4
- package/src/core/session-manager.ts +11 -22
- package/src/core/settings-manager.ts +66 -1
- package/src/core/slash-commands.ts +12 -5
- package/src/core/system-prompt.ts +27 -3
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/ask.ts +88 -1
- package/src/core/tools/bash-interceptor.ts +7 -0
- package/src/core/tools/bash.ts +106 -0
- package/src/core/tools/edit-diff.ts +73 -24
- package/src/core/tools/edit.ts +214 -20
- package/src/core/tools/find.ts +162 -1
- package/src/core/tools/gemini-image.ts +279 -56
- package/src/core/tools/git.ts +4 -0
- package/src/core/tools/grep.ts +191 -0
- package/src/core/tools/index.ts +3 -6
- package/src/core/tools/ls.ts +142 -2
- package/src/core/tools/lsp/render.ts +34 -14
- package/src/core/tools/notebook.ts +110 -0
- package/src/core/tools/output.ts +179 -7
- package/src/core/tools/read.ts +122 -9
- package/src/core/tools/render-utils.ts +241 -0
- package/src/core/tools/renderers.ts +40 -828
- package/src/core/tools/review.ts +26 -7
- package/src/core/tools/rulebook.ts +3 -1
- package/src/core/tools/task/index.ts +18 -3
- package/src/core/tools/task/render.ts +7 -2
- package/src/core/tools/task/types.ts +1 -1
- package/src/core/tools/truncate.ts +27 -1
- package/src/core/tools/web-fetch.ts +23 -15
- package/src/core/tools/web-search/index.ts +130 -45
- package/src/core/tools/web-search/providers/anthropic.ts +7 -2
- package/src/core/tools/web-search/providers/exa.ts +2 -1
- package/src/core/tools/web-search/providers/perplexity.ts +6 -1
- package/src/core/tools/web-search/render.ts +5 -0
- package/src/core/tools/web-search/types.ts +13 -0
- package/src/core/tools/write.ts +90 -0
- package/src/core/voice.ts +1 -1
- package/src/lib/worktree/constants.ts +6 -6
- package/src/main.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/custom-message.ts +1 -1
- package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/footer.ts +1 -1
- package/src/modes/interactive/components/hook-message.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +1 -1
- package/src/modes/interactive/components/oauth-selector.ts +1 -1
- package/src/modes/interactive/components/settings-defs.ts +49 -0
- package/src/modes/interactive/components/status-line.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +93 -538
- package/src/modes/interactive/interactive-mode.ts +19 -7
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/prompts/system-prompt.md +4 -0
- package/src/prompts/tools/gemini-image.md +5 -1
- package/src/prompts/tools/output.md +4 -0
- package/src/prompts/tools/web-fetch.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/utils/image-convert.ts +8 -2
- package/src/utils/image-magick.ts +247 -0
- package/src/utils/image-resize.ts +53 -13
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
2
|
import {
|
|
4
3
|
Box,
|
|
@@ -14,10 +13,9 @@ import {
|
|
|
14
13
|
import stripAnsi from "strip-ansi";
|
|
15
14
|
import { computeEditDiff, type EditDiffError, type EditDiffResult } from "../../../core/tools/edit-diff";
|
|
16
15
|
import { toolRenderers } from "../../../core/tools/renderers";
|
|
17
|
-
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate";
|
|
18
16
|
import { convertToPng } from "../../../utils/image-convert";
|
|
19
17
|
import { sanitizeBinaryOutput } from "../../../utils/shell";
|
|
20
|
-
import {
|
|
18
|
+
import { theme } from "../theme/theme";
|
|
21
19
|
import { renderDiff } from "./diff";
|
|
22
20
|
import { truncateToVisualLines } from "./visual-truncate";
|
|
23
21
|
|
|
@@ -26,230 +24,6 @@ const BASH_PREVIEW_LINES = 5;
|
|
|
26
24
|
const GENERIC_PREVIEW_LINES = 6;
|
|
27
25
|
const GENERIC_ARG_PREVIEW = 6;
|
|
28
26
|
const GENERIC_VALUE_MAX = 80;
|
|
29
|
-
const EDIT_DIFF_PREVIEW_HUNKS = 2;
|
|
30
|
-
const EDIT_DIFF_PREVIEW_LINES = 24;
|
|
31
|
-
|
|
32
|
-
function wrapBrackets(text: string): string {
|
|
33
|
-
return `${theme.format.bracketLeft}${text}${theme.format.bracketRight}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function countLines(text: string): number {
|
|
37
|
-
if (!text) return 0;
|
|
38
|
-
return text.split("\n").length;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function formatMetadataLine(lineCount: number | null, language: string | undefined): string {
|
|
42
|
-
const icon = theme.getLangIcon(language);
|
|
43
|
-
if (lineCount !== null) {
|
|
44
|
-
return theme.fg("dim", `${icon} ${lineCount} lines`);
|
|
45
|
-
}
|
|
46
|
-
return theme.fg("dim", `${icon}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "tiff"]);
|
|
50
|
-
const BINARY_EXTENSIONS = new Set(["pdf", "zip", "tar", "gz", "exe", "dll", "so", "dylib", "wasm"]);
|
|
51
|
-
|
|
52
|
-
function getFileType(filePath: string): "image" | "binary" | "text" {
|
|
53
|
-
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
54
|
-
if (!ext) return "text";
|
|
55
|
-
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
56
|
-
if (BINARY_EXTENSIONS.has(ext)) return "binary";
|
|
57
|
-
return "text";
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function formatDiffStats(added: number, removed: number, hunks: number): string {
|
|
61
|
-
const parts: string[] = [];
|
|
62
|
-
if (added > 0) parts.push(theme.fg("success", `+${added}`));
|
|
63
|
-
if (removed > 0) parts.push(theme.fg("error", `-${removed}`));
|
|
64
|
-
if (hunks > 0) parts.push(theme.fg("dim", `${hunks} hunk${hunks !== 1 ? "s" : ""}`));
|
|
65
|
-
return parts.join(theme.fg("dim", " / "));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
type DiffStats = {
|
|
69
|
-
added: number;
|
|
70
|
-
removed: number;
|
|
71
|
-
hunks: number;
|
|
72
|
-
lines: number;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
function getDiffStats(diffText: string): DiffStats {
|
|
76
|
-
const lines = diffText ? diffText.split("\n") : [];
|
|
77
|
-
let added = 0;
|
|
78
|
-
let removed = 0;
|
|
79
|
-
let hunks = 0;
|
|
80
|
-
let inHunk = false;
|
|
81
|
-
|
|
82
|
-
for (const line of lines) {
|
|
83
|
-
const isAdded = line.startsWith("+");
|
|
84
|
-
const isRemoved = line.startsWith("-");
|
|
85
|
-
const isChange = isAdded || isRemoved;
|
|
86
|
-
|
|
87
|
-
if (isAdded) added++;
|
|
88
|
-
if (isRemoved) removed++;
|
|
89
|
-
|
|
90
|
-
if (isChange && !inHunk) {
|
|
91
|
-
hunks++;
|
|
92
|
-
inHunk = true;
|
|
93
|
-
} else if (!isChange) {
|
|
94
|
-
inHunk = false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { added, removed, hunks, lines: lines.length };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function truncateDiffByHunk(
|
|
102
|
-
diffText: string,
|
|
103
|
-
maxHunks: number,
|
|
104
|
-
maxLines: number,
|
|
105
|
-
): { text: string; hiddenHunks: number; hiddenLines: number } {
|
|
106
|
-
const lines = diffText ? diffText.split("\n") : [];
|
|
107
|
-
const totalStats = getDiffStats(diffText);
|
|
108
|
-
const kept: string[] = [];
|
|
109
|
-
let inHunk = false;
|
|
110
|
-
let currentHunks = 0;
|
|
111
|
-
let reachedLimit = false;
|
|
112
|
-
|
|
113
|
-
for (const line of lines) {
|
|
114
|
-
const isChange = line.startsWith("+") || line.startsWith("-");
|
|
115
|
-
if (isChange && !inHunk) {
|
|
116
|
-
currentHunks++;
|
|
117
|
-
inHunk = true;
|
|
118
|
-
}
|
|
119
|
-
if (!isChange) {
|
|
120
|
-
inHunk = false;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (currentHunks > maxHunks) {
|
|
124
|
-
reachedLimit = true;
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
kept.push(line);
|
|
129
|
-
if (kept.length >= maxLines) {
|
|
130
|
-
reachedLimit = true;
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!reachedLimit) {
|
|
136
|
-
return { text: diffText, hiddenHunks: 0, hiddenLines: 0 };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const keptStats = getDiffStats(kept.join("\n"));
|
|
140
|
-
return {
|
|
141
|
-
text: kept.join("\n"),
|
|
142
|
-
hiddenHunks: Math.max(0, totalStats.hunks - keptStats.hunks),
|
|
143
|
-
hiddenLines: Math.max(0, totalStats.lines - kept.length),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
interface ParsedDiagnostic {
|
|
148
|
-
filePath: string;
|
|
149
|
-
line: number;
|
|
150
|
-
col: number;
|
|
151
|
-
severity: "error" | "warning" | "info" | "hint";
|
|
152
|
-
source?: string;
|
|
153
|
-
message: string;
|
|
154
|
-
code?: string;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function parseDiagnosticMessage(msg: string): ParsedDiagnostic | null {
|
|
158
|
-
// Format: filePath:line:col [severity] [source] message (code)
|
|
159
|
-
const match = msg.match(/^(.+?):(\d+):(\d+)\s+\[(\w+)\]\s+(?:\[([^\]]+)\]\s+)?(.+?)(?:\s+\(([^)]+)\))?$/);
|
|
160
|
-
if (!match) return null;
|
|
161
|
-
return {
|
|
162
|
-
filePath: match[1],
|
|
163
|
-
line: parseInt(match[2], 10),
|
|
164
|
-
col: parseInt(match[3], 10),
|
|
165
|
-
severity: match[4] as ParsedDiagnostic["severity"],
|
|
166
|
-
source: match[5],
|
|
167
|
-
message: match[6],
|
|
168
|
-
code: match[7],
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function formatDiagnostics(diag: { errored: boolean; summary: string; messages: string[] }, expanded: boolean): string {
|
|
173
|
-
if (diag.messages.length === 0) return "";
|
|
174
|
-
|
|
175
|
-
// Parse and group diagnostics by file
|
|
176
|
-
const byFile = new Map<string, ParsedDiagnostic[]>();
|
|
177
|
-
const unparsed: string[] = [];
|
|
178
|
-
|
|
179
|
-
for (const msg of diag.messages) {
|
|
180
|
-
const parsed = parseDiagnosticMessage(msg);
|
|
181
|
-
if (parsed) {
|
|
182
|
-
const existing = byFile.get(parsed.filePath) ?? [];
|
|
183
|
-
existing.push(parsed);
|
|
184
|
-
byFile.set(parsed.filePath, existing);
|
|
185
|
-
} else {
|
|
186
|
-
unparsed.push(msg);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const headerIcon = diag.errored
|
|
191
|
-
? theme.styledSymbol("status.error", "error")
|
|
192
|
-
: theme.styledSymbol("status.warning", "warning");
|
|
193
|
-
let output = `\n\n${headerIcon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", `(${diag.summary})`)}`;
|
|
194
|
-
|
|
195
|
-
const maxDiags = expanded ? diag.messages.length : 5;
|
|
196
|
-
let shown = 0;
|
|
197
|
-
|
|
198
|
-
// Render grouped diagnostics with file icons
|
|
199
|
-
const files = Array.from(byFile.entries());
|
|
200
|
-
for (let fi = 0; fi < files.length && shown < maxDiags; fi++) {
|
|
201
|
-
const [filePath, diagnostics] = files[fi];
|
|
202
|
-
const isLastFile = fi === files.length - 1 && unparsed.length === 0;
|
|
203
|
-
const fileBranch = isLastFile ? theme.tree.last : theme.tree.branch;
|
|
204
|
-
|
|
205
|
-
// File header with icon
|
|
206
|
-
const fileLang = getLanguageFromPath(filePath);
|
|
207
|
-
const fileIcon = theme.fg("muted", theme.getLangIcon(fileLang));
|
|
208
|
-
output += `\n ${theme.fg("dim", fileBranch)} ${fileIcon} ${theme.fg("accent", filePath)}`;
|
|
209
|
-
shown++;
|
|
210
|
-
|
|
211
|
-
// Render diagnostics for this file
|
|
212
|
-
for (let di = 0; di < diagnostics.length && shown < maxDiags; di++) {
|
|
213
|
-
const d = diagnostics[di];
|
|
214
|
-
const isLastDiag = di === diagnostics.length - 1;
|
|
215
|
-
const diagBranch = isLastFile
|
|
216
|
-
? isLastDiag
|
|
217
|
-
? ` ${theme.tree.last}`
|
|
218
|
-
: ` ${theme.tree.branch}`
|
|
219
|
-
: isLastDiag
|
|
220
|
-
? ` ${theme.tree.vertical} ${theme.tree.last}`
|
|
221
|
-
: ` ${theme.tree.vertical} ${theme.tree.branch}`;
|
|
222
|
-
|
|
223
|
-
const sevIcon =
|
|
224
|
-
d.severity === "error"
|
|
225
|
-
? theme.styledSymbol("status.error", "error")
|
|
226
|
-
: d.severity === "warning"
|
|
227
|
-
? theme.styledSymbol("status.warning", "warning")
|
|
228
|
-
: theme.styledSymbol("status.info", "muted");
|
|
229
|
-
const location = theme.fg("dim", `:${d.line}:${d.col}`);
|
|
230
|
-
const codeTag = d.code ? theme.fg("dim", ` (${d.code})`) : "";
|
|
231
|
-
const msgColor = d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "toolOutput";
|
|
232
|
-
|
|
233
|
-
output += `\n ${theme.fg("dim", diagBranch)} ${sevIcon}${location} ${theme.fg(msgColor, d.message)}${codeTag}`;
|
|
234
|
-
shown++;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Render unparsed messages (fallback)
|
|
239
|
-
for (const msg of unparsed) {
|
|
240
|
-
if (shown >= maxDiags) break;
|
|
241
|
-
const color = msg.includes("[error]") ? "error" : msg.includes("[warning]") ? "warning" : "dim";
|
|
242
|
-
output += `\n ${theme.fg("dim", theme.tree.branch)} ${theme.fg(color, msg)}`;
|
|
243
|
-
shown++;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (diag.messages.length > shown) {
|
|
247
|
-
const remaining = diag.messages.length - shown;
|
|
248
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", `${theme.format.ellipsis} ${remaining} more`)} ${theme.fg("dim", "(Ctrl+O to expand)")}`;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return output;
|
|
252
|
-
}
|
|
253
27
|
|
|
254
28
|
function formatCompactValue(value: unknown, maxLength: number): string {
|
|
255
29
|
let rendered = "";
|
|
@@ -309,24 +83,6 @@ function formatArgsPreview(
|
|
|
309
83
|
return { lines, remaining: Math.max(total - visible.length, 0), total };
|
|
310
84
|
}
|
|
311
85
|
|
|
312
|
-
/**
|
|
313
|
-
* Convert absolute path to tilde notation if it's in home directory
|
|
314
|
-
*/
|
|
315
|
-
function shortenPath(path: string): string {
|
|
316
|
-
const home = homedir();
|
|
317
|
-
if (home && path.startsWith(home)) {
|
|
318
|
-
return `~${path.slice(home.length)}`;
|
|
319
|
-
}
|
|
320
|
-
return path;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Replace tabs with spaces for consistent rendering
|
|
325
|
-
*/
|
|
326
|
-
function replaceTabs(text: string): string {
|
|
327
|
-
return text.replace(/\t/g, " ");
|
|
328
|
-
}
|
|
329
|
-
|
|
330
86
|
export interface ToolExecutionOptions {
|
|
331
87
|
showImages?: boolean; // default: true (only used if terminal supports images)
|
|
332
88
|
}
|
|
@@ -340,6 +96,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
340
96
|
private imageComponents: Image[] = [];
|
|
341
97
|
private imageSpacers: Spacer[] = [];
|
|
342
98
|
private toolName: string;
|
|
99
|
+
private toolLabel: string;
|
|
343
100
|
private args: any;
|
|
344
101
|
private expanded = false;
|
|
345
102
|
private showImages: boolean;
|
|
@@ -371,6 +128,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
371
128
|
) {
|
|
372
129
|
super();
|
|
373
130
|
this.toolName = toolName;
|
|
131
|
+
this.toolLabel = tool?.label ?? toolName;
|
|
374
132
|
this.args = args;
|
|
375
133
|
this.showImages = options.showImages ?? true;
|
|
376
134
|
this.tool = tool;
|
|
@@ -383,10 +141,10 @@ export class ToolExecutionComponent extends Container {
|
|
|
383
141
|
this.contentBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
|
|
384
142
|
this.contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text));
|
|
385
143
|
|
|
386
|
-
// Use Box for custom tools
|
|
144
|
+
// Use Box for custom tools or built-in tools that have renderers
|
|
387
145
|
const hasRenderer = toolName in toolRenderers;
|
|
388
146
|
const hasCustomRenderer = !!(tool?.renderCall || tool?.renderResult);
|
|
389
|
-
if (hasCustomRenderer ||
|
|
147
|
+
if (hasCustomRenderer || hasRenderer) {
|
|
390
148
|
this.addChild(this.contentBox);
|
|
391
149
|
} else {
|
|
392
150
|
this.addChild(this.contentText);
|
|
@@ -418,12 +176,13 @@ export class ToolExecutionComponent extends Container {
|
|
|
418
176
|
const path = this.args?.path;
|
|
419
177
|
const oldText = this.args?.oldText;
|
|
420
178
|
const newText = this.args?.newText;
|
|
179
|
+
const all = this.args?.all;
|
|
421
180
|
|
|
422
181
|
// Need all three params to compute diff
|
|
423
182
|
if (!path || oldText === undefined || newText === undefined) return;
|
|
424
183
|
|
|
425
184
|
// Create a key to track which args this computation is for
|
|
426
|
-
const argsKey = JSON.stringify({ path, oldText, newText });
|
|
185
|
+
const argsKey = JSON.stringify({ path, oldText, newText, all });
|
|
427
186
|
|
|
428
187
|
// Skip if we already computed for these exact args
|
|
429
188
|
if (this.editDiffArgsKey === argsKey) return;
|
|
@@ -431,7 +190,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
431
190
|
this.editDiffArgsKey = argsKey;
|
|
432
191
|
|
|
433
192
|
// Compute diff async
|
|
434
|
-
computeEditDiff(path, oldText, newText, this.cwd).then((result) => {
|
|
193
|
+
computeEditDiff(path, oldText, newText, this.cwd, true, all).then((result) => {
|
|
435
194
|
// Only update if args haven't changed since we started
|
|
436
195
|
if (this.editDiffArgsKey === argsKey) {
|
|
437
196
|
this.editDiffPreview = result;
|
|
@@ -457,6 +216,17 @@ export class ToolExecutionComponent extends Container {
|
|
|
457
216
|
this.maybeConvertImagesForKitty();
|
|
458
217
|
}
|
|
459
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Get all image blocks from result content and details.images.
|
|
221
|
+
* Some tools (like generate_image) store images in details to avoid bloating model context.
|
|
222
|
+
*/
|
|
223
|
+
private getAllImageBlocks(): Array<{ data?: string; mimeType?: string }> {
|
|
224
|
+
if (!this.result) return [];
|
|
225
|
+
const contentImages = this.result.content?.filter((c: any) => c.type === "image") || [];
|
|
226
|
+
const detailImages = this.result.details?.images || [];
|
|
227
|
+
return [...contentImages, ...detailImages];
|
|
228
|
+
}
|
|
229
|
+
|
|
460
230
|
/**
|
|
461
231
|
* Convert non-PNG images to PNG for Kitty graphics protocol.
|
|
462
232
|
* Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
|
|
@@ -467,7 +237,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
467
237
|
if (caps.images !== "kitty") return;
|
|
468
238
|
if (!this.result) return;
|
|
469
239
|
|
|
470
|
-
const imageBlocks = this.
|
|
240
|
+
const imageBlocks = this.getAllImageBlocks();
|
|
471
241
|
|
|
472
242
|
for (let i = 0; i < imageBlocks.length; i++) {
|
|
473
243
|
const img = imageBlocks[i];
|
|
@@ -556,11 +326,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
556
326
|
}
|
|
557
327
|
} catch {
|
|
558
328
|
// Fall back to default on error
|
|
559
|
-
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.
|
|
329
|
+
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
|
|
560
330
|
}
|
|
561
331
|
} else {
|
|
562
332
|
// No custom renderCall, show tool name
|
|
563
|
-
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.
|
|
333
|
+
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
|
|
564
334
|
}
|
|
565
335
|
|
|
566
336
|
// Render result component if we have a result
|
|
@@ -593,13 +363,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
593
363
|
this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
|
594
364
|
}
|
|
595
365
|
}
|
|
596
|
-
} else if (this.toolName === "bash") {
|
|
597
|
-
// Bash uses Box with visual line truncation
|
|
598
|
-
this.contentBox.setBgFn(bgFn);
|
|
599
|
-
this.contentBox.clear();
|
|
600
|
-
this.renderBashContent();
|
|
601
366
|
} else if (this.toolName in toolRenderers) {
|
|
602
|
-
// Built-in tools with
|
|
367
|
+
// Built-in tools with renderers
|
|
603
368
|
const renderer = toolRenderers[this.toolName];
|
|
604
369
|
this.contentBox.setBgFn(bgFn);
|
|
605
370
|
this.contentBox.clear();
|
|
@@ -617,16 +382,25 @@ export class ToolExecutionComponent extends Container {
|
|
|
617
382
|
}
|
|
618
383
|
} catch {
|
|
619
384
|
// Fall back to default on error
|
|
620
|
-
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.
|
|
385
|
+
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
|
|
621
386
|
}
|
|
622
387
|
|
|
623
388
|
// Render result component if we have a result
|
|
624
389
|
if (this.result) {
|
|
625
390
|
try {
|
|
391
|
+
// Build render context for tools that need extra state
|
|
392
|
+
const renderContext = this.buildRenderContext();
|
|
393
|
+
|
|
626
394
|
const resultComponent = renderer.renderResult(
|
|
627
|
-
{ content: this.result.content as any, details: this.result.details },
|
|
628
|
-
{
|
|
395
|
+
{ content: this.result.content as any, details: this.result.details, isError: this.result.isError },
|
|
396
|
+
{
|
|
397
|
+
expanded: this.expanded,
|
|
398
|
+
isPartial: this.isPartial,
|
|
399
|
+
spinnerFrame: this.spinnerFrame,
|
|
400
|
+
renderContext,
|
|
401
|
+
},
|
|
629
402
|
theme,
|
|
403
|
+
this.args, // Pass args for tools that need them
|
|
630
404
|
);
|
|
631
405
|
if (resultComponent) {
|
|
632
406
|
// Ensure component has invalidate() method for Component interface
|
|
@@ -661,7 +435,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
661
435
|
this.imageSpacers = [];
|
|
662
436
|
|
|
663
437
|
if (this.result) {
|
|
664
|
-
const imageBlocks = this.
|
|
438
|
+
const imageBlocks = this.getAllImageBlocks();
|
|
665
439
|
const caps = getCapabilities();
|
|
666
440
|
|
|
667
441
|
for (let i = 0; i < imageBlocks.length; i++) {
|
|
@@ -694,93 +468,42 @@ export class ToolExecutionComponent extends Container {
|
|
|
694
468
|
}
|
|
695
469
|
|
|
696
470
|
/**
|
|
697
|
-
*
|
|
471
|
+
* Build render context for tools that need extra state (bash, edit)
|
|
698
472
|
*/
|
|
699
|
-
private
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
// Header
|
|
703
|
-
this.contentBox.addChild(
|
|
704
|
-
new Text(
|
|
705
|
-
theme.fg("toolTitle", theme.bold(`$ ${command || theme.fg("toolOutput", theme.format.ellipsis)}`)),
|
|
706
|
-
0,
|
|
707
|
-
0,
|
|
708
|
-
),
|
|
709
|
-
);
|
|
473
|
+
private buildRenderContext(): Record<string, unknown> {
|
|
474
|
+
const context: Record<string, unknown> = {};
|
|
710
475
|
|
|
711
|
-
if (this.result) {
|
|
476
|
+
if (this.toolName === "bash" && this.result) {
|
|
477
|
+
// Bash needs visual line truncation context
|
|
712
478
|
const output = this.getTextOutput().trim();
|
|
713
|
-
|
|
714
|
-
if (output) {
|
|
715
|
-
// Style each line for the output
|
|
479
|
+
if (output && !this.expanded) {
|
|
716
480
|
const styledOutput = output
|
|
717
481
|
.split("\n")
|
|
718
482
|
.map((line) => theme.fg("toolOutput", line))
|
|
719
483
|
.join("\n");
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
this.
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
`\n${styledOutput}`,
|
|
729
|
-
BASH_PREVIEW_LINES,
|
|
730
|
-
this.ui.terminal.columns - 2,
|
|
731
|
-
);
|
|
732
|
-
|
|
733
|
-
const totalVisualLines = skippedCount + visualLines.length;
|
|
734
|
-
if (skippedCount > 0) {
|
|
735
|
-
this.contentBox.addChild(
|
|
736
|
-
new Text(
|
|
737
|
-
theme.fg(
|
|
738
|
-
"dim",
|
|
739
|
-
`\n${theme.format.ellipsis} (${skippedCount} earlier lines, showing ${visualLines.length} of ${totalVisualLines}) (ctrl+o to expand)`,
|
|
740
|
-
),
|
|
741
|
-
0,
|
|
742
|
-
0,
|
|
743
|
-
),
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Add pre-rendered visual lines as a raw component
|
|
748
|
-
this.contentBox.addChild({
|
|
749
|
-
render: () => visualLines,
|
|
750
|
-
invalidate: () => {},
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Truncation warnings
|
|
756
|
-
const truncation = this.result.details?.truncation;
|
|
757
|
-
const fullOutputPath = this.result.details?.fullOutputPath;
|
|
758
|
-
if (truncation?.truncated || fullOutputPath) {
|
|
759
|
-
const warnings: string[] = [];
|
|
760
|
-
if (fullOutputPath) {
|
|
761
|
-
warnings.push(`Full output: ${fullOutputPath}`);
|
|
762
|
-
}
|
|
763
|
-
if (truncation?.truncated) {
|
|
764
|
-
if (truncation.truncatedBy === "lines") {
|
|
765
|
-
warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
766
|
-
} else {
|
|
767
|
-
warnings.push(
|
|
768
|
-
`Truncated: ${truncation.outputLines} lines shown (${formatSize(
|
|
769
|
-
truncation.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
770
|
-
)} limit)`,
|
|
771
|
-
);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
this.contentBox.addChild(new Text(`\n${theme.fg("warning", wrapBrackets(warnings.join(". ")))}`, 0, 0));
|
|
484
|
+
const { visualLines, skippedCount } = truncateToVisualLines(
|
|
485
|
+
`\n${styledOutput}`,
|
|
486
|
+
BASH_PREVIEW_LINES,
|
|
487
|
+
this.ui.terminal.columns - 2,
|
|
488
|
+
);
|
|
489
|
+
context.visualLines = visualLines;
|
|
490
|
+
context.skippedCount = skippedCount;
|
|
491
|
+
context.totalVisualLines = skippedCount + visualLines.length;
|
|
775
492
|
}
|
|
493
|
+
} else if (this.toolName === "edit") {
|
|
494
|
+
// Edit needs diff preview and renderDiff function
|
|
495
|
+
context.editDiffPreview = this.editDiffPreview;
|
|
496
|
+
context.renderDiff = renderDiff;
|
|
776
497
|
}
|
|
498
|
+
|
|
499
|
+
return context;
|
|
777
500
|
}
|
|
778
501
|
|
|
779
502
|
private getTextOutput(): string {
|
|
780
503
|
if (!this.result) return "";
|
|
781
504
|
|
|
782
505
|
const textBlocks = this.result.content?.filter((c: any) => c.type === "text") || [];
|
|
783
|
-
const imageBlocks = this.
|
|
506
|
+
const imageBlocks = this.getAllImageBlocks();
|
|
784
507
|
|
|
785
508
|
let output = textBlocks
|
|
786
509
|
.map((c: any) => {
|
|
@@ -803,214 +526,46 @@ export class ToolExecutionComponent extends Container {
|
|
|
803
526
|
return output;
|
|
804
527
|
}
|
|
805
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Format a generic tool execution (fallback for tools without custom renderers)
|
|
531
|
+
*/
|
|
806
532
|
private formatToolExecution(): string {
|
|
807
|
-
let text = "";
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`;
|
|
824
|
-
|
|
825
|
-
if (this.result) {
|
|
826
|
-
const output = this.getTextOutput();
|
|
827
|
-
|
|
828
|
-
if (fileType === "image") {
|
|
829
|
-
// Image file - use image icon
|
|
830
|
-
const ext = rawPath.split(".").pop()?.toLowerCase() ?? "image";
|
|
831
|
-
text += `${theme.sep.dot}${theme.fg("dim", theme.getLangIcon(ext))}`;
|
|
832
|
-
// Images are rendered by the image component, just show hint
|
|
833
|
-
text += `\n${theme.fg("muted", "Image rendered below")}`;
|
|
834
|
-
} else if (fileType === "binary") {
|
|
835
|
-
// Binary file - use binary/pdf/archive icon based on extension
|
|
836
|
-
const ext = rawPath.split(".").pop()?.toLowerCase() ?? "binary";
|
|
837
|
-
text += `${theme.sep.dot}${theme.fg("dim", theme.getLangIcon(ext))}`;
|
|
838
|
-
} else {
|
|
839
|
-
// Text file - show line count and language on same line
|
|
840
|
-
const lang = getLanguageFromPath(rawPath);
|
|
841
|
-
const lines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
|
|
842
|
-
text += `${theme.sep.dot}${formatMetadataLine(null, lang)}`;
|
|
843
|
-
|
|
844
|
-
// Content is hidden by default, only shown when expanded
|
|
845
|
-
if (this.expanded) {
|
|
846
|
-
text +=
|
|
847
|
-
"\n\n" +
|
|
848
|
-
lines
|
|
849
|
-
.map((line: string) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
|
|
850
|
-
.join("\n");
|
|
851
|
-
} else {
|
|
852
|
-
text += `\n${theme.fg("dim", `${theme.nav.expand} Ctrl+O to show content`)}`;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// Truncation warning
|
|
856
|
-
const truncation = this.result.details?.truncation;
|
|
857
|
-
if (truncation?.truncated) {
|
|
858
|
-
let warning: string;
|
|
859
|
-
if (truncation.firstLineExceedsLimit) {
|
|
860
|
-
warning = `First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`;
|
|
861
|
-
} else if (truncation.truncatedBy === "lines") {
|
|
862
|
-
warning = `Truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)`;
|
|
863
|
-
} else {
|
|
864
|
-
warning = `Truncated: ${truncation.outputLines} lines (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`;
|
|
865
|
-
}
|
|
866
|
-
text += `\n${theme.fg("warning", wrapBrackets(warning))}`;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
} else if (this.toolName === "write") {
|
|
871
|
-
const rawPath = this.args?.file_path || this.args?.path || "";
|
|
872
|
-
const path = shortenPath(rawPath);
|
|
873
|
-
const fileContent = this.args?.content || "";
|
|
874
|
-
const lang = getLanguageFromPath(rawPath);
|
|
875
|
-
const lines = fileContent
|
|
876
|
-
? lang
|
|
877
|
-
? highlightCode(replaceTabs(fileContent), lang)
|
|
878
|
-
: fileContent.split("\n")
|
|
879
|
-
: [];
|
|
880
|
-
const totalLines = lines.length;
|
|
881
|
-
|
|
882
|
-
text =
|
|
883
|
-
theme.fg("toolTitle", theme.bold("write")) +
|
|
884
|
-
" " +
|
|
885
|
-
(path ? theme.fg("accent", path) : theme.fg("toolOutput", theme.format.ellipsis));
|
|
886
|
-
|
|
887
|
-
text += `\n${formatMetadataLine(countLines(fileContent), lang ?? "text")}`;
|
|
888
|
-
|
|
889
|
-
if (fileContent) {
|
|
890
|
-
const maxLines = this.expanded ? lines.length : 10;
|
|
891
|
-
const displayLines = lines.slice(0, maxLines);
|
|
892
|
-
const remaining = lines.length - maxLines;
|
|
893
|
-
|
|
894
|
-
text +=
|
|
895
|
-
"\n\n" +
|
|
896
|
-
displayLines
|
|
897
|
-
.map((line: string) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
|
|
898
|
-
.join("\n");
|
|
899
|
-
if (remaining > 0) {
|
|
900
|
-
text += theme.fg(
|
|
901
|
-
"toolOutput",
|
|
902
|
-
`\n${theme.format.ellipsis} (${remaining} more lines, ${totalLines} total) ${wrapBrackets("Ctrl+O to expand")}`,
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// Show LSP diagnostics if available
|
|
908
|
-
if (this.result?.details?.diagnostics) {
|
|
909
|
-
text += formatDiagnostics(this.result.details.diagnostics, this.expanded);
|
|
910
|
-
}
|
|
911
|
-
} else if (this.toolName === "edit") {
|
|
912
|
-
const rawPath = this.args?.file_path || this.args?.path || "";
|
|
913
|
-
const path = shortenPath(rawPath);
|
|
914
|
-
const editLanguage = getLanguageFromPath(rawPath) ?? "text";
|
|
915
|
-
const editIcon = theme.fg("muted", theme.getLangIcon(editLanguage));
|
|
916
|
-
|
|
917
|
-
// Build path display, appending :line if we have diff info
|
|
918
|
-
let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", theme.format.ellipsis);
|
|
919
|
-
const firstChangedLine =
|
|
920
|
-
(this.editDiffPreview && "firstChangedLine" in this.editDiffPreview
|
|
921
|
-
? this.editDiffPreview.firstChangedLine
|
|
922
|
-
: undefined) ||
|
|
923
|
-
(this.result && !this.result.isError ? this.result.details?.firstChangedLine : undefined);
|
|
924
|
-
if (firstChangedLine) {
|
|
925
|
-
pathDisplay += theme.fg("warning", `:${firstChangedLine}`);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
text = `${theme.fg("toolTitle", theme.bold("edit"))} ${editIcon} ${pathDisplay}`;
|
|
929
|
-
|
|
930
|
-
const editLineCount = countLines(this.args?.newText ?? this.args?.oldText ?? "");
|
|
931
|
-
text += `\n${formatMetadataLine(editLineCount, editLanguage)}`;
|
|
932
|
-
|
|
933
|
-
if (this.result?.isError) {
|
|
934
|
-
// Show error from result
|
|
935
|
-
const errorText = this.getTextOutput();
|
|
936
|
-
if (errorText) {
|
|
937
|
-
text += `\n\n${theme.fg("error", errorText)}`;
|
|
938
|
-
}
|
|
939
|
-
} else if (this.editDiffPreview) {
|
|
940
|
-
// Use cached diff preview (works both before and after execution)
|
|
941
|
-
if ("error" in this.editDiffPreview) {
|
|
942
|
-
text += `\n\n${theme.fg("error", this.editDiffPreview.error)}`;
|
|
943
|
-
} else if (this.editDiffPreview.diff) {
|
|
944
|
-
const diffStats = getDiffStats(this.editDiffPreview.diff);
|
|
945
|
-
text += `\n${theme.fg("dim", theme.format.bracketLeft)}${formatDiffStats(diffStats.added, diffStats.removed, diffStats.hunks)}${theme.fg("dim", theme.format.bracketRight)}`;
|
|
946
|
-
|
|
947
|
-
const {
|
|
948
|
-
text: diffText,
|
|
949
|
-
hiddenHunks,
|
|
950
|
-
hiddenLines,
|
|
951
|
-
} = this.expanded
|
|
952
|
-
? { text: this.editDiffPreview.diff, hiddenHunks: 0, hiddenLines: 0 }
|
|
953
|
-
: truncateDiffByHunk(this.editDiffPreview.diff, EDIT_DIFF_PREVIEW_HUNKS, EDIT_DIFF_PREVIEW_LINES);
|
|
954
|
-
|
|
955
|
-
text += `\n\n${renderDiff(diffText, { filePath: rawPath })}`;
|
|
956
|
-
if (!this.expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
|
|
957
|
-
const remainder: string[] = [];
|
|
958
|
-
if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
|
|
959
|
-
if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
|
|
960
|
-
text += theme.fg(
|
|
961
|
-
"toolOutput",
|
|
962
|
-
`\n${theme.format.ellipsis} (${remainder.join(", ")}) ${wrapBrackets("Ctrl+O to expand")}`,
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Show LSP diagnostics if available
|
|
969
|
-
if (this.result?.details?.diagnostics) {
|
|
970
|
-
text += formatDiagnostics(this.result.details.diagnostics, this.expanded);
|
|
971
|
-
}
|
|
533
|
+
let text = theme.fg("toolTitle", theme.bold(this.toolLabel));
|
|
534
|
+
|
|
535
|
+
const argTotal =
|
|
536
|
+
this.args && typeof this.args === "object"
|
|
537
|
+
? Object.keys(this.args as Record<string, unknown>).length
|
|
538
|
+
: this.args === undefined
|
|
539
|
+
? 0
|
|
540
|
+
: 1;
|
|
541
|
+
const argPreviewLimit = this.expanded ? argTotal : GENERIC_ARG_PREVIEW;
|
|
542
|
+
const valueLimit = this.expanded ? 2000 : GENERIC_VALUE_MAX;
|
|
543
|
+
const argsPreview = formatArgsPreview(this.args, argPreviewLimit, valueLimit);
|
|
544
|
+
|
|
545
|
+
text += `\n\n${theme.fg("toolTitle", "Args")} ${theme.fg("dim", `(${argsPreview.total})`)}`;
|
|
546
|
+
if (argsPreview.lines.length > 0) {
|
|
547
|
+
text += `\n${argsPreview.lines.join("\n")}`;
|
|
972
548
|
} else {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
? Object.keys(this.args as Record<string, unknown>).length
|
|
979
|
-
: this.args === undefined
|
|
980
|
-
? 0
|
|
981
|
-
: 1;
|
|
982
|
-
const argPreviewLimit = this.expanded ? argTotal : GENERIC_ARG_PREVIEW;
|
|
983
|
-
const valueLimit = this.expanded ? 2000 : GENERIC_VALUE_MAX;
|
|
984
|
-
const argsPreview = formatArgsPreview(this.args, argPreviewLimit, valueLimit);
|
|
985
|
-
|
|
986
|
-
text += `\n\n${theme.fg("toolTitle", "Args")} ${theme.fg("dim", `(${argsPreview.total})`)}`;
|
|
987
|
-
if (argsPreview.lines.length > 0) {
|
|
988
|
-
text += `\n${argsPreview.lines.join("\n")}`;
|
|
989
|
-
} else {
|
|
990
|
-
text += `\n${theme.fg("dim", "(none)")}`;
|
|
991
|
-
}
|
|
992
|
-
if (argsPreview.remaining > 0) {
|
|
993
|
-
text += theme.fg(
|
|
994
|
-
"dim",
|
|
995
|
-
`\n${theme.format.ellipsis} (${argsPreview.remaining} more args) (ctrl+o to expand)`,
|
|
996
|
-
);
|
|
997
|
-
}
|
|
549
|
+
text += `\n${theme.fg("dim", "(none)")}`;
|
|
550
|
+
}
|
|
551
|
+
if (argsPreview.remaining > 0) {
|
|
552
|
+
text += theme.fg("dim", `\n${theme.format.ellipsis} (${argsPreview.remaining} more args) (ctrl+o to expand)`);
|
|
553
|
+
}
|
|
998
554
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
}
|
|
1011
|
-
} else {
|
|
1012
|
-
text += ` ${theme.fg("dim", "(empty)")}`;
|
|
555
|
+
const output = this.getTextOutput().trim();
|
|
556
|
+
text += `\n\n${theme.fg("toolTitle", "Output")}`;
|
|
557
|
+
if (output) {
|
|
558
|
+
const lines = output.split("\n");
|
|
559
|
+
const maxLines = this.expanded ? lines.length : GENERIC_PREVIEW_LINES;
|
|
560
|
+
const displayLines = lines.slice(-maxLines);
|
|
561
|
+
const remaining = lines.length - displayLines.length;
|
|
562
|
+
text += ` ${theme.fg("dim", `(${lines.length} lines)`)}`;
|
|
563
|
+
text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
|
|
564
|
+
if (remaining > 0) {
|
|
565
|
+
text += theme.fg("dim", `\n${theme.format.ellipsis} (${remaining} earlier lines) (ctrl+o to expand)`);
|
|
1013
566
|
}
|
|
567
|
+
} else {
|
|
568
|
+
text += ` ${theme.fg("dim", "(empty)")}`;
|
|
1014
569
|
}
|
|
1015
570
|
|
|
1016
571
|
return text;
|