@renxqoo/renx-code 0.0.3 → 0.0.4

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 (306) hide show
  1. package/README.md +58 -223
  2. package/bin/renx.cjs +34 -0
  3. package/package.json +27 -83
  4. package/src/App.tsx +297 -0
  5. package/src/agent/runtime/event-format.ts +258 -0
  6. package/src/agent/runtime/model-types.ts +13 -0
  7. package/src/agent/runtime/runtime.context-usage.test.ts +193 -0
  8. package/src/agent/runtime/runtime.error-handling.test.ts +236 -0
  9. package/src/agent/runtime/runtime.simple.test.ts +16 -0
  10. package/src/agent/runtime/runtime.test.ts +293 -0
  11. package/src/agent/runtime/runtime.ts +881 -0
  12. package/src/agent/runtime/runtime.usage-forwarding.test.ts +229 -0
  13. package/src/agent/runtime/source-modules.test.ts +57 -0
  14. package/src/agent/runtime/source-modules.ts +353 -0
  15. package/src/agent/runtime/tool-call-buffer.test.ts +65 -0
  16. package/src/agent/runtime/tool-call-buffer.ts +60 -0
  17. package/src/agent/runtime/tool-confirmation.test.ts +56 -0
  18. package/src/agent/runtime/tool-confirmation.ts +15 -0
  19. package/src/agent/runtime/types.ts +99 -0
  20. package/src/commands/slash-commands.test.ts +216 -0
  21. package/src/commands/slash-commands.ts +64 -0
  22. package/src/components/chat/assistant-reply.test.tsx +47 -0
  23. package/src/components/chat/assistant-reply.tsx +136 -0
  24. package/src/components/chat/assistant-segment.test.ts +99 -0
  25. package/src/components/chat/assistant-segment.tsx +125 -0
  26. package/src/components/chat/assistant-tool-group.tsx +900 -0
  27. package/src/components/chat/code-block.test.tsx +206 -0
  28. package/src/components/chat/code-block.tsx +313 -0
  29. package/src/components/chat/prompt-card.tsx +81 -0
  30. package/src/components/chat/segment-groups.test.ts +52 -0
  31. package/src/components/chat/segment-groups.ts +106 -0
  32. package/src/components/chat/turn-item.tsx +39 -0
  33. package/src/components/conversation-panel.tsx +43 -0
  34. package/src/components/file-mention-menu.tsx +77 -0
  35. package/src/components/file-picker-dialog.tsx +206 -0
  36. package/src/components/footer-hints.tsx +75 -0
  37. package/src/components/model-picker-dialog.tsx +248 -0
  38. package/src/components/prompt.tsx +233 -0
  39. package/src/components/slash-command-menu.tsx +65 -0
  40. package/src/components/tool-confirm-dialog-content.test.ts +103 -0
  41. package/src/components/tool-confirm-dialog-content.ts +186 -0
  42. package/src/components/tool-confirm-dialog.tsx +187 -0
  43. package/src/components/tool-display-config.ts +119 -0
  44. package/src/context-usage-regressions.test.ts +26 -0
  45. package/src/files/attachment-capabilities.test.ts +30 -0
  46. package/src/files/attachment-capabilities.ts +50 -0
  47. package/src/files/attachment-content.ts +153 -0
  48. package/src/files/file-mention-query.test.ts +34 -0
  49. package/src/files/file-mention-query.ts +32 -0
  50. package/src/files/prompt-display.ts +13 -0
  51. package/src/files/types.ts +5 -0
  52. package/src/files/workspace-files.ts +63 -0
  53. package/src/hooks/agent-event-handlers.test.ts +207 -0
  54. package/src/hooks/agent-event-handlers.ts +196 -0
  55. package/src/hooks/chat-local-replies.fixed.test.ts +119 -0
  56. package/src/hooks/chat-local-replies.test.ts +153 -0
  57. package/src/hooks/chat-local-replies.ts +63 -0
  58. package/src/hooks/turn-updater.test.ts +70 -0
  59. package/src/hooks/turn-updater.ts +166 -0
  60. package/src/hooks/use-agent-chat.context.test.ts +10 -0
  61. package/src/hooks/use-agent-chat.status.test.ts +14 -0
  62. package/src/hooks/use-agent-chat.test.ts +80 -0
  63. package/src/hooks/use-agent-chat.ts +621 -0
  64. package/src/hooks/use-file-mention-menu.ts +196 -0
  65. package/src/hooks/use-file-picker.ts +185 -0
  66. package/src/hooks/use-model-picker.ts +196 -0
  67. package/src/hooks/use-slash-command-menu.ts +154 -0
  68. package/src/index.tsx +55 -0
  69. package/src/runtime/clipboard.test.ts +43 -0
  70. package/src/runtime/clipboard.ts +89 -0
  71. package/src/runtime/exit.test.ts +177 -0
  72. package/src/runtime/exit.ts +98 -0
  73. package/src/runtime/runtime-support.test.ts +31 -0
  74. package/src/runtime/terminal-theme.test.ts +55 -0
  75. package/src/runtime/terminal-theme.ts +196 -0
  76. package/src/types/chat.ts +32 -0
  77. package/src/types/message-content.ts +48 -0
  78. package/src/ui/open-code-theme.ts +176 -0
  79. package/src/ui/opencode-markdown.ts +211 -0
  80. package/src/ui/theme.simple.test.ts +52 -0
  81. package/src/ui/theme.test.ts +151 -0
  82. package/src/ui/theme.ts +152 -0
  83. package/src/utils/time.test.ts +144 -0
  84. package/src/utils/time.ts +7 -0
  85. package/tsconfig.json +30 -0
  86. package/LICENSE +0 -21
  87. package/dist/App.d.ts +0 -2
  88. package/dist/App.d.ts.map +0 -1
  89. package/dist/App.js +0 -170
  90. package/dist/App.js.map +0 -1
  91. package/dist/agent/prompts/system.d.ts +0 -24
  92. package/dist/agent/prompts/system.d.ts.map +0 -1
  93. package/dist/agent/prompts/system.js +0 -222
  94. package/dist/agent/prompts/system.js.map +0 -1
  95. package/dist/agent/runtime/event-format.d.ts +0 -17
  96. package/dist/agent/runtime/event-format.d.ts.map +0 -1
  97. package/dist/agent/runtime/event-format.js +0 -194
  98. package/dist/agent/runtime/event-format.js.map +0 -1
  99. package/dist/agent/runtime/model-types.d.ts +0 -13
  100. package/dist/agent/runtime/model-types.d.ts.map +0 -1
  101. package/dist/agent/runtime/model-types.js +0 -1
  102. package/dist/agent/runtime/model-types.js.map +0 -1
  103. package/dist/agent/runtime/runtime.d.ts +0 -16
  104. package/dist/agent/runtime/runtime.d.ts.map +0 -1
  105. package/dist/agent/runtime/runtime.js +0 -691
  106. package/dist/agent/runtime/runtime.js.map +0 -1
  107. package/dist/agent/runtime/source-modules.d.ts +0 -176
  108. package/dist/agent/runtime/source-modules.d.ts.map +0 -1
  109. package/dist/agent/runtime/source-modules.js +0 -110
  110. package/dist/agent/runtime/source-modules.js.map +0 -1
  111. package/dist/agent/runtime/tool-call-buffer.d.ts +0 -12
  112. package/dist/agent/runtime/tool-call-buffer.d.ts.map +0 -1
  113. package/dist/agent/runtime/tool-call-buffer.js +0 -48
  114. package/dist/agent/runtime/tool-call-buffer.js.map +0 -1
  115. package/dist/agent/runtime/tool-confirmation.d.ts +0 -3
  116. package/dist/agent/runtime/tool-confirmation.d.ts.map +0 -1
  117. package/dist/agent/runtime/tool-confirmation.js +0 -9
  118. package/dist/agent/runtime/tool-confirmation.js.map +0 -1
  119. package/dist/agent/runtime/types.d.ts +0 -86
  120. package/dist/agent/runtime/types.d.ts.map +0 -1
  121. package/dist/agent/runtime/types.js +0 -1
  122. package/dist/agent/runtime/types.js.map +0 -1
  123. package/dist/cli.d.ts +0 -3
  124. package/dist/cli.d.ts.map +0 -1
  125. package/dist/cli.js +0 -12
  126. package/dist/cli.js.map +0 -1
  127. package/dist/commands/slash-commands.d.ts +0 -11
  128. package/dist/commands/slash-commands.d.ts.map +0 -1
  129. package/dist/commands/slash-commands.js +0 -48
  130. package/dist/commands/slash-commands.js.map +0 -1
  131. package/dist/components/chat/assistant-reply.d.ts +0 -13
  132. package/dist/components/chat/assistant-reply.d.ts.map +0 -1
  133. package/dist/components/chat/assistant-reply.js +0 -78
  134. package/dist/components/chat/assistant-reply.js.map +0 -1
  135. package/dist/components/chat/assistant-segment.d.ts +0 -8
  136. package/dist/components/chat/assistant-segment.d.ts.map +0 -1
  137. package/dist/components/chat/assistant-segment.js +0 -54
  138. package/dist/components/chat/assistant-segment.js.map +0 -1
  139. package/dist/components/chat/assistant-tool-group.d.ts +0 -7
  140. package/dist/components/chat/assistant-tool-group.d.ts.map +0 -1
  141. package/dist/components/chat/assistant-tool-group.js +0 -695
  142. package/dist/components/chat/assistant-tool-group.js.map +0 -1
  143. package/dist/components/chat/code-block.d.ts +0 -16
  144. package/dist/components/chat/code-block.d.ts.map +0 -1
  145. package/dist/components/chat/code-block.js +0 -194
  146. package/dist/components/chat/code-block.js.map +0 -1
  147. package/dist/components/chat/prompt-card.d.ts +0 -9
  148. package/dist/components/chat/prompt-card.d.ts.map +0 -1
  149. package/dist/components/chat/prompt-card.js +0 -18
  150. package/dist/components/chat/prompt-card.js.map +0 -1
  151. package/dist/components/chat/segment-groups.d.ts +0 -24
  152. package/dist/components/chat/segment-groups.d.ts.map +0 -1
  153. package/dist/components/chat/segment-groups.js +0 -69
  154. package/dist/components/chat/segment-groups.js.map +0 -1
  155. package/dist/components/chat/turn-item.d.ts +0 -9
  156. package/dist/components/chat/turn-item.d.ts.map +0 -1
  157. package/dist/components/chat/turn-item.js +0 -11
  158. package/dist/components/chat/turn-item.js.map +0 -1
  159. package/dist/components/conversation-panel.d.ts +0 -8
  160. package/dist/components/conversation-panel.d.ts.map +0 -1
  161. package/dist/components/conversation-panel.js +0 -8
  162. package/dist/components/conversation-panel.js.map +0 -1
  163. package/dist/components/file-mention-menu.d.ts +0 -11
  164. package/dist/components/file-mention-menu.d.ts.map +0 -1
  165. package/dist/components/file-mention-menu.js +0 -15
  166. package/dist/components/file-mention-menu.js.map +0 -1
  167. package/dist/components/file-picker-dialog.d.ts +0 -21
  168. package/dist/components/file-picker-dialog.d.ts.map +0 -1
  169. package/dist/components/file-picker-dialog.js +0 -48
  170. package/dist/components/file-picker-dialog.js.map +0 -1
  171. package/dist/components/footer-hints.d.ts +0 -7
  172. package/dist/components/footer-hints.d.ts.map +0 -1
  173. package/dist/components/footer-hints.js +0 -29
  174. package/dist/components/footer-hints.js.map +0 -1
  175. package/dist/components/model-picker-dialog.d.ts +0 -20
  176. package/dist/components/model-picker-dialog.d.ts.map +0 -1
  177. package/dist/components/model-picker-dialog.js +0 -72
  178. package/dist/components/model-picker-dialog.js.map +0 -1
  179. package/dist/components/prompt.d.ts +0 -18
  180. package/dist/components/prompt.d.ts.map +0 -1
  181. package/dist/components/prompt.js +0 -96
  182. package/dist/components/prompt.js.map +0 -1
  183. package/dist/components/slash-command-menu.d.ts +0 -9
  184. package/dist/components/slash-command-menu.d.ts.map +0 -1
  185. package/dist/components/slash-command-menu.js +0 -20
  186. package/dist/components/slash-command-menu.js.map +0 -1
  187. package/dist/components/tool-confirm-dialog-content.d.ts +0 -15
  188. package/dist/components/tool-confirm-dialog-content.d.ts.map +0 -1
  189. package/dist/components/tool-confirm-dialog-content.js +0 -143
  190. package/dist/components/tool-confirm-dialog-content.js.map +0 -1
  191. package/dist/components/tool-confirm-dialog.d.ts +0 -12
  192. package/dist/components/tool-confirm-dialog.d.ts.map +0 -1
  193. package/dist/components/tool-confirm-dialog.js +0 -21
  194. package/dist/components/tool-confirm-dialog.js.map +0 -1
  195. package/dist/components/tool-display-config.d.ts +0 -11
  196. package/dist/components/tool-display-config.d.ts.map +0 -1
  197. package/dist/components/tool-display-config.js +0 -94
  198. package/dist/components/tool-display-config.js.map +0 -1
  199. package/dist/config/paths.d.ts +0 -7
  200. package/dist/config/paths.d.ts.map +0 -1
  201. package/dist/config/paths.js +0 -24
  202. package/dist/config/paths.js.map +0 -1
  203. package/dist/files/attachment-capabilities.d.ts +0 -19
  204. package/dist/files/attachment-capabilities.d.ts.map +0 -1
  205. package/dist/files/attachment-capabilities.js +0 -26
  206. package/dist/files/attachment-capabilities.js.map +0 -1
  207. package/dist/files/attachment-content.d.ts +0 -5
  208. package/dist/files/attachment-content.d.ts.map +0 -1
  209. package/dist/files/attachment-content.js +0 -117
  210. package/dist/files/attachment-content.js.map +0 -1
  211. package/dist/files/file-mention-query.d.ts +0 -9
  212. package/dist/files/file-mention-query.d.ts.map +0 -1
  213. package/dist/files/file-mention-query.js +0 -23
  214. package/dist/files/file-mention-query.js.map +0 -1
  215. package/dist/files/prompt-display.d.ts +0 -3
  216. package/dist/files/prompt-display.d.ts.map +0 -1
  217. package/dist/files/prompt-display.js +0 -11
  218. package/dist/files/prompt-display.js.map +0 -1
  219. package/dist/files/types.d.ts +0 -6
  220. package/dist/files/types.d.ts.map +0 -1
  221. package/dist/files/types.js +0 -1
  222. package/dist/files/types.js.map +0 -1
  223. package/dist/files/workspace-files.d.ts +0 -3
  224. package/dist/files/workspace-files.d.ts.map +0 -1
  225. package/dist/files/workspace-files.js +0 -48
  226. package/dist/files/workspace-files.js.map +0 -1
  227. package/dist/hooks/agent-event-handlers.d.ts +0 -11
  228. package/dist/hooks/agent-event-handlers.d.ts.map +0 -1
  229. package/dist/hooks/agent-event-handlers.js +0 -137
  230. package/dist/hooks/agent-event-handlers.js.map +0 -1
  231. package/dist/hooks/chat-local-replies.d.ts +0 -9
  232. package/dist/hooks/chat-local-replies.d.ts.map +0 -1
  233. package/dist/hooks/chat-local-replies.js +0 -54
  234. package/dist/hooks/chat-local-replies.js.map +0 -1
  235. package/dist/hooks/turn-updater.d.ts +0 -9
  236. package/dist/hooks/turn-updater.d.ts.map +0 -1
  237. package/dist/hooks/turn-updater.js +0 -103
  238. package/dist/hooks/turn-updater.js.map +0 -1
  239. package/dist/hooks/use-agent-chat.d.ts +0 -29
  240. package/dist/hooks/use-agent-chat.d.ts.map +0 -1
  241. package/dist/hooks/use-agent-chat.js +0 -455
  242. package/dist/hooks/use-agent-chat.js.map +0 -1
  243. package/dist/hooks/use-file-mention-menu.d.ts +0 -22
  244. package/dist/hooks/use-file-mention-menu.d.ts.map +0 -1
  245. package/dist/hooks/use-file-mention-menu.js +0 -137
  246. package/dist/hooks/use-file-mention-menu.js.map +0 -1
  247. package/dist/hooks/use-file-picker.d.ts +0 -21
  248. package/dist/hooks/use-file-picker.d.ts.map +0 -1
  249. package/dist/hooks/use-file-picker.js +0 -145
  250. package/dist/hooks/use-file-picker.js.map +0 -1
  251. package/dist/hooks/use-model-picker.d.ts +0 -23
  252. package/dist/hooks/use-model-picker.d.ts.map +0 -1
  253. package/dist/hooks/use-model-picker.js +0 -151
  254. package/dist/hooks/use-model-picker.js.map +0 -1
  255. package/dist/hooks/use-slash-command-menu.d.ts +0 -19
  256. package/dist/hooks/use-slash-command-menu.d.ts.map +0 -1
  257. package/dist/hooks/use-slash-command-menu.js +0 -101
  258. package/dist/hooks/use-slash-command-menu.js.map +0 -1
  259. package/dist/index.d.ts +0 -2
  260. package/dist/index.d.ts.map +0 -1
  261. package/dist/index.js +0 -6
  262. package/dist/index.js.map +0 -1
  263. package/dist/run-cli-app.d.ts +0 -2
  264. package/dist/run-cli-app.d.ts.map +0 -1
  265. package/dist/run-cli-app.js +0 -41
  266. package/dist/run-cli-app.js.map +0 -1
  267. package/dist/runtime/clipboard.d.ts +0 -10
  268. package/dist/runtime/clipboard.d.ts.map +0 -1
  269. package/dist/runtime/clipboard.js +0 -64
  270. package/dist/runtime/clipboard.js.map +0 -1
  271. package/dist/runtime/exit.d.ts +0 -7
  272. package/dist/runtime/exit.d.ts.map +0 -1
  273. package/dist/runtime/exit.js +0 -85
  274. package/dist/runtime/exit.js.map +0 -1
  275. package/dist/runtime/runtime-support.d.ts +0 -4
  276. package/dist/runtime/runtime-support.d.ts.map +0 -1
  277. package/dist/runtime/runtime-support.js +0 -19
  278. package/dist/runtime/runtime-support.js.map +0 -1
  279. package/dist/runtime/terminal-theme.d.ts +0 -25
  280. package/dist/runtime/terminal-theme.d.ts.map +0 -1
  281. package/dist/runtime/terminal-theme.js +0 -148
  282. package/dist/runtime/terminal-theme.js.map +0 -1
  283. package/dist/types/chat.d.ts +0 -29
  284. package/dist/types/chat.d.ts.map +0 -1
  285. package/dist/types/chat.js +0 -1
  286. package/dist/types/chat.js.map +0 -1
  287. package/dist/types/message-content.d.ts +0 -38
  288. package/dist/types/message-content.d.ts.map +0 -1
  289. package/dist/types/message-content.js +0 -1
  290. package/dist/types/message-content.js.map +0 -1
  291. package/dist/ui/open-code-theme.d.ts +0 -58
  292. package/dist/ui/open-code-theme.d.ts.map +0 -1
  293. package/dist/ui/open-code-theme.js +0 -113
  294. package/dist/ui/open-code-theme.js.map +0 -1
  295. package/dist/ui/opencode-markdown.d.ts +0 -7
  296. package/dist/ui/opencode-markdown.d.ts.map +0 -1
  297. package/dist/ui/opencode-markdown.js +0 -169
  298. package/dist/ui/opencode-markdown.js.map +0 -1
  299. package/dist/ui/theme.d.ts +0 -68
  300. package/dist/ui/theme.d.ts.map +0 -1
  301. package/dist/ui/theme.js +0 -80
  302. package/dist/ui/theme.js.map +0 -1
  303. package/dist/utils/time.d.ts +0 -2
  304. package/dist/utils/time.d.ts.map +0 -1
  305. package/dist/utils/time.js +0 -7
  306. package/dist/utils/time.js.map +0 -1
