@oh-my-pi/pi-coding-agent 1.337.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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,675 @@
1
+ import * as os from "node:os";
2
+ import {
3
+ Box,
4
+ Container,
5
+ getCapabilities,
6
+ getImageDimensions,
7
+ Image,
8
+ imageFallback,
9
+ Spacer,
10
+ Text,
11
+ type TUI,
12
+ } from "@oh-my-pi/pi-tui";
13
+ import stripAnsi from "strip-ansi";
14
+ import type { CustomTool } from "../../../core/custom-tools/types.js";
15
+ import { computeEditDiff, type EditDiffError, type EditDiffResult } from "../../../core/tools/edit-diff.js";
16
+ import { toolRenderers } from "../../../core/tools/renderers.js";
17
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js";
18
+ import { sanitizeBinaryOutput } from "../../../utils/shell.js";
19
+ import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
20
+ import { renderDiff } from "./diff.js";
21
+ import { truncateToVisualLines } from "./visual-truncate.js";
22
+
23
+ // Preview line limit for bash when not expanded
24
+ const BASH_PREVIEW_LINES = 5;
25
+
26
+ /**
27
+ * Convert absolute path to tilde notation if it's in home directory
28
+ */
29
+ function shortenPath(path: string): string {
30
+ const home = os.homedir();
31
+ if (path.startsWith(home)) {
32
+ return `~${path.slice(home.length)}`;
33
+ }
34
+ return path;
35
+ }
36
+
37
+ /**
38
+ * Replace tabs with spaces for consistent rendering
39
+ */
40
+ function replaceTabs(text: string): string {
41
+ return text.replace(/\t/g, " ");
42
+ }
43
+
44
+ export interface ToolExecutionOptions {
45
+ showImages?: boolean; // default: true (only used if terminal supports images)
46
+ }
47
+
48
+ /**
49
+ * Component that renders a tool call with its result (updateable)
50
+ */
51
+ export class ToolExecutionComponent extends Container {
52
+ private contentBox: Box; // Used for custom tools and bash visual truncation
53
+ private contentText: Text; // For built-in tools (with its own padding/bg)
54
+ private imageComponents: Image[] = [];
55
+ private imageSpacers: Spacer[] = [];
56
+ private toolName: string;
57
+ private args: any;
58
+ private expanded = false;
59
+ private showImages: boolean;
60
+ private isPartial = true;
61
+ private customTool?: CustomTool;
62
+ private ui: TUI;
63
+ private cwd: string;
64
+ private result?: {
65
+ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
66
+ isError: boolean;
67
+ details?: any;
68
+ };
69
+ // Cached edit diff preview (computed when args arrive, before tool executes)
70
+ private editDiffPreview?: EditDiffResult | EditDiffError;
71
+ private editDiffArgsKey?: string; // Track which args the preview is for
72
+
73
+ constructor(
74
+ toolName: string,
75
+ args: any,
76
+ options: ToolExecutionOptions = {},
77
+ customTool: CustomTool | undefined,
78
+ ui: TUI,
79
+ cwd: string = process.cwd(),
80
+ ) {
81
+ super();
82
+ this.toolName = toolName;
83
+ this.args = args;
84
+ this.showImages = options.showImages ?? true;
85
+ this.customTool = customTool;
86
+ this.ui = ui;
87
+ this.cwd = cwd;
88
+
89
+ this.addChild(new Spacer(1));
90
+
91
+ // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins
92
+ this.contentBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
93
+ this.contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text));
94
+
95
+ // Use Box for custom tools, bash, or built-in tools that have renderers
96
+ const hasRenderer = toolName in toolRenderers;
97
+ if (customTool || toolName === "bash" || hasRenderer) {
98
+ this.addChild(this.contentBox);
99
+ } else {
100
+ this.addChild(this.contentText);
101
+ }
102
+
103
+ this.updateDisplay();
104
+ }
105
+
106
+ updateArgs(args: any): void {
107
+ this.args = args;
108
+ this.updateDisplay();
109
+ }
110
+
111
+ /**
112
+ * Signal that args are complete (tool is about to execute).
113
+ * This triggers diff computation for edit tool.
114
+ */
115
+ setArgsComplete(): void {
116
+ this.maybeComputeEditDiff();
117
+ }
118
+
119
+ /**
120
+ * Compute edit diff preview when we have complete args.
121
+ * This runs async and updates display when done.
122
+ */
123
+ private maybeComputeEditDiff(): void {
124
+ if (this.toolName !== "edit") return;
125
+
126
+ const path = this.args?.path;
127
+ const oldText = this.args?.oldText;
128
+ const newText = this.args?.newText;
129
+
130
+ // Need all three params to compute diff
131
+ if (!path || oldText === undefined || newText === undefined) return;
132
+
133
+ // Create a key to track which args this computation is for
134
+ const argsKey = JSON.stringify({ path, oldText, newText });
135
+
136
+ // Skip if we already computed for these exact args
137
+ if (this.editDiffArgsKey === argsKey) return;
138
+
139
+ this.editDiffArgsKey = argsKey;
140
+
141
+ // Compute diff async
142
+ computeEditDiff(path, oldText, newText, this.cwd).then((result) => {
143
+ // Only update if args haven't changed since we started
144
+ if (this.editDiffArgsKey === argsKey) {
145
+ this.editDiffPreview = result;
146
+ this.updateDisplay();
147
+ this.ui.requestRender();
148
+ }
149
+ });
150
+ }
151
+
152
+ updateResult(
153
+ result: {
154
+ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
155
+ details?: any;
156
+ isError: boolean;
157
+ },
158
+ isPartial = false,
159
+ ): void {
160
+ this.result = result;
161
+ this.isPartial = isPartial;
162
+ this.updateDisplay();
163
+ }
164
+
165
+ setExpanded(expanded: boolean): void {
166
+ this.expanded = expanded;
167
+ this.updateDisplay();
168
+ }
169
+
170
+ setShowImages(show: boolean): void {
171
+ this.showImages = show;
172
+ this.updateDisplay();
173
+ }
174
+
175
+ private updateDisplay(): void {
176
+ // Set background based on state
177
+ const bgFn = this.isPartial
178
+ ? (text: string) => theme.bg("toolPendingBg", text)
179
+ : this.result?.isError
180
+ ? (text: string) => theme.bg("toolErrorBg", text)
181
+ : (text: string) => theme.bg("toolSuccessBg", text);
182
+
183
+ // Check for custom tool rendering
184
+ if (this.customTool) {
185
+ // Custom tools use Box for flexible component rendering
186
+ this.contentBox.setBgFn(bgFn);
187
+ this.contentBox.clear();
188
+
189
+ // Render call component
190
+ if (this.customTool.renderCall) {
191
+ try {
192
+ const callComponent = this.customTool.renderCall(this.args, theme);
193
+ if (callComponent) {
194
+ this.contentBox.addChild(callComponent);
195
+ }
196
+ } catch {
197
+ // Fall back to default on error
198
+ this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
199
+ }
200
+ } else {
201
+ // No custom renderCall, show tool name
202
+ this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
203
+ }
204
+
205
+ // Render result component if we have a result
206
+ if (this.result && this.customTool.renderResult) {
207
+ try {
208
+ const resultComponent = this.customTool.renderResult(
209
+ { content: this.result.content as any, details: this.result.details },
210
+ { expanded: this.expanded, isPartial: this.isPartial },
211
+ theme,
212
+ );
213
+ if (resultComponent) {
214
+ this.contentBox.addChild(resultComponent);
215
+ }
216
+ } catch {
217
+ // Fall back to showing raw output on error
218
+ const output = this.getTextOutput();
219
+ if (output) {
220
+ this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
221
+ }
222
+ }
223
+ } else if (this.result) {
224
+ // Has result but no custom renderResult
225
+ const output = this.getTextOutput();
226
+ if (output) {
227
+ this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
228
+ }
229
+ }
230
+ } else if (this.toolName === "bash") {
231
+ // Bash uses Box with visual line truncation
232
+ this.contentBox.setBgFn(bgFn);
233
+ this.contentBox.clear();
234
+ this.renderBashContent();
235
+ } else if (this.toolName in toolRenderers) {
236
+ // Built-in tools with custom renderers
237
+ const renderer = toolRenderers[this.toolName];
238
+ this.contentBox.setBgFn(bgFn);
239
+ this.contentBox.clear();
240
+
241
+ // Render call component
242
+ try {
243
+ const callComponent = renderer.renderCall(this.args, theme);
244
+ if (callComponent) {
245
+ this.contentBox.addChild(callComponent);
246
+ }
247
+ } catch {
248
+ // Fall back to default on error
249
+ this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
250
+ }
251
+
252
+ // Render result component if we have a result
253
+ if (this.result) {
254
+ try {
255
+ const resultComponent = renderer.renderResult(
256
+ { content: this.result.content as any, details: this.result.details },
257
+ { expanded: this.expanded, isPartial: this.isPartial },
258
+ theme,
259
+ );
260
+ if (resultComponent) {
261
+ this.contentBox.addChild(resultComponent);
262
+ }
263
+ } catch {
264
+ // Fall back to showing raw output on error
265
+ const output = this.getTextOutput();
266
+ if (output) {
267
+ this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
268
+ }
269
+ }
270
+ }
271
+ } else {
272
+ // Other built-in tools: use Text directly with caching
273
+ this.contentText.setCustomBgFn(bgFn);
274
+ this.contentText.setText(this.formatToolExecution());
275
+ }
276
+
277
+ // Handle images (same for both custom and built-in)
278
+ for (const img of this.imageComponents) {
279
+ this.removeChild(img);
280
+ }
281
+ this.imageComponents = [];
282
+ for (const spacer of this.imageSpacers) {
283
+ this.removeChild(spacer);
284
+ }
285
+ this.imageSpacers = [];
286
+
287
+ if (this.result) {
288
+ const imageBlocks = this.result.content?.filter((c: any) => c.type === "image") || [];
289
+ const caps = getCapabilities();
290
+
291
+ for (const img of imageBlocks) {
292
+ if (caps.images && this.showImages && img.data && img.mimeType) {
293
+ const spacer = new Spacer(1);
294
+ this.addChild(spacer);
295
+ this.imageSpacers.push(spacer);
296
+ const imageComponent = new Image(
297
+ img.data,
298
+ img.mimeType,
299
+ { fallbackColor: (s: string) => theme.fg("toolOutput", s) },
300
+ { maxWidthCells: 60 },
301
+ );
302
+ this.imageComponents.push(imageComponent);
303
+ this.addChild(imageComponent);
304
+ }
305
+ }
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Render bash content using visual line truncation (like bash-execution.ts)
311
+ */
312
+ private renderBashContent(): void {
313
+ const command = this.args?.command || "";
314
+
315
+ // Header
316
+ this.contentBox.addChild(
317
+ new Text(theme.fg("toolTitle", theme.bold(`$ ${command || theme.fg("toolOutput", "...")}`)), 0, 0),
318
+ );
319
+
320
+ if (this.result) {
321
+ const output = this.getTextOutput().trim();
322
+
323
+ if (output) {
324
+ // Style each line for the output
325
+ const styledOutput = output
326
+ .split("\n")
327
+ .map((line) => theme.fg("toolOutput", line))
328
+ .join("\n");
329
+
330
+ if (this.expanded) {
331
+ // Show all lines when expanded
332
+ this.contentBox.addChild(new Text(`\n${styledOutput}`, 0, 0));
333
+ } else {
334
+ // Use visual line truncation when collapsed
335
+ // Box has paddingX=1, so content width = terminal.columns - 2
336
+ const { visualLines, skippedCount } = truncateToVisualLines(
337
+ `\n${styledOutput}`,
338
+ BASH_PREVIEW_LINES,
339
+ this.ui.terminal.columns - 2,
340
+ );
341
+
342
+ if (skippedCount > 0) {
343
+ this.contentBox.addChild(
344
+ new Text(theme.fg("toolOutput", `\n... (${skippedCount} earlier lines)`), 0, 0),
345
+ );
346
+ }
347
+
348
+ // Add pre-rendered visual lines as a raw component
349
+ this.contentBox.addChild({
350
+ render: () => visualLines,
351
+ invalidate: () => {},
352
+ });
353
+ }
354
+ }
355
+
356
+ // Truncation warnings
357
+ const truncation = this.result.details?.truncation;
358
+ const fullOutputPath = this.result.details?.fullOutputPath;
359
+ if (truncation?.truncated || fullOutputPath) {
360
+ const warnings: string[] = [];
361
+ if (fullOutputPath) {
362
+ warnings.push(`Full output: ${fullOutputPath}`);
363
+ }
364
+ if (truncation?.truncated) {
365
+ if (truncation.truncatedBy === "lines") {
366
+ warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
367
+ } else {
368
+ warnings.push(
369
+ `Truncated: ${truncation.outputLines} lines shown (${formatSize(
370
+ truncation.maxBytes ?? DEFAULT_MAX_BYTES,
371
+ )} limit)`,
372
+ );
373
+ }
374
+ }
375
+ this.contentBox.addChild(new Text(`\n${theme.fg("warning", `[${warnings.join(". ")}]`)}`, 0, 0));
376
+ }
377
+ }
378
+ }
379
+
380
+ private getTextOutput(): string {
381
+ if (!this.result) return "";
382
+
383
+ const textBlocks = this.result.content?.filter((c: any) => c.type === "text") || [];
384
+ const imageBlocks = this.result.content?.filter((c: any) => c.type === "image") || [];
385
+
386
+ let output = textBlocks
387
+ .map((c: any) => {
388
+ // Use sanitizeBinaryOutput to handle binary data that crashes string-width
389
+ return sanitizeBinaryOutput(stripAnsi(c.text || "")).replace(/\r/g, "");
390
+ })
391
+ .join("\n");
392
+
393
+ const caps = getCapabilities();
394
+ if (imageBlocks.length > 0 && (!caps.images || !this.showImages)) {
395
+ const imageIndicators = imageBlocks
396
+ .map((img: any) => {
397
+ const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
398
+ return imageFallback(img.mimeType, dims);
399
+ })
400
+ .join("\n");
401
+ output = output ? `${output}\n${imageIndicators}` : imageIndicators;
402
+ }
403
+
404
+ return output;
405
+ }
406
+
407
+ private formatToolExecution(): string {
408
+ let text = "";
409
+
410
+ if (this.toolName === "read") {
411
+ const path = shortenPath(this.args?.file_path || this.args?.path || "");
412
+ const offset = this.args?.offset;
413
+ const limit = this.args?.limit;
414
+
415
+ let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
416
+ if (offset !== undefined || limit !== undefined) {
417
+ const startLine = offset ?? 1;
418
+ const endLine = limit !== undefined ? startLine + limit - 1 : "";
419
+ pathDisplay += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
420
+ }
421
+
422
+ text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`;
423
+
424
+ if (this.result) {
425
+ const output = this.getTextOutput();
426
+ const rawPath = this.args?.file_path || this.args?.path || "";
427
+ const lang = getLanguageFromPath(rawPath);
428
+ const lines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
429
+
430
+ const maxLines = this.expanded ? lines.length : 10;
431
+ const displayLines = lines.slice(0, maxLines);
432
+ const remaining = lines.length - maxLines;
433
+
434
+ text +=
435
+ "\n\n" +
436
+ displayLines
437
+ .map((line: string) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
438
+ .join("\n");
439
+ if (remaining > 0) {
440
+ text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
441
+ }
442
+
443
+ const truncation = this.result.details?.truncation;
444
+ if (truncation?.truncated) {
445
+ if (truncation.firstLineExceedsLimit) {
446
+ text +=
447
+ "\n" +
448
+ theme.fg(
449
+ "warning",
450
+ `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`,
451
+ );
452
+ } else if (truncation.truncatedBy === "lines") {
453
+ text +=
454
+ "\n" +
455
+ theme.fg(
456
+ "warning",
457
+ `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${
458
+ truncation.maxLines ?? DEFAULT_MAX_LINES
459
+ } line limit)]`,
460
+ );
461
+ } else {
462
+ text +=
463
+ "\n" +
464
+ theme.fg(
465
+ "warning",
466
+ `[Truncated: ${truncation.outputLines} lines shown (${formatSize(
467
+ truncation.maxBytes ?? DEFAULT_MAX_BYTES,
468
+ )} limit)]`,
469
+ );
470
+ }
471
+ }
472
+ }
473
+ } else if (this.toolName === "write") {
474
+ const rawPath = this.args?.file_path || this.args?.path || "";
475
+ const path = shortenPath(rawPath);
476
+ const fileContent = this.args?.content || "";
477
+ const lang = getLanguageFromPath(rawPath);
478
+ const lines = fileContent
479
+ ? lang
480
+ ? highlightCode(replaceTabs(fileContent), lang)
481
+ : fileContent.split("\n")
482
+ : [];
483
+ const totalLines = lines.length;
484
+
485
+ text =
486
+ theme.fg("toolTitle", theme.bold("write")) +
487
+ " " +
488
+ (path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."));
489
+
490
+ if (fileContent) {
491
+ const maxLines = this.expanded ? lines.length : 10;
492
+ const displayLines = lines.slice(0, maxLines);
493
+ const remaining = lines.length - maxLines;
494
+
495
+ text +=
496
+ "\n\n" +
497
+ displayLines
498
+ .map((line: string) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
499
+ .join("\n");
500
+ if (remaining > 0) {
501
+ text += theme.fg("toolOutput", `\n... (${remaining} more lines, ${totalLines} total)`);
502
+ }
503
+ }
504
+ } else if (this.toolName === "edit") {
505
+ const rawPath = this.args?.file_path || this.args?.path || "";
506
+ const path = shortenPath(rawPath);
507
+
508
+ // Build path display, appending :line if we have diff info
509
+ let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
510
+ const firstChangedLine =
511
+ (this.editDiffPreview && "firstChangedLine" in this.editDiffPreview
512
+ ? this.editDiffPreview.firstChangedLine
513
+ : undefined) ||
514
+ (this.result && !this.result.isError ? this.result.details?.firstChangedLine : undefined);
515
+ if (firstChangedLine) {
516
+ pathDisplay += theme.fg("warning", `:${firstChangedLine}`);
517
+ }
518
+
519
+ text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
520
+
521
+ if (this.result?.isError) {
522
+ // Show error from result
523
+ const errorText = this.getTextOutput();
524
+ if (errorText) {
525
+ text += `\n\n${theme.fg("error", errorText)}`;
526
+ }
527
+ } else if (this.editDiffPreview) {
528
+ // Use cached diff preview (works both before and after execution)
529
+ if ("error" in this.editDiffPreview) {
530
+ text += `\n\n${theme.fg("error", this.editDiffPreview.error)}`;
531
+ } else if (this.editDiffPreview.diff) {
532
+ text += `\n\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath })}`;
533
+ }
534
+ }
535
+ } else if (this.toolName === "ls") {
536
+ const path = shortenPath(this.args?.path || ".");
537
+ const limit = this.args?.limit;
538
+
539
+ text = `${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", path)}`;
540
+ if (limit !== undefined) {
541
+ text += theme.fg("toolOutput", ` (limit ${limit})`);
542
+ }
543
+
544
+ if (this.result) {
545
+ const output = this.getTextOutput().trim();
546
+ if (output) {
547
+ const lines = output.split("\n");
548
+ const maxLines = this.expanded ? lines.length : 20;
549
+ const displayLines = lines.slice(0, maxLines);
550
+ const remaining = lines.length - maxLines;
551
+
552
+ text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`;
553
+ if (remaining > 0) {
554
+ text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
555
+ }
556
+ }
557
+
558
+ const entryLimit = this.result.details?.entryLimitReached;
559
+ const truncation = this.result.details?.truncation;
560
+ if (entryLimit || truncation?.truncated) {
561
+ const warnings: string[] = [];
562
+ if (entryLimit) {
563
+ warnings.push(`${entryLimit} entries limit`);
564
+ }
565
+ if (truncation?.truncated) {
566
+ warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
567
+ }
568
+ text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
569
+ }
570
+ }
571
+ } else if (this.toolName === "find") {
572
+ const pattern = this.args?.pattern || "";
573
+ const path = shortenPath(this.args?.path || ".");
574
+ const limit = this.args?.limit;
575
+
576
+ text =
577
+ theme.fg("toolTitle", theme.bold("find")) +
578
+ " " +
579
+ theme.fg("accent", pattern) +
580
+ theme.fg("toolOutput", ` in ${path}`);
581
+ if (limit !== undefined) {
582
+ text += theme.fg("toolOutput", ` (limit ${limit})`);
583
+ }
584
+
585
+ if (this.result) {
586
+ const output = this.getTextOutput().trim();
587
+ if (output) {
588
+ const lines = output.split("\n");
589
+ const maxLines = this.expanded ? lines.length : 20;
590
+ const displayLines = lines.slice(0, maxLines);
591
+ const remaining = lines.length - maxLines;
592
+
593
+ text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`;
594
+ if (remaining > 0) {
595
+ text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
596
+ }
597
+ }
598
+
599
+ const resultLimit = this.result.details?.resultLimitReached;
600
+ const truncation = this.result.details?.truncation;
601
+ if (resultLimit || truncation?.truncated) {
602
+ const warnings: string[] = [];
603
+ if (resultLimit) {
604
+ warnings.push(`${resultLimit} results limit`);
605
+ }
606
+ if (truncation?.truncated) {
607
+ warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
608
+ }
609
+ text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
610
+ }
611
+ }
612
+ } else if (this.toolName === "grep") {
613
+ const pattern = this.args?.pattern || "";
614
+ const path = shortenPath(this.args?.path || ".");
615
+ const glob = this.args?.glob;
616
+ const limit = this.args?.limit;
617
+
618
+ text =
619
+ theme.fg("toolTitle", theme.bold("grep")) +
620
+ " " +
621
+ theme.fg("accent", `/${pattern}/`) +
622
+ theme.fg("toolOutput", ` in ${path}`);
623
+ if (glob) {
624
+ text += theme.fg("toolOutput", ` (${glob})`);
625
+ }
626
+ if (limit !== undefined) {
627
+ text += theme.fg("toolOutput", ` limit ${limit}`);
628
+ }
629
+
630
+ if (this.result) {
631
+ const output = this.getTextOutput().trim();
632
+ if (output) {
633
+ const lines = output.split("\n");
634
+ const maxLines = this.expanded ? lines.length : 15;
635
+ const displayLines = lines.slice(0, maxLines);
636
+ const remaining = lines.length - maxLines;
637
+
638
+ text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`;
639
+ if (remaining > 0) {
640
+ text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
641
+ }
642
+ }
643
+
644
+ const matchLimit = this.result.details?.matchLimitReached;
645
+ const truncation = this.result.details?.truncation;
646
+ const linesTruncated = this.result.details?.linesTruncated;
647
+ if (matchLimit || truncation?.truncated || linesTruncated) {
648
+ const warnings: string[] = [];
649
+ if (matchLimit) {
650
+ warnings.push(`${matchLimit} matches limit`);
651
+ }
652
+ if (truncation?.truncated) {
653
+ warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
654
+ }
655
+ if (linesTruncated) {
656
+ warnings.push("some lines truncated");
657
+ }
658
+ text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
659
+ }
660
+ }
661
+ } else {
662
+ // Generic tool (shouldn't reach here for custom tools)
663
+ text = theme.fg("toolTitle", theme.bold(this.toolName));
664
+
665
+ const content = JSON.stringify(this.args, null, 2);
666
+ text += `\n\n${content}`;
667
+ const output = this.getTextOutput();
668
+ if (output) {
669
+ text += `\n${output}`;
670
+ }
671
+ }
672
+
673
+ return text;
674
+ }
675
+ }