@nghyane/arcane 0.1.13 → 0.1.15

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 (303) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +21 -70
  3. package/scripts/format-prompts.ts +1 -3
  4. package/src/cli/args.ts +2 -7
  5. package/src/cli/config-cli.ts +1 -1
  6. package/src/cli/plugin-cli.ts +1 -1
  7. package/src/cli/setup-cli.ts +1 -1
  8. package/src/cli/update-cli.ts +1 -1
  9. package/src/cli/web-search-cli.ts +1 -1
  10. package/src/cli.ts +0 -1
  11. package/src/commands/config.ts +1 -1
  12. package/src/commands/grep.ts +1 -1
  13. package/src/commands/jupyter.ts +1 -1
  14. package/src/commands/plugin.ts +1 -1
  15. package/src/commands/setup.ts +1 -1
  16. package/src/commands/shell.ts +1 -1
  17. package/src/commands/ssh.ts +1 -1
  18. package/src/commands/stats.ts +1 -1
  19. package/src/commands/update.ts +1 -1
  20. package/src/config/model-registry.ts +3 -4
  21. package/src/config/model-resolver.ts +36 -9
  22. package/src/config/prompt-templates.ts +1 -9
  23. package/src/config/settings-schema.ts +32 -88
  24. package/src/config/settings.ts +3 -4
  25. package/src/debug/index.ts +1 -1
  26. package/src/debug/log-formatting.ts +1 -1
  27. package/src/debug/log-viewer.ts +2 -2
  28. package/src/discovery/helpers.ts +13 -3
  29. package/src/exa/index.ts +1 -35
  30. package/src/exa/render.ts +30 -190
  31. package/src/export/html/index.ts +1 -1
  32. package/src/extensibility/custom-tools/loader.ts +1 -1
  33. package/src/extensibility/custom-tools/types.ts +5 -1
  34. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  35. package/src/extensibility/extensions/runner.ts +1 -1
  36. package/src/extensibility/extensions/types.ts +1 -1
  37. package/src/extensibility/extensions/wrapper.ts +7 -15
  38. package/src/extensibility/hooks/runner.ts +1 -1
  39. package/src/extensibility/hooks/types.ts +1 -1
  40. package/src/extensibility/plugins/doctor.ts +1 -1
  41. package/src/index.ts +13 -13
  42. package/src/lsp/index.ts +77 -24
  43. package/src/lsp/render.ts +34 -583
  44. package/src/lsp/types.ts +3 -3
  45. package/src/lsp/utils.ts +1 -1
  46. package/src/main.ts +1 -1
  47. package/src/mcp/tool-bridge.ts +1 -24
  48. package/src/modes/components/assistant-message.ts +7 -7
  49. package/src/modes/components/bash-execution.ts +50 -112
  50. package/src/modes/components/bordered-loader.ts +1 -1
  51. package/src/modes/components/branch-summary-message.ts +16 -10
  52. package/src/modes/components/compaction-summary-message.ts +20 -12
  53. package/src/modes/components/context-group.ts +106 -0
  54. package/src/modes/components/custom-message.ts +4 -5
  55. package/src/modes/components/diff.ts +2 -2
  56. package/src/modes/components/dynamic-border.ts +1 -1
  57. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  58. package/src/modes/components/extensions/extension-list.ts +1 -1
  59. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  60. package/src/modes/components/footer.ts +2 -2
  61. package/src/modes/components/history-search.ts +1 -1
  62. package/src/modes/components/hook-editor.ts +1 -1
  63. package/src/modes/components/hook-input.ts +1 -1
  64. package/src/modes/components/hook-message.ts +4 -5
  65. package/src/modes/components/hook-selector.ts +1 -1
  66. package/src/modes/components/index.ts +0 -2
  67. package/src/modes/components/keybinding-hints.ts +1 -1
  68. package/src/modes/components/login-dialog.ts +1 -1
  69. package/src/modes/components/mcp-add-wizard.ts +1 -1
  70. package/src/modes/components/model-selector.ts +1 -1
  71. package/src/modes/components/oauth-selector.ts +1 -1
  72. package/src/modes/components/plugin-settings.ts +1 -1
  73. package/src/modes/components/python-execution.ts +51 -91
  74. package/src/modes/components/queue-mode-selector.ts +1 -1
  75. package/src/modes/components/session-selector.ts +1 -1
  76. package/src/modes/components/settings-defs.ts +5 -10
  77. package/src/modes/components/settings-selector.ts +1 -1
  78. package/src/modes/components/show-images-selector.ts +1 -1
  79. package/src/modes/components/skill-message.ts +4 -4
  80. package/src/modes/components/status-line/segments.ts +2 -2
  81. package/src/modes/components/status-line/separators.ts +1 -1
  82. package/src/modes/components/status-line-segment-editor.ts +1 -1
  83. package/src/modes/components/status-line.ts +1 -1
  84. package/src/modes/components/theme-selector.ts +1 -1
  85. package/src/modes/components/thinking-selector.ts +1 -1
  86. package/src/modes/components/todo-display.ts +2 -4
  87. package/src/modes/components/todo-reminder.ts +4 -4
  88. package/src/modes/components/tool-execution.ts +118 -440
  89. package/src/modes/components/tool-image-display.ts +107 -0
  90. package/src/modes/components/tree-selector.ts +2 -2
  91. package/src/modes/components/ttsr-notification.ts +4 -17
  92. package/src/modes/components/user-message-selector.ts +1 -1
  93. package/src/modes/components/user-message.ts +9 -10
  94. package/src/modes/components/welcome.ts +1 -1
  95. package/src/modes/controllers/command-controller.ts +1 -1
  96. package/src/modes/controllers/event-controller.ts +58 -187
  97. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  98. package/src/modes/controllers/input-controller.ts +3 -1
  99. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  100. package/src/modes/controllers/selector-controller.ts +3 -26
  101. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  102. package/src/modes/interactive-mode.ts +3 -7
  103. package/src/modes/print-mode.ts +5 -5
  104. package/src/modes/rpc/rpc-mode.ts +1 -1
  105. package/src/modes/types.ts +1 -2
  106. package/src/modes/utils/ui-helpers.ts +34 -32
  107. package/src/patch/edit-tool.ts +742 -0
  108. package/src/patch/index.ts +32 -898
  109. package/src/patch/schemas.ts +208 -0
  110. package/src/patch/shared.ts +83 -151
  111. package/src/prompts/agents/explore.md +22 -37
  112. package/src/prompts/agents/init.md +1 -1
  113. package/src/prompts/agents/librarian.md +29 -20
  114. package/src/prompts/agents/oracle.md +9 -2
  115. package/src/prompts/agents/reviewer.md +14 -48
  116. package/src/prompts/agents/task.md +16 -8
  117. package/src/prompts/compaction/branch-summary.md +4 -1
  118. package/src/prompts/compaction/compaction-summary.md +4 -1
  119. package/src/prompts/system/subagent-system-prompt.md +1 -1
  120. package/src/prompts/system/system-prompt.md +162 -178
  121. package/src/prompts/system/verification-reminder.md +6 -0
  122. package/src/sdk.ts +0 -9
  123. package/src/session/agent-session.ts +244 -1459
  124. package/src/session/model-controller.ts +406 -0
  125. package/src/session/retry-utils.ts +71 -0
  126. package/src/session/session-manager.ts +22 -186
  127. package/src/session/session-types.ts +312 -0
  128. package/src/session/stats.ts +387 -0
  129. package/src/session/streaming-edit.ts +258 -0
  130. package/src/session/ttsr.ts +213 -0
  131. package/src/slash-commands/builtin-registry.ts +0 -8
  132. package/src/stt/recorder.ts +2 -2
  133. package/src/system-prompt.ts +1 -14
  134. package/src/task/agents.ts +7 -33
  135. package/src/task/executor.ts +50 -438
  136. package/src/task/index.ts +104 -71
  137. package/src/task/progress-tracker.ts +390 -0
  138. package/src/task/render.ts +371 -187
  139. package/src/task/subprocess-tool-registry.ts +1 -1
  140. package/src/task/types.ts +14 -47
  141. package/src/tools/ask.ts +31 -42
  142. package/src/tools/bash-interactive.ts +2 -2
  143. package/src/tools/bash-interceptor.ts +2 -2
  144. package/src/tools/bash-normalize.ts +1 -1
  145. package/src/tools/bash-skill-urls.ts +2 -2
  146. package/src/tools/bash.ts +87 -136
  147. package/src/tools/browser.ts +54 -84
  148. package/src/tools/create-tools.ts +186 -0
  149. package/src/tools/default-renderer.ts +104 -0
  150. package/src/tools/explore.ts +11 -10
  151. package/src/tools/fetch.ts +24 -114
  152. package/src/tools/find.ts +48 -132
  153. package/src/tools/gemini-image.ts +5 -15
  154. package/src/tools/github.ts +450 -0
  155. package/src/tools/grep.ts +43 -179
  156. package/src/tools/index.ts +35 -198
  157. package/src/tools/json-tree.ts +3 -3
  158. package/src/tools/librarian.ts +18 -18
  159. package/src/tools/list-limit.ts +2 -2
  160. package/src/tools/notebook.ts +35 -87
  161. package/src/tools/oracle.ts +25 -25
  162. package/src/tools/output-meta.ts +89 -4
  163. package/src/tools/output-utils.ts +2 -2
  164. package/src/tools/python.ts +86 -637
  165. package/src/tools/read.ts +36 -119
  166. package/src/tools/reviewer-tool.ts +19 -21
  167. package/src/tools/search-code.ts +128 -0
  168. package/src/tools/ssh.ts +67 -126
  169. package/src/tools/subagent-tool.ts +197 -123
  170. package/src/tools/todo-write.ts +15 -31
  171. package/src/tools/tool-errors.ts +0 -30
  172. package/src/tools/undo-edit.ts +30 -67
  173. package/src/tools/write.ts +78 -127
  174. package/src/tui/code-cell.ts +4 -4
  175. package/src/tui/file-list.ts +2 -2
  176. package/src/tui/output-block.ts +1 -1
  177. package/src/tui/status-line.ts +1 -1
  178. package/src/tui/tree-list.ts +2 -2
  179. package/src/tui/types.ts +1 -1
  180. package/src/tui/utils.ts +1 -1
  181. package/src/{tools → ui}/render-utils.ts +87 -126
  182. package/src/utils/external-editor.ts +4 -4
  183. package/src/utils/file-mentions.ts +1 -1
  184. package/src/utils/index.ts +30 -0
  185. package/src/utils/tools-manager.ts +9 -19
  186. package/src/web/github-client.ts +290 -0
  187. package/src/web/scrapers/github.ts +11 -62
  188. package/src/web/search/auth.ts +1 -3
  189. package/src/web/search/index.ts +82 -46
  190. package/src/web/search/provider.ts +11 -16
  191. package/src/web/search/providers/grep.ts +160 -0
  192. package/src/web/search/render.ts +48 -235
  193. package/src/web/search/types.ts +1 -1
  194. package/src/commands/commit.ts +0 -36
  195. package/src/commit/agentic/agent.ts +0 -311
  196. package/src/commit/agentic/fallback.ts +0 -96
  197. package/src/commit/agentic/index.ts +0 -359
  198. package/src/commit/agentic/prompts/analyze-file.md +0 -22
  199. package/src/commit/agentic/prompts/session-user.md +0 -25
  200. package/src/commit/agentic/prompts/split-confirm.md +0 -1
  201. package/src/commit/agentic/prompts/system.md +0 -38
  202. package/src/commit/agentic/state.ts +0 -69
  203. package/src/commit/agentic/tools/analyze-file.ts +0 -118
  204. package/src/commit/agentic/tools/git-file-diff.ts +0 -194
  205. package/src/commit/agentic/tools/git-hunk.ts +0 -50
  206. package/src/commit/agentic/tools/git-overview.ts +0 -84
  207. package/src/commit/agentic/tools/index.ts +0 -56
  208. package/src/commit/agentic/tools/propose-changelog.ts +0 -128
  209. package/src/commit/agentic/tools/propose-commit.ts +0 -154
  210. package/src/commit/agentic/tools/recent-commits.ts +0 -81
  211. package/src/commit/agentic/tools/split-commit.ts +0 -280
  212. package/src/commit/agentic/topo-sort.ts +0 -44
  213. package/src/commit/agentic/trivial.ts +0 -51
  214. package/src/commit/agentic/validation.ts +0 -200
  215. package/src/commit/analysis/conventional.ts +0 -165
  216. package/src/commit/analysis/index.ts +0 -4
  217. package/src/commit/analysis/scope.ts +0 -242
  218. package/src/commit/analysis/summary.ts +0 -112
  219. package/src/commit/analysis/validation.ts +0 -66
  220. package/src/commit/changelog/detect.ts +0 -37
  221. package/src/commit/changelog/generate.ts +0 -110
  222. package/src/commit/changelog/index.ts +0 -234
  223. package/src/commit/changelog/parse.ts +0 -44
  224. package/src/commit/cli.ts +0 -93
  225. package/src/commit/git/diff.ts +0 -148
  226. package/src/commit/git/errors.ts +0 -9
  227. package/src/commit/git/index.ts +0 -211
  228. package/src/commit/git/operations.ts +0 -54
  229. package/src/commit/index.ts +0 -5
  230. package/src/commit/map-reduce/index.ts +0 -64
  231. package/src/commit/map-reduce/map-phase.ts +0 -178
  232. package/src/commit/map-reduce/reduce-phase.ts +0 -145
  233. package/src/commit/map-reduce/utils.ts +0 -9
  234. package/src/commit/message.ts +0 -11
  235. package/src/commit/model-selection.ts +0 -69
  236. package/src/commit/pipeline.ts +0 -243
  237. package/src/commit/prompts/analysis-system.md +0 -148
  238. package/src/commit/prompts/analysis-user.md +0 -38
  239. package/src/commit/prompts/changelog-system.md +0 -50
  240. package/src/commit/prompts/changelog-user.md +0 -18
  241. package/src/commit/prompts/file-observer-system.md +0 -24
  242. package/src/commit/prompts/file-observer-user.md +0 -8
  243. package/src/commit/prompts/reduce-system.md +0 -50
  244. package/src/commit/prompts/reduce-user.md +0 -17
  245. package/src/commit/prompts/summary-retry.md +0 -3
  246. package/src/commit/prompts/summary-system.md +0 -38
  247. package/src/commit/prompts/summary-user.md +0 -13
  248. package/src/commit/prompts/types-description.md +0 -2
  249. package/src/commit/types.ts +0 -109
  250. package/src/commit/utils/exclusions.ts +0 -42
  251. package/src/mcp/render.ts +0 -123
  252. package/src/modes/components/agent-dashboard.ts +0 -1130
  253. package/src/modes/components/codemode-group.ts +0 -369
  254. package/src/modes/components/read-tool-group.ts +0 -119
  255. package/src/modes/components/visual-truncate.ts +0 -63
  256. package/src/prompts/system/subagent-user-prompt.md +0 -8
  257. package/src/prompts/tools/ask.md +0 -44
  258. package/src/prompts/tools/bash.md +0 -24
  259. package/src/prompts/tools/browser.md +0 -33
  260. package/src/prompts/tools/calculator.md +0 -12
  261. package/src/prompts/tools/explore.md +0 -29
  262. package/src/prompts/tools/fetch.md +0 -16
  263. package/src/prompts/tools/find.md +0 -18
  264. package/src/prompts/tools/gemini-image.md +0 -23
  265. package/src/prompts/tools/grep.md +0 -28
  266. package/src/prompts/tools/hashline.md +0 -232
  267. package/src/prompts/tools/librarian.md +0 -24
  268. package/src/prompts/tools/lsp.md +0 -28
  269. package/src/prompts/tools/oracle.md +0 -26
  270. package/src/prompts/tools/patch.md +0 -74
  271. package/src/prompts/tools/python.md +0 -66
  272. package/src/prompts/tools/read.md +0 -36
  273. package/src/prompts/tools/replace.md +0 -38
  274. package/src/prompts/tools/reviewer.md +0 -41
  275. package/src/prompts/tools/ssh.md +0 -51
  276. package/src/prompts/tools/task-summary.md +0 -28
  277. package/src/prompts/tools/task.md +0 -146
  278. package/src/prompts/tools/todo-write.md +0 -65
  279. package/src/prompts/tools/undo-edit.md +0 -7
  280. package/src/prompts/tools/web-search.md +0 -19
  281. package/src/prompts/tools/write.md +0 -18
  282. package/src/task/batch.ts +0 -102
  283. package/src/task/discovery.ts +0 -126
  284. package/src/task/parallel.ts +0 -84
  285. package/src/task/template.ts +0 -32
  286. package/src/tools/calculator.ts +0 -537
  287. package/src/tools/jtd-to-typescript.ts +0 -198
  288. package/src/tools/renderers.ts +0 -60
  289. package/src/tools/tool-result.ts +0 -86
  290. /package/src/{modes/theme → theme}/dark.json +0 -0
  291. /package/src/{modes/theme → theme}/defaults/dark-catppuccin.json +0 -0
  292. /package/src/{modes/theme → theme}/defaults/dark-dracula.json +0 -0
  293. /package/src/{modes/theme → theme}/defaults/dark-gruvbox.json +0 -0
  294. /package/src/{modes/theme → theme}/defaults/dark-solarized.json +0 -0
  295. /package/src/{modes/theme → theme}/defaults/dark-tokyo-night.json +0 -0
  296. /package/src/{modes/theme → theme}/defaults/index.ts +0 -0
  297. /package/src/{modes/theme → theme}/defaults/light-catppuccin.json +0 -0
  298. /package/src/{modes/theme → theme}/defaults/light-github.json +0 -0
  299. /package/src/{modes/theme → theme}/defaults/light-solarized.json +0 -0
  300. /package/src/{modes/theme → theme}/light.json +0 -0
  301. /package/src/{modes/theme → theme}/mermaid-cache.ts +0 -0
  302. /package/src/{modes/theme → theme}/theme-schema.json +0 -0
  303. /package/src/{modes/theme → theme}/theme.ts +0 -0
