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