@@ -0,0 +1,900 @@
1
+ import { useState } from 'react';
2
+
3
+ import { uiTheme } from '../../ui/theme';
4
+ import { getToolDisplayIcon, getToolDisplayName } from '../tool-display-config';
5
+ import { CodeBlock } from './code-block';
6
+ import type { ToolSegmentGroup } from './segment-groups';
7
+
8
+ type AssistantToolGroupProps = {
9
+ group: ToolSegmentGroup;
10
+ };
11
+
12
+ type ParsedToolUse = {
13
+ name: string;
14
+ callId: string;
15
+ command?: string;
16
+ details?: string;
17
+ args?: Record<string, unknown> | null;
18
+ };
19
+
20
+ type ParsedToolResult = {
21
+ name: string;
22
+ callId: string;
23
+ status: 'success' | 'error' | 'unknown';
24
+ details?: string;
25
+ summary?: string;
26
+ output?: string;
27
+ payload?: unknown;
28
+ metadata?: unknown;
29
+ error?: string;
30
+ };
31
+
32
+ type ToolSection = {
33
+ label?: string;
34
+ content: string;
35
+ tone?: 'body' | 'code';
36
+ };
37
+
38
+ type SpecialToolPresentation = {
39
+ toolLabel?: string;
40
+ headerDetail?: string;
41
+ sections: ToolSection[];
42
+ };
43
+
44
+ const COLLAPSIBLE_OUTPUT_LINES = 16;
45
+ const COLLAPSIBLE_OUTPUT_LABELS = new Set(['output', 'error', 'result', 'details']);
46
+
47
+ const asObjectLike = (value: unknown): Record<string, unknown> | null => {
48
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
49
+ return null;
50
+ }
51
+ return value as Record<string, unknown>;
52
+ };
53
+
54
+ const parseToolArgumentsObject = (raw?: string): Record<string, unknown> | null => {
55
+ if (!raw) {
56
+ return null;
57
+ }
58
+ try {
59
+ return asObjectLike(JSON.parse(raw));
60
+ } catch {
61
+ return null;
62
+ }
63
+ };
64
+
65
+ const parseToolUseFromData = (value: unknown): ParsedToolUse | null => {
66
+ const toolCall = asObjectLike(value);
67
+ const toolFunction = asObjectLike(toolCall?.function);
68
+ if (!toolFunction) {
69
+ return null;
70
+ }
71
+ const name = typeof toolFunction.name === 'string' ? toolFunction.name : undefined;
72
+ const callId = typeof toolCall?.id === 'string' ? toolCall.id : undefined;
73
+ if (!name || !callId) {
74
+ return null;
75
+ }
76
+
77
+ const rawArguments =
78
+ typeof toolFunction.arguments === 'string' ? toolFunction.arguments : undefined;
79
+ const args = parseToolArgumentsObject(rawArguments);
80
+ const command = name === 'bash' && typeof args?.command === 'string' ? args.command : undefined;
81
+
82
+ return {
83
+ name,
84
+ callId,
85
+ command,
86
+ details: rawArguments,
87
+ args,
88
+ };
89
+ };
90
+
91
+ const parseToolUse = (content?: string, data?: unknown): ParsedToolUse | null => {
92
+ const structured = parseToolUseFromData(data);
93
+ if (structured) {
94
+ return structured;
95
+ }
96
+ if (!content) {
97
+ return null;
98
+ }
99
+ const lines = content.split('\n');
100
+ const header = lines[0]?.trim();
101
+ if (!header) {
102
+ return null;
103
+ }
104
+ const match = header.match(/^# Tool:\s+(.+?)\s+\(([^)]+)\)$/);
105
+ if (!match || !match[1] || !match[2]) {
106
+ return null;
107
+ }
108
+
109
+ const [_, name, callId] = match;
110
+ const bodyLines = lines.slice(1);
111
+ const commandLine = bodyLines.find(line => line.trim().startsWith('$ '));
112
+ const command = commandLine ? commandLine.trim().slice(2).trim() : undefined;
113
+ const details = bodyLines
114
+ .filter(line => !line.trim().startsWith('$ '))
115
+ .join('\n')
116
+ .trim();
117
+
118
+ return {
119
+ name,
120
+ callId,
121
+ command: command || undefined,
122
+ details: details || undefined,
123
+ args: parseJsonObject(details),
124
+ };
125
+ };
126
+
127
+ const parseToolResultFromData = (value: unknown): ParsedToolResult | null => {
128
+ const event = asObjectLike(value);
129
+ const toolCall = asObjectLike(event?.toolCall);
130
+ const toolFunction = asObjectLike(toolCall?.function);
131
+ const result = asObjectLike(event?.result);
132
+ const data = asObjectLike(result?.data);
133
+ const name = typeof toolFunction?.name === 'string' ? toolFunction.name : undefined;
134
+ const callId = typeof toolCall?.id === 'string' ? toolCall.id : undefined;
135
+ if (!name || !callId) {
136
+ return null;
137
+ }
138
+
139
+ const successValue = result?.success;
140
+ const status = successValue === true ? 'success' : successValue === false ? 'error' : 'unknown';
141
+ const summary = typeof data?.summary === 'string' ? data.summary : undefined;
142
+ const output = typeof data?.output === 'string' ? data.output : undefined;
143
+ const error = typeof result?.error === 'string' ? result.error : undefined;
144
+
145
+ return {
146
+ name,
147
+ callId,
148
+ status,
149
+ details: output || summary || error,
150
+ summary,
151
+ output,
152
+ payload: data?.payload,
153
+ metadata: data?.metadata,
154
+ error,
155
+ };
156
+ };
157
+
158
+ const parseToolResult = (content?: string, data?: unknown): ParsedToolResult | null => {
159
+ const structured = parseToolResultFromData(data);
160
+ if (structured) {
161
+ return structured;
162
+ }
163
+ if (!content) {
164
+ return null;
165
+ }
166
+ const lines = content.split('\n');
167
+ const header = lines[0]?.trim();
168
+ if (!header) {
169
+ return null;
170
+ }
171
+ const match = header.match(/^# Result:\s+(.+?)\s+\(([^)]+)\)\s+(success|error)$/);
172
+ if (!match || !match[1] || !match[2] || !match[3]) {
173
+ return null;
174
+ }
175
+
176
+ const [_, name, callId, status] = match;
177
+ const details = lines.slice(1).join('\n').trim();
178
+
179
+ return {
180
+ name,
181
+ callId,
182
+ status: status === 'success' || status === 'error' ? status : 'unknown',
183
+ details: details || undefined,
184
+ ...(status === 'error' ? { error: details || undefined } : {}),
185
+ };
186
+ };
187
+
188
+ const resolveToolIcon = (toolName: string): string => {
189
+ return getToolDisplayIcon(toolName);
190
+ };
191
+
192
+ const mergeOutputLines = (
193
+ group: ToolSegmentGroup,
194
+ parsedResult: ParsedToolResult | null
195
+ ): string => {
196
+ const streamText = group.streams
197
+ .map(segment => segment.content)
198
+ .join('')
199
+ .trim();
200
+ const resultText = parsedResult?.output?.trim() || parsedResult?.details?.trim();
201
+ if (streamText && resultText && streamText === resultText) {
202
+ return streamText;
203
+ }
204
+ if (streamText && resultText) {
205
+ return `${streamText}\n${resultText}`;
206
+ }
207
+ return streamText || resultText || parsedResult?.summary?.trim() || '';
208
+ };
209
+
210
+ const readObject = (value: unknown): Record<string, unknown> | null => {
211
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
212
+ return null;
213
+ }
214
+ return value as Record<string, unknown>;
215
+ };
216
+
217
+ const readArray = (value: unknown): unknown[] => {
218
+ return Array.isArray(value) ? value : [];
219
+ };
220
+
221
+ const readString = (value: unknown): string | undefined => {
222
+ return typeof value === 'string' ? value : undefined;
223
+ };
224
+
225
+ const readNumber = (value: unknown): number | undefined => {
226
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
227
+ };
228
+
229
+ const readBoolean = (value: unknown): boolean | undefined => {
230
+ return typeof value === 'boolean' ? value : undefined;
231
+ };
232
+
233
+ const parseJsonObject = (content?: string): Record<string, unknown> | null => {
234
+ if (!content) {
235
+ return null;
236
+ }
237
+ try {
238
+ return readObject(JSON.parse(content));
239
+ } catch {
240
+ return null;
241
+ }
242
+ };
243
+
244
+ const parseJsonValue = (content?: string): unknown => {
245
+ if (!content) {
246
+ return undefined;
247
+ }
248
+ try {
249
+ return JSON.parse(content);
250
+ } catch {
251
+ return undefined;
252
+ }
253
+ };
254
+
255
+ const countEscapeMarkers = (value: string): number => {
256
+ return (value.match(/\\r\\n|\\n|\\t|\\"|\\\\/g) ?? []).length;
257
+ };
258
+
259
+ const decodeEscapeSequencesOnce = (value: string): string => {
260
+ return value
261
+ .replace(/\\r\\n/g, '\n')
262
+ .replace(/\\n/g, '\n')
263
+ .replace(/\\t/g, '\t')
264
+ .replace(/\\"/g, '"')
265
+ .replace(/\\\\/g, '\\');
266
+ };
267
+
268
+ const normalizeToolDisplayText = (value: string): string => {
269
+ let current = value.replace(/\r\n/g, '\n').trimEnd();
270
+ for (let index = 0; index < 2; index += 1) {
271
+ const next = decodeEscapeSequencesOnce(current);
272
+ if (countEscapeMarkers(next) >= countEscapeMarkers(current)) {
273
+ break;
274
+ }
275
+ current = next;
276
+ }
277
+
278
+ const parsed = parseJsonValue(current);
279
+ if (parsed !== undefined && typeof parsed !== 'string') {
280
+ try {
281
+ return JSON.stringify(parsed, null, 2);
282
+ } catch {
283
+ return current;
284
+ }
285
+ }
286
+
287
+ return current;
288
+ };
289
+
290
+ const resolveSectionLanguageHint = (
291
+ toolName: string,
292
+ section: Pick<ToolSection, 'label' | 'tone'>
293
+ ): string | undefined => {
294
+ if (section.tone !== 'code') {
295
+ return undefined;
296
+ }
297
+ if (section.label === 'command') {
298
+ return toolName === 'bash' ? 'bash' : undefined;
299
+ }
300
+ if (section.label === 'arguments') {
301
+ return 'json';
302
+ }
303
+ return undefined;
304
+ };
305
+
306
+ const isCollapsibleResultSection = (section: ToolSection): boolean => {
307
+ if (section.tone !== 'code') {
308
+ return false;
309
+ }
310
+
311
+ if (!section.label) {
312
+ return false;
313
+ }
314
+
315
+ return COLLAPSIBLE_OUTPUT_LABELS.has(section.label.toLowerCase());
316
+ };
317
+
318
+ const resolveStructuredResultObject = (
319
+ result: ParsedToolResult | null
320
+ ): Record<string, unknown> | null => {
321
+ return (
322
+ readObject(result?.payload) ?? readObject(result?.metadata) ?? parseJsonObject(result?.details)
323
+ );
324
+ };
325
+
326
+ const formatToolName = (toolName: string): string => {
327
+ return getToolDisplayName(toolName);
328
+ };
329
+
330
+ const truncate = (value: string, maxLength = 88): string => {
331
+ const normalized = value.trim().replace(/\s+/g, ' ');
332
+ if (normalized.length <= maxLength) {
333
+ return normalized;
334
+ }
335
+ return `${normalized.slice(0, maxLength - 1)}…`;
336
+ };
337
+
338
+ const compactDetail = (value?: string, maxLength = 72): string | null => {
339
+ if (!value) {
340
+ return null;
341
+ }
342
+ const normalized = value.trim().replace(/\s+/g, ' ');
343
+ if (!normalized) {
344
+ return null;
345
+ }
346
+ return truncate(normalized, maxLength);
347
+ };
348
+
349
+ const formatStatusLabel = (status?: string): string | null => {
350
+ if (!status) {
351
+ return null;
352
+ }
353
+ return status.replace(/_/g, ' ');
354
+ };
355
+
356
+ const formatTaskStatusIcon = (status?: string): string => {
357
+ switch (status) {
358
+ case 'completed':
359
+ return '●';
360
+ case 'in_progress':
361
+ case 'running':
362
+ return '◐';
363
+ case 'pending':
364
+ case 'queued':
365
+ return '○';
366
+ case 'cancelled':
367
+ return '⊘';
368
+ case 'failed':
369
+ case 'timed_out':
370
+ return '×';
371
+ case 'paused':
372
+ return '⏸';
373
+ default:
374
+ return '•';
375
+ }
376
+ };
377
+
378
+ const formatSummaryMeta = (parts: Array<string | null | undefined>): string | null => {
379
+ const filtered = parts.map(part => part?.trim()).filter((part): part is string => Boolean(part));
380
+ return filtered.length > 0 ? filtered.join(' · ') : null;
381
+ };
382
+
383
+ const summarizeTaskRecord = (
384
+ task: Record<string, unknown>,
385
+ options?: { canStart?: Record<string, unknown> | null }
386
+ ): string[] => {
387
+ const subject =
388
+ readString(task.subject) ?? readString(task.activeForm) ?? readString(task.id) ?? 'task';
389
+ const id = readString(task.id);
390
+ const status = readString(task.status);
391
+ const priority = readString(task.priority);
392
+ const progress = readNumber(task.effective_progress) ?? readNumber(task.progress);
393
+ const owner = readString(task.owner);
394
+ const blockers = readArray(task.blockers).length || readArray(task.blocked_by).length;
395
+ const blocks = readArray(task.blocked_tasks).length || readArray(task.blocks).length;
396
+
397
+ const lines = [`${formatTaskStatusIcon(status)} ${truncate(subject)}`];
398
+ const meta = formatSummaryMeta([
399
+ id,
400
+ formatStatusLabel(status),
401
+ priority,
402
+ progress !== undefined ? `${Math.round(progress)}%` : null,
403
+ owner ? `owner ${owner}` : null,
404
+ blockers > 0 ? `${blockers} blocker${blockers === 1 ? '' : 's'}` : null,
405
+ blocks > 0 ? `blocks ${blocks}` : null,
406
+ ]);
407
+ if (meta) {
408
+ lines.push(meta);
409
+ }
410
+
411
+ const canStart = options?.canStart;
412
+ if (canStart && readBoolean(canStart.canStart) === false) {
413
+ const reason = readString(canStart.reason);
414
+ if (reason) {
415
+ lines.push(`blocked: ${truncate(reason, 96)}`);
416
+ }
417
+ }
418
+
419
+ return lines;
420
+ };
421
+
422
+ const summarizeAgentRun = (
423
+ run: Record<string, unknown>,
424
+ extras?: Record<string, unknown>
425
+ ): { lines: string[]; output?: string } => {
426
+ const agentId = readString(run.agentId);
427
+ const status = readString(run.status);
428
+ const subagentType = readString(run.subagentType);
429
+ const description = readString(run.description);
430
+ const linkedTaskId = readString(run.linkedTaskId);
431
+ const progress = readNumber(run.progress);
432
+ const error = readString(run.error);
433
+ const output = readString(run.output);
434
+
435
+ const headline = description ?? agentId ?? 'agent run';
436
+ const lines = [`${formatTaskStatusIcon(status)} ${truncate(headline)}`];
437
+ const meta = formatSummaryMeta([
438
+ agentId,
439
+ formatStatusLabel(status),
440
+ subagentType,
441
+ progress !== undefined ? `${Math.round(progress)}%` : null,
442
+ linkedTaskId ? `task ${linkedTaskId}` : null,
443
+ readBoolean(extras?.completed) === false ? 'still running' : null,
444
+ readBoolean(extras?.timeout_hit) === true ? 'timeout hit' : null,
445
+ readNumber(extras?.waited_ms) !== undefined
446
+ ? `${Math.round((readNumber(extras?.waited_ms) ?? 0) / 1000)}s waited`
447
+ : null,
448
+ ]);
449
+ if (meta) {
450
+ lines.push(meta);
451
+ }
452
+ if (error && !output) {
453
+ lines.push(`error: ${truncate(error, 96)}`);
454
+ }
455
+
456
+ return {
457
+ lines,
458
+ output: output?.trim() ? output : undefined,
459
+ };
460
+ };
461
+
462
+ const buildTaskHeaderDetail = (
463
+ toolName: string,
464
+ args: Record<string, unknown> | null
465
+ ): string | null => {
466
+ if (!args) {
467
+ return null;
468
+ }
469
+
470
+ if (toolName === 'task_create') {
471
+ const subject = readString(args.subject);
472
+ return formatSummaryMeta([
473
+ subject ? `create ${truncate(subject, 56)}` : null,
474
+ readString(args.namespace),
475
+ readString(args.priority),
476
+ readArray(args.checkpoints).length > 0
477
+ ? `${readArray(args.checkpoints).length} checkpoints`
478
+ : null,
479
+ ]);
480
+ }
481
+
482
+ if (toolName === 'task_get') {
483
+ return formatSummaryMeta([
484
+ `inspect ${readString(args.task_id) ?? 'task'}`,
485
+ readBoolean(args.include_history) ? 'include history' : null,
486
+ ]);
487
+ }
488
+
489
+ if (toolName === 'task_list') {
490
+ const statuses = readArray(args.statuses)
491
+ .map(value => readString(value))
492
+ .filter(Boolean)
493
+ .join(', ');
494
+ return formatSummaryMeta([
495
+ `list${readString(args.namespace) ? ` in ${readString(args.namespace)}` : ''}`,
496
+ statuses ? `status ${statuses}` : null,
497
+ readString(args.owner) ? `owner ${readString(args.owner)}` : null,
498
+ readString(args.tag) ? `tag ${readString(args.tag)}` : null,
499
+ ]);
500
+ }
501
+
502
+ if (toolName === 'task_update') {
503
+ const changes: string[] = [`update ${readString(args.task_id) ?? 'task'}`];
504
+ if (readString(args.status))
505
+ changes.push(`status -> ${readString(args.status)?.replace(/_/g, ' ')}`);
506
+ if (readNumber(args.progress) !== undefined)
507
+ changes.push(`progress ${Math.round(readNumber(args.progress) ?? 0)}%`);
508
+ if (readString(args.owner)) changes.push(`owner ${readString(args.owner)}`);
509
+ if (readArray(args.add_blocked_by).length > 0)
510
+ changes.push(`+${readArray(args.add_blocked_by).length} blockers`);
511
+ if (readArray(args.remove_blocked_by).length > 0)
512
+ changes.push(`-${readArray(args.remove_blocked_by).length} blockers`);
513
+ return changes.join(' · ');
514
+ }
515
+
516
+ if (toolName === 'task_stop') {
517
+ return formatSummaryMeta([
518
+ `stop ${readString(args.task_id) ?? readString(args.agent_id) ?? 'agent run'}`,
519
+ readBoolean(args.cancel_linked_task) !== false ? 'cancel linked tasks' : null,
520
+ ]);
521
+ }
522
+
523
+ if (toolName === 'task_output') {
524
+ return formatSummaryMeta([
525
+ `watch ${readString(args.task_id) ?? readString(args.agent_id) ?? 'agent run'}`,
526
+ readBoolean(args.block) === false ? 'non-blocking poll' : 'wait for completion',
527
+ ]);
528
+ }
529
+
530
+ if (toolName === 'agent' || toolName === 'task') {
531
+ const prompt = readString(args.prompt);
532
+ const description = readString(args.description);
533
+ return formatSummaryMeta([
534
+ description ? truncate(description, 56) : prompt ? truncate(prompt, 56) : null,
535
+ readString(args.subagent_type),
536
+ readBoolean(args.run_in_background) ? 'background' : 'foreground',
537
+ readString(args.linked_task_id) ? `task ${readString(args.linked_task_id)}` : null,
538
+ ]);
539
+ }
540
+
541
+ return null;
542
+ };
543
+
544
+ const buildTaskResultSections = (
545
+ toolName: string,
546
+ result: ParsedToolResult | null
547
+ ): ToolSection[] => {
548
+ const resultDetails = result?.details?.trim();
549
+ const summary = result?.summary?.trim();
550
+ if (!resultDetails && !summary && !result?.payload && !result?.metadata) {
551
+ return [];
552
+ }
553
+
554
+ if (result?.status === 'error') {
555
+ return [
556
+ {
557
+ label: 'result',
558
+ content: result?.error?.trim() || resultDetails || summary || 'task failed',
559
+ tone: 'body',
560
+ },
561
+ ];
562
+ }
563
+
564
+ const payload = resolveStructuredResultObject(result);
565
+ if (!payload) {
566
+ return [
567
+ {
568
+ label: 'result',
569
+ content: resultDetails || summary || '',
570
+ tone: 'body',
571
+ },
572
+ ];
573
+ }
574
+
575
+ if (toolName === 'task_list') {
576
+ const namespace = readString(payload.namespace) ?? 'default';
577
+ const tasks = readArray(payload.tasks)
578
+ .map(item => readObject(item))
579
+ .filter((item): item is Record<string, unknown> => Boolean(item));
580
+ const total = readNumber(payload.total) ?? tasks.length;
581
+ const lines = [`${total} task${total === 1 ? '' : 's'} in ${namespace}`];
582
+ tasks.slice(0, 5).forEach(task => {
583
+ const summary = summarizeTaskRecord(task);
584
+ if (summary[0]) {
585
+ lines.push(summary[0]);
586
+ }
587
+ if (summary[1]) {
588
+ lines.push(` ${summary[1]}`);
589
+ }
590
+ });
591
+ if (tasks.length > 5) {
592
+ lines.push(`+${tasks.length - 5} more`);
593
+ }
594
+ return [{ label: 'result', content: lines.join('\n'), tone: 'body' }];
595
+ }
596
+
597
+ if (toolName === 'task_stop') {
598
+ const run = readObject(payload.agent_run);
599
+ const cancelledTaskIds = readArray(payload.cancelled_task_ids)
600
+ .map(item => readString(item))
601
+ .filter(Boolean);
602
+ const sections: ToolSection[] = [];
603
+ if (run) {
604
+ sections.push({
605
+ label: 'result',
606
+ content: summarizeAgentRun(run).lines.join('\n'),
607
+ tone: 'body',
608
+ });
609
+ }
610
+ if (cancelledTaskIds.length > 0) {
611
+ sections.push({
612
+ label: 'cancelled',
613
+ content: cancelledTaskIds.join(', '),
614
+ tone: 'body',
615
+ });
616
+ }
617
+ return sections;
618
+ }
619
+
620
+ if (toolName === 'task_output' || toolName === 'task' || toolName === 'agent') {
621
+ const run = readObject(payload.agent_run);
622
+ if (run) {
623
+ const summary = summarizeAgentRun(run, payload);
624
+ const sections: ToolSection[] = [
625
+ {
626
+ label: 'result',
627
+ content: summary.lines.join('\n'),
628
+ tone: 'body',
629
+ },
630
+ ];
631
+ if (summary.output) {
632
+ sections.push({
633
+ label: 'output',
634
+ content: summary.output,
635
+ tone: 'body',
636
+ });
637
+ }
638
+ return sections;
639
+ }
640
+ }
641
+
642
+ const task = readObject(payload.task);
643
+ if (task) {
644
+ const canStart = readObject(payload.can_start) ?? readObject(task.can_start);
645
+ return [
646
+ {
647
+ label: 'result',
648
+ content: summarizeTaskRecord(task, { canStart }).join('\n'),
649
+ tone: 'body',
650
+ },
651
+ ];
652
+ }
653
+
654
+ return [
655
+ {
656
+ label: 'result',
657
+ content: resultDetails || summary || '',
658
+ tone: 'body',
659
+ },
660
+ ];
661
+ };
662
+
663
+ const buildSearchHeaderDetail = (
664
+ toolName: string,
665
+ args: Record<string, unknown> | null
666
+ ): string | null => {
667
+ if (!args) {
668
+ return null;
669
+ }
670
+
671
+ if (toolName === 'grep') {
672
+ const pattern = readString(args.pattern);
673
+ if (!pattern) {
674
+ return null;
675
+ }
676
+
677
+ return formatSummaryMeta([
678
+ JSON.stringify(pattern),
679
+ readString(args.path) ? `in ${readString(args.path)}` : null,
680
+ readString(args.glob) ? `glob ${readString(args.glob)}` : null,
681
+ readNumber(args.max_results) !== undefined
682
+ ? `limit ${Math.round(readNumber(args.max_results) ?? 0)}`
683
+ : null,
684
+ readNumber(args.timeout_ms) !== undefined
685
+ ? `${Math.round((readNumber(args.timeout_ms) ?? 0) / 1000)}s timeout`
686
+ : null,
687
+ ]);
688
+ }
689
+
690
+ if (toolName === 'glob') {
691
+ const pattern = readString(args.pattern);
692
+ if (!pattern) {
693
+ return null;
694
+ }
695
+
696
+ return formatSummaryMeta([
697
+ pattern,
698
+ readString(args.path) ? `in ${readString(args.path)}` : null,
699
+ readBoolean(args.include_hidden) ? 'include hidden' : null,
700
+ readNumber(args.max_results) !== undefined
701
+ ? `limit ${Math.round(readNumber(args.max_results) ?? 0)}`
702
+ : null,
703
+ ]);
704
+ }
705
+
706
+ return null;
707
+ };
708
+
709
+ const buildSearchResultSections = (result: ParsedToolResult | null): ToolSection[] => {
710
+ const summary = result?.summary?.trim();
711
+ const output = result?.output?.trim() || result?.details?.trim();
712
+ const metadata = readObject(result?.metadata) ?? readObject(result?.payload);
713
+
714
+ if (!summary && !output && !metadata) {
715
+ return [];
716
+ }
717
+
718
+ if (metadata) {
719
+ const matchCount = readNumber(metadata.countMatches);
720
+ const fileCount = readNumber(metadata.countFiles);
721
+ const path = readString(metadata.path);
722
+ const flags = formatSummaryMeta([
723
+ matchCount !== undefined ? `${matchCount} matches` : null,
724
+ fileCount !== undefined ? `${fileCount} files` : null,
725
+ path ? `in ${path}` : null,
726
+ readBoolean(metadata.truncated) ? 'truncated' : null,
727
+ readBoolean(metadata.timed_out) ? 'timed out' : null,
728
+ ]);
729
+ if (summary && output && summary !== output && flags) {
730
+ return [
731
+ { label: 'result', content: summary, tone: 'body' },
732
+ { label: 'details', content: `${flags}\n${output}`, tone: 'body' },
733
+ ];
734
+ }
735
+ }
736
+
737
+ return [
738
+ {
739
+ label: 'result',
740
+ content: output || summary || '',
741
+ tone: 'body',
742
+ },
743
+ ];
744
+ };
745
+
746
+ const buildSpecialToolPresentation = (
747
+ toolName: string,
748
+ parsedUse: ParsedToolUse | null,
749
+ parsedResult: ParsedToolResult | null
750
+ ): SpecialToolPresentation | null => {
751
+ const args = parsedUse?.args ?? parseJsonObject(parsedUse?.details);
752
+ if (toolName === 'agent' || toolName === 'task' || toolName.startsWith('task_')) {
753
+ const sections = buildTaskResultSections(toolName, parsedResult);
754
+
755
+ return {
756
+ toolLabel: formatToolName(toolName),
757
+ headerDetail: buildTaskHeaderDetail(toolName, args) ?? undefined,
758
+ sections,
759
+ };
760
+ }
761
+
762
+ if (toolName === 'grep' || toolName === 'glob') {
763
+ const sections = buildSearchResultSections(parsedResult);
764
+
765
+ return {
766
+ toolLabel: formatToolName(toolName),
767
+ headerDetail: buildSearchHeaderDetail(toolName, args) ?? undefined,
768
+ sections,
769
+ };
770
+ }
771
+
772
+ return null;
773
+ };
774
+
775
+ export const AssistantToolGroup = ({ group }: AssistantToolGroupProps) => {
776
+ const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
777
+ const parsedUse = parseToolUse(group.use?.content, group.use?.data);
778
+ const parsedResult = parseToolResult(group.result?.content, group.result?.data);
779
+ const toolName = parsedUse?.name ?? parsedResult?.name ?? 'tool';
780
+ const commandText = parsedUse?.command;
781
+ const invocationDetails = parsedUse?.details;
782
+ const icon = resolveToolIcon(toolName);
783
+ const outputText = mergeOutputLines(group, parsedResult);
784
+ const _hasInvocationDetails = Boolean(invocationDetails);
785
+ const hasOutput = outputText.length > 0;
786
+ const specialPresentation = buildSpecialToolPresentation(toolName, parsedUse, parsedResult);
787
+ const titleDetail =
788
+ specialPresentation?.headerDetail ??
789
+ compactDetail(commandText, 64) ??
790
+ compactDetail(invocationDetails, 64);
791
+ const defaultSections: ToolSection[] = [];
792
+ if (commandText && !titleDetail) {
793
+ defaultSections.push({
794
+ label: 'command',
795
+ content: `$ ${commandText}`,
796
+ tone: 'code',
797
+ });
798
+ }
799
+ if (invocationDetails && !titleDetail) {
800
+ defaultSections.push({
801
+ label: 'arguments',
802
+ content: invocationDetails,
803
+ tone: 'code',
804
+ });
805
+ }
806
+ if (hasOutput) {
807
+ defaultSections.push({
808
+ label: parsedResult?.status === 'error' ? 'error' : 'output',
809
+ content: outputText,
810
+ tone: 'code',
811
+ });
812
+ }
813
+ const sections = specialPresentation?.sections ?? defaultSections;
814
+ const hasBody = sections.length > 0;
815
+ const statusText =
816
+ parsedResult?.status === 'success'
817
+ ? 'completed'
818
+ : parsedResult?.status === 'error'
819
+ ? 'error'
820
+ : group.result
821
+ ? 'finished'
822
+ : 'running';
823
+
824
+ return (
825
+ <box flexDirection="column">
826
+ <box paddingLeft={3}>
827
+ <text fg={uiTheme.text} attributes={uiTheme.typography.note} wrapMode="word">
828
+ <span fg={uiTheme.accent}>{icon}</span>{' '}
829
+ {specialPresentation?.toolLabel ?? formatToolName(toolName)}
830
+ {titleDetail ? <span fg={uiTheme.muted}>({titleDetail})</span> : null}
831
+ <span fg={uiTheme.subtle}> ({statusText})</span>
832
+ </text>
833
+ </box>
834
+ {hasBody ? (
835
+ <box flexDirection="row" marginTop={1}>
836
+ <box width={1} backgroundColor={uiTheme.divider} />
837
+ <box
838
+ flexGrow={1}
839
+ backgroundColor={uiTheme.panel}
840
+ paddingLeft={2}
841
+ paddingRight={1}
842
+ paddingTop={1}
843
+ paddingBottom={1}
844
+ >
845
+ {sections.map((section, index) => {
846
+ const content = normalizeToolDisplayText(section.content);
847
+ const isCode = section.tone === 'code';
848
+ const sectionId = `${toolName}:section:${index}`;
849
+ const collapsible = isCollapsibleResultSection(section);
850
+ const expanded = Boolean(expandedSections[sectionId]);
851
+
852
+ return (
853
+ <box
854
+ key={sectionId}
855
+ flexDirection="column"
856
+ paddingBottom={index < sections.length - 1 ? 1 : 0}
857
+ >
858
+ {section.label ? (
859
+ isCode ? null : (
860
+ <text fg={uiTheme.muted} attributes={uiTheme.typography.note}>
861
+ {section.label}
862
+ </text>
863
+ )
864
+ ) : null}
865
+ {isCode ? (
866
+ <box>
867
+ <CodeBlock
868
+ content={content}
869
+ label={section.label}
870
+ languageHint={resolveSectionLanguageHint(toolName, section)}
871
+ collapsible={collapsible}
872
+ collapsedLines={COLLAPSIBLE_OUTPUT_LINES}
873
+ expanded={expanded}
874
+ onToggleExpanded={() => {
875
+ if (!collapsible) {
876
+ return;
877
+ }
878
+ setExpandedSections(previous => ({
879
+ ...previous,
880
+ [sectionId]: !previous[sectionId],
881
+ }));
882
+ }}
883
+ />
884
+ </box>
885
+ ) : (
886
+ <box marginTop={section.label ? 1 : 0}>
887
+ <text fg={uiTheme.text} attributes={uiTheme.typography.body} wrapMode="word">
888
+ {content}
889
+ </text>
890
+ </box>
891
+ )}
892
+ </box>
893
+ );
894
+ })}
895
+ </box>
896
+ </box>
897
+ ) : null}
898
+ </box>
899
+ );
900
+ };