@@ -1,46 +1,12 @@
1
1
  import type { AgentTool } from "@nghyane/arcane-agent";
2
2
  import { sanitizeText } from "@nghyane/arcane-natives";
3
- import {
4
- Box,
5
- type Component,
6
- Container,
7
- getImageDimensions,
8
- Image,
9
- ImageProtocol,
10
- imageFallback,
11
- Spacer,
12
- TERMINAL,
13
- Text,
14
- type TUI,
15
- } from "@nghyane/arcane-tui";
3
+ import { Box, type Component, Container, Spacer, TERMINAL, Text, type TUI } from "@nghyane/arcane-tui";
16
4
  import { logger } from "@nghyane/arcane-utils";
17
5
  import { getProjectDir } from "@nghyane/arcane-utils/dirs";
18
- import type { Theme } from "../../modes/theme/theme";
19
- import { theme } from "../../modes/theme/theme";
20
- import {
21
- computeEditDiff,
22
- computeHashlineDiff,
23
- computePatchDiff,
24
- type EditDiffError,
25
- type EditDiffResult,
26
- } from "../../patch";
27
- import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
28
- import {
29
- formatArgsInline,
30
- JSON_TREE_MAX_DEPTH_COLLAPSED,
31
- JSON_TREE_MAX_DEPTH_EXPANDED,
32
- JSON_TREE_MAX_LINES_COLLAPSED,
33
- JSON_TREE_MAX_LINES_EXPANDED,
34
- JSON_TREE_SCALAR_LEN_COLLAPSED,
35
- JSON_TREE_SCALAR_LEN_EXPANDED,
36
- renderJsonTreeLines,
37
- } from "../../tools/json-tree";
38
- import { PYTHON_DEFAULT_PREVIEW_LINES } from "../../tools/python";
39
- import { formatExpandHint, truncateToWidth } from "../../tools/render-utils";
40
- import { toolRenderers } from "../../tools/renderers";
41
- import { renderStatusLine } from "../../tui";
42
- import { convertToPng } from "../../utils/image-convert";
43
- import { renderDiff } from "./diff";
6
+ import { theme } from "../../theme/theme";
7
+ import { defaultRenderer } from "../../tools/default-renderer";
8
+ import type { ToolTier } from "../../ui/render-utils";
9
+ import { ToolImageDisplay } from "./tool-image-display";
44
10
 
