@alumbwe/anvil 1.0.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 (529) hide show
  1. package/README.md +84 -0
  2. package/bin/anvil +2 -0
  3. package/bin/tree-sitter.wasm +0 -0
  4. package/package.json +67 -0
  5. package/src/__tests__/README.md +256 -0
  6. package/src/__tests__/bash-mode.test.ts +442 -0
  7. package/src/__tests__/cli-args.test.ts +181 -0
  8. package/src/__tests__/cli-harness.test.ts +52 -0
  9. package/src/__tests__/e2e/first-time-login.test.ts +173 -0
  10. package/src/__tests__/e2e/logout-relogin-flow.test.ts +110 -0
  11. package/src/__tests__/e2e/returning-user-auth.test.ts +113 -0
  12. package/src/__tests__/e2e-cli.test.ts +183 -0
  13. package/src/__tests__/helpers/launcher-disconnect-fixture.cjs +176 -0
  14. package/src/__tests__/helpers/mock-api-client.ts +64 -0
  15. package/src/__tests__/helpers/renderer-cleanup-fixture.tsx +89 -0
  16. package/src/__tests__/helpers/terminal-watchdog-fixture.ts +75 -0
  17. package/src/__tests__/home-directory-detection.test.ts +213 -0
  18. package/src/__tests__/integration/api-integration.test.ts +288 -0
  19. package/src/__tests__/integration/credentials-storage.test.ts +486 -0
  20. package/src/__tests__/integration/local-agents.test.ts +1187 -0
  21. package/src/__tests__/integration/login-flow-instrumentation.test.ts +134 -0
  22. package/src/__tests__/integration/login-polling-working.test.ts +285 -0
  23. package/src/__tests__/integration/queue-editing.test.tsx +261 -0
  24. package/src/__tests__/integration/usage-refresh-on-completion.test.ts +168 -0
  25. package/src/__tests__/integration-tmux.test.ts +188 -0
  26. package/src/__tests__/launcher-avx2-fallback.test.ts +630 -0
  27. package/src/__tests__/launcher-disconnect.test.ts +104 -0
  28. package/src/__tests__/mocks/hover-toggle-controller.ts +71 -0
  29. package/src/__tests__/open-tui-native-bundle.test.ts +378 -0
  30. package/src/__tests__/path-completion.test.ts +243 -0
  31. package/src/__tests__/project-files-chat-id.test.ts +34 -0
  32. package/src/__tests__/release/proxy-http-get.test.ts +667 -0
  33. package/src/__tests__/release/wrapper-safety.test.ts +412 -0
  34. package/src/__tests__/renderer-cleanup.test.ts +174 -0
  35. package/src/__tests__/rerender-perf.integration.test.ts +283 -0
  36. package/src/__tests__/terminal-reset-sequences.test.ts +36 -0
  37. package/src/__tests__/terminal-watchdog.test.ts +388 -0
  38. package/src/__tests__/test-utils.ts +271 -0
  39. package/src/__tests__/tmux-poc.ts +150 -0
  40. package/src/__tests__/unit/agent-mode-toggle.test.ts +137 -0
  41. package/src/__tests__/unit/copy-button.test.ts +157 -0
  42. package/src/__tests__/unit/create-run-config.test.ts +66 -0
  43. package/src/__tests__/unit/publish-confirmation.test.ts +70 -0
  44. package/src/__tests__/unit/segmented-control.test.ts +152 -0
  45. package/src/__tests__/utils/env.test.ts +169 -0
  46. package/src/__tests__/utils/project-picker.test.ts +39 -0
  47. package/src/agents/bundled-agents.generated.d.ts +14 -0
  48. package/src/agents/bundled-agents.generated.ts +3692 -0
  49. package/src/app.tsx +399 -0
  50. package/src/chat.tsx +1698 -0
  51. package/src/cli-args.ts +134 -0
  52. package/src/commands/__tests__/bash-command.test.ts +445 -0
  53. package/src/commands/__tests__/command-args.test.ts +349 -0
  54. package/src/commands/__tests__/copy-conversation-clipboard.test.ts +108 -0
  55. package/src/commands/__tests__/copy-conversation.test.ts +226 -0
  56. package/src/commands/__tests__/diagnostics-command.test.ts +35 -0
  57. package/src/commands/__tests__/freebuff-command-aliases.test.ts +56 -0
  58. package/src/commands/__tests__/image.test.ts +99 -0
  59. package/src/commands/__tests__/init.test.ts +456 -0
  60. package/src/commands/__tests__/new-command-rotates-chat.test.ts +91 -0
  61. package/src/commands/__tests__/process-diagnostics.test.ts +54 -0
  62. package/src/commands/__tests__/prompt-builders.test.ts +28 -0
  63. package/src/commands/__tests__/router-input.test.ts +298 -0
  64. package/src/commands/ads.ts +44 -0
  65. package/src/commands/command-registry.ts +726 -0
  66. package/src/commands/copy-conversation.ts +392 -0
  67. package/src/commands/help.ts +13 -0
  68. package/src/commands/image.ts +20 -0
  69. package/src/commands/init.ts +121 -0
  70. package/src/commands/process-diagnostics.ts +120 -0
  71. package/src/commands/prompt-builders.ts +100 -0
  72. package/src/commands/publish.ts +203 -0
  73. package/src/commands/router-utils.ts +80 -0
  74. package/src/commands/router.ts +487 -0
  75. package/src/commands/usage.ts +26 -0
  76. package/src/components/__tests__/ad-banner.test.tsx +83 -0
  77. package/src/components/__tests__/chat-input-bar.test.tsx +130 -0
  78. package/src/components/__tests__/freebuff-landing-heading-row.test.tsx +91 -0
  79. package/src/components/__tests__/freebuff-model-selector.test.tsx +519 -0
  80. package/src/components/__tests__/freebuff-offer-invariants.test.ts +54 -0
  81. package/src/components/__tests__/freebuff-takeover-prompt.test.tsx +212 -0
  82. package/src/components/__tests__/grid-layout.integration.test.tsx +304 -0
  83. package/src/components/__tests__/grid-layout.test.tsx +1051 -0
  84. package/src/components/__tests__/message-block.completion.test.tsx +104 -0
  85. package/src/components/__tests__/message-block.streaming.test.tsx +78 -0
  86. package/src/components/__tests__/message-with-agents.test.tsx +579 -0
  87. package/src/components/__tests__/multiline-input.test.tsx +1132 -0
  88. package/src/components/__tests__/queue-panel.test.tsx +398 -0
  89. package/src/components/__tests__/selectable-list.test.ts +232 -0
  90. package/src/components/__tests__/status-bar.test.tsx +44 -0
  91. package/src/components/__tests__/status-indicator.test.tsx +204 -0
  92. package/src/components/__tests__/terminal-thinking-orb.test.tsx +111 -0
  93. package/src/components/__tests__/user-error-banner.test.tsx +102 -0
  94. package/src/components/ad-banner.tsx +323 -0
  95. package/src/components/agent-checklist.tsx +350 -0
  96. package/src/components/agent-mode-toggle.tsx +241 -0
  97. package/src/components/anvil-mark.tsx +52 -0
  98. package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +432 -0
  99. package/src/components/ask-user/__tests__/validation.test.ts +213 -0
  100. package/src/components/ask-user/components/accordion-question.tsx +153 -0
  101. package/src/components/ask-user/components/custom-answer-input.tsx +66 -0
  102. package/src/components/ask-user/components/options-list.tsx +133 -0
  103. package/src/components/ask-user/components/question-header.tsx +80 -0
  104. package/src/components/ask-user/components/question-option.tsx +89 -0
  105. package/src/components/ask-user/constants.ts +38 -0
  106. package/src/components/ask-user/index.tsx +631 -0
  107. package/src/components/ask-user/utils/validation.ts +85 -0
  108. package/src/components/attachment-card.tsx +65 -0
  109. package/src/components/blocks/agent-block-grid.tsx +58 -0
  110. package/src/components/blocks/agent-branch-item.tsx +311 -0
  111. package/src/components/blocks/agent-branch-wrapper.tsx +493 -0
  112. package/src/components/blocks/agent-list-branch.tsx +77 -0
  113. package/src/components/blocks/ask-user-branch.tsx +81 -0
  114. package/src/components/blocks/block-helpers.ts +11 -0
  115. package/src/components/blocks/blocks-renderer.tsx +289 -0
  116. package/src/components/blocks/content-with-markdown.tsx +33 -0
  117. package/src/components/blocks/image-block.tsx +128 -0
  118. package/src/components/blocks/implementor-row.tsx +429 -0
  119. package/src/components/blocks/single-block.tsx +201 -0
  120. package/src/components/blocks/thinking-block.tsx +67 -0
  121. package/src/components/blocks/tool-block-group.tsx +57 -0
  122. package/src/components/blocks/tool-branch.tsx +206 -0
  123. package/src/components/blocks/user-content-copy.tsx +157 -0
  124. package/src/components/bottom-banner.tsx +136 -0
  125. package/src/components/build-mode-buttons.tsx +110 -0
  126. package/src/components/button.tsx +66 -0
  127. package/src/components/chat-header.tsx +77 -0
  128. package/src/components/chat-history-screen.tsx +413 -0
  129. package/src/components/chat-input-bar.tsx +523 -0
  130. package/src/components/clickable-title-box.tsx +66 -0
  131. package/src/components/clickable.tsx +119 -0
  132. package/src/components/collapse-button.tsx +34 -0
  133. package/src/components/copy-button.tsx +203 -0
  134. package/src/components/elapsed-timer.tsx +55 -0
  135. package/src/components/error-boundary.tsx +55 -0
  136. package/src/components/feedback-container.tsx +181 -0
  137. package/src/components/feedback-icon-button.tsx +89 -0
  138. package/src/components/feedback-input-mode.tsx +344 -0
  139. package/src/components/file-attachment-card.tsx +98 -0
  140. package/src/components/freebuff-active-session-summary.tsx +63 -0
  141. package/src/components/freebuff-landing-screen.tsx +865 -0
  142. package/src/components/freebuff-model-selector.tsx +1087 -0
  143. package/src/components/freebuff-referral-banner.tsx +604 -0
  144. package/src/components/freebuff-superseded-screen.tsx +62 -0
  145. package/src/components/grid-layout.tsx +80 -0
  146. package/src/components/help-banner.tsx +127 -0
  147. package/src/components/highlighted-text.tsx +52 -0
  148. package/src/components/image-card.tsx +167 -0
  149. package/src/components/image-thumbnail.tsx +114 -0
  150. package/src/components/input-cursor.tsx +77 -0
  151. package/src/components/input-mode-banner.tsx +57 -0
  152. package/src/components/load-previous-button.tsx +48 -0
  153. package/src/components/login-modal.tsx +475 -0
  154. package/src/components/message-block.tsx +346 -0
  155. package/src/components/message-footer.tsx +258 -0
  156. package/src/components/message-with-agents.tsx +506 -0
  157. package/src/components/mode-divider.tsx +43 -0
  158. package/src/components/multiline-input.tsx +1241 -0
  159. package/src/components/out-of-credits-banner.tsx +160 -0
  160. package/src/components/pending-attachments-banner.tsx +116 -0
  161. package/src/components/pending-bash-message.tsx +59 -0
  162. package/src/components/progress-bar.tsx +81 -0
  163. package/src/components/project-picker-screen.tsx +485 -0
  164. package/src/components/publish-confirmation.tsx +493 -0
  165. package/src/components/publish-container.tsx +687 -0
  166. package/src/components/queue-panel.tsx +310 -0
  167. package/src/components/raised-pill.tsx +95 -0
  168. package/src/components/renderers/plan-box.tsx +59 -0
  169. package/src/components/review-screen.tsx +112 -0
  170. package/src/components/scroll-to-bottom-button.tsx +34 -0
  171. package/src/components/segmented-control.tsx +187 -0
  172. package/src/components/selectable-list.tsx +248 -0
  173. package/src/components/selected-chips.tsx +80 -0
  174. package/src/components/separator.tsx +40 -0
  175. package/src/components/session-ended-banner.tsx +226 -0
  176. package/src/components/shimmer-text.tsx +248 -0
  177. package/src/components/status-bar.tsx +313 -0
  178. package/src/components/subscription-limit-banner.tsx +206 -0
  179. package/src/components/suggested-prompts.tsx +184 -0
  180. package/src/components/suggestion-menu.tsx +215 -0
  181. package/src/components/terminal-command-display.tsx +151 -0
  182. package/src/components/terminal-link.tsx +97 -0
  183. package/src/components/terminal-thinking-orb.tsx +95 -0
  184. package/src/components/text-attachment-card.tsx +77 -0
  185. package/src/components/thinking.tsx +109 -0
  186. package/src/components/tools/__tests__/apply-patch.test.tsx +84 -0
  187. package/src/components/tools/__tests__/code-search.test.tsx +45 -0
  188. package/src/components/tools/__tests__/gravity-index.test.ts +76 -0
  189. package/src/components/tools/__tests__/render-ui.test.tsx +92 -0
  190. package/src/components/tools/__tests__/run-terminal-command.test.ts +233 -0
  191. package/src/components/tools/apply-patch.tsx +95 -0
  192. package/src/components/tools/code-search.tsx +45 -0
  193. package/src/components/tools/composio.tsx +152 -0
  194. package/src/components/tools/diff-viewer.tsx +76 -0
  195. package/src/components/tools/glob.tsx +61 -0
  196. package/src/components/tools/gravity-index.tsx +77 -0
  197. package/src/components/tools/list-directory.tsx +61 -0
  198. package/src/components/tools/read-docs.tsx +37 -0
  199. package/src/components/tools/read-files.tsx +91 -0
  200. package/src/components/tools/read-subtree.tsx +44 -0
  201. package/src/components/tools/read-url.tsx +33 -0
  202. package/src/components/tools/registry.ts +126 -0
  203. package/src/components/tools/render-ui.tsx +122 -0
  204. package/src/components/tools/run-terminal-command.tsx +79 -0
  205. package/src/components/tools/skill.tsx +29 -0
  206. package/src/components/tools/str-replace.tsx +77 -0
  207. package/src/components/tools/suggest-followups.tsx +369 -0
  208. package/src/components/tools/task-completed.tsx +16 -0
  209. package/src/components/tools/tool-call-item.tsx +273 -0
  210. package/src/components/tools/types.ts +55 -0
  211. package/src/components/tools/web-search.tsx +33 -0
  212. package/src/components/tools/write-file.tsx +16 -0
  213. package/src/components/tools/write-todos.tsx +95 -0
  214. package/src/components/top-banner.tsx +178 -0
  215. package/src/components/usage-banner.tsx +244 -0
  216. package/src/components/user-error-banner.tsx +56 -0
  217. package/src/components/validation-error-popover.tsx +192 -0
  218. package/src/contexts/__tests__/chat-runtime-context.test.tsx +149 -0
  219. package/src/contexts/chat-runtime-context.tsx +240 -0
  220. package/src/data/slash-commands.test.ts +24 -0
  221. package/src/data/slash-commands.ts +249 -0
  222. package/src/entry.ts +12 -0
  223. package/src/hooks/__tests__/holds-live-freebuff-slot.test.ts +49 -0
  224. package/src/hooks/__tests__/session-fetch-signal.test.ts +63 -0
  225. package/src/hooks/__tests__/use-activity-query.test.ts +1000 -0
  226. package/src/hooks/__tests__/use-ask-user-bridge.test.ts +176 -0
  227. package/src/hooks/__tests__/use-auth-query.test.ts +21 -0
  228. package/src/hooks/__tests__/use-chat-input-remount.test.tsx +66 -0
  229. package/src/hooks/__tests__/use-clipboard.test.tsx +73 -0
  230. package/src/hooks/__tests__/use-connection-status.test.ts +112 -0
  231. package/src/hooks/__tests__/use-directory-browser.test.ts +203 -0
  232. package/src/hooks/__tests__/use-gravity-ad.test.ts +73 -0
  233. package/src/hooks/__tests__/use-grid-layout.test.ts +349 -0
  234. package/src/hooks/__tests__/use-input-history.test.ts +699 -0
  235. package/src/hooks/__tests__/use-message-queue-editing.test.tsx +209 -0
  236. package/src/hooks/__tests__/use-message-queue.test.tsx +71 -0
  237. package/src/hooks/__tests__/use-path-tab-completion.test.ts +315 -0
  238. package/src/hooks/__tests__/use-queue-controls.test.ts +68 -0
  239. package/src/hooks/__tests__/use-queue-ui.test.tsx +83 -0
  240. package/src/hooks/__tests__/use-searchable-list.test.ts +359 -0
  241. package/src/hooks/__tests__/use-send-message-timer.test.ts +146 -0
  242. package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +361 -0
  243. package/src/hooks/__tests__/use-suggestion-engine.test.ts +485 -0
  244. package/src/hooks/__tests__/use-terminal-focus.test.ts +66 -0
  245. package/src/hooks/__tests__/use-terminal-layout.test.ts +675 -0
  246. package/src/hooks/__tests__/use-timeout.test.ts +330 -0
  247. package/src/hooks/__tests__/use-usage-query.test.ts +502 -0
  248. package/src/hooks/__tests__/use-user-details-query.test.ts +195 -0
  249. package/src/hooks/helpers/__tests__/send-message.test.ts +1918 -0
  250. package/src/hooks/helpers/send-message.ts +654 -0
  251. package/src/hooks/stream-state.ts +83 -0
  252. package/src/hooks/use-activity-query.ts +668 -0
  253. package/src/hooks/use-agent-validation.ts +74 -0
  254. package/src/hooks/use-ask-user-bridge.ts +92 -0
  255. package/src/hooks/use-auth-query.ts +253 -0
  256. package/src/hooks/use-auth-state.ts +152 -0
  257. package/src/hooks/use-chat-input.ts +105 -0
  258. package/src/hooks/use-chat-keyboard.ts +321 -0
  259. package/src/hooks/use-chat-messages.ts +254 -0
  260. package/src/hooks/use-chat-state.ts +165 -0
  261. package/src/hooks/use-chat-streaming.ts +180 -0
  262. package/src/hooks/use-chat-ui.ts +131 -0
  263. package/src/hooks/use-clipboard.ts +151 -0
  264. package/src/hooks/use-connection-status.ts +142 -0
  265. package/src/hooks/use-directory-browser.ts +88 -0
  266. package/src/hooks/use-elapsed-time.ts +116 -0
  267. package/src/hooks/use-event.ts +34 -0
  268. package/src/hooks/use-exit-handler.ts +66 -0
  269. package/src/hooks/use-fetch-login-url.ts +65 -0
  270. package/src/hooks/use-fingerprint.ts +61 -0
  271. package/src/hooks/use-freebuff-ctrl-c-exit.ts +23 -0
  272. package/src/hooks/use-freebuff-session-progress.ts +34 -0
  273. package/src/hooks/use-freebuff-session.ts +806 -0
  274. package/src/hooks/use-freebuff-streak-query.ts +61 -0
  275. package/src/hooks/use-gravity-ad.ts +656 -0
  276. package/src/hooks/use-grid-layout.ts +73 -0
  277. package/src/hooks/use-input-history.ts +173 -0
  278. package/src/hooks/use-login-keyboard-handlers.ts +75 -0
  279. package/src/hooks/use-login-polling.ts +114 -0
  280. package/src/hooks/use-logo.tsx +161 -0
  281. package/src/hooks/use-message-queue.ts +387 -0
  282. package/src/hooks/use-now.ts +20 -0
  283. package/src/hooks/use-path-tab-completion.ts +88 -0
  284. package/src/hooks/use-publish-mutation.ts +60 -0
  285. package/src/hooks/use-queue-controls.ts +68 -0
  286. package/src/hooks/use-queue-ui.ts +74 -0
  287. package/src/hooks/use-scroll-management.ts +184 -0
  288. package/src/hooks/use-searchable-list.ts +98 -0
  289. package/src/hooks/use-send-message.ts +773 -0
  290. package/src/hooks/use-sheen-animation.tsx +88 -0
  291. package/src/hooks/use-subscription-query.ts +71 -0
  292. package/src/hooks/use-suggestion-engine.ts +783 -0
  293. package/src/hooks/use-terminal-breakpoints.ts +77 -0
  294. package/src/hooks/use-terminal-dimensions.ts +46 -0
  295. package/src/hooks/use-terminal-focus.ts +29 -0
  296. package/src/hooks/use-terminal-layout.ts +189 -0
  297. package/src/hooks/use-theme.tsx +140 -0
  298. package/src/hooks/use-timeout.ts +85 -0
  299. package/src/hooks/use-update-preference.ts +66 -0
  300. package/src/hooks/use-usage-monitor.ts +55 -0
  301. package/src/hooks/use-usage-query.ts +124 -0
  302. package/src/hooks/use-user-details-query.ts +93 -0
  303. package/src/hooks/use-why-did-you-update.ts +177 -0
  304. package/src/index.tsx +441 -0
  305. package/src/init/__tests__/init-direnv.test.ts +526 -0
  306. package/src/init/init-app.ts +34 -0
  307. package/src/init/init-direnv.ts +133 -0
  308. package/src/login/constants.ts +60 -0
  309. package/src/login/login-flow.ts +204 -0
  310. package/src/login/plain-login.ts +108 -0
  311. package/src/login/utils.ts +183 -0
  312. package/src/native/ripgrep.ts +80 -0
  313. package/src/polyfills/bun-strip-ansi.ts +12 -0
  314. package/src/pre-init/tree-sitter-wasm.ts +96 -0
  315. package/src/project-files.ts +102 -0
  316. package/src/smoke/terminal-command-broker.ts +439 -0
  317. package/src/state/__tests__/chat-store-focus.test.ts +22 -0
  318. package/src/state/__tests__/feedback-store.test.ts +257 -0
  319. package/src/state/chat-history-store.ts +30 -0
  320. package/src/state/chat-store.ts +544 -0
  321. package/src/state/feedback-store.ts +160 -0
  322. package/src/state/freebuff-model-store.ts +42 -0
  323. package/src/state/freebuff-session-store.ts +53 -0
  324. package/src/state/login-store.ts +150 -0
  325. package/src/state/message-block-store.ts +149 -0
  326. package/src/state/publish-store.ts +146 -0
  327. package/src/state/queue-panel-store.ts +26 -0
  328. package/src/state/review-store.ts +24 -0
  329. package/src/testing/env.ts +46 -0
  330. package/src/types/chat-state.ts +15 -0
  331. package/src/types/chat.ts +241 -0
  332. package/src/types/contracts/send-message.ts +12 -0
  333. package/src/types/env.ts +98 -0
  334. package/src/types/freebuff-session.ts +17 -0
  335. package/src/types/function-params.ts +28 -0
  336. package/src/types/react19-compat.d.ts +19 -0
  337. package/src/types/store.ts +111 -0
  338. package/src/types/theme-system.ts +144 -0
  339. package/src/types/utils.ts +3 -0
  340. package/src/utils/__tests__/active-run.test.ts +140 -0
  341. package/src/utils/__tests__/activity-tracker.test.ts +440 -0
  342. package/src/utils/__tests__/agent-display.test.ts +139 -0
  343. package/src/utils/__tests__/analytics-client.test.ts +262 -0
  344. package/src/utils/__tests__/anonymous-id.test.ts +74 -0
  345. package/src/utils/__tests__/anvil-api.test.ts +578 -0
  346. package/src/utils/__tests__/arrays.test.ts +264 -0
  347. package/src/utils/__tests__/bash-context-processor.test.ts +38 -0
  348. package/src/utils/__tests__/block-processor.test.ts +810 -0
  349. package/src/utils/__tests__/chat-history.test.ts +203 -0
  350. package/src/utils/__tests__/chat-input-key-intercept.test.ts +40 -0
  351. package/src/utils/__tests__/chat-meta.test.ts +130 -0
  352. package/src/utils/__tests__/clipboard-linux.test.ts +359 -0
  353. package/src/utils/__tests__/clipboard.test.ts +910 -0
  354. package/src/utils/__tests__/code-search-summary.test.ts +84 -0
  355. package/src/utils/__tests__/collapse-helpers.test.ts +1109 -0
  356. package/src/utils/__tests__/error-handling.test.ts +658 -0
  357. package/src/utils/__tests__/exit-cleanly.test.ts +110 -0
  358. package/src/utils/__tests__/feedback-helpers.test.ts +444 -0
  359. package/src/utils/__tests__/feedback-submission.test.ts +26 -0
  360. package/src/utils/__tests__/fetch-usage.test.ts +355 -0
  361. package/src/utils/__tests__/fingerprint.test.ts +144 -0
  362. package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +7 -0
  363. package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +3 -0
  364. package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +33 -0
  365. package/src/utils/__tests__/format-elapsed-time.test.ts +103 -0
  366. package/src/utils/__tests__/format-timeout.test.ts +87 -0
  367. package/src/utils/__tests__/freebuff-instance-owner.test.ts +69 -0
  368. package/src/utils/__tests__/freebuff-model-navigation.test.ts +102 -0
  369. package/src/utils/__tests__/freebuff-premium-reset.test.ts +79 -0
  370. package/src/utils/__tests__/freebuff-referral-cache.test.ts +116 -0
  371. package/src/utils/__tests__/freebuff-session-api.test.ts +215 -0
  372. package/src/utils/__tests__/freebuff-session-display.test.ts +23 -0
  373. package/src/utils/__tests__/freebuff-streak-line.test.ts +217 -0
  374. package/src/utils/__tests__/image-dimensions.test.ts +249 -0
  375. package/src/utils/__tests__/image-processor.test.ts +209 -0
  376. package/src/utils/__tests__/implementor-helpers.test.ts +1458 -0
  377. package/src/utils/__tests__/keyboard-actions.test.ts +660 -0
  378. package/src/utils/__tests__/layout-helpers.test.ts +36 -0
  379. package/src/utils/__tests__/lazy-response-ads.test.ts +167 -0
  380. package/src/utils/__tests__/log-shipper.test.ts +53 -0
  381. package/src/utils/__tests__/markdown-renderer.test.tsx +421 -0
  382. package/src/utils/__tests__/message-block-helpers.test.ts +1263 -0
  383. package/src/utils/__tests__/message-updater.test.ts +645 -0
  384. package/src/utils/__tests__/osc-timeout-scenarios.test.ts +172 -0
  385. package/src/utils/__tests__/pending-attachments.test.ts +277 -0
  386. package/src/utils/__tests__/polling-backoff.test.ts +51 -0
  387. package/src/utils/__tests__/queue-panel-actions.test.ts +148 -0
  388. package/src/utils/__tests__/run-state-storage.test.ts +919 -0
  389. package/src/utils/__tests__/safe-json.test.ts +77 -0
  390. package/src/utils/__tests__/sdk-event-handlers.test.ts +685 -0
  391. package/src/utils/__tests__/send-message-helpers.test.ts +1898 -0
  392. package/src/utils/__tests__/send-message-timer.test.ts +64 -0
  393. package/src/utils/__tests__/settings.test.ts +99 -0
  394. package/src/utils/__tests__/strings.test.ts +192 -0
  395. package/src/utils/__tests__/terminal-color-detection.test.ts +425 -0
  396. package/src/utils/__tests__/terminal-command-broker.test.ts +528 -0
  397. package/src/utils/__tests__/terminal-enter-detection.test.ts +77 -0
  398. package/src/utils/__tests__/terminal-io.test.ts +137 -0
  399. package/src/utils/__tests__/terminal-protocol-controller.test.ts +119 -0
  400. package/src/utils/__tests__/text-layout.test.ts +82 -0
  401. package/src/utils/__tests__/theme-platform-detection.test.ts +125 -0
  402. package/src/utils/__tests__/think-tag-parser.test.ts +159 -0
  403. package/src/utils/__tests__/trace-writer.test.ts +223 -0
  404. package/src/utils/__tests__/trim-chat-logs.test.ts +78 -0
  405. package/src/utils/__tests__/usage-banner-state.test.ts +298 -0
  406. package/src/utils/__tests__/validation-error-formatting.test.ts +126 -0
  407. package/src/utils/__tests__/windows-terminal-health.test.ts +101 -0
  408. package/src/utils/__tests__/write-file-atomic.test.ts +127 -0
  409. package/src/utils/active-run.ts +131 -0
  410. package/src/utils/activity-tracker.ts +66 -0
  411. package/src/utils/agent-display.ts +87 -0
  412. package/src/utils/agent-helpers.ts +27 -0
  413. package/src/utils/agent-id-utils.ts +14 -0
  414. package/src/utils/ai-message-id.ts +12 -0
  415. package/src/utils/analytics.ts +361 -0
  416. package/src/utils/anonymous-id.ts +87 -0
  417. package/src/utils/anvil-api.ts +612 -0
  418. package/src/utils/anvil-client.ts +217 -0
  419. package/src/utils/arrays.ts +117 -0
  420. package/src/utils/auth.ts +258 -0
  421. package/src/utils/bash-context-processor.ts +47 -0
  422. package/src/utils/bash-messages.ts +108 -0
  423. package/src/utils/block-margins.ts +35 -0
  424. package/src/utils/block-operations.ts +523 -0
  425. package/src/utils/block-processor.ts +213 -0
  426. package/src/utils/chat-history.ts +253 -0
  427. package/src/utils/chat-input-key-intercept.ts +52 -0
  428. package/src/utils/chat-meta.ts +90 -0
  429. package/src/utils/chat-scroll-accel.ts +151 -0
  430. package/src/utils/clipboard-image.ts +588 -0
  431. package/src/utils/clipboard.ts +397 -0
  432. package/src/utils/code-search-summary.ts +70 -0
  433. package/src/utils/collapse-helpers.ts +256 -0
  434. package/src/utils/config-dir.ts +24 -0
  435. package/src/utils/constants.ts +190 -0
  436. package/src/utils/create-event-handler-state.ts +73 -0
  437. package/src/utils/create-run-config.ts +121 -0
  438. package/src/utils/detect-shell.ts +116 -0
  439. package/src/utils/directory-browser.ts +72 -0
  440. package/src/utils/engagement.ts +55 -0
  441. package/src/utils/env.ts +92 -0
  442. package/src/utils/error-handling.ts +255 -0
  443. package/src/utils/error-messages.ts +62 -0
  444. package/src/utils/exit-cleanly.ts +86 -0
  445. package/src/utils/feedback-helpers.ts +103 -0
  446. package/src/utils/feedback-submission.ts +22 -0
  447. package/src/utils/fetch-usage.ts +76 -0
  448. package/src/utils/fingerprint.ts +245 -0
  449. package/src/utils/format-elapsed-time.ts +33 -0
  450. package/src/utils/format-session-units.ts +6 -0
  451. package/src/utils/format-timeout.ts +28 -0
  452. package/src/utils/format-validation-errors-for-message.ts +84 -0
  453. package/src/utils/freebuff-agent-selection.ts +33 -0
  454. package/src/utils/freebuff-instance-owner.ts +66 -0
  455. package/src/utils/freebuff-model-navigation.ts +50 -0
  456. package/src/utils/freebuff-premium-reset.ts +55 -0
  457. package/src/utils/freebuff-referral-cache.ts +51 -0
  458. package/src/utils/freebuff-session-api.ts +243 -0
  459. package/src/utils/freebuff-session-display.ts +21 -0
  460. package/src/utils/freebuff-streak-line.ts +93 -0
  461. package/src/utils/git.ts +17 -0
  462. package/src/utils/helpers.ts +47 -0
  463. package/src/utils/image-display.ts +67 -0
  464. package/src/utils/image-handler.ts +325 -0
  465. package/src/utils/image-processor.ts +133 -0
  466. package/src/utils/image-thumbnail.ts +78 -0
  467. package/src/utils/implementor-helpers.ts +787 -0
  468. package/src/utils/input-modes.ts +174 -0
  469. package/src/utils/keyboard-actions.ts +392 -0
  470. package/src/utils/keypad-keys.ts +47 -0
  471. package/src/utils/layout-helpers.ts +33 -0
  472. package/src/utils/lazy-response-ads.ts +1 -0
  473. package/src/utils/local-agent-registry.ts +474 -0
  474. package/src/utils/log-shipper.ts +126 -0
  475. package/src/utils/logger.ts +327 -0
  476. package/src/utils/markdown-renderer.tsx +1090 -0
  477. package/src/utils/math.ts +3 -0
  478. package/src/utils/message-block-helpers.ts +684 -0
  479. package/src/utils/message-history.ts +143 -0
  480. package/src/utils/message-tree-utils.ts +67 -0
  481. package/src/utils/message-updater.ts +279 -0
  482. package/src/utils/open-file.ts +146 -0
  483. package/src/utils/open-url.ts +63 -0
  484. package/src/utils/path-completion.ts +111 -0
  485. package/src/utils/path-helpers.ts +38 -0
  486. package/src/utils/pending-attachments.ts +360 -0
  487. package/src/utils/polling-backoff.ts +59 -0
  488. package/src/utils/project-picker.ts +12 -0
  489. package/src/utils/queue-panel-actions.ts +70 -0
  490. package/src/utils/recent-projects.ts +156 -0
  491. package/src/utils/renderer-cleanup.ts +233 -0
  492. package/src/utils/response-ad-positions.ts +1 -0
  493. package/src/utils/run-state-storage.ts +603 -0
  494. package/src/utils/safe-json.ts +153 -0
  495. package/src/utils/sdk-event-handlers.ts +571 -0
  496. package/src/utils/send-message-helpers.ts +210 -0
  497. package/src/utils/send-message-timer.ts +110 -0
  498. package/src/utils/settings.ts +226 -0
  499. package/src/utils/skill-registry.ts +94 -0
  500. package/src/utils/spawn-agent-matcher.ts +55 -0
  501. package/src/utils/status-indicator-state.ts +107 -0
  502. package/src/utils/stream-chunk-processor.ts +57 -0
  503. package/src/utils/strings.ts +271 -0
  504. package/src/utils/subscription.ts +31 -0
  505. package/src/utils/syntax-highlighter.tsx +19 -0
  506. package/src/utils/terminal-color-detection.ts +474 -0
  507. package/src/utils/terminal-command-broker.ts +467 -0
  508. package/src/utils/terminal-enter-detection.ts +63 -0
  509. package/src/utils/terminal-images.ts +229 -0
  510. package/src/utils/terminal-io.ts +85 -0
  511. package/src/utils/terminal-protocol-controller.ts +173 -0
  512. package/src/utils/terminal-reset-sequences.ts +33 -0
  513. package/src/utils/terminal-title.ts +84 -0
  514. package/src/utils/terminal-watchdog.ts +366 -0
  515. package/src/utils/text-layout.ts +149 -0
  516. package/src/utils/theme-config.ts +141 -0
  517. package/src/utils/theme-system.ts +1206 -0
  518. package/src/utils/think-tag-parser.ts +95 -0
  519. package/src/utils/time-format.ts +21 -0
  520. package/src/utils/trace-writer.ts +162 -0
  521. package/src/utils/ui-constants.ts +69 -0
  522. package/src/utils/update-checker.ts +110 -0
  523. package/src/utils/usage-banner-state.ts +146 -0
  524. package/src/utils/validation-error-formatting.ts +86 -0
  525. package/src/utils/validation-error-helpers.ts +19 -0
  526. package/src/utils/windows-terminal-health.ts +124 -0
  527. package/src/utils/word-wrap-utils.ts +53 -0
  528. package/src/utils/write-file-atomic.ts +54 -0
  529. package/src/utils/yield-to-event-loop.ts +9 -0
