@codex-infinity/pi-infinity 0.61.2 → 0.62.1
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 +54 -0
- package/README.md +2 -2
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +4 -0
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/core/agent-session.d.ts +10 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +60 -46
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/export-html/index.d.ts +2 -2
- package/dist/core/export-html/index.d.ts.map +1 -1
- package/dist/core/export-html/index.js +2 -2
- package/dist/core/export-html/index.js.map +1 -1
- package/dist/core/export-html/tool-renderer.d.ts +2 -2
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js +41 -16
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/index.d.ts +3 -2
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +12 -2
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +4 -7
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +27 -38
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +44 -9
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/extensions/wrapper.d.ts.map +1 -1
- package/dist/core/extensions/wrapper.js +2 -8
- package/dist/core/extensions/wrapper.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/output-guard.d.ts +6 -0
- package/dist/core/output-guard.d.ts.map +1 -0
- package/dist/core/output-guard.js +59 -0
- package/dist/core/output-guard.js.map +1 -0
- package/dist/core/package-manager.d.ts +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +27 -8
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/prompt-templates.d.ts +2 -1
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +30 -32
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/resource-loader.d.ts +6 -5
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +133 -107
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/skills.d.ts +2 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +25 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts +2 -3
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/source-info.d.ts +18 -0
- package/dist/core/source-info.d.ts.map +1 -0
- package/dist/core/source-info.js +19 -0
- package/dist/core/source-info.js.map +1 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +3 -38
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +19 -9
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +154 -59
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts +14 -2
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +92 -21
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +11 -4
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +76 -27
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +15 -4
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +83 -29
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +57 -19
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +50 -26
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +9 -3
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +67 -13
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +10 -3
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +110 -51
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +21 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -0
- package/dist/core/tools/render-utils.js +49 -0
- package/dist/core/tools/render-utils.js.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.js +30 -0
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -0
- package/dist/core/tools/write.d.ts +9 -3
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +161 -27
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +29 -9
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +15 -40
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +120 -670
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -11
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +144 -92
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +3 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +1 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +5 -11
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +27 -20
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -4
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/image-resize.d.ts +5 -5
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +45 -94
- package/dist/utils/image-resize.js.map +1 -1
- package/docs/extensions.md +72 -32
- package/docs/tui.md +2 -2
- package/examples/extensions/built-in-tool-renderer.ts +8 -8
- package/examples/extensions/commands.ts +3 -3
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
- package/examples/extensions/minimal-mode.ts +14 -14
- package/examples/extensions/question.ts +2 -2
- package/examples/extensions/questionnaire.ts +2 -2
- package/examples/extensions/subagent/index.ts +2 -2
- package/examples/extensions/todo.ts +2 -2
- package/examples/extensions/truncated-tool.ts +2 -2
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/04-skills.ts +8 -2
- package/examples/sdk/08-prompt-templates.ts +2 -1
- package/examples/sdk/12-full-control.ts +0 -1
- package/package.json +4 -4
|
@@ -1,80 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { computeEditDiff } from "../../../core/tools/edit-diff.js";
|
|
5
|
-
import { allTools } from "../../../core/tools/index.js";
|
|
6
|
-
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js";
|
|
1
|
+
import { Box, Container, getCapabilities, Image, Spacer, Text } from "@mariozechner/pi-tui";
|
|
2
|
+
import { allToolDefinitions } from "../../../core/tools/index.js";
|
|
3
|
+
import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.js";
|
|
7
4
|
import { convertToPng } from "../../../utils/image-convert.js";
|
|
8
|
-
import {
|
|
9
|
-
import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
|
|
10
|
-
import { renderDiff } from "./diff.js";
|
|
11
|
-
import { keyHint } from "./keybinding-hints.js";
|
|
12
|
-
import { truncateToVisualLines } from "./visual-truncate.js";
|
|
13
|
-
// Preview line limit for bash when not expanded
|
|
14
|
-
const BASH_PREVIEW_LINES = 5;
|
|
15
|
-
// During partial write tool-call streaming, re-highlight the first N lines fully
|
|
16
|
-
// to keep multiline tokenization mostly correct without re-highlighting the full file.
|
|
17
|
-
const WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50;
|
|
18
|
-
/**
|
|
19
|
-
* Convert absolute path to tilde notation if it's in home directory
|
|
20
|
-
*/
|
|
21
|
-
function shortenPath(path) {
|
|
22
|
-
if (typeof path !== "string")
|
|
23
|
-
return "";
|
|
24
|
-
const home = os.homedir();
|
|
25
|
-
if (path.startsWith(home)) {
|
|
26
|
-
return `~${path.slice(home.length)}`;
|
|
27
|
-
}
|
|
28
|
-
return path;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Replace tabs with spaces for consistent rendering
|
|
32
|
-
*/
|
|
33
|
-
function replaceTabs(text) {
|
|
34
|
-
return text.replace(/\t/g, " ");
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Normalize control characters for terminal preview rendering.
|
|
38
|
-
* Keep tool arguments unchanged, sanitize only display text.
|
|
39
|
-
*/
|
|
40
|
-
function normalizeDisplayText(text) {
|
|
41
|
-
return text.replace(/\r/g, "");
|
|
42
|
-
}
|
|
43
|
-
/** Safely coerce value to string for display. Returns null if invalid type. */
|
|
44
|
-
function str(value) {
|
|
45
|
-
if (typeof value === "string")
|
|
46
|
-
return value;
|
|
47
|
-
if (value == null)
|
|
48
|
-
return "";
|
|
49
|
-
return null; // Invalid type
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Component that renders a tool call with its result (updateable)
|
|
53
|
-
*/
|
|
5
|
+
import { theme } from "../theme/theme.js";
|
|
54
6
|
export class ToolExecutionComponent extends Container {
|
|
55
|
-
constructor(toolName, args, options = {}, toolDefinition, ui, cwd = process.cwd()) {
|
|
7
|
+
constructor(toolName, toolCallId, args, options = {}, toolDefinition, ui, cwd = process.cwd()) {
|
|
56
8
|
super();
|
|
9
|
+
this.rendererState = {};
|
|
57
10
|
this.imageComponents = [];
|
|
58
11
|
this.imageSpacers = [];
|
|
59
12
|
this.expanded = false;
|
|
60
13
|
this.isPartial = true;
|
|
61
|
-
|
|
14
|
+
this.executionStarted = false;
|
|
15
|
+
this.argsComplete = false;
|
|
62
16
|
this.convertedImages = new Map();
|
|
63
|
-
// When true, this component intentionally renders no lines
|
|
64
17
|
this.hideComponent = false;
|
|
65
18
|
this.toolName = toolName;
|
|
19
|
+
this.toolCallId = toolCallId;
|
|
66
20
|
this.args = args;
|
|
67
|
-
this.showImages = options.showImages ?? true;
|
|
68
21
|
this.toolDefinition = toolDefinition;
|
|
22
|
+
this.builtInToolDefinition = allToolDefinitions[toolName];
|
|
23
|
+
this.showImages = options.showImages ?? true;
|
|
69
24
|
this.ui = ui;
|
|
70
25
|
this.cwd = cwd;
|
|
71
26
|
this.addChild(new Spacer(1));
|
|
72
|
-
// Always create both
|
|
27
|
+
// Always create both. contentBox is used for tools with renderer-based call/result composition.
|
|
28
|
+
// contentText is reserved for generic fallback rendering when no tool definition exists.
|
|
73
29
|
this.contentBox = new Box(1, 1, (text) => theme.bg("toolPendingBg", text));
|
|
74
30
|
this.contentText = new Text("", 1, 1, (text) => theme.bg("toolPendingBg", text));
|
|
75
|
-
|
|
76
|
-
// Use contentText for built-in tools (including overrides without custom renderers)
|
|
77
|
-
if (toolName === "bash" || (toolDefinition && !this.shouldUseBuiltInRenderer())) {
|
|
31
|
+
if (this.hasRendererDefinition()) {
|
|
78
32
|
this.addChild(this.contentBox);
|
|
79
33
|
}
|
|
80
34
|
else {
|
|
@@ -82,217 +36,96 @@ export class ToolExecutionComponent extends Container {
|
|
|
82
36
|
}
|
|
83
37
|
this.updateDisplay();
|
|
84
38
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*/
|
|
90
|
-
shouldUseBuiltInRenderer() {
|
|
91
|
-
const isBuiltInName = this.toolName in allTools;
|
|
92
|
-
const hasCustomRenderers = this.toolDefinition?.renderCall || this.toolDefinition?.renderResult;
|
|
93
|
-
return isBuiltInName && !hasCustomRenderers;
|
|
39
|
+
isBuiltInDefinition(definition) {
|
|
40
|
+
return (definition !== undefined &&
|
|
41
|
+
this.builtInToolDefinition !== undefined &&
|
|
42
|
+
definition.parameters === this.builtInToolDefinition.parameters);
|
|
94
43
|
}
|
|
95
|
-
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
this.updateWriteHighlightCacheIncremental();
|
|
44
|
+
getCallRenderer() {
|
|
45
|
+
if (!this.builtInToolDefinition) {
|
|
46
|
+
return this.toolDefinition?.renderCall;
|
|
99
47
|
}
|
|
100
|
-
this.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return;
|
|
105
|
-
this.bashStartedAt = Date.now();
|
|
106
|
-
this.ensureBashElapsedTimer();
|
|
107
|
-
this.updateDisplay();
|
|
108
|
-
this.ui.requestRender();
|
|
109
|
-
}
|
|
110
|
-
ensureBashElapsedTimer() {
|
|
111
|
-
if (this.toolName !== "bash" || !this.isPartial || this.bashStartedAt === undefined || this.bashElapsedInterval)
|
|
112
|
-
return;
|
|
113
|
-
this.bashElapsedInterval = setInterval(() => {
|
|
114
|
-
this.updateDisplay();
|
|
115
|
-
this.ui.requestRender();
|
|
116
|
-
}, 1000);
|
|
48
|
+
if (!this.toolDefinition || this.isBuiltInDefinition(this.toolDefinition)) {
|
|
49
|
+
return this.builtInToolDefinition.renderCall;
|
|
50
|
+
}
|
|
51
|
+
return this.toolDefinition.renderCall ?? this.builtInToolDefinition.renderCall;
|
|
117
52
|
}
|
|
118
|
-
|
|
119
|
-
if (!this.
|
|
120
|
-
return;
|
|
121
|
-
|
|
122
|
-
this.
|
|
53
|
+
getResultRenderer() {
|
|
54
|
+
if (!this.builtInToolDefinition) {
|
|
55
|
+
return this.toolDefinition?.renderResult;
|
|
56
|
+
}
|
|
57
|
+
if (!this.toolDefinition || this.isBuiltInDefinition(this.toolDefinition)) {
|
|
58
|
+
return this.builtInToolDefinition.renderResult;
|
|
59
|
+
}
|
|
60
|
+
return this.toolDefinition.renderResult ?? this.builtInToolDefinition.renderResult;
|
|
123
61
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return undefined;
|
|
127
|
-
return Date.now() - this.bashStartedAt;
|
|
62
|
+
hasRendererDefinition() {
|
|
63
|
+
return this.builtInToolDefinition !== undefined || this.toolDefinition !== undefined;
|
|
128
64
|
}
|
|
129
|
-
|
|
130
|
-
return
|
|
65
|
+
getRenderContext(lastComponent) {
|
|
66
|
+
return {
|
|
67
|
+
args: this.args,
|
|
68
|
+
toolCallId: this.toolCallId,
|
|
69
|
+
invalidate: () => {
|
|
70
|
+
this.invalidate();
|
|
71
|
+
this.ui.requestRender();
|
|
72
|
+
},
|
|
73
|
+
lastComponent,
|
|
74
|
+
state: this.rendererState,
|
|
75
|
+
cwd: this.cwd,
|
|
76
|
+
executionStarted: this.executionStarted,
|
|
77
|
+
argsComplete: this.argsComplete,
|
|
78
|
+
isPartial: this.isPartial,
|
|
79
|
+
expanded: this.expanded,
|
|
80
|
+
showImages: this.showImages,
|
|
81
|
+
isError: this.result?.isError ?? false,
|
|
82
|
+
};
|
|
131
83
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return highlighted[0] ?? "";
|
|
84
|
+
createCallFallback() {
|
|
85
|
+
return new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0);
|
|
135
86
|
}
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
return;
|
|
140
|
-
const prefixSource = cache.normalizedLines.slice(0, prefixCount).join("\n");
|
|
141
|
-
const prefixHighlighted = highlightCode(prefixSource, cache.lang);
|
|
142
|
-
for (let i = 0; i < prefixCount; i++) {
|
|
143
|
-
cache.highlightedLines[i] =
|
|
144
|
-
prefixHighlighted[i] ?? this.highlightSingleLine(cache.normalizedLines[i] ?? "", cache.lang);
|
|
87
|
+
createResultFallback() {
|
|
88
|
+
const output = this.getTextOutput();
|
|
89
|
+
if (!output) {
|
|
90
|
+
return undefined;
|
|
145
91
|
}
|
|
92
|
+
return new Text(theme.fg("toolOutput", output), 0, 0);
|
|
146
93
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.writeHighlightCache = undefined;
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const displayContent = normalizeDisplayText(fileContent);
|
|
154
|
-
const normalized = replaceTabs(displayContent);
|
|
155
|
-
this.writeHighlightCache = {
|
|
156
|
-
rawPath,
|
|
157
|
-
lang,
|
|
158
|
-
rawContent: fileContent,
|
|
159
|
-
normalizedLines: normalized.split("\n"),
|
|
160
|
-
highlightedLines: highlightCode(normalized, lang),
|
|
161
|
-
};
|
|
94
|
+
updateArgs(args) {
|
|
95
|
+
this.args = args;
|
|
96
|
+
this.updateDisplay();
|
|
162
97
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
this.writeHighlightCache = undefined;
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
|
171
|
-
if (!lang) {
|
|
172
|
-
this.writeHighlightCache = undefined;
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (!this.writeHighlightCache) {
|
|
176
|
-
this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const cache = this.writeHighlightCache;
|
|
180
|
-
if (cache.lang !== lang || cache.rawPath !== rawPath) {
|
|
181
|
-
this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (!fileContent.startsWith(cache.rawContent)) {
|
|
185
|
-
this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (fileContent.length === cache.rawContent.length) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const deltaRaw = fileContent.slice(cache.rawContent.length);
|
|
192
|
-
const deltaDisplay = normalizeDisplayText(deltaRaw);
|
|
193
|
-
const deltaNormalized = replaceTabs(deltaDisplay);
|
|
194
|
-
cache.rawContent = fileContent;
|
|
195
|
-
if (cache.normalizedLines.length === 0) {
|
|
196
|
-
cache.normalizedLines.push("");
|
|
197
|
-
cache.highlightedLines.push("");
|
|
198
|
-
}
|
|
199
|
-
const segments = deltaNormalized.split("\n");
|
|
200
|
-
const lastIndex = cache.normalizedLines.length - 1;
|
|
201
|
-
cache.normalizedLines[lastIndex] += segments[0];
|
|
202
|
-
cache.highlightedLines[lastIndex] = this.highlightSingleLine(cache.normalizedLines[lastIndex], cache.lang);
|
|
203
|
-
for (let i = 1; i < segments.length; i++) {
|
|
204
|
-
cache.normalizedLines.push(segments[i]);
|
|
205
|
-
cache.highlightedLines.push(this.highlightSingleLine(segments[i], cache.lang));
|
|
206
|
-
}
|
|
207
|
-
this.refreshWriteHighlightPrefix(cache);
|
|
98
|
+
markExecutionStarted() {
|
|
99
|
+
this.executionStarted = true;
|
|
100
|
+
this.updateDisplay();
|
|
101
|
+
this.ui.requestRender();
|
|
208
102
|
}
|
|
209
|
-
/**
|
|
210
|
-
* Signal that args are complete (tool is about to execute).
|
|
211
|
-
* This triggers diff computation for edit tool.
|
|
212
|
-
*/
|
|
213
103
|
setArgsComplete() {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (rawPath !== null && fileContent !== null) {
|
|
218
|
-
this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
this.maybeComputeEditDiff();
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Compute edit diff preview when we have complete args.
|
|
225
|
-
* This runs async and updates display when done.
|
|
226
|
-
*/
|
|
227
|
-
maybeComputeEditDiff() {
|
|
228
|
-
if (this.toolName !== "edit")
|
|
229
|
-
return;
|
|
230
|
-
const path = this.args?.path;
|
|
231
|
-
const oldText = this.args?.oldText;
|
|
232
|
-
const newText = this.args?.newText;
|
|
233
|
-
// Need all three params to compute diff
|
|
234
|
-
if (!path || oldText === undefined || newText === undefined)
|
|
235
|
-
return;
|
|
236
|
-
// Create a key to track which args this computation is for
|
|
237
|
-
const argsKey = JSON.stringify({ path, oldText, newText });
|
|
238
|
-
// Skip if we already computed for these exact args
|
|
239
|
-
if (this.editDiffArgsKey === argsKey)
|
|
240
|
-
return;
|
|
241
|
-
this.editDiffArgsKey = argsKey;
|
|
242
|
-
// Compute diff async
|
|
243
|
-
computeEditDiff(path, oldText, newText, this.cwd).then((result) => {
|
|
244
|
-
// Only update if args haven't changed since we started
|
|
245
|
-
if (this.editDiffArgsKey === argsKey) {
|
|
246
|
-
this.editDiffPreview = result;
|
|
247
|
-
this.updateDisplay();
|
|
248
|
-
this.ui.requestRender();
|
|
249
|
-
}
|
|
250
|
-
});
|
|
104
|
+
this.argsComplete = true;
|
|
105
|
+
this.updateDisplay();
|
|
106
|
+
this.ui.requestRender();
|
|
251
107
|
}
|
|
252
108
|
updateResult(result, isPartial = false) {
|
|
253
109
|
this.result = result;
|
|
254
110
|
this.isPartial = isPartial;
|
|
255
|
-
if (this.toolName === "bash") {
|
|
256
|
-
if (isPartial) {
|
|
257
|
-
this.ensureBashElapsedTimer();
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
this.stopBashElapsedTimer();
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (this.toolName === "write" && !isPartial) {
|
|
264
|
-
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
265
|
-
const fileContent = str(this.args?.content);
|
|
266
|
-
if (rawPath !== null && fileContent !== null) {
|
|
267
|
-
this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
111
|
this.updateDisplay();
|
|
271
|
-
// Convert non-PNG images to PNG for Kitty protocol (async)
|
|
272
112
|
this.maybeConvertImagesForKitty();
|
|
273
113
|
}
|
|
274
|
-
/**
|
|
275
|
-
* Convert non-PNG images to PNG for Kitty graphics protocol.
|
|
276
|
-
* Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
|
|
277
|
-
*/
|
|
278
114
|
maybeConvertImagesForKitty() {
|
|
279
115
|
const caps = getCapabilities();
|
|
280
|
-
// Only needed for Kitty protocol
|
|
281
116
|
if (caps.images !== "kitty")
|
|
282
117
|
return;
|
|
283
118
|
if (!this.result)
|
|
284
119
|
return;
|
|
285
|
-
const imageBlocks = this.result.content
|
|
120
|
+
const imageBlocks = this.result.content.filter((c) => c.type === "image");
|
|
286
121
|
for (let i = 0; i < imageBlocks.length; i++) {
|
|
287
122
|
const img = imageBlocks[i];
|
|
288
123
|
if (!img.data || !img.mimeType)
|
|
289
124
|
continue;
|
|
290
|
-
// Skip if already PNG or already converted
|
|
291
125
|
if (img.mimeType === "image/png")
|
|
292
126
|
continue;
|
|
293
127
|
if (this.convertedImages.has(i))
|
|
294
128
|
continue;
|
|
295
|
-
// Convert async
|
|
296
129
|
const index = i;
|
|
297
130
|
convertToPng(img.data, img.mimeType).then((converted) => {
|
|
298
131
|
if (converted) {
|
|
@@ -322,86 +155,66 @@ export class ToolExecutionComponent extends Container {
|
|
|
322
155
|
return super.render(width);
|
|
323
156
|
}
|
|
324
157
|
updateDisplay() {
|
|
325
|
-
// Set background based on state
|
|
326
158
|
const bgFn = this.isPartial
|
|
327
159
|
? (text) => theme.bg("toolPendingBg", text)
|
|
328
160
|
: this.result?.isError
|
|
329
161
|
? (text) => theme.bg("toolErrorBg", text)
|
|
330
162
|
: (text) => theme.bg("toolSuccessBg", text);
|
|
331
|
-
|
|
332
|
-
let customRendererHasContent = false;
|
|
163
|
+
let hasContent = false;
|
|
333
164
|
this.hideComponent = false;
|
|
334
|
-
|
|
335
|
-
if (useBuiltInRenderer) {
|
|
336
|
-
if (this.toolName === "bash") {
|
|
337
|
-
// Bash uses Box with visual line truncation
|
|
338
|
-
this.contentBox.setBgFn(bgFn);
|
|
339
|
-
this.contentBox.clear();
|
|
340
|
-
this.renderBashContent();
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
// Other built-in tools: use Text directly with caching
|
|
344
|
-
this.contentText.setCustomBgFn(bgFn);
|
|
345
|
-
this.contentText.setText(this.formatToolExecution());
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
else if (this.toolDefinition) {
|
|
349
|
-
// Custom tools use Box for flexible component rendering
|
|
165
|
+
if (this.hasRendererDefinition()) {
|
|
350
166
|
this.contentBox.setBgFn(bgFn);
|
|
351
167
|
this.contentBox.clear();
|
|
352
|
-
|
|
353
|
-
if (
|
|
168
|
+
const callRenderer = this.getCallRenderer();
|
|
169
|
+
if (!callRenderer) {
|
|
170
|
+
this.contentBox.addChild(this.createCallFallback());
|
|
171
|
+
hasContent = true;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
354
174
|
try {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
175
|
+
const component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));
|
|
176
|
+
this.callRendererComponent = component;
|
|
177
|
+
this.contentBox.addChild(component);
|
|
178
|
+
hasContent = true;
|
|
360
179
|
}
|
|
361
180
|
catch {
|
|
362
|
-
|
|
363
|
-
this.contentBox.addChild(
|
|
364
|
-
|
|
181
|
+
this.callRendererComponent = undefined;
|
|
182
|
+
this.contentBox.addChild(this.createCallFallback());
|
|
183
|
+
hasContent = true;
|
|
365
184
|
}
|
|
366
185
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
try {
|
|
375
|
-
const resultComponent = this.toolDefinition.renderResult({ content: this.result.content, details: this.result.details }, { expanded: this.expanded, isPartial: this.isPartial }, theme);
|
|
376
|
-
if (resultComponent !== undefined) {
|
|
377
|
-
this.contentBox.addChild(resultComponent);
|
|
378
|
-
customRendererHasContent = true;
|
|
186
|
+
if (this.result) {
|
|
187
|
+
const resultRenderer = this.getResultRenderer();
|
|
188
|
+
if (!resultRenderer) {
|
|
189
|
+
const component = this.createResultFallback();
|
|
190
|
+
if (component) {
|
|
191
|
+
this.contentBox.addChild(component);
|
|
192
|
+
hasContent = true;
|
|
379
193
|
}
|
|
380
194
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
this.contentBox.addChild(
|
|
386
|
-
|
|
195
|
+
else {
|
|
196
|
+
try {
|
|
197
|
+
const component = resultRenderer({ content: this.result.content, details: this.result.details }, { expanded: this.expanded, isPartial: this.isPartial }, theme, this.getRenderContext(this.resultRendererComponent));
|
|
198
|
+
this.resultRendererComponent = component;
|
|
199
|
+
this.contentBox.addChild(component);
|
|
200
|
+
hasContent = true;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
this.resultRendererComponent = undefined;
|
|
204
|
+
const component = this.createResultFallback();
|
|
205
|
+
if (component) {
|
|
206
|
+
this.contentBox.addChild(component);
|
|
207
|
+
hasContent = true;
|
|
208
|
+
}
|
|
387
209
|
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
else if (this.result) {
|
|
391
|
-
// Has result but no custom renderResult
|
|
392
|
-
const output = this.getTextOutput();
|
|
393
|
-
if (output) {
|
|
394
|
-
this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
|
395
|
-
customRendererHasContent = true;
|
|
396
210
|
}
|
|
397
211
|
}
|
|
398
212
|
}
|
|
399
213
|
else {
|
|
400
|
-
// Unknown tool with no registered definition - show generic fallback
|
|
401
214
|
this.contentText.setCustomBgFn(bgFn);
|
|
402
215
|
this.contentText.setText(this.formatToolExecution());
|
|
216
|
+
hasContent = true;
|
|
403
217
|
}
|
|
404
|
-
// Handle images (same for both custom and built-in)
|
|
405
218
|
for (const img of this.imageComponents) {
|
|
406
219
|
this.removeChild(img);
|
|
407
220
|
}
|
|
@@ -411,19 +224,16 @@ export class ToolExecutionComponent extends Container {
|
|
|
411
224
|
}
|
|
412
225
|
this.imageSpacers = [];
|
|
413
226
|
if (this.result) {
|
|
414
|
-
const imageBlocks = this.result.content
|
|
227
|
+
const imageBlocks = this.result.content.filter((c) => c.type === "image");
|
|
415
228
|
const caps = getCapabilities();
|
|
416
229
|
for (let i = 0; i < imageBlocks.length; i++) {
|
|
417
230
|
const img = imageBlocks[i];
|
|
418
231
|
if (caps.images && this.showImages && img.data && img.mimeType) {
|
|
419
|
-
// Use converted PNG for Kitty protocol if available
|
|
420
232
|
const converted = this.convertedImages.get(i);
|
|
421
233
|
const imageData = converted?.data ?? img.data;
|
|
422
234
|
const imageMimeType = converted?.mimeType ?? img.mimeType;
|
|
423
|
-
|
|
424
|
-
if (caps.images === "kitty" && imageMimeType !== "image/png") {
|
|
235
|
+
if (caps.images === "kitty" && imageMimeType !== "image/png")
|
|
425
236
|
continue;
|
|
426
|
-
}
|
|
427
237
|
const spacer = new Spacer(1);
|
|
428
238
|
this.addChild(spacer);
|
|
429
239
|
this.imageSpacers.push(spacer);
|
|
@@ -433,382 +243,22 @@ export class ToolExecutionComponent extends Container {
|
|
|
433
243
|
}
|
|
434
244
|
}
|
|
435
245
|
}
|
|
436
|
-
if (!
|
|
437
|
-
this.hideComponent =
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Render bash content using visual line truncation (like bash-execution.ts)
|
|
442
|
-
*/
|
|
443
|
-
renderBashContent() {
|
|
444
|
-
const command = str(this.args?.command);
|
|
445
|
-
const timeout = this.args?.timeout;
|
|
446
|
-
// Header
|
|
447
|
-
const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
|
|
448
|
-
const commandDisplay = command === null ? theme.fg("error", "[invalid arg]") : command ? command : theme.fg("toolOutput", "...");
|
|
449
|
-
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix, 0, 0));
|
|
450
|
-
if (this.result) {
|
|
451
|
-
const output = this.getTextOutput().trim();
|
|
452
|
-
if (output) {
|
|
453
|
-
// Style each line for the output
|
|
454
|
-
const styledOutput = output
|
|
455
|
-
.split("\n")
|
|
456
|
-
.map((line) => theme.fg("toolOutput", line))
|
|
457
|
-
.join("\n");
|
|
458
|
-
if (this.expanded) {
|
|
459
|
-
// Show all lines when expanded
|
|
460
|
-
this.contentBox.addChild(new Text(`\n${styledOutput}`, 0, 0));
|
|
461
|
-
}
|
|
462
|
-
else {
|
|
463
|
-
// Use visual line truncation when collapsed with width-aware caching
|
|
464
|
-
let cachedWidth;
|
|
465
|
-
let cachedLines;
|
|
466
|
-
let cachedSkipped;
|
|
467
|
-
this.contentBox.addChild({
|
|
468
|
-
render: (width) => {
|
|
469
|
-
if (cachedLines === undefined || cachedWidth !== width) {
|
|
470
|
-
const result = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);
|
|
471
|
-
cachedLines = result.visualLines;
|
|
472
|
-
cachedSkipped = result.skippedCount;
|
|
473
|
-
cachedWidth = width;
|
|
474
|
-
}
|
|
475
|
-
if (cachedSkipped && cachedSkipped > 0) {
|
|
476
|
-
const hint = theme.fg("muted", `... (${cachedSkipped} earlier lines,`) +
|
|
477
|
-
` ${keyHint("app.tools.expand", "to expand")})`;
|
|
478
|
-
return ["", truncateToWidth(hint, width, "..."), ...cachedLines];
|
|
479
|
-
}
|
|
480
|
-
// Add blank line for spacing (matches expanded case)
|
|
481
|
-
return ["", ...cachedLines];
|
|
482
|
-
},
|
|
483
|
-
invalidate: () => {
|
|
484
|
-
cachedWidth = undefined;
|
|
485
|
-
cachedLines = undefined;
|
|
486
|
-
cachedSkipped = undefined;
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
// Truncation warnings
|
|
492
|
-
const truncation = this.result.details?.truncation;
|
|
493
|
-
const fullOutputPath = this.result.details?.fullOutputPath;
|
|
494
|
-
if (truncation?.truncated || fullOutputPath) {
|
|
495
|
-
const warnings = [];
|
|
496
|
-
if (fullOutputPath) {
|
|
497
|
-
warnings.push(`Full output: ${fullOutputPath}`);
|
|
498
|
-
}
|
|
499
|
-
if (truncation?.truncated) {
|
|
500
|
-
if (truncation.truncatedBy === "lines") {
|
|
501
|
-
warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
502
|
-
}
|
|
503
|
-
else {
|
|
504
|
-
warnings.push(`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
this.contentBox.addChild(new Text(`\n${theme.fg("warning", `[${warnings.join(". ")}]`)}`, 0, 0));
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
const bashDurationMs = this.getBashDurationMs();
|
|
511
|
-
if (bashDurationMs !== undefined) {
|
|
512
|
-
const label = this.isPartial ? "Elapsed" : "Took";
|
|
513
|
-
this.contentBox.addChild(new Text(`\n${theme.fg("muted", `${label} ${this.formatDuration(bashDurationMs)}`)}`, 0, 0));
|
|
246
|
+
if (this.hasRendererDefinition() && !hasContent && this.imageComponents.length === 0) {
|
|
247
|
+
this.hideComponent = true;
|
|
514
248
|
}
|
|
515
249
|
}
|
|
516
250
|
getTextOutput() {
|
|
517
|
-
|
|
518
|
-
return "";
|
|
519
|
-
const textBlocks = this.result.content?.filter((c) => c.type === "text") || [];
|
|
520
|
-
const imageBlocks = this.result.content?.filter((c) => c.type === "image") || [];
|
|
521
|
-
let output = textBlocks
|
|
522
|
-
.map((c) => {
|
|
523
|
-
// Use sanitizeBinaryOutput to handle binary data that crashes string-width
|
|
524
|
-
return sanitizeBinaryOutput(stripAnsi(c.text || "")).replace(/\r/g, "");
|
|
525
|
-
})
|
|
526
|
-
.join("\n");
|
|
527
|
-
const caps = getCapabilities();
|
|
528
|
-
if (imageBlocks.length > 0 && (!caps.images || !this.showImages)) {
|
|
529
|
-
const imageIndicators = imageBlocks
|
|
530
|
-
.map((img) => {
|
|
531
|
-
const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
|
|
532
|
-
return imageFallback(img.mimeType, dims);
|
|
533
|
-
})
|
|
534
|
-
.join("\n");
|
|
535
|
-
output = output ? `${output}\n${imageIndicators}` : imageIndicators;
|
|
536
|
-
}
|
|
537
|
-
return output;
|
|
251
|
+
return getRenderedTextOutput(this.result, this.showImages);
|
|
538
252
|
}
|
|
539
253
|
formatToolExecution() {
|
|
540
|
-
let text = "";
|
|
541
|
-
const
|
|
542
|
-
if (
|
|
543
|
-
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
544
|
-
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
545
|
-
const offset = this.args?.offset;
|
|
546
|
-
const limit = this.args?.limit;
|
|
547
|
-
let pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
|
|
548
|
-
if (offset !== undefined || limit !== undefined) {
|
|
549
|
-
const startLine = offset ?? 1;
|
|
550
|
-
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
551
|
-
pathDisplay += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
552
|
-
}
|
|
553
|
-
text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`;
|
|
554
|
-
if (this.result) {
|
|
555
|
-
const output = this.getTextOutput();
|
|
556
|
-
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
557
|
-
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
|
558
|
-
const lines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
|
|
559
|
-
const maxLines = this.expanded ? lines.length : 10;
|
|
560
|
-
const displayLines = lines.slice(0, maxLines);
|
|
561
|
-
const remaining = lines.length - maxLines;
|
|
562
|
-
text +=
|
|
563
|
-
"\n\n" +
|
|
564
|
-
displayLines
|
|
565
|
-
.map((line) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
|
|
566
|
-
.join("\n");
|
|
567
|
-
if (remaining > 0) {
|
|
568
|
-
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
569
|
-
}
|
|
570
|
-
const truncation = this.result.details?.truncation;
|
|
571
|
-
if (truncation?.truncated) {
|
|
572
|
-
if (truncation.firstLineExceedsLimit) {
|
|
573
|
-
text +=
|
|
574
|
-
"\n" +
|
|
575
|
-
theme.fg("warning", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`);
|
|
576
|
-
}
|
|
577
|
-
else if (truncation.truncatedBy === "lines") {
|
|
578
|
-
text +=
|
|
579
|
-
"\n" +
|
|
580
|
-
theme.fg("warning", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`);
|
|
581
|
-
}
|
|
582
|
-
else {
|
|
583
|
-
text +=
|
|
584
|
-
"\n" +
|
|
585
|
-
theme.fg("warning", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
else if (this.toolName === "write") {
|
|
591
|
-
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
592
|
-
const fileContent = str(this.args?.content);
|
|
593
|
-
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
594
|
-
text =
|
|
595
|
-
theme.fg("toolTitle", theme.bold("write")) +
|
|
596
|
-
" " +
|
|
597
|
-
(path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."));
|
|
598
|
-
if (fileContent === null) {
|
|
599
|
-
text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`;
|
|
600
|
-
}
|
|
601
|
-
else if (fileContent) {
|
|
602
|
-
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
|
603
|
-
let lines;
|
|
604
|
-
if (lang) {
|
|
605
|
-
const cache = this.writeHighlightCache;
|
|
606
|
-
if (cache && cache.lang === lang && cache.rawPath === rawPath && cache.rawContent === fileContent) {
|
|
607
|
-
lines = cache.highlightedLines;
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
const displayContent = normalizeDisplayText(fileContent);
|
|
611
|
-
const normalized = replaceTabs(displayContent);
|
|
612
|
-
lines = highlightCode(normalized, lang);
|
|
613
|
-
this.writeHighlightCache = {
|
|
614
|
-
rawPath,
|
|
615
|
-
lang,
|
|
616
|
-
rawContent: fileContent,
|
|
617
|
-
normalizedLines: normalized.split("\n"),
|
|
618
|
-
highlightedLines: lines,
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
else {
|
|
623
|
-
lines = normalizeDisplayText(fileContent).split("\n");
|
|
624
|
-
this.writeHighlightCache = undefined;
|
|
625
|
-
}
|
|
626
|
-
const totalLines = lines.length;
|
|
627
|
-
const maxLines = this.expanded ? lines.length : 10;
|
|
628
|
-
const displayLines = lines.slice(0, maxLines);
|
|
629
|
-
const remaining = lines.length - maxLines;
|
|
630
|
-
text +=
|
|
631
|
-
"\n\n" +
|
|
632
|
-
displayLines.map((line) => (lang ? line : theme.fg("toolOutput", replaceTabs(line)))).join("\n");
|
|
633
|
-
if (remaining > 0) {
|
|
634
|
-
text +=
|
|
635
|
-
theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total,`) +
|
|
636
|
-
` ${keyHint("app.tools.expand", "to expand")})`;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
// Show error if tool execution failed
|
|
640
|
-
if (this.result?.isError) {
|
|
641
|
-
const errorText = this.getTextOutput();
|
|
642
|
-
if (errorText) {
|
|
643
|
-
text += `\n\n${theme.fg("error", errorText)}`;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
else if (this.toolName === "edit") {
|
|
648
|
-
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
649
|
-
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
650
|
-
// Build path display, appending :line if we have diff info
|
|
651
|
-
let pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
|
|
652
|
-
const firstChangedLine = (this.editDiffPreview && "firstChangedLine" in this.editDiffPreview
|
|
653
|
-
? this.editDiffPreview.firstChangedLine
|
|
654
|
-
: undefined) ||
|
|
655
|
-
(this.result && !this.result.isError ? this.result.details?.firstChangedLine : undefined);
|
|
656
|
-
if (firstChangedLine) {
|
|
657
|
-
pathDisplay += theme.fg("warning", `:${firstChangedLine}`);
|
|
658
|
-
}
|
|
659
|
-
text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
|
|
660
|
-
if (this.result?.isError) {
|
|
661
|
-
// Show error from result
|
|
662
|
-
const errorText = this.getTextOutput();
|
|
663
|
-
if (errorText) {
|
|
664
|
-
text += `\n\n${theme.fg("error", errorText)}`;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
else if (this.result?.details?.diff) {
|
|
668
|
-
// Tool executed successfully - use the diff from result
|
|
669
|
-
// This takes priority over editDiffPreview which may have a stale error
|
|
670
|
-
// due to race condition (async preview computed after file was modified)
|
|
671
|
-
text += `\n\n${renderDiff(this.result.details.diff, { filePath: rawPath ?? undefined })}`;
|
|
672
|
-
}
|
|
673
|
-
else if (this.editDiffPreview) {
|
|
674
|
-
// Use cached diff preview (before tool executes)
|
|
675
|
-
if ("error" in this.editDiffPreview) {
|
|
676
|
-
text += `\n\n${theme.fg("error", this.editDiffPreview.error)}`;
|
|
677
|
-
}
|
|
678
|
-
else if (this.editDiffPreview.diff) {
|
|
679
|
-
text += `\n\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath ?? undefined })}`;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
else if (this.toolName === "ls") {
|
|
684
|
-
const rawPath = str(this.args?.path);
|
|
685
|
-
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
686
|
-
const limit = this.args?.limit;
|
|
687
|
-
text = `${theme.fg("toolTitle", theme.bold("ls"))} ${path === null ? invalidArg : theme.fg("accent", path)}`;
|
|
688
|
-
if (limit !== undefined) {
|
|
689
|
-
text += theme.fg("toolOutput", ` (limit ${limit})`);
|
|
690
|
-
}
|
|
691
|
-
if (this.result) {
|
|
692
|
-
const output = this.getTextOutput().trim();
|
|
693
|
-
if (output) {
|
|
694
|
-
const lines = output.split("\n");
|
|
695
|
-
const maxLines = this.expanded ? lines.length : 20;
|
|
696
|
-
const displayLines = lines.slice(0, maxLines);
|
|
697
|
-
const remaining = lines.length - maxLines;
|
|
698
|
-
text += `\n\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
|
|
699
|
-
if (remaining > 0) {
|
|
700
|
-
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
const entryLimit = this.result.details?.entryLimitReached;
|
|
704
|
-
const truncation = this.result.details?.truncation;
|
|
705
|
-
if (entryLimit || truncation?.truncated) {
|
|
706
|
-
const warnings = [];
|
|
707
|
-
if (entryLimit) {
|
|
708
|
-
warnings.push(`${entryLimit} entries limit`);
|
|
709
|
-
}
|
|
710
|
-
if (truncation?.truncated) {
|
|
711
|
-
warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
|
|
712
|
-
}
|
|
713
|
-
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
else if (this.toolName === "find") {
|
|
718
|
-
const pattern = str(this.args?.pattern);
|
|
719
|
-
const rawPath = str(this.args?.path);
|
|
720
|
-
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
721
|
-
const limit = this.args?.limit;
|
|
722
|
-
text =
|
|
723
|
-
theme.fg("toolTitle", theme.bold("find")) +
|
|
724
|
-
" " +
|
|
725
|
-
(pattern === null ? invalidArg : theme.fg("accent", pattern || "")) +
|
|
726
|
-
theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
|
|
727
|
-
if (limit !== undefined) {
|
|
728
|
-
text += theme.fg("toolOutput", ` (limit ${limit})`);
|
|
729
|
-
}
|
|
730
|
-
if (this.result) {
|
|
731
|
-
const output = this.getTextOutput().trim();
|
|
732
|
-
if (output) {
|
|
733
|
-
const lines = output.split("\n");
|
|
734
|
-
const maxLines = this.expanded ? lines.length : 20;
|
|
735
|
-
const displayLines = lines.slice(0, maxLines);
|
|
736
|
-
const remaining = lines.length - maxLines;
|
|
737
|
-
text += `\n\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
|
|
738
|
-
if (remaining > 0) {
|
|
739
|
-
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
const resultLimit = this.result.details?.resultLimitReached;
|
|
743
|
-
const truncation = this.result.details?.truncation;
|
|
744
|
-
if (resultLimit || truncation?.truncated) {
|
|
745
|
-
const warnings = [];
|
|
746
|
-
if (resultLimit) {
|
|
747
|
-
warnings.push(`${resultLimit} results limit`);
|
|
748
|
-
}
|
|
749
|
-
if (truncation?.truncated) {
|
|
750
|
-
warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
|
|
751
|
-
}
|
|
752
|
-
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
else if (this.toolName === "grep") {
|
|
757
|
-
const pattern = str(this.args?.pattern);
|
|
758
|
-
const rawPath = str(this.args?.path);
|
|
759
|
-
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
760
|
-
const glob = str(this.args?.glob);
|
|
761
|
-
const limit = this.args?.limit;
|
|
762
|
-
text =
|
|
763
|
-
theme.fg("toolTitle", theme.bold("grep")) +
|
|
764
|
-
" " +
|
|
765
|
-
(pattern === null ? invalidArg : theme.fg("accent", `/${pattern || ""}/`)) +
|
|
766
|
-
theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
|
|
767
|
-
if (glob) {
|
|
768
|
-
text += theme.fg("toolOutput", ` (${glob})`);
|
|
769
|
-
}
|
|
770
|
-
if (limit !== undefined) {
|
|
771
|
-
text += theme.fg("toolOutput", ` limit ${limit}`);
|
|
772
|
-
}
|
|
773
|
-
if (this.result) {
|
|
774
|
-
const output = this.getTextOutput().trim();
|
|
775
|
-
if (output) {
|
|
776
|
-
const lines = output.split("\n");
|
|
777
|
-
const maxLines = this.expanded ? lines.length : 15;
|
|
778
|
-
const displayLines = lines.slice(0, maxLines);
|
|
779
|
-
const remaining = lines.length - maxLines;
|
|
780
|
-
text += `\n\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
|
|
781
|
-
if (remaining > 0) {
|
|
782
|
-
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
const matchLimit = this.result.details?.matchLimitReached;
|
|
786
|
-
const truncation = this.result.details?.truncation;
|
|
787
|
-
const linesTruncated = this.result.details?.linesTruncated;
|
|
788
|
-
if (matchLimit || truncation?.truncated || linesTruncated) {
|
|
789
|
-
const warnings = [];
|
|
790
|
-
if (matchLimit) {
|
|
791
|
-
warnings.push(`${matchLimit} matches limit`);
|
|
792
|
-
}
|
|
793
|
-
if (truncation?.truncated) {
|
|
794
|
-
warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
|
|
795
|
-
}
|
|
796
|
-
if (linesTruncated) {
|
|
797
|
-
warnings.push("some lines truncated");
|
|
798
|
-
}
|
|
799
|
-
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
else {
|
|
804
|
-
// Generic tool (shouldn't reach here for custom tools)
|
|
805
|
-
text = theme.fg("toolTitle", theme.bold(this.toolName));
|
|
806
|
-
const content = JSON.stringify(this.args, null, 2);
|
|
254
|
+
let text = theme.fg("toolTitle", theme.bold(this.toolName));
|
|
255
|
+
const content = JSON.stringify(this.args, null, 2);
|
|
256
|
+
if (content) {
|
|
807
257
|
text += `\n\n${content}`;
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
258
|
+
}
|
|
259
|
+
const output = this.getTextOutput();
|
|
260
|
+
if (output) {
|
|
261
|
+
text += `\n${output}`;
|
|
812
262
|
}
|
|
813
263
|
return text;
|
|
814
264
|
}
|