45
11
  function ensureInvalidate(component: unknown): Component {
46
12
  const c = component as { render: Component["render"]; invalidate?: () => void };
@@ -61,8 +27,7 @@ function cloneToolArgs<T>(args: T): T {
61
27
 
62
28
  export interface ToolExecutionOptions {
63
29
  showImages?: boolean; // default: true (only used if terminal supports images)
64
- editFuzzyThreshold?: number;
65
- editAllowFuzzy?: boolean;
30
+ tier?: ToolTier;
66
31
  }
67
32
 
68
33
  export interface ToolExecutionHandle {
@@ -84,20 +49,17 @@ export interface ToolExecutionHandle {
84
49
  * Component that renders a tool call with its result (updateable)
85
50
  */
86
51
  export class ToolExecutionComponent extends Container {
87
- #contentBox: Box; // Used for custom tools and bash visual truncation
88
- #contentText: Text; // For built-in tools (with its own padding/bg)
89
- #imageComponents: Image[] = [];
90
- #imageSpacers: Spacer[] = [];
52
+ #contentBox: Box;
53
+ #topSpacer?: Spacer;
54
+ #tier: ToolTier;
55
+ #imageDisplay: ToolImageDisplay;
91
56
  #toolName: string;
92
57
  #toolLabel: string;
58
+ #tool: AgentTool | undefined;
93
59
  #args: any;
94
60
  #expanded = false;
95
61
  #showImages: boolean;
96
- #editFuzzyThreshold: number | undefined;
97
- #editAllowFuzzy: boolean | undefined;
98
62
  #isPartial = true;
99
- #compact: boolean;
100
- #tool?: AgentTool;
101
63
  #ui: TUI;
102
64
  #cwd: string;
103
65
  #result?: {
@@ -105,11 +67,9 @@ export class ToolExecutionComponent extends Container {
105
67
  isError?: boolean;
106
68
  details?: any;
107
69
  };
108
- // Cached edit diff preview (computed when args arrive, before tool executes)
109
- #editDiffPreview?: EditDiffResult | EditDiffError;
110
- #editDiffArgsKey?: string; // Track which args the preview is for
111
- // Cached converted images for Kitty protocol (which requires PNG), keyed by index
112
- #convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
70
+ // Tool-specific state from onArgsComplete (e.g. edit diff preview)
71
+ #toolState?: unknown;
72
+ #toolStateKey?: string;
113
73
  // Spinner animation for partial task results
114
74
  #spinnerFrame = 0;
115
75
  #spinnerInterval?: NodeJS.Timeout;
@@ -119,12 +79,23 @@ export class ToolExecutionComponent extends Container {
119
79
  spinnerFrame: number;
120
80
  expanded: boolean;
121
81
  isPartial: boolean;
82
+ label?: string;
122
83
  renderContext?: Record<string, unknown>;
123
84
  } = {
124
85
  spinnerFrame: 0,
125
86
  expanded: false,
126
87
  isPartial: true,
127
88
  };
89
+ // Cached components to avoid clear+rebuild flicker
90
+ #structureKey = "";
91
+ #cachedCallComponent?: Component;
92
+ #cachedResultComponent?: Component;
93
+ // Mutable result ref for subagent closures to read fresh data
94
+ #resultRef: {
95
+ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
96
+ details?: any;
97
+ isError?: boolean;
98
+ } = { content: [] };
128
99
 
129
100
  constructor(
130
101
  toolName: string,
@@ -133,45 +104,46 @@ export class ToolExecutionComponent extends Container {
133
104
  tool: AgentTool | undefined,
134
105
  ui: TUI,
135
106
  cwd: string = getProjectDir(),
136
- { compact = false }: { compact?: boolean } = {},
137
107
  ) {
138
108
  super();
139
109
  this.#toolName = toolName;
140
110
  this.#toolLabel = tool?.label ?? toolName;
111
+ this.#tool = tool;
141
112
  this.#args = cloneToolArgs(args);
142
113
  this.#showImages = options.showImages ?? true;
143
- this.#editFuzzyThreshold = options.editFuzzyThreshold;
144
- this.#editAllowFuzzy = options.editAllowFuzzy;
145
- this.#compact = compact;
146
- this.#tool = tool;
147
114
  this.#ui = ui;
148
115
  this.#cwd = cwd;
116
+ this.#imageDisplay = new ToolImageDisplay(this, () => {
117
+ this.#updateDisplay();
118
+ this.#ui.requestRender();
119
+ });
149
120
 
150
- if (!compact) {
151
- this.addChild(new Spacer(1));
152
- }
153
-
154
- // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins
155
- const px = compact ? 0 : 1;
156
- const py = compact ? 0 : 1;
157
- const initialBg = compact ? undefined : (text: string) => theme.bg("toolPendingBg", text);
158
- this.#contentBox = new Box(px, py, initialBg);
159
- this.#contentText = new Text("", px, py, initialBg);
160
-
161
- // Use Box for custom tools or built-in tools that have renderers
162
- const hasRenderer = toolName in toolRenderers;
163
- const hasCustomRenderer = !!(tool?.renderCall || tool?.renderResult);
164
- if (hasCustomRenderer || hasRenderer) {
121
+ this.#tier = options.tier ?? "default";
122
+ const tier = this.#tier;
123
+ if (tier === "quiet") {
124
+ this.#contentBox = new Box(0, 0);
165
125
  this.addChild(this.#contentBox);
166
126
  } else {
167
- this.addChild(this.#contentText);
127
+ this.#topSpacer = new Spacer(1);
128
+ this.addChild(this.#topSpacer);
129
+ this.#contentBox = new Box(2, 0);
130
+ this.addChild(this.#contentBox);
168
131
  }
169
132
 
170
133
  this.#updateDisplay();
171
134
  }
172
135
 
136
+ setMarginTop(lines: number): void {
137
+ if (this.#topSpacer) {
138
+ this.#topSpacer.setLines(lines);
139
+ }
140
+ }
141
+
173
142
  updateArgs(args: any, _toolCallId?: string): void {
174
143
  this.#args = cloneToolArgs(args);
144
+ // Force call component rebuild — renderCall returns static content
145
+ // that won't update on invalidate alone
146
+ this.#structureKey = "";
175
147
  this.#updateSpinnerAnimation();
176
148
  this.#updateDisplay();
177
149
  }
@@ -183,76 +155,22 @@ export class ToolExecutionComponent extends Container {
183
155
  setArgsComplete(_toolCallId?: string): void {
184
156
  this.#argsComplete = true;
185
157
  this.#updateSpinnerAnimation();
186
- this.#maybeComputeEditDiff();
158
+ this.#callOnArgsComplete();
187
159
  }
188
160
 
189
161
  /**
190
- * Compute edit diff preview when we have complete args.
191
- * This runs async and updates display when done.
162
+ * Delegate to tool.onArgsComplete when args are fully streamed.
163
+ * Stores result in #toolState for use by buildRenderContext.
192
164
  */
193
- #maybeComputeEditDiff(): void {
194
- if (this.#toolName !== "edit") return;
195
-
196
- const path = this.#args?.path;
197
- const op = this.#args?.op;
198
-
199
- if (op) {
200
- const diff = this.#args?.diff;
201
- const rename = this.#args?.rename;
202
- if (!path) return;
203
-
204
- const argsKey = JSON.stringify({ path, op, rename, diff });
205
- if (this.#editDiffArgsKey === argsKey) return;
206
- this.#editDiffArgsKey = argsKey;
207
-
208
- computePatchDiff({ path, op, rename, diff }, this.#cwd, {
209
- fuzzyThreshold: this.#editFuzzyThreshold,
210
- allowFuzzy: this.#editAllowFuzzy,
211
- }).then(result => {
212
- if (this.#editDiffArgsKey === argsKey) {
213
- this.#editDiffPreview = result;
214
- this.#updateDisplay();
215
- this.#ui.requestRender();
216
- }
217
- });
218
- return;
219
- }
220
- const edits = this.#args?.edits;
221
- if (path && Array.isArray(edits)) {
222
- const argsKey = JSON.stringify({ path, edits });
223
- if (this.#editDiffArgsKey === argsKey) return;
224
- this.#editDiffArgsKey = argsKey;
225
-
226
- computeHashlineDiff({ path, edits }, this.#cwd).then(result => {
227
- if (this.#editDiffArgsKey === argsKey) {
228
- this.#editDiffPreview = result;
229
- this.#updateDisplay();
230
- this.#ui.requestRender();
231
- }
232
- });
233
- return;
234
- }
235
-
236
- const oldText = this.#args?.old_text;
237
- const newText = this.#args?.new_text;
238
- const all = this.#args?.all;
239
-
240
- // Need all three params to compute diff
241
- if (!path || oldText === undefined || newText === undefined) return;
242
-
243
- // Create a key to track which args this computation is for
244
- const argsKey = JSON.stringify({ path, oldText, newText, all });
245
-
246
- // Skip if we already computed for these exact args
247
- if (this.#editDiffArgsKey === argsKey) return;
248
-
249
- this.#editDiffArgsKey = argsKey;
250
-
251
- // Compute diff async
252
- computeEditDiff(path, oldText, newText, this.#cwd, true, all, this.#editFuzzyThreshold).then(result => {
253
- // Only update if args haven't changed since we started
254
- if (this.#editDiffArgsKey === argsKey) {
255
- this.#editDiffPreview = result;
165
+ #callOnArgsComplete(): void {
166
+ if (!this.#tool?.onArgsComplete) return;
167
+ const argsKey = JSON.stringify(this.#args);
168
+ if (this.#toolStateKey === argsKey) return;
169
+ this.#toolStateKey = argsKey;
170
+
171
+ this.#tool.onArgsComplete(this.#args, this.#cwd).then(state => {
172
+ if (this.#toolStateKey === argsKey) {
173
+ this.#toolState = state;
256
174
  this.#updateDisplay();
257
175
  this.#ui.requestRender();
258
176
  }
@@ -276,8 +194,7 @@ export class ToolExecutionComponent extends Container {
276
194
  }
277
195
  this.#updateSpinnerAnimation();
278
196
  this.#updateDisplay();
279
- // Convert non-PNG images to PNG for Kitty protocol (async)
280
- this.#maybeConvertImagesForKitty();
197
+ this.#imageDisplay.convertForKitty(this.#getAllImageBlocks());
281
198
  }
282
199
 
283
200
  /**
@@ -291,47 +208,13 @@ export class ToolExecutionComponent extends Container {
291
208
  return [...contentImages, ...detailImages];
292
209
  }
293
210
 
294
- /**
295
- * Convert non-PNG images to PNG for Kitty graphics protocol.
296
- * Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
297
- */
298
- #maybeConvertImagesForKitty(): void {
299
- // Only needed for Kitty protocol
300
- if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
301
- if (!this.#result) return;
302
-
303
- const imageBlocks = this.#getAllImageBlocks();
304
-
305
- for (let i = 0; i < imageBlocks.length; i++) {
306
- const img = imageBlocks[i];
307
- if (!img.data || !img.mimeType) continue;
308
- // Skip if already PNG or already converted
309
- if (img.mimeType === "image/png") continue;
310
- if (this.#convertedImages.has(i)) continue;
311
-
312
- // Convert async - catch errors from processing
313
- const index = i;
314
- convertToPng(img.data, img.mimeType)
315
- .then(converted => {
316
- if (converted) {
317
- this.#convertedImages.set(index, converted);
318
- this.#updateDisplay();
319
- this.#ui.requestRender();
320
- }
321
- })
322
- .catch(() => {
323
- // Ignore conversion failures - display will use original image format
324
- });
325
- }
326
- }
327
-
328
211
  /**
329
212
  * Start or stop spinner animation based on whether this is a partial task result.
330
213
  */
331
214
  #updateSpinnerAnimation(): void {
332
215
  // Spinner for: task tool with partial result, or edit/write while args streaming
333
216
  const isStreamingArgs = !this.#argsComplete && (this.#toolName === "edit" || this.#toolName === "write");
334
- const isPartialTask = this.#isPartial && this.#toolName === "task";
217
+ const isPartialTask = this.#isPartial && this.#tool?.mergeCallAndResult === true;
335
218
  const needsSpinner = isStreamingArgs || isPartialTask;
336
219
  if (needsSpinner && !this.#spinnerInterval) {
337
220
  this.#spinnerInterval = setInterval(() => {
@@ -359,6 +242,7 @@ export class ToolExecutionComponent extends Container {
359
242
  }
360
243
 
361
244
  setExpanded(expanded: boolean): void {
245
+ if (this.#expanded === expanded) return;
362
246
  this.#expanded = expanded;
363
247
  this.#updateDisplay();
364
248
  }
@@ -373,130 +257,67 @@ export class ToolExecutionComponent extends Container {
373
257
  this.#updateDisplay();
374
258
  }
375
259
 
376
- #getBgFn(): ((text: string) => string) | undefined {
377
- if (this.#compact) return undefined;
378
- if (this.#isPartial) return (text: string) => theme.bg("toolPendingBg", text);
379
- if (this.#result?.isError) return (text: string) => theme.bg("toolErrorBg", text);
380
- return (text: string) => theme.bg("toolSuccessBg", text);
381
- }
382
-
383
260
  #updateDisplay(): void {
384
- const bgFn = this.#getBgFn();
385
-
386
261
  // Sync shared mutable render state for component closures
387
262
  this.#renderState.expanded = this.#expanded;
388
263
  this.#renderState.isPartial = this.#isPartial;
389
264
  this.#renderState.spinnerFrame = this.#spinnerFrame;
265
+ this.#renderState.label = this.#toolLabel;
390
266
 
391
- // Check for custom tool rendering
392
- if (this.#tool && (this.#tool.renderCall || this.#tool.renderResult)) {
393
- const tool = this.#tool;
394
- const mergeCallAndResult = Boolean((tool as { mergeCallAndResult?: boolean }).mergeCallAndResult);
395
- // Custom tools use Box for flexible component rendering
396
- const inline = Boolean((tool as { inline?: boolean }).inline);
397
- this.#contentBox.setBgFn(inline ? undefined : bgFn);
398
- this.#contentBox.clear();
267
+ const mergeCallAndResult = this.#tool?.mergeCallAndResult ?? true;
399
268
 
400
- // Render call component
401
- const shouldRenderCall = !this.#result || !mergeCallAndResult;
402
- if (shouldRenderCall && tool.renderCall) {
403
- try {
404
- const callComponent = tool.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
405
- if (callComponent) {
406
- this.#contentBox.addChild(ensureInvalidate(callComponent));
407
- }
408
- } catch (err) {
409
- logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
410
- // Fall back to default on error
411
- this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
412
- }
413
- } else {
414
- // No custom renderCall, show tool name
415
- this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
416
- }
269
+ // Determine which components are needed
270
+ const needsCall = !this.#result || !mergeCallAndResult;
271
+ const needsResult = !!this.#result;
272
+ const structureKey = `${needsCall}|${needsResult}|${!!this.#toolState}`;
273
+
274
+ // Update mutable result ref so existing closures read fresh data
275
+ if (this.#result) {
276
+ this.#resultRef.content = this.#result.content as any;
277
+ this.#resultRef.details = this.#result.details;
278
+ this.#resultRef.isError = this.#result.isError;
279
+ }
280
+
281
+ if (structureKey !== this.#structureKey) {
282
+ // Structure changed — rebuild components
283
+ this.#structureKey = structureKey;
284
+ this.#contentBox.clear();
285
+ this.#cachedCallComponent = undefined;
286
+ this.#cachedResultComponent = undefined;
417
287
 
418
- // Render result component if we have a result
419
- if (this.#result && tool.renderResult) {
288
+ if (needsCall) {
420
289
  try {
421
- const renderResult = tool.renderResult as (
422
- result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
423
- options: { expanded: boolean; isPartial: boolean; spinnerFrame?: number },
424
- theme: Theme,
425
- args?: unknown,
426
- ) => Component;
427
- const resultComponent = renderResult(
428
- {
429
- content: this.#result.content as any,
430
- details: this.#result.details,
431
- isError: this.#result.isError,
432
- },
290
+ const comp = (this.#tool?.renderCall ?? defaultRenderer.renderCall)(
291
+ this.#getCallArgsForRender(),
433
292
  this.#renderState,
434
293
  theme,
435
- this.#args,
436
294
  );
437
- if (resultComponent) {
438
- this.#contentBox.addChild(ensureInvalidate(resultComponent));
295
+ if (comp) {
296
+ this.#cachedCallComponent = ensureInvalidate(comp);
297
+ this.#contentBox.addChild(this.#cachedCallComponent);
439
298
  }
440
299
  } catch (err) {
441
300
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
442
- // Fall back to showing raw output on error
443
- const output = this.#getTextOutput();
444
- if (output) {
445
- this.#contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
446
- }
447
- }
448
- } else if (this.#result) {
449
- // Has result but no custom renderResult
450
- const output = this.#getTextOutput();
451
- if (output) {
452
- this.#contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
301
+ this.#cachedCallComponent = new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0);
302
+ this.#contentBox.addChild(this.#cachedCallComponent);
453
303
  }
454
304
  }
455
- } else if (this.#toolName in toolRenderers) {
456
- // Built-in tools with renderers
457
- const renderer = toolRenderers[this.#toolName];
458
- // Inline renderers skip background styling
459
- this.#contentBox.setBgFn(renderer.inline ? undefined : bgFn);
460
- this.#contentBox.clear();
461
305
 
462
- const shouldRenderCall = !this.#result || !renderer.mergeCallAndResult;
463
- if (shouldRenderCall) {
464
- // Render call component
306
+ if (needsResult) {
465
307
  try {
466
- const callComponent = renderer.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
467
- if (callComponent) {
468
- this.#contentBox.addChild(ensureInvalidate(callComponent));
469
- }
470
- } catch (err) {
471
- logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
472
- // Fall back to default on error
473
- this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
474
- }
475
- }
476
-
477
- // Render result component if we have a result
478
- if (this.#result) {
479
- try {
480
- // Build render context for tools that need extra state
481
- const renderContext = this.#buildRenderContext();
482
- this.#renderState.renderContext = renderContext;
483
-
484
- const resultComponent = renderer.renderResult(
485
- {
486
- content: this.#result.content as any,
487
- details: this.#result.details,
488
- isError: this.#result.isError,
489
- },
308
+ this.#renderState.renderContext = this.#buildRenderContext();
309
+ const comp = (this.#tool?.renderResult ?? defaultRenderer.renderResult)(
310
+ this.#resultRef as any,
490
311
  this.#renderState,
491
312
  theme,
492
- this.#args, // Pass args for tools that need them
313
+ this.#args,
493
314
  );
494
- if (resultComponent) {
495
- this.#contentBox.addChild(ensureInvalidate(resultComponent));
315
+ if (comp) {
316
+ this.#cachedResultComponent = ensureInvalidate(comp);
317
+ this.#contentBox.addChild(this.#cachedResultComponent);
496
318
  }
497
319
  } catch (err) {
498
320
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
499
- // Fall back to showing raw output on error
500
321
  const output = this.#getTextOutput();
501
322
  if (output) {
502
323
  this.#contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
@@ -504,93 +325,38 @@ export class ToolExecutionComponent extends Container {
504
325
  }
505
326
  }
506
327
  } else {
507
- // Other built-in tools: use Text directly with caching
508
- this.#contentText.setCustomBgFn(bgFn);
509
- this.#contentText.setText(this.#formatToolExecution());
328
+ // Structure unchanged invalidate existing components so they re-render
329
+ this.#renderState.renderContext = this.#buildRenderContext();
330
+ this.#cachedCallComponent?.invalidate();
331
+ this.#cachedResultComponent?.invalidate();
510
332
  }
511
333
 
512
- // Handle images (same for both custom and built-in)
513
- for (const img of this.#imageComponents) {
514
- this.removeChild(img);
515
- }
516
- this.#imageComponents = [];
517
- for (const spacer of this.#imageSpacers) {
518
- this.removeChild(spacer);
519
- }
520
- this.#imageSpacers = [];
521
-
334
+ // Handle images
522
335
  if (this.#result) {
523
- const imageBlocks = this.#getAllImageBlocks();
524
-
525
- for (let i = 0; i < imageBlocks.length; i++) {
526
- const img = imageBlocks[i];
527
- if (TERMINAL.imageProtocol && this.#showImages && img.data && img.mimeType) {
528
- // Use converted PNG for Kitty protocol if available
529
- const converted = this.#convertedImages.get(i);
530
- const imageData = converted?.data ?? img.data;
531
- const imageMimeType = converted?.mimeType ?? img.mimeType;
532
-
533
- // For Kitty, skip non-PNG images that haven't been converted yet
534
- if (TERMINAL.imageProtocol === ImageProtocol.Kitty && imageMimeType !== "image/png") {
535
- continue;
536
- }
537
-
538
- const spacer = new Spacer(1);
539
- this.addChild(spacer);
540
- this.#imageSpacers.push(spacer);
541
- const imageComponent = new Image(
542
- imageData,
543
- imageMimeType,
544
- { fallbackColor: (s: string) => theme.fg("toolOutput", s) },
545
- { maxWidthCells: 60 },
546
- );
547
- this.#imageComponents.push(imageComponent);
548
- this.addChild(imageComponent);
549
- }
550
- }
336
+ this.#imageDisplay.update(this.#getAllImageBlocks(), this.#showImages, (s: string) =>
337
+ theme.fg("toolOutput", s),
338
+ );
551
339
  }
552
340
  }
553
341
 
554
342
  #getCallArgsForRender(): any {
555
- if (this.#toolName !== "edit") {
556
- return this.#args;
557
- }
558
- if (!this.#editDiffPreview || !("diff" in this.#editDiffPreview) || !this.#editDiffPreview.diff) {
559
- return this.#args;
560
- }
561
- return { ...(this.#args as Record<string, unknown>), previewDiff: this.#editDiffPreview.diff };
343
+ return this.#args;
562
344
  }
563
345
 
564
346
  /**
565
- * Build render context for tools that need extra state (bash, python, edit)
347
+ * Build render context. Delegates to tool.buildRenderContext if defined.
566
348
  */
567
349
  #buildRenderContext(): Record<string, unknown> {
568
- const context: Record<string, unknown> = {};
569
- const normalizeTimeoutSeconds = (value: unknown, maxSeconds: number): number | undefined => {
570
- if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
571
- return Math.max(1, Math.min(maxSeconds, value));
572
- };
573
-
574
- if (this.#toolName === "bash" && this.#result) {
575
- // Pass raw output and expanded state - renderer handles width-aware truncation
576
- const output = this.#getTextOutput().trimEnd();
577
- context.output = output;
578
- context.expanded = this.#expanded;
579
- context.previewLines = BASH_DEFAULT_PREVIEW_LINES;
580
- context.timeout = normalizeTimeoutSeconds(this.#args?.timeout, 3600);
581
- } else if (this.#toolName === "python" && this.#result) {
582
- const output = this.#getTextOutput().trimEnd();
583
- context.output = output;
584
- context.expanded = this.#expanded;
585
- context.previewLines = PYTHON_DEFAULT_PREVIEW_LINES;
586
- context.timeout = normalizeTimeoutSeconds(this.#args?.timeout, 600);
587
- } else if (this.#toolName === "edit") {
588
- // Edit needs diff preview and renderDiff function
589
- context.editDiffPreview = this.#editDiffPreview;
590
- context.renderDiff = renderDiff;
350
+ if (this.#tool?.buildRenderContext) {
351
+ return this.#tool.buildRenderContext({
352
+ args: this.#args,
353
+ result: this.#result as any,
354
+ toolState: this.#toolState,
355
+ expanded: this.#expanded,
356
+ getTextOutput: () => this.#getTextOutput(),
357
+ });
591
358
  }
592
-
593
- return context;
359
+ return {};
594
360
  }
595
361
 
596
362
  #getTextOutput(): string {
@@ -606,98 +372,10 @@ export class ToolExecutionComponent extends Container {
606
372
  .join("\n");
607
373
 
608
374
  if (imageBlocks.length > 0 && (!TERMINAL.imageProtocol || !this.#showImages)) {
609
- const imageIndicators = imageBlocks
610
- .map((img: any) => {
611
- const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
612
- return imageFallback(img.mimeType, dims);
613
- })
614
- .join("\n");
375
+ const imageIndicators = ToolImageDisplay.fallbackText(imageBlocks);
615
376
  output = output ? `${output}\n${imageIndicators}` : imageIndicators;
616
377
  }
617
378
 
618
379
  return output;
619
380
  }
620
-
621
- /**
622
- * Format a generic tool execution (fallback for tools without custom renderers)
623
- */
624
- #formatToolExecution(): string {
625
- const lines: string[] = [];
626
- const icon = this.#isPartial ? "pending" : this.#result?.isError ? "error" : "success";
627
- lines.push(renderStatusLine({ icon, title: this.#toolLabel }, theme));
628
-
629
- const argsObject = this.#args && typeof this.#args === "object" ? (this.#args as Record<string, unknown>) : null;
630
- if (!this.#expanded && argsObject && Object.keys(argsObject).length > 0) {
631
- const preview = formatArgsInline(argsObject, 70);
632
- if (preview) {
633
- lines.push(` ${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", preview)}`);
634
- }
635
- }
636
-
637
- if (this.#expanded && this.#args !== undefined) {
638
- lines.push("");
639
- lines.push(theme.fg("dim", "Args"));
640
- const tree = renderJsonTreeLines(
641
- this.#args,
642
- theme,
643
- JSON_TREE_MAX_DEPTH_EXPANDED,
644
- JSON_TREE_MAX_LINES_EXPANDED,
645
- JSON_TREE_SCALAR_LEN_EXPANDED,
646
- );
647
- lines.push(...tree.lines);
648
- if (tree.truncated) {
649
- lines.push(theme.fg("dim", "…"));
650
- }
651
- lines.push("");
652
- }
653
-
654
- if (!this.#result) {
655
- return lines.join("\n");
656
- }
657
-
658
- const textContent = this.#getTextOutput().trimEnd();
659
- if (!textContent) {
660
- lines.push(theme.fg("dim", "(no output)"));
661
- return lines.join("\n");
662
- }
663
-
664
- if (textContent.startsWith("{") || textContent.startsWith("[")) {
665
- try {
666
- const parsed = JSON.parse(textContent);
667
- const maxDepth = this.#expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
668
- const maxLines = this.#expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
669
- const maxScalarLen = this.#expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
670
- const tree = renderJsonTreeLines(parsed, theme, maxDepth, maxLines, maxScalarLen);
671
-
672
- if (tree.lines.length > 0) {
673
- lines.push(...tree.lines);
674
- if (!this.#expanded) {
675
- lines.push(formatExpandHint(theme, this.#expanded, true));
676
- } else if (tree.truncated) {
677
- lines.push(theme.fg("dim", "…"));
678
- }
679
- return lines.join("\n");
680
- }
681
- } catch {
682
- // Fall through to raw output
683
- }
684
- }
685
-
686
- const outputLines = textContent.split("\n");
687
- const maxOutputLines = this.#expanded ? 12 : 4;
688
- const displayLines = outputLines.slice(0, maxOutputLines);
689
-
690
- for (const line of displayLines) {
691
- lines.push(theme.fg("toolOutput", truncateToWidth(line, 80)));
692
- }
693
-
694
- if (outputLines.length > maxOutputLines) {
695
- const remaining = outputLines.length - maxOutputLines;
696
- lines.push(`${theme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(theme, this.#expanded, true)}`);
697
- } else if (!this.#expanded) {
698
- lines.push(formatExpandHint(theme, this.#expanded, true));
699
- }
700
-
701
- return lines.join("\n");
702
- }
703
381
  }