@@ -0,0 +1,369 @@
1
+ import { TextAttributes } from '@opentui/core'
2
+ import { useCallback, useEffect, useState } from 'react'
3
+
4
+ import { defineToolComponent } from './types'
5
+ import { useTerminalDimensions } from '../../hooks/use-terminal-dimensions'
6
+ import { useTheme } from '../../hooks/use-theme'
7
+ import { getLatestFollowupToolCallId, useChatStore } from '../../state/chat-store'
8
+ import { useFreebuffSessionStore } from '../../state/freebuff-session-store'
9
+ import { IS_FREEBUFF } from '../../utils/constants'
10
+ import { Button } from '../button'
11
+
12
+ import type { ToolRenderConfig } from './types'
13
+ import type { SuggestedFollowup } from '../../types/store'
14
+
15
+ const EMPTY_CLICKED_SET = new Set<number>()
16
+ const MIN_LABEL_COLUMN_WIDTH = 12
17
+ const MAX_LABEL_COLUMN_WIDTH = 60
18
+ /** Minimum terminal width to show the prompt description on hover */
19
+ const MIN_WIDTH_FOR_DESCRIPTION = 80
20
+
21
+ interface FollowupLineProps {
22
+ followup: SuggestedFollowup
23
+ index: number
24
+ isClicked: boolean
25
+ isHovered: boolean
26
+ onSendFollowup: (prompt: string, index: number) => void
27
+ onHover: (index: number | null) => void
28
+ disabled?: boolean
29
+ /** Width of the label column (for fixed-width alignment) */
30
+ labelColumnWidth: number
31
+ }
32
+
33
+ const FollowupLine = ({
34
+ followup,
35
+ index,
36
+ isClicked,
37
+ isHovered,
38
+ onSendFollowup,
39
+ onHover,
40
+ disabled,
41
+ labelColumnWidth,
42
+ }: FollowupLineProps) => {
43
+ const theme = useTheme()
44
+ const { terminalWidth } = useTerminalDimensions()
45
+
46
+ const handleClick = useCallback(() => {
47
+ if (!disabled) {
48
+ onSendFollowup(followup.prompt, index)
49
+ }
50
+ }, [followup.prompt, index, onSendFollowup, disabled])
51
+
52
+ const handleMouseOver = useCallback(() => onHover(index), [onHover, index])
53
+ const handleMouseOut = useCallback(() => onHover(null), [onHover])
54
+
55
+ // Compute effective hover state declaratively
56
+ // Show hover effects if actually hovered AND not disabled AND not already clicked
57
+ const showHoverState = isHovered && !disabled && !isClicked
58
+
59
+ const hasLabel = Boolean(followup.label)
60
+ const displayText = hasLabel ? followup.label : followup.prompt
61
+
62
+ // Show description when hovered, has a label, and terminal is wide enough
63
+ const showDescription =
64
+ showHoverState && hasLabel && terminalWidth >= MIN_WIDTH_FOR_DESCRIPTION
65
+
66
+ // Calculate truncated prompt with ellipsis only when needed
67
+ const truncatedPrompt = showDescription
68
+ ? (() => {
69
+ const availableWidth = Math.max(0, terminalWidth - labelColumnWidth - 4)
70
+ return followup.prompt.length > availableWidth
71
+ ? followup.prompt.slice(0, availableWidth - 1) + '…'
72
+ : followup.prompt
73
+ })()
74
+ : ''
75
+
76
+ // Determine colors based on state
77
+ // When hovered, use primary color (violet) for both arrow and title
78
+ const iconColor = isClicked
79
+ ? theme.success
80
+ : showHoverState
81
+ ? theme.primary
82
+ : theme.muted
83
+ const labelColor = isClicked
84
+ ? theme.muted
85
+ : showHoverState
86
+ ? theme.primary
87
+ : theme.foreground
88
+
89
+ // Calculate padding spaces needed to align descriptions (only when showing description)
90
+ const labelLength = (displayText ?? '').length
91
+ const paddingSpaces = showDescription
92
+ ? ' '.repeat(Math.max(0, labelColumnWidth - 2 - labelLength)) // -2 for "→ " prefix
93
+ : ''
94
+
95
+ return (
96
+ <box style={{ flexDirection: 'column' }}>
97
+ {/* Row layout: clickable label + non-clickable description */}
98
+ <box style={{ flexDirection: 'row', width: '100%' }}>
99
+ {/* Clickable label area - only the text itself is clickable */}
100
+ <Button
101
+ onClick={handleClick}
102
+ onMouseOver={handleMouseOver}
103
+ onMouseOut={handleMouseOut}
104
+ style={{
105
+ flexShrink: 0,
106
+ flexGrow: 0,
107
+ backgroundColor: showHoverState ? theme.surface : undefined,
108
+ }}
109
+ >
110
+ <text style={{ wrapMode: hasLabel ? 'none' : 'word' }}>
111
+ <span fg={iconColor}>{isClicked ? '✓' : '→'}</span>
112
+ <span
113
+ fg={labelColor}
114
+ attributes={showHoverState ? TextAttributes.BOLD : undefined}
115
+ >
116
+ {' '}
117
+ {displayText}
118
+ </span>
119
+ </text>
120
+ </Button>
121
+ {/* Flexible description column - NOT clickable, with padding for alignment */}
122
+ {showDescription && hasLabel && (
123
+ <box style={{ flexGrow: 1 }}>
124
+ <text style={{ wrapMode: 'none' }}>
125
+ <span fg={theme.muted} attributes={TextAttributes.ITALIC}>
126
+ {paddingSpaces}{truncatedPrompt}
127
+ </span>
128
+ </text>
129
+ </box>
130
+ )}
131
+ </box>
132
+ </box>
133
+ )
134
+ }
135
+
136
+ interface SuggestFollowupsItemProps {
137
+ toolCallId: string
138
+ followups: SuggestedFollowup[]
139
+ onSendFollowup: (prompt: string, index: number) => void
140
+ }
141
+
142
+ interface PastFollowupItemProps {
143
+ followup: SuggestedFollowup
144
+ isClicked: boolean
145
+ }
146
+
147
+ const PastFollowupItem = ({ followup, isClicked }: PastFollowupItemProps) => {
148
+ const theme = useTheme()
149
+ const displayLabel = followup.label || followup.prompt
150
+ const showFullPrompt = followup.label && followup.label !== followup.prompt
151
+
152
+ return (
153
+ <box style={{ flexDirection: 'column', marginLeft: 2 }}>
154
+ <text>
155
+ <span fg={isClicked ? theme.success : theme.muted}>
156
+ {isClicked ? '✓' : '→'}
157
+ </span>
158
+ <span fg={isClicked ? theme.muted : theme.foreground}>
159
+ {' '}
160
+ {displayLabel}
161
+ </span>
162
+ </text>
163
+ {showFullPrompt && (
164
+ <text style={{ marginLeft: 2 }}>
165
+ <span fg={theme.muted} attributes={TextAttributes.ITALIC}>
166
+ {followup.prompt}
167
+ </span>
168
+ </text>
169
+ )}
170
+ </box>
171
+ )
172
+ }
173
+
174
+ interface PastFollowupsToggleProps {
175
+ toolCallId: string
176
+ followups: SuggestedFollowup[]
177
+ }
178
+
179
+ const PastFollowupsToggle = ({
180
+ toolCallId,
181
+ followups,
182
+ }: PastFollowupsToggleProps) => {
183
+ const theme = useTheme()
184
+ const [isExpanded, setIsExpanded] = useState(false)
185
+ const clickedIndices = useChatStore(
186
+ (state) => state.clickedFollowupsMap.get(toolCallId) ?? EMPTY_CLICKED_SET,
187
+ )
188
+
189
+ const handleToggle = useCallback(() => {
190
+ setIsExpanded((prev) => !prev)
191
+ }, [])
192
+
193
+ const toggleIndicator = isExpanded ? '▾' : '▸'
194
+
195
+ return (
196
+ <box style={{ flexDirection: 'column' }}>
197
+ <Button onClick={handleToggle}>
198
+ <text>
199
+ <span fg={theme.muted}>{toggleIndicator}</span>
200
+ <span fg={theme.muted} attributes={TextAttributes.ITALIC}>
201
+ {' '}
202
+ Previously suggested followups
203
+ </span>
204
+ </text>
205
+ </Button>
206
+ {isExpanded && (
207
+ <box style={{ flexDirection: 'column', marginTop: 0 }}>
208
+ {followups.map((followup, index) => (
209
+ <PastFollowupItem
210
+ key={`past-followup-${index}`}
211
+ followup={followup}
212
+ isClicked={clickedIndices.has(index)}
213
+ />
214
+ ))}
215
+ </box>
216
+ )}
217
+ </box>
218
+ )
219
+ }
220
+
221
+ const SuggestFollowupsItem = ({
222
+ toolCallId,
223
+ followups,
224
+ onSendFollowup,
225
+ }: SuggestFollowupsItemProps) => {
226
+ const theme = useTheme()
227
+ const inputFocused = useChatStore((state) => state.inputFocused)
228
+ const isFreebuffSessionOver = useFreebuffSessionStore(
229
+ (state) => IS_FREEBUFF && state.session?.status === 'ended',
230
+ )
231
+ const setSuggestedFollowups = useChatStore(
232
+ (state) => state.setSuggestedFollowups,
233
+ )
234
+ const latestFollowupToolCallId = useChatStore((state) =>
235
+ getLatestFollowupToolCallId(state.messages),
236
+ )
237
+ const clickedIndices = useChatStore(
238
+ (state) => state.clickedFollowupsMap.get(toolCallId) ?? EMPTY_CLICKED_SET,
239
+ )
240
+ const currentSuggestedFollowups = useChatStore(
241
+ (state) => state.suggestedFollowups,
242
+ )
243
+
244
+ const isActive = latestFollowupToolCallId === toolCallId
245
+
246
+ useEffect(() => {
247
+ if (!isActive) return
248
+
249
+ const hasSameTool = currentSuggestedFollowups?.toolCallId === toolCallId
250
+ const hasSameFollowups =
251
+ hasSameTool &&
252
+ currentSuggestedFollowups?.followups.length === followups.length &&
253
+ currentSuggestedFollowups.followups.every((f, idx) => {
254
+ const next = followups[idx]
255
+ return f?.prompt === next?.prompt && f?.label === next?.label
256
+ })
257
+ const hasSameClicks =
258
+ hasSameTool &&
259
+ currentSuggestedFollowups?.clickedIndices.size === clickedIndices.size &&
260
+ Array.from(currentSuggestedFollowups.clickedIndices).every((idx) =>
261
+ clickedIndices.has(idx),
262
+ )
263
+
264
+ if (hasSameFollowups && hasSameClicks) return
265
+
266
+ // Track the currently active followups set so we can persist clicked state and avoid races
267
+ setSuggestedFollowups({
268
+ toolCallId,
269
+ followups,
270
+ clickedIndices: new Set(clickedIndices),
271
+ })
272
+ }, [
273
+ clickedIndices,
274
+ currentSuggestedFollowups,
275
+ followups,
276
+ isActive,
277
+ setSuggestedFollowups,
278
+ toolCallId,
279
+ ])
280
+
281
+ // Track which item is hovered (for passing to children)
282
+ const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
283
+
284
+ // For past messages, show collapsed toggle view
285
+ if (!isActive) {
286
+ return <PastFollowupsToggle toolCallId={toolCallId} followups={followups} />
287
+ }
288
+
289
+ // Calculate label column width for alignment across all followups
290
+ // Width = "→ " (2 chars) + max label/prompt length + " " spacing (2 chars)
291
+ const maxDisplayLength = Math.max(
292
+ 0,
293
+ ...followups.map((f) => (f.label ?? f.prompt).length),
294
+ )
295
+ const labelColumnWidth = Math.min(
296
+ MAX_LABEL_COLUMN_WIDTH,
297
+ Math.max(MIN_LABEL_COLUMN_WIDTH, 2 + maxDisplayLength + 2),
298
+ ) // "→ " + label/prompt + " "
299
+
300
+ return (
301
+ <box style={{ flexDirection: 'column' }}>
302
+ <text style={{ fg: theme.muted }}>Suggested followups:</text>
303
+ <box style={{ flexDirection: 'column' }}>
304
+ {followups.map((followup, index) => (
305
+ <FollowupLine
306
+ key={`followup-${index}`}
307
+ followup={followup}
308
+ index={index}
309
+ isClicked={clickedIndices.has(index)}
310
+ isHovered={hoveredIndex === index}
311
+ onSendFollowup={onSendFollowup}
312
+ onHover={setHoveredIndex}
313
+ disabled={!inputFocused || isFreebuffSessionOver}
314
+ labelColumnWidth={labelColumnWidth}
315
+ />
316
+ ))}
317
+ </box>
318
+ </box>
319
+ )
320
+ }
321
+
322
+ /**
323
+ * UI component for suggest_followups tool.
324
+ * Displays clickable cards that send the followup prompt as a user message when clicked.
325
+ */
326
+ export const SuggestFollowupsComponent = defineToolComponent({
327
+ toolName: 'suggest_followups',
328
+
329
+ render(toolBlock, _theme, options): ToolRenderConfig {
330
+ const { input, toolCallId } = toolBlock
331
+
332
+ // Extract followups from input
333
+ let followups: SuggestedFollowup[] = []
334
+
335
+ if (Array.isArray(input?.followups)) {
336
+ followups = input.followups.filter(
337
+ (f: unknown): f is SuggestedFollowup =>
338
+ typeof f === 'object' &&
339
+ f !== null &&
340
+ typeof (f as SuggestedFollowup).prompt === 'string',
341
+ )
342
+ }
343
+
344
+ if (followups.length === 0) {
345
+ return { content: null }
346
+ }
347
+
348
+ // The actual click handling is done in chat.tsx via the global handler
349
+ // Here we just pass a placeholder that will be replaced
350
+ const handleSendFollowup = (prompt: string, index: number) => {
351
+ // This gets called from the FollowupCard component
352
+ // The actual logic is handled via the global followup handler
353
+ const event = new CustomEvent('anvil:send-followup', {
354
+ detail: { prompt, index, toolCallId },
355
+ })
356
+ globalThis.dispatchEvent(event)
357
+ }
358
+
359
+ return {
360
+ content: (
361
+ <SuggestFollowupsItem
362
+ toolCallId={toolCallId}
363
+ followups={followups}
364
+ onSendFollowup={handleSendFollowup}
365
+ />
366
+ ),
367
+ }
368
+ },
369
+ })
@@ -0,0 +1,16 @@
1
+ import { defineToolComponent } from './types'
2
+
3
+ import type { ToolRenderConfig } from './types'
4
+
5
+ /**
6
+ * UI component for task_completed tool.
7
+ */
8
+ export const TaskCompleteComponent = defineToolComponent({
9
+ toolName: 'task_completed',
10
+
11
+ render(): ToolRenderConfig {
12
+ return {
13
+ content: null,
14
+ }
15
+ },
16
+ })
@@ -0,0 +1,273 @@
1
+ import { TextAttributes } from '@opentui/core'
2
+ import React, { type ReactNode } from 'react'
3
+ import stringWidth from 'string-width'
4
+
5
+ import { useTheme } from '../../hooks/use-theme'
6
+ import { Button } from '../button'
7
+
8
+ import type { ChatTheme } from '../../types/theme-system'
9
+
10
+ interface ToolCallItemProps {
11
+ name: string
12
+ content: ReactNode
13
+ isCollapsed: boolean
14
+ isStreaming: boolean
15
+ streamingPreview: string
16
+ finishedPreview: string
17
+ onToggle?: () => void
18
+ titleSuffix?: string
19
+ dense?: boolean
20
+ }
21
+
22
+ const isTextRenderable = (value: ReactNode): boolean => {
23
+ if (value === null || value === undefined || typeof value === 'boolean') {
24
+ return false
25
+ }
26
+
27
+ if (typeof value === 'string' || typeof value === 'number') {
28
+ return true
29
+ }
30
+
31
+ if (Array.isArray(value)) {
32
+ return value.every((child) => isTextRenderable(child))
33
+ }
34
+
35
+ if (React.isValidElement(value)) {
36
+ const elProps = value.props as Record<string, unknown>
37
+ if (value.type === React.Fragment) {
38
+ return isTextRenderable(elProps.children as ReactNode)
39
+ }
40
+
41
+ if (typeof value.type === 'string') {
42
+ if (
43
+ value.type === 'span' ||
44
+ value.type === 'strong' ||
45
+ value.type === 'em'
46
+ ) {
47
+ return isTextRenderable(elProps.children as ReactNode)
48
+ }
49
+
50
+ return false
51
+ }
52
+ }
53
+
54
+ return false
55
+ }
56
+
57
+ const renderExpandedContent = (
58
+ value: ReactNode,
59
+ theme: ChatTheme,
60
+ getAttributes: (extra?: number) => number | undefined,
61
+ ): ReactNode => {
62
+ if (
63
+ value === null ||
64
+ value === undefined ||
65
+ value === false ||
66
+ value === true
67
+ ) {
68
+ return null
69
+ }
70
+
71
+ if (isTextRenderable(value)) {
72
+ return (
73
+ <text
74
+ fg={theme.foreground}
75
+ key="tool-expanded-text"
76
+ attributes={getAttributes()}
77
+ >
78
+ {value}
79
+ </text>
80
+ )
81
+ }
82
+
83
+ if (React.isValidElement(value)) {
84
+ if (value.key === null || value.key === undefined) {
85
+ return (
86
+ <box
87
+ key="tool-expanded-node"
88
+ style={{ flexDirection: 'column', gap: 0 }}
89
+ >
90
+ {value}
91
+ </box>
92
+ )
93
+ }
94
+ return value
95
+ }
96
+
97
+ if (Array.isArray(value)) {
98
+ return (
99
+ <box
100
+ key="tool-expanded-array"
101
+ style={{ flexDirection: 'column', gap: 0 }}
102
+ >
103
+ {value.map((child, idx) => (
104
+ <box
105
+ key={`tool-expanded-array-${idx}`}
106
+ style={{ flexDirection: 'column', gap: 0 }}
107
+ >
108
+ {child}
109
+ </box>
110
+ ))}
111
+ </box>
112
+ )
113
+ }
114
+
115
+ return (
116
+ <box
117
+ key="tool-expanded-unknown"
118
+ style={{ flexDirection: 'column', gap: 0 }}
119
+ >
120
+ {value}
121
+ </box>
122
+ )
123
+ }
124
+
125
+ interface SimpleToolCallItemProps {
126
+ name: string
127
+ /** Description - can be a string or ReactNode for rich formatting */
128
+ description: string | ReactNode
129
+ descriptionColor?: string
130
+ }
131
+
132
+ export const SimpleToolCallItem = ({
133
+ name,
134
+ description,
135
+ descriptionColor,
136
+ }: SimpleToolCallItemProps) => {
137
+ const theme = useTheme()
138
+ const bulletChar = '• '
139
+
140
+ return (
141
+ <box style={{ flexDirection: 'row', alignItems: 'center', width: '100%' }}>
142
+ <text style={{ wrapMode: 'word' }}>
143
+ <span fg={theme.foreground}>{bulletChar}</span>
144
+ <span fg={theme.foreground} attributes={TextAttributes.BOLD}>
145
+ {name}
146
+ </span>
147
+ <span fg={theme.foreground}> </span>
148
+ {typeof description === 'string' ? (
149
+ <span fg={descriptionColor ?? theme.foreground}>{description}</span>
150
+ ) : (
151
+ description
152
+ )}
153
+ </text>
154
+ </box>
155
+ )
156
+ }
157
+
158
+ export const ToolCallItem = ({
159
+ name,
160
+ content,
161
+ isCollapsed,
162
+ isStreaming,
163
+ streamingPreview,
164
+ finishedPreview,
165
+ onToggle,
166
+ titleSuffix,
167
+ dense = false,
168
+ }: ToolCallItemProps) => {
169
+ const theme = useTheme()
170
+
171
+ const baseTextAttributes = theme.messageTextAttributes ?? 0
172
+ const getAttributes = (extra: number = 0): number | undefined => {
173
+ const combined = baseTextAttributes | extra
174
+ return combined === 0 ? undefined : combined
175
+ }
176
+
177
+ const isExpanded = !isCollapsed
178
+ const bulletChar = '• '
179
+ const toggleIndicator = onToggle ? (isCollapsed ? '▸ ' : '▾ ') : ''
180
+ const toggleLabel = onToggle ? toggleIndicator : bulletChar
181
+ // Width in cells of the toggle label (toggle arrow or bullet). Used to align
182
+ // expanded content directly under the toggle icon.
183
+ const toggleIndent = stringWidth(toggleLabel)
184
+ const collapsedPreviewText = isStreaming ? streamingPreview : finishedPreview
185
+ const showCollapsedPreview = collapsedPreviewText.length > 0
186
+
187
+ return (
188
+ <box style={{ flexDirection: 'column', gap: 0, width: '100%' }}>
189
+ <box
190
+ style={{
191
+ flexDirection: 'column',
192
+ gap: 0,
193
+ paddingLeft: 0,
194
+ paddingRight: 0,
195
+ paddingTop: 0,
196
+ paddingBottom: 0,
197
+ width: '100%',
198
+ }}
199
+ >
200
+ <Button
201
+ style={{
202
+ flexDirection: 'row',
203
+ alignItems: 'center',
204
+ paddingLeft: 0,
205
+ paddingRight: 0,
206
+ paddingTop: 0,
207
+ paddingBottom: 0,
208
+ width: '100%',
209
+ }}
210
+ onClick={onToggle}
211
+ >
212
+ <text style={{ wrapMode: 'none' }}>
213
+ <span
214
+ fg={theme.foreground}
215
+ attributes={isExpanded ? TextAttributes.BOLD : undefined}
216
+ >
217
+ {toggleLabel}
218
+ </span>
219
+ <span fg={theme.foreground} attributes={TextAttributes.BOLD}>
220
+ {name}
221
+ </span>
222
+ {titleSuffix ? (
223
+ <span fg={theme.foreground} attributes={TextAttributes.BOLD}>
224
+ {` ${titleSuffix}`}
225
+ </span>
226
+ ) : null}
227
+ {isStreaming ? (
228
+ <span fg={theme.primary} attributes={TextAttributes.DIM}>
229
+ {' running'}
230
+ </span>
231
+ ) : null}
232
+ </text>
233
+ </Button>
234
+
235
+ {isCollapsed ? (
236
+ showCollapsedPreview ? (
237
+ <box
238
+ style={{
239
+ paddingLeft: 0,
240
+ paddingRight: 0,
241
+ paddingTop: 0,
242
+ paddingBottom: 0,
243
+ width: '100%',
244
+ }}
245
+ >
246
+ <text
247
+ fg={isStreaming ? theme.foreground : theme.muted}
248
+ attributes={getAttributes(TextAttributes.ITALIC)}
249
+ style={{ wrapMode: 'word' }}
250
+ >
251
+ {collapsedPreviewText}
252
+ </text>
253
+ </box>
254
+ ) : null
255
+ ) : (
256
+ <box
257
+ style={{
258
+ flexDirection: 'column',
259
+ gap: 0,
260
+ // Indent expanded content underneath the toggle icon
261
+ paddingLeft: toggleIndent,
262
+ paddingRight: dense ? 0 : toggleIndent,
263
+ paddingTop: 0,
264
+ paddingBottom: 0,
265
+ }}
266
+ >
267
+ {renderExpandedContent(content, theme, getAttributes)}
268
+ </box>
269
+ )}
270
+ </box>
271
+ </box>
272
+ )
273
+ }
@@ -0,0 +1,55 @@
1
+ import type { ContentBlock } from '../../types/chat'
2
+ import type { ChatTheme } from '../../types/theme-system'
3
+ import type { ToolName } from '@anvil/sdk'
4
+ import type { ReactNode } from 'react'
5
+
6
+ export type ToolBlock = Extract<ContentBlock, { type: 'tool' }>
7
+
8
+ export type ToolRenderOptions = {
9
+ availableWidth: number
10
+ indentationOffset: number
11
+ previewPrefix?: string
12
+ labelWidth: number
13
+ }
14
+
15
+ export type ToolRenderConfig = {
16
+ /** Optional path to display in the tool header */
17
+ path?: string
18
+ /** Custom content to render in the tool body */
19
+ content?: ReactNode
20
+ /** Preview text to show when the tool is collapsed */
21
+ collapsedPreview?: string
22
+ }
23
+
24
+ /**
25
+ * Base interface for tool-specific UI components.
26
+ * Implement this interface to create a custom renderer for a specific tool.
27
+ */
28
+ export interface ToolComponent<T extends ToolName = ToolName> {
29
+ /** The tool name this component handles */
30
+ toolName: T
31
+
32
+ /**
33
+ * Render function that returns configuration for how to display this tool.
34
+ *
35
+ * @param toolBlock - The tool block data containing input/output
36
+ * @param theme - The current chat theme
37
+ * @param options - Rendering options like width and indentation
38
+ * @returns Configuration for rendering the tool, or null to use default rendering
39
+ */
40
+ render(
41
+ toolBlock: ToolBlock & { toolName: T },
42
+ theme: ChatTheme,
43
+ options: ToolRenderOptions,
44
+ ): ToolRenderConfig
45
+ }
46
+
47
+ /**
48
+ * Type-safe tool component definition.
49
+ * Use this helper to create tool components with full type inference.
50
+ */
51
+ export function defineToolComponent<T extends ToolName>(
52
+ component: ToolComponent<T>,
53
+ ): ToolComponent<T> {
54
+ return component
55
+ }