@alumbwe/anvil 1.0.18 → 1.0.20

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 (494) hide show
  1. package/README.md +28 -30
  2. package/bin/anvil +2 -0
  3. package/package.json +71 -75
  4. package/src/agents/bundled-agents.generated.d.ts +14 -0
  5. package/src/agents/bundled-agents.generated.ts +3760 -0
  6. package/src/app.tsx +399 -0
  7. package/src/chat.tsx +1698 -0
  8. package/src/cli-args.ts +134 -0
  9. package/src/commands/__tests__/anvil-command-aliases.test.ts +56 -0
  10. package/src/commands/__tests__/bash-command.test.ts +445 -0
  11. package/src/commands/__tests__/command-args.test.ts +349 -0
  12. package/src/commands/__tests__/copy-conversation-clipboard.test.ts +108 -0
  13. package/src/commands/__tests__/copy-conversation.test.ts +226 -0
  14. package/src/commands/__tests__/diagnostics-command.test.ts +35 -0
  15. package/src/commands/__tests__/image.test.ts +99 -0
  16. package/src/commands/__tests__/init.test.ts +456 -0
  17. package/src/commands/__tests__/new-command-rotates-chat.test.ts +91 -0
  18. package/src/commands/__tests__/process-diagnostics.test.ts +54 -0
  19. package/src/commands/__tests__/prompt-builders.test.ts +28 -0
  20. package/src/commands/__tests__/router-input.test.ts +298 -0
  21. package/src/commands/ads.ts +44 -0
  22. package/src/commands/command-registry.ts +726 -0
  23. package/src/commands/copy-conversation.ts +392 -0
  24. package/src/commands/help.ts +13 -0
  25. package/src/commands/image.ts +20 -0
  26. package/src/commands/init.ts +121 -0
  27. package/src/commands/process-diagnostics.ts +120 -0
  28. package/src/commands/prompt-builders.ts +100 -0
  29. package/src/commands/publish.ts +203 -0
  30. package/src/commands/router-utils.ts +80 -0
  31. package/src/commands/router.ts +487 -0
  32. package/src/commands/usage.ts +26 -0
  33. package/src/components/__tests__/ad-banner.test.tsx +83 -0
  34. package/src/components/__tests__/anvil-landing-heading-row.test.tsx +91 -0
  35. package/src/components/__tests__/anvil-model-selector.test.tsx +519 -0
  36. package/src/components/__tests__/anvil-offer-invariants.test.ts +54 -0
  37. package/src/components/__tests__/anvil-takeover-prompt.test.tsx +212 -0
  38. package/src/components/__tests__/chat-input-bar.test.tsx +130 -0
  39. package/src/components/__tests__/grid-layout.integration.test.tsx +304 -0
  40. package/src/components/__tests__/grid-layout.test.tsx +1051 -0
  41. package/src/components/__tests__/message-block.completion.test.tsx +104 -0
  42. package/src/components/__tests__/message-block.streaming.test.tsx +78 -0
  43. package/src/components/__tests__/message-with-agents.test.tsx +579 -0
  44. package/src/components/__tests__/multiline-input.test.tsx +1132 -0
  45. package/src/components/__tests__/queue-panel.test.tsx +398 -0
  46. package/src/components/__tests__/selectable-list.test.ts +232 -0
  47. package/src/components/__tests__/status-bar.test.tsx +44 -0
  48. package/src/components/__tests__/status-indicator.test.tsx +204 -0
  49. package/src/components/__tests__/terminal-thinking-orb.test.tsx +111 -0
  50. package/src/components/__tests__/user-error-banner.test.tsx +102 -0
  51. package/src/components/ad-banner.tsx +323 -0
  52. package/src/components/agent-checklist.tsx +350 -0
  53. package/src/components/agent-mode-toggle.tsx +241 -0
  54. package/src/components/anvil-active-session-summary.tsx +63 -0
  55. package/src/components/anvil-landing-screen.tsx +865 -0
  56. package/src/components/anvil-mark.tsx +52 -0
  57. package/src/components/anvil-model-selector.tsx +1087 -0
  58. package/src/components/anvil-referral-banner.tsx +604 -0
  59. package/src/components/anvil-superseded-screen.tsx +62 -0
  60. package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +432 -0
  61. package/src/components/ask-user/__tests__/validation.test.ts +213 -0
  62. package/src/components/ask-user/components/accordion-question.tsx +153 -0
  63. package/src/components/ask-user/components/custom-answer-input.tsx +66 -0
  64. package/src/components/ask-user/components/options-list.tsx +133 -0
  65. package/src/components/ask-user/components/question-header.tsx +80 -0
  66. package/src/components/ask-user/components/question-option.tsx +89 -0
  67. package/src/components/ask-user/constants.ts +38 -0
  68. package/src/components/ask-user/index.tsx +631 -0
  69. package/src/components/ask-user/utils/validation.ts +85 -0
  70. package/src/components/attachment-card.tsx +65 -0
  71. package/src/components/blocks/agent-block-grid.tsx +58 -0
  72. package/src/components/blocks/agent-branch-item.tsx +311 -0
  73. package/src/components/blocks/agent-branch-wrapper.tsx +493 -0
  74. package/src/components/blocks/agent-list-branch.tsx +77 -0
  75. package/src/components/blocks/ask-user-branch.tsx +81 -0
  76. package/src/components/blocks/block-helpers.ts +11 -0
  77. package/src/components/blocks/blocks-renderer.tsx +289 -0
  78. package/src/components/blocks/content-with-markdown.tsx +33 -0
  79. package/src/components/blocks/image-block.tsx +128 -0
  80. package/src/components/blocks/implementor-row.tsx +429 -0
  81. package/src/components/blocks/single-block.tsx +201 -0
  82. package/src/components/blocks/thinking-block.tsx +67 -0
  83. package/src/components/blocks/tool-block-group.tsx +57 -0
  84. package/src/components/blocks/tool-branch.tsx +206 -0
  85. package/src/components/blocks/user-content-copy.tsx +157 -0
  86. package/src/components/bottom-banner.tsx +136 -0
  87. package/src/components/build-mode-buttons.tsx +110 -0
  88. package/src/components/button.tsx +66 -0
  89. package/src/components/chat-header.tsx +77 -0
  90. package/src/components/chat-history-screen.tsx +413 -0
  91. package/src/components/chat-input-bar.tsx +523 -0
  92. package/src/components/clickable-title-box.tsx +66 -0
  93. package/src/components/clickable.tsx +119 -0
  94. package/src/components/collapse-button.tsx +34 -0
  95. package/src/components/copy-button.tsx +203 -0
  96. package/src/components/elapsed-timer.tsx +55 -0
  97. package/src/components/error-boundary.tsx +55 -0
  98. package/src/components/feedback-container.tsx +181 -0
  99. package/src/components/feedback-icon-button.tsx +89 -0
  100. package/src/components/feedback-input-mode.tsx +344 -0
  101. package/src/components/file-attachment-card.tsx +98 -0
  102. package/src/components/grid-layout.tsx +80 -0
  103. package/src/components/help-banner.tsx +127 -0
  104. package/src/components/highlighted-text.tsx +52 -0
  105. package/src/components/image-card.tsx +167 -0
  106. package/src/components/image-thumbnail.tsx +114 -0
  107. package/src/components/input-cursor.tsx +77 -0
  108. package/src/components/input-mode-banner.tsx +57 -0
  109. package/src/components/load-previous-button.tsx +48 -0
  110. package/src/components/login-modal.tsx +475 -0
  111. package/src/components/message-block.tsx +346 -0
  112. package/src/components/message-footer.tsx +258 -0
  113. package/src/components/message-with-agents.tsx +506 -0
  114. package/src/components/mode-divider.tsx +43 -0
  115. package/src/components/multiline-input.tsx +1241 -0
  116. package/src/components/out-of-credits-banner.tsx +160 -0
  117. package/src/components/pending-attachments-banner.tsx +116 -0
  118. package/src/components/pending-bash-message.tsx +59 -0
  119. package/src/components/progress-bar.tsx +81 -0
  120. package/src/components/project-picker-screen.tsx +485 -0
  121. package/src/components/publish-confirmation.tsx +493 -0
  122. package/src/components/publish-container.tsx +687 -0
  123. package/src/components/queue-panel.tsx +310 -0
  124. package/src/components/raised-pill.tsx +95 -0
  125. package/src/components/renderers/plan-box.tsx +59 -0
  126. package/src/components/review-screen.tsx +112 -0
  127. package/src/components/scroll-to-bottom-button.tsx +34 -0
  128. package/src/components/segmented-control.tsx +187 -0
  129. package/src/components/selectable-list.tsx +248 -0
  130. package/src/components/selected-chips.tsx +80 -0
  131. package/src/components/separator.tsx +40 -0
  132. package/src/components/session-ended-banner.tsx +226 -0
  133. package/src/components/shimmer-text.tsx +248 -0
  134. package/src/components/status-bar.tsx +313 -0
  135. package/src/components/subscription-limit-banner.tsx +206 -0
  136. package/src/components/suggested-prompts.tsx +184 -0
  137. package/src/components/suggestion-menu.tsx +215 -0
  138. package/src/components/terminal-command-display.tsx +151 -0
  139. package/src/components/terminal-link.tsx +97 -0
  140. package/src/components/terminal-thinking-orb.tsx +95 -0
  141. package/src/components/text-attachment-card.tsx +77 -0
  142. package/src/components/thinking.tsx +109 -0
  143. package/src/components/tools/__tests__/apply-patch.test.tsx +84 -0
  144. package/src/components/tools/__tests__/code-search.test.tsx +45 -0
  145. package/src/components/tools/__tests__/gravity-index.test.ts +76 -0
  146. package/src/components/tools/__tests__/render-ui.test.tsx +92 -0
  147. package/src/components/tools/__tests__/run-terminal-command.test.ts +233 -0
  148. package/src/components/tools/apply-patch.tsx +95 -0
  149. package/src/components/tools/code-search.tsx +45 -0
  150. package/src/components/tools/composio.tsx +152 -0
  151. package/src/components/tools/diff-viewer.tsx +76 -0
  152. package/src/components/tools/glob.tsx +61 -0
  153. package/src/components/tools/gravity-index.tsx +77 -0
  154. package/src/components/tools/list-directory.tsx +61 -0
  155. package/src/components/tools/read-docs.tsx +37 -0
  156. package/src/components/tools/read-files.tsx +91 -0
  157. package/src/components/tools/read-subtree.tsx +44 -0
  158. package/src/components/tools/read-url.tsx +33 -0
  159. package/src/components/tools/registry.ts +126 -0
  160. package/src/components/tools/render-ui.tsx +122 -0
  161. package/src/components/tools/run-terminal-command.tsx +79 -0
  162. package/src/components/tools/skill.tsx +29 -0
  163. package/src/components/tools/str-replace.tsx +77 -0
  164. package/src/components/tools/suggest-followups.tsx +369 -0
  165. package/src/components/tools/task-completed.tsx +16 -0
  166. package/src/components/tools/tool-call-item.tsx +273 -0
  167. package/src/components/tools/types.ts +55 -0
  168. package/src/components/tools/web-search.tsx +33 -0
  169. package/src/components/tools/write-file.tsx +16 -0
  170. package/src/components/tools/write-todos.tsx +95 -0
  171. package/src/components/top-banner.tsx +178 -0
  172. package/src/components/usage-banner.tsx +244 -0
  173. package/src/components/user-error-banner.tsx +56 -0
  174. package/src/components/validation-error-popover.tsx +192 -0
  175. package/src/contexts/__tests__/chat-runtime-context.test.tsx +149 -0
  176. package/src/contexts/chat-runtime-context.tsx +240 -0
  177. package/src/data/slash-commands.test.ts +24 -0
  178. package/src/data/slash-commands.ts +249 -0
  179. package/src/entry.ts +12 -0
  180. package/src/hooks/__tests__/holds-live-anvil-slot.test.ts +49 -0
  181. package/src/hooks/__tests__/session-fetch-signal.test.ts +63 -0
  182. package/src/hooks/__tests__/use-activity-query.test.ts +1000 -0
  183. package/src/hooks/__tests__/use-ask-user-bridge.test.ts +176 -0
  184. package/src/hooks/__tests__/use-auth-query.test.ts +21 -0
  185. package/src/hooks/__tests__/use-chat-input-remount.test.tsx +66 -0
  186. package/src/hooks/__tests__/use-clipboard.test.tsx +73 -0
  187. package/src/hooks/__tests__/use-connection-status.test.ts +112 -0
  188. package/src/hooks/__tests__/use-directory-browser.test.ts +203 -0
  189. package/src/hooks/__tests__/use-gravity-ad.test.ts +73 -0
  190. package/src/hooks/__tests__/use-grid-layout.test.ts +349 -0
  191. package/src/hooks/__tests__/use-input-history.test.ts +699 -0
  192. package/src/hooks/__tests__/use-message-queue-editing.test.tsx +209 -0
  193. package/src/hooks/__tests__/use-message-queue.test.tsx +71 -0
  194. package/src/hooks/__tests__/use-path-tab-completion.test.ts +315 -0
  195. package/src/hooks/__tests__/use-queue-controls.test.ts +68 -0
  196. package/src/hooks/__tests__/use-queue-ui.test.tsx +83 -0
  197. package/src/hooks/__tests__/use-searchable-list.test.ts +359 -0
  198. package/src/hooks/__tests__/use-send-message-timer.test.ts +146 -0
  199. package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +361 -0
  200. package/src/hooks/__tests__/use-suggestion-engine.test.ts +485 -0
  201. package/src/hooks/__tests__/use-terminal-focus.test.ts +66 -0
  202. package/src/hooks/__tests__/use-terminal-layout.test.ts +675 -0
  203. package/src/hooks/__tests__/use-timeout.test.ts +330 -0
  204. package/src/hooks/__tests__/use-usage-query.test.ts +502 -0
  205. package/src/hooks/__tests__/use-user-details-query.test.ts +195 -0
  206. package/src/hooks/helpers/__tests__/send-message.test.ts +1918 -0
  207. package/src/hooks/helpers/send-message.ts +654 -0
  208. package/src/hooks/stream-state.ts +83 -0
  209. package/src/hooks/use-activity-query.ts +668 -0
  210. package/src/hooks/use-agent-validation.ts +74 -0
  211. package/src/hooks/use-anvil-ctrl-c-exit.ts +23 -0
  212. package/src/hooks/use-anvil-session-progress.ts +34 -0
  213. package/src/hooks/use-anvil-session.ts +806 -0
  214. package/src/hooks/use-anvil-streak-query.ts +61 -0
  215. package/src/hooks/use-ask-user-bridge.ts +92 -0
  216. package/src/hooks/use-auth-query.ts +262 -0
  217. package/src/hooks/use-auth-state.ts +152 -0
  218. package/src/hooks/use-chat-input.ts +105 -0
  219. package/src/hooks/use-chat-keyboard.ts +321 -0
  220. package/src/hooks/use-chat-messages.ts +254 -0
  221. package/src/hooks/use-chat-state.ts +165 -0
  222. package/src/hooks/use-chat-streaming.ts +180 -0
  223. package/src/hooks/use-chat-ui.ts +131 -0
  224. package/src/hooks/use-clipboard.ts +151 -0
  225. package/src/hooks/use-connection-status.ts +142 -0
  226. package/src/hooks/use-directory-browser.ts +88 -0
  227. package/src/hooks/use-elapsed-time.ts +116 -0
  228. package/src/hooks/use-event.ts +34 -0
  229. package/src/hooks/use-exit-handler.ts +66 -0
  230. package/src/hooks/use-fetch-login-url.ts +65 -0
  231. package/src/hooks/use-fingerprint.ts +61 -0
  232. package/src/hooks/use-gravity-ad.ts +656 -0
  233. package/src/hooks/use-grid-layout.ts +73 -0
  234. package/src/hooks/use-input-history.ts +173 -0
  235. package/src/hooks/use-login-keyboard-handlers.ts +75 -0
  236. package/src/hooks/use-login-polling.ts +114 -0
  237. package/src/hooks/use-logo.tsx +161 -0
  238. package/src/hooks/use-message-queue.ts +387 -0
  239. package/src/hooks/use-now.ts +20 -0
  240. package/src/hooks/use-path-tab-completion.ts +88 -0
  241. package/src/hooks/use-publish-mutation.ts +60 -0
  242. package/src/hooks/use-queue-controls.ts +68 -0
  243. package/src/hooks/use-queue-ui.ts +74 -0
  244. package/src/hooks/use-scroll-management.ts +184 -0
  245. package/src/hooks/use-searchable-list.ts +98 -0
  246. package/src/hooks/use-send-message.ts +773 -0
  247. package/src/hooks/use-sheen-animation.tsx +88 -0
  248. package/src/hooks/use-subscription-query.ts +71 -0
  249. package/src/hooks/use-suggestion-engine.ts +783 -0
  250. package/src/hooks/use-terminal-breakpoints.ts +77 -0
  251. package/src/hooks/use-terminal-dimensions.ts +46 -0
  252. package/src/hooks/use-terminal-focus.ts +29 -0
  253. package/src/hooks/use-terminal-layout.ts +189 -0
  254. package/src/hooks/use-theme.tsx +140 -0
  255. package/src/hooks/use-timeout.ts +85 -0
  256. package/src/hooks/use-update-preference.ts +66 -0
  257. package/src/hooks/use-usage-monitor.ts +55 -0
  258. package/src/hooks/use-usage-query.ts +124 -0
  259. package/src/hooks/use-user-details-query.ts +93 -0
  260. package/src/hooks/use-why-did-you-update.ts +177 -0
  261. package/src/index.tsx +473 -0
  262. package/src/init/__tests__/init-direnv.test.ts +526 -0
  263. package/src/init/init-app.ts +34 -0
  264. package/src/init/init-direnv.ts +133 -0
  265. package/src/login/constants.ts +60 -0
  266. package/src/login/login-flow.ts +204 -0
  267. package/src/login/plain-login.ts +108 -0
  268. package/src/login/utils.ts +183 -0
  269. package/src/native/ripgrep.ts +80 -0
  270. package/src/polyfills/bun-strip-ansi.ts +12 -0
  271. package/src/pre-init/tree-sitter-wasm.ts +96 -0
  272. package/src/project-files.ts +102 -0
  273. package/src/setup/key-setup.ts +295 -0
  274. package/src/setup/load-env.ts +106 -0
  275. package/src/state/__tests__/chat-store-focus.test.ts +22 -0
  276. package/src/state/__tests__/feedback-store.test.ts +257 -0
  277. package/src/state/anvil-model-store.ts +42 -0
  278. package/src/state/anvil-session-store.ts +53 -0
  279. package/src/state/chat-history-store.ts +30 -0
  280. package/src/state/chat-store.ts +544 -0
  281. package/src/state/feedback-store.ts +160 -0
  282. package/src/state/login-store.ts +150 -0
  283. package/src/state/message-block-store.ts +149 -0
  284. package/src/state/publish-store.ts +146 -0
  285. package/src/state/queue-panel-store.ts +26 -0
  286. package/src/state/review-store.ts +24 -0
  287. package/src/types/anvil-session.ts +17 -0
  288. package/src/types/chat-state.ts +15 -0
  289. package/src/types/chat.ts +241 -0
  290. package/src/types/contracts/send-message.ts +12 -0
  291. package/src/types/env.ts +98 -0
  292. package/src/types/function-params.ts +28 -0
  293. package/src/types/react19-compat.d.ts +19 -0
  294. package/src/types/store.ts +111 -0
  295. package/src/types/theme-system.ts +144 -0
  296. package/src/types/utils.ts +3 -0
  297. package/src/utils/__tests__/active-run.test.ts +140 -0
  298. package/src/utils/__tests__/activity-tracker.test.ts +440 -0
  299. package/src/utils/__tests__/agent-display.test.ts +139 -0
  300. package/src/utils/__tests__/analytics-client.test.ts +262 -0
  301. package/src/utils/__tests__/anonymous-id.test.ts +74 -0
  302. package/src/utils/__tests__/anvil-api.test.ts +578 -0
  303. package/src/utils/__tests__/anvil-instance-owner.test.ts +69 -0
  304. package/src/utils/__tests__/anvil-model-navigation.test.ts +102 -0
  305. package/src/utils/__tests__/anvil-premium-reset.test.ts +79 -0
  306. package/src/utils/__tests__/anvil-referral-cache.test.ts +116 -0
  307. package/src/utils/__tests__/anvil-session-api.test.ts +215 -0
  308. package/src/utils/__tests__/anvil-session-display.test.ts +23 -0
  309. package/src/utils/__tests__/anvil-streak-line.test.ts +217 -0
  310. package/src/utils/__tests__/arrays.test.ts +264 -0
  311. package/src/utils/__tests__/bash-context-processor.test.ts +38 -0
  312. package/src/utils/__tests__/block-processor.test.ts +810 -0
  313. package/src/utils/__tests__/chat-history.test.ts +203 -0
  314. package/src/utils/__tests__/chat-input-key-intercept.test.ts +40 -0
  315. package/src/utils/__tests__/chat-meta.test.ts +130 -0
  316. package/src/utils/__tests__/clipboard-linux.test.ts +359 -0
  317. package/src/utils/__tests__/clipboard.test.ts +910 -0
  318. package/src/utils/__tests__/code-search-summary.test.ts +84 -0
  319. package/src/utils/__tests__/collapse-helpers.test.ts +1109 -0
  320. package/src/utils/__tests__/error-handling.test.ts +658 -0
  321. package/src/utils/__tests__/exit-cleanly.test.ts +110 -0
  322. package/src/utils/__tests__/feedback-helpers.test.ts +444 -0
  323. package/src/utils/__tests__/feedback-submission.test.ts +26 -0
  324. package/src/utils/__tests__/fetch-usage.test.ts +355 -0
  325. package/src/utils/__tests__/fingerprint.test.ts +144 -0
  326. package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +7 -0
  327. package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +3 -0
  328. package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +33 -0
  329. package/src/utils/__tests__/format-elapsed-time.test.ts +103 -0
  330. package/src/utils/__tests__/format-timeout.test.ts +87 -0
  331. package/src/utils/__tests__/image-dimensions.test.ts +249 -0
  332. package/src/utils/__tests__/image-processor.test.ts +209 -0
  333. package/src/utils/__tests__/implementor-helpers.test.ts +1458 -0
  334. package/src/utils/__tests__/keyboard-actions.test.ts +660 -0
  335. package/src/utils/__tests__/layout-helpers.test.ts +36 -0
  336. package/src/utils/__tests__/lazy-response-ads.test.ts +167 -0
  337. package/src/utils/__tests__/log-shipper.test.ts +53 -0
  338. package/src/utils/__tests__/markdown-renderer.test.tsx +421 -0
  339. package/src/utils/__tests__/message-block-helpers.test.ts +1263 -0
  340. package/src/utils/__tests__/message-updater.test.ts +645 -0
  341. package/src/utils/__tests__/osc-timeout-scenarios.test.ts +172 -0
  342. package/src/utils/__tests__/pending-attachments.test.ts +277 -0
  343. package/src/utils/__tests__/polling-backoff.test.ts +51 -0
  344. package/src/utils/__tests__/queue-panel-actions.test.ts +148 -0
  345. package/src/utils/__tests__/run-state-storage.test.ts +919 -0
  346. package/src/utils/__tests__/safe-json.test.ts +77 -0
  347. package/src/utils/__tests__/sdk-event-handlers.test.ts +685 -0
  348. package/src/utils/__tests__/send-message-helpers.test.ts +1898 -0
  349. package/src/utils/__tests__/send-message-timer.test.ts +64 -0
  350. package/src/utils/__tests__/settings.test.ts +99 -0
  351. package/src/utils/__tests__/strings.test.ts +192 -0
  352. package/src/utils/__tests__/terminal-color-detection.test.ts +425 -0
  353. package/src/utils/__tests__/terminal-command-broker.test.ts +528 -0
  354. package/src/utils/__tests__/terminal-enter-detection.test.ts +77 -0
  355. package/src/utils/__tests__/terminal-io.test.ts +137 -0
  356. package/src/utils/__tests__/terminal-protocol-controller.test.ts +119 -0
  357. package/src/utils/__tests__/text-layout.test.ts +82 -0
  358. package/src/utils/__tests__/theme-platform-detection.test.ts +125 -0
  359. package/src/utils/__tests__/think-tag-parser.test.ts +159 -0
  360. package/src/utils/__tests__/trace-writer.test.ts +223 -0
  361. package/src/utils/__tests__/trim-chat-logs.test.ts +78 -0
  362. package/src/utils/__tests__/usage-banner-state.test.ts +298 -0
  363. package/src/utils/__tests__/validation-error-formatting.test.ts +126 -0
  364. package/src/utils/__tests__/windows-terminal-health.test.ts +101 -0
  365. package/src/utils/__tests__/write-file-atomic.test.ts +127 -0
  366. package/src/utils/active-run.ts +131 -0
  367. package/src/utils/activity-tracker.ts +66 -0
  368. package/src/utils/agent-display.ts +87 -0
  369. package/src/utils/agent-helpers.ts +27 -0
  370. package/src/utils/agent-id-utils.ts +14 -0
  371. package/src/utils/ai-message-id.ts +12 -0
  372. package/src/utils/analytics.ts +361 -0
  373. package/src/utils/anonymous-id.ts +87 -0
  374. package/src/utils/anvil-agent-selection.ts +33 -0
  375. package/src/utils/anvil-api.ts +612 -0
  376. package/src/utils/anvil-client.ts +217 -0
  377. package/src/utils/anvil-instance-owner.ts +66 -0
  378. package/src/utils/anvil-model-navigation.ts +50 -0
  379. package/src/utils/anvil-premium-reset.ts +55 -0
  380. package/src/utils/anvil-referral-cache.ts +51 -0
  381. package/src/utils/anvil-session-api.ts +243 -0
  382. package/src/utils/anvil-session-display.ts +21 -0
  383. package/src/utils/anvil-streak-line.ts +93 -0
  384. package/src/utils/arrays.ts +117 -0
  385. package/src/utils/auth.ts +258 -0
  386. package/src/utils/bash-context-processor.ts +47 -0
  387. package/src/utils/bash-messages.ts +108 -0
  388. package/src/utils/block-margins.ts +35 -0
  389. package/src/utils/block-operations.ts +523 -0
  390. package/src/utils/block-processor.ts +213 -0
  391. package/src/utils/chat-history.ts +253 -0
  392. package/src/utils/chat-input-key-intercept.ts +52 -0
  393. package/src/utils/chat-meta.ts +90 -0
  394. package/src/utils/chat-scroll-accel.ts +151 -0
  395. package/src/utils/clipboard-image.ts +588 -0
  396. package/src/utils/clipboard.ts +397 -0
  397. package/src/utils/code-search-summary.ts +70 -0
  398. package/src/utils/collapse-helpers.ts +256 -0
  399. package/src/utils/config-dir.ts +24 -0
  400. package/src/utils/constants.ts +190 -0
  401. package/src/utils/create-event-handler-state.ts +73 -0
  402. package/src/utils/create-run-config.ts +121 -0
  403. package/src/utils/detect-shell.ts +116 -0
  404. package/src/utils/directory-browser.ts +72 -0
  405. package/src/utils/engagement.ts +55 -0
  406. package/src/utils/env.ts +92 -0
  407. package/src/utils/error-handling.ts +255 -0
  408. package/src/utils/error-messages.ts +62 -0
  409. package/src/utils/exit-cleanly.ts +86 -0
  410. package/src/utils/feedback-helpers.ts +103 -0
  411. package/src/utils/feedback-submission.ts +22 -0
  412. package/src/utils/fetch-usage.ts +76 -0
  413. package/src/utils/fingerprint.ts +245 -0
  414. package/src/utils/format-elapsed-time.ts +33 -0
  415. package/src/utils/format-session-units.ts +6 -0
  416. package/src/utils/format-timeout.ts +28 -0
  417. package/src/utils/format-validation-errors-for-message.ts +84 -0
  418. package/src/utils/git.ts +17 -0
  419. package/src/utils/helpers.ts +47 -0
  420. package/src/utils/image-display.ts +67 -0
  421. package/src/utils/image-handler.ts +325 -0
  422. package/src/utils/image-processor.ts +133 -0
  423. package/src/utils/image-thumbnail.ts +78 -0
  424. package/src/utils/implementor-helpers.ts +787 -0
  425. package/src/utils/input-modes.ts +174 -0
  426. package/src/utils/keyboard-actions.ts +392 -0
  427. package/src/utils/keypad-keys.ts +47 -0
  428. package/src/utils/layout-helpers.ts +33 -0
  429. package/src/utils/lazy-response-ads.ts +1 -0
  430. package/src/utils/local-agent-registry.ts +474 -0
  431. package/src/utils/log-shipper.ts +126 -0
  432. package/src/utils/logger.ts +327 -0
  433. package/src/utils/markdown-renderer.tsx +1090 -0
  434. package/src/utils/math.ts +3 -0
  435. package/src/utils/message-block-helpers.ts +684 -0
  436. package/src/utils/message-history.ts +143 -0
  437. package/src/utils/message-tree-utils.ts +67 -0
  438. package/src/utils/message-updater.ts +279 -0
  439. package/src/utils/open-file.ts +146 -0
  440. package/src/utils/open-url.ts +63 -0
  441. package/src/utils/path-completion.ts +111 -0
  442. package/src/utils/path-helpers.ts +38 -0
  443. package/src/utils/pending-attachments.ts +360 -0
  444. package/src/utils/polling-backoff.ts +59 -0
  445. package/src/utils/project-picker.ts +12 -0
  446. package/src/utils/queue-panel-actions.ts +70 -0
  447. package/src/utils/recent-projects.ts +156 -0
  448. package/src/utils/renderer-cleanup.ts +233 -0
  449. package/src/utils/response-ad-positions.ts +1 -0
  450. package/src/utils/run-state-storage.ts +603 -0
  451. package/src/utils/safe-json.ts +153 -0
  452. package/src/utils/sdk-event-handlers.ts +571 -0
  453. package/src/utils/send-message-helpers.ts +210 -0
  454. package/src/utils/send-message-timer.ts +110 -0
  455. package/src/utils/settings.ts +226 -0
  456. package/src/utils/skill-registry.ts +94 -0
  457. package/src/utils/spawn-agent-matcher.ts +55 -0
  458. package/src/utils/status-indicator-state.ts +107 -0
  459. package/src/utils/stream-chunk-processor.ts +57 -0
  460. package/src/utils/strings.ts +271 -0
  461. package/src/utils/subscription.ts +31 -0
  462. package/src/utils/syntax-highlighter.tsx +19 -0
  463. package/src/utils/terminal-color-detection.ts +474 -0
  464. package/src/utils/terminal-command-broker.ts +467 -0
  465. package/src/utils/terminal-enter-detection.ts +63 -0
  466. package/src/utils/terminal-images.ts +229 -0
  467. package/src/utils/terminal-io.ts +85 -0
  468. package/src/utils/terminal-protocol-controller.ts +173 -0
  469. package/src/utils/terminal-reset-sequences.ts +33 -0
  470. package/src/utils/terminal-title.ts +84 -0
  471. package/src/utils/terminal-watchdog.ts +366 -0
  472. package/src/utils/text-layout.ts +149 -0
  473. package/src/utils/theme-config.ts +141 -0
  474. package/src/utils/theme-system.ts +1206 -0
  475. package/src/utils/think-tag-parser.ts +95 -0
  476. package/src/utils/time-format.ts +21 -0
  477. package/src/utils/trace-writer.ts +162 -0
  478. package/src/utils/ui-constants.ts +69 -0
  479. package/src/utils/update-checker.ts +110 -0
  480. package/src/utils/usage-banner-state.ts +146 -0
  481. package/src/utils/validation-error-formatting.ts +86 -0
  482. package/src/utils/validation-error-helpers.ts +19 -0
  483. package/src/utils/windows-terminal-health.ts +124 -0
  484. package/src/utils/word-wrap-utils.ts +53 -0
  485. package/src/utils/write-file-atomic.ts +54 -0
  486. package/src/utils/yield-to-event-loop.ts +9 -0
  487. package/bin/anvil.cjs +0 -40
  488. package/dist/bundled.js +0 -123450
  489. package/dist/bundled.js.map +0 -827
  490. package/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
  491. package/dist/vendor/ripgrep/arm64-linux/rg +0 -0
  492. package/dist/vendor/ripgrep/x64-darwin/rg +0 -0
  493. package/dist/vendor/ripgrep/x64-linux/rg +0 -0
  494. package/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
@@ -0,0 +1,487 @@
1
+ import { AnalyticsEvent } from '@anvil/common/constants/analytics-events'
2
+ import { runTerminalCommand } from '@anvil/sdk'
3
+
4
+
5
+ import {
6
+ findCommand,
7
+ type RouterParams,
8
+ type CommandResult,
9
+ } from './command-registry'
10
+ import {
11
+ isSlashCommand,
12
+ parseCommandInput,
13
+ } from './router-utils'
14
+ import { buildInterviewPrompt, buildPlanPrompt, buildReviewPrompt } from './prompt-builders'
15
+ import { getProjectRoot } from '../project-files'
16
+ import { useChatStore } from '../state/chat-store'
17
+ import { useAnvilSessionStore } from '../state/anvil-session-store'
18
+ import { trackEvent } from '../utils/analytics'
19
+ import {
20
+ buildBashHistoryMessages,
21
+ createRunTerminalToolResult,
22
+ } from '../utils/bash-messages'
23
+ import { showClipboardMessage } from '../utils/clipboard'
24
+ import { IS_ANVIL } from '../utils/constants'
25
+ import { getSystemProcessEnv } from '../utils/env'
26
+ import { terminalCommandBroker } from '../utils/terminal-command-broker'
27
+ import { getSystemMessage, getUserMessage } from '../utils/message-history'
28
+ import {
29
+ capturePendingAttachments,
30
+ hasProcessingFiles,
31
+ hasProcessingImages,
32
+ validateAndAddImage,
33
+ } from '../utils/pending-attachments'
34
+
35
+ /**
36
+ * Run a bash command with automatic ghost/direct mode selection.
37
+ * Uses ghost mode when streaming or chain in progress, otherwise adds directly to chat history.
38
+ */
39
+ export function runBashCommand(command: string) {
40
+ const {
41
+ streamingAgents,
42
+ isChainInProgress,
43
+ setMessages,
44
+ addPendingBashMessage,
45
+ updatePendingBashMessage,
46
+ } = useChatStore.getState()
47
+
48
+ const ghost = streamingAgents.size > 0 || isChainInProgress
49
+ const id = crypto.randomUUID()
50
+ const commandCwd = process.cwd()
51
+ const startTime = Date.now()
52
+
53
+ if (ghost) {
54
+ // Ghost mode: add to pending messages
55
+ addPendingBashMessage({
56
+ id,
57
+ command,
58
+ stdout: '',
59
+ stderr: '',
60
+ exitCode: 0,
61
+ isRunning: true,
62
+ startTime: Date.now(),
63
+ cwd: commandCwd,
64
+ })
65
+ } else {
66
+ // Direct mode: add to chat history with placeholder output (user + assistant)
67
+ const { assistantMessage } = buildBashHistoryMessages({
68
+ command,
69
+ cwd: commandCwd,
70
+ toolCallId: id,
71
+ output: '...',
72
+ })
73
+ setMessages((prev) => [...prev, assistantMessage])
74
+ }
75
+
76
+ runTerminalCommand({
77
+ command,
78
+ process_type: 'SYNC',
79
+ cwd: commandCwd,
80
+ timeout_seconds: -1,
81
+ env: getSystemProcessEnv(),
82
+ terminalCommandBroker,
83
+ })
84
+ .then(([{ value }]) => {
85
+ const stdout = 'stdout' in value ? value.stdout || '' : ''
86
+ const stderr = 'stderr' in value ? value.stderr || '' : ''
87
+ const exitCode = 'exitCode' in value ? value.exitCode ?? 0 : 0
88
+
89
+ // Track terminal command completion
90
+ const durationMs = Date.now() - startTime
91
+ trackEvent(AnalyticsEvent.TERMINAL_COMMAND_COMPLETED, {
92
+ command: command.split(' ')[0], // Just the command name, not args
93
+ exitCode,
94
+ success: exitCode === 0,
95
+ ghost,
96
+ durationMs,
97
+ hasStdout: stdout.length > 0,
98
+ hasStderr: stderr.length > 0,
99
+ stdoutLength: stdout.length,
100
+ stderrLength: stderr.length,
101
+ })
102
+
103
+ if (ghost) {
104
+ updatePendingBashMessage(id, {
105
+ stdout,
106
+ stderr,
107
+ exitCode,
108
+ isRunning: false,
109
+ })
110
+ } else {
111
+ const toolResultOutput = createRunTerminalToolResult({
112
+ command,
113
+ cwd: commandCwd,
114
+ stdout: stdout || null,
115
+ stderr: stderr || null,
116
+ exitCode,
117
+ })
118
+ const outputJson = JSON.stringify(toolResultOutput)
119
+
120
+ setMessages((prev) =>
121
+ prev.map((msg) => {
122
+ if (!msg.blocks) return msg
123
+ let didUpdate = false
124
+ const blocks = msg.blocks.map((block) => {
125
+ if ('toolCallId' in block && block.toolCallId === id) {
126
+ didUpdate = true
127
+ return { ...block, output: outputJson }
128
+ }
129
+ return block
130
+ })
131
+ return didUpdate ? { ...msg, blocks, isComplete: true } : msg
132
+ }),
133
+ )
134
+
135
+ // Also add to pending bash messages so the next user message includes this context for the LLM
136
+ // Mark as already added to history to avoid duplicate UI entries
137
+ addPendingBashMessage({
138
+ id,
139
+ command,
140
+ stdout,
141
+ stderr,
142
+ exitCode,
143
+ isRunning: false,
144
+ cwd: commandCwd,
145
+ addedToHistory: true,
146
+ })
147
+ }
148
+ })
149
+ .catch((error) => {
150
+ const errorMessage =
151
+ error instanceof Error ? error.message : String(error)
152
+
153
+ // Track terminal command completion with error
154
+ const durationMs = Date.now() - startTime
155
+ trackEvent(AnalyticsEvent.TERMINAL_COMMAND_COMPLETED, {
156
+ command: command.split(' ')[0], // Just the command name, not args
157
+ exitCode: 1,
158
+ success: false,
159
+ ghost,
160
+ durationMs,
161
+ hasStdout: false,
162
+ hasStderr: true,
163
+ stdoutLength: 0,
164
+ stderrLength: errorMessage.length,
165
+ isException: true,
166
+ })
167
+
168
+ if (ghost) {
169
+ updatePendingBashMessage(id, {
170
+ stdout: '',
171
+ stderr: errorMessage,
172
+ exitCode: 1,
173
+ isRunning: false,
174
+ })
175
+ } else {
176
+ const errorToolResultOutput = createRunTerminalToolResult({
177
+ command,
178
+ cwd: commandCwd,
179
+ stdout: null,
180
+ stderr: null,
181
+ exitCode: 1,
182
+ errorMessage,
183
+ })
184
+ const errorOutputJson = JSON.stringify(errorToolResultOutput)
185
+
186
+ setMessages((prev) =>
187
+ prev.map((msg) => {
188
+ if (!msg.blocks) return msg
189
+ let didUpdate = false
190
+ const blocks = msg.blocks.map((block) => {
191
+ if ('toolCallId' in block && block.toolCallId === id) {
192
+ didUpdate = true
193
+ return { ...block, output: errorOutputJson }
194
+ }
195
+ return block
196
+ })
197
+ return didUpdate ? { ...msg, blocks, isComplete: true } : msg
198
+ }),
199
+ )
200
+
201
+ // Also add to pending bash messages so the next user message includes this context for the LLM
202
+ // Mark as already added to history to avoid duplicate UI entries
203
+ addPendingBashMessage({
204
+ id,
205
+ command,
206
+ stdout: '',
207
+ stderr: errorMessage,
208
+ exitCode: 1,
209
+ isRunning: false,
210
+ cwd: commandCwd,
211
+ addedToHistory: true,
212
+ })
213
+ }
214
+ })
215
+ }
216
+
217
+ /**
218
+ * Add a completed bash command result to the chat message history.
219
+ * Note: This is UI-only; we no longer send these commands to the AI context.
220
+ */
221
+ export function addBashMessageToHistory(params: {
222
+ command: string
223
+ stdout: string
224
+ stderr: string | null
225
+ exitCode: number
226
+ cwd: string
227
+ setMessages: RouterParams['setMessages']
228
+ }) {
229
+ const { command, stdout, stderr, exitCode, cwd, setMessages } = params
230
+ const toolResultOutput = createRunTerminalToolResult({
231
+ command,
232
+ cwd,
233
+ stdout: stdout || null,
234
+ stderr: stderr ?? null,
235
+ exitCode,
236
+ })
237
+ const toolCallId = crypto.randomUUID()
238
+ const outputJson = JSON.stringify(toolResultOutput)
239
+ const { assistantMessage } = buildBashHistoryMessages({
240
+ command,
241
+ cwd,
242
+ toolCallId,
243
+ output: outputJson,
244
+ isComplete: true,
245
+ })
246
+
247
+ setMessages((prev) => [...prev, assistantMessage])
248
+ }
249
+
250
+ export async function routeUserPrompt(
251
+ params: RouterParams,
252
+ ): Promise<CommandResult> {
253
+ const {
254
+ agentMode,
255
+ inputRef,
256
+ inputValue,
257
+ isChainInProgressRef,
258
+ isStreaming,
259
+ streamMessageIdRef,
260
+ addToQueue,
261
+ saveToHistory,
262
+ scrollToLatest,
263
+ sendMessage,
264
+ setInputFocused,
265
+ setInputValue,
266
+ setMessages,
267
+ } = params
268
+
269
+ const inputMode = useChatStore.getState().inputMode
270
+ const setInputMode = useChatStore.getState().setInputMode
271
+ const pendingAttachments = useChatStore.getState().pendingAttachments
272
+ const pendingImages = pendingAttachments.filter((a) => a.kind === 'image')
273
+ const pendingTextAttachments = pendingAttachments.filter(
274
+ (a) => a.kind === 'text',
275
+ )
276
+
277
+ const trimmed = inputValue.trim()
278
+ // Allow empty messages if there are pending attachments (images or text)
279
+ const hasAttachments = pendingAttachments.length > 0
280
+ if (!trimmed && !hasAttachments) return
281
+
282
+ // Track user input complete
283
+ // Count @ mentions (simple pattern match - more accurate than nothing)
284
+ const mentionMatches = trimmed.match(/@\S+/g) || []
285
+ trackEvent(AnalyticsEvent.USER_INPUT_COMPLETE, {
286
+ inputLength: trimmed.length,
287
+ mode: agentMode,
288
+ inputMode,
289
+ hasImages: pendingImages.length > 0,
290
+ imageCount: pendingImages.length,
291
+ hasTextAttachments: pendingTextAttachments.length > 0,
292
+ textAttachmentCount: pendingTextAttachments.length,
293
+ isSlashCommand: isSlashCommand(trimmed),
294
+ isBashCommand: trimmed.startsWith('!'),
295
+ hasMentions: mentionMatches.length > 0,
296
+ mentionCount: mentionMatches.length,
297
+ })
298
+
299
+ // DAU signal: one un-sampled event per user-submitted prompt. The CLI's
300
+ // distinct id resolves to the canonical anvil user id (anonymous id is
301
+ // aliased to the real user id on login), matching the web and chat surfaces
302
+ // so combined DAU is a single unique-users query. Anvil-only: anvil
303
+ // CLI usage is intentionally excluded.
304
+ if (IS_ANVIL) {
305
+ const anvilSession = useAnvilSessionStore.getState().session
306
+ trackEvent(AnalyticsEvent.MESSAGE_SENT, {
307
+ surface: 'cli',
308
+ accessTier:
309
+ anvilSession && 'accessTier' in anvilSession
310
+ ? anvilSession.accessTier
311
+ : 'unknown',
312
+ mode: agentMode,
313
+ inputMode,
314
+ inputLength: trimmed.length,
315
+ isSlashCommand: isSlashCommand(trimmed),
316
+ isBashCommand: trimmed.startsWith('!'),
317
+ hasImages: pendingImages.length > 0,
318
+ })
319
+ }
320
+
321
+ // Handle bash mode commands
322
+ if (inputMode === 'bash') {
323
+ const commandWithBang = '!' + trimmed
324
+ saveToHistory(commandWithBang)
325
+ setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
326
+ setInputMode('default')
327
+ setInputFocused(true)
328
+ inputRef.current?.focus()
329
+
330
+ runBashCommand(trimmed)
331
+ return
332
+ }
333
+
334
+ // Handle plan mode input
335
+ if (inputMode === 'plan') {
336
+ if (!trimmed) return
337
+ saveToHistory(trimmed)
338
+ setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
339
+ setInputMode('default')
340
+ setInputFocused(true)
341
+ inputRef.current?.focus()
342
+
343
+ sendMessage({ content: buildPlanPrompt(trimmed), agentMode })
344
+ setTimeout(() => {
345
+ scrollToLatest()
346
+ }, 0)
347
+ return
348
+ }
349
+
350
+ // Handle interview mode input
351
+ if (inputMode === 'interview') {
352
+ if (!trimmed) return
353
+ saveToHistory(trimmed)
354
+ setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
355
+ setInputMode('default')
356
+ setInputFocused(true)
357
+ inputRef.current?.focus()
358
+
359
+ sendMessage({ content: buildInterviewPrompt(trimmed), agentMode })
360
+ setTimeout(() => {
361
+ scrollToLatest()
362
+ }, 0)
363
+ return
364
+ }
365
+
366
+ // Handle review mode input
367
+ if (inputMode === 'review') {
368
+ if (!trimmed) return
369
+ saveToHistory(trimmed)
370
+ setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
371
+ setInputMode('default')
372
+ setInputFocused(true)
373
+ inputRef.current?.focus()
374
+
375
+ sendMessage({ content: buildReviewPrompt('custom', trimmed), agentMode })
376
+ setTimeout(() => {
377
+ scrollToLatest()
378
+ }, 0)
379
+ return
380
+ }
381
+
382
+ // Handle bash commands from queue (starts with '!')
383
+ if (trimmed.startsWith('!') && trimmed.length > 1) {
384
+ const command = trimmed.slice(1)
385
+ saveToHistory(trimmed)
386
+ setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
387
+ runBashCommand(command)
388
+ return
389
+ }
390
+
391
+ // Handle image mode input
392
+ if (inputMode === 'image') {
393
+ const imagePath = trimmed
394
+ const projectRoot = getProjectRoot()
395
+
396
+ // Validate and add the image (handles path resolution, format check, and processing)
397
+ const result = await validateAndAddImage(imagePath, projectRoot)
398
+ if (!result.success) {
399
+ setMessages((prev) => [
400
+ ...prev,
401
+ getUserMessage(trimmed),
402
+ getSystemMessage(`❌ ${result.error}`),
403
+ ])
404
+ }
405
+
406
+ // Note: No system message added here - the PendingImagesBanner shows attached images
407
+ saveToHistory(trimmed)
408
+ setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
409
+ setInputMode('default')
410
+ return
411
+ }
412
+
413
+ // Handle slash commands or configured slashless exact commands.
414
+ const parsedCommand = parseCommandInput(trimmed)
415
+ if (parsedCommand) {
416
+ const commandDef = findCommand(parsedCommand.command)
417
+ if (commandDef) {
418
+ const argsLength = parsedCommand.args.length
419
+ const analyticsPayload = {
420
+ command: commandDef.name,
421
+ hasArgs: argsLength > 0,
422
+ argsLength,
423
+ agentMode,
424
+ ...(parsedCommand.implicitCommand ? { implicitCommand: true } : {}),
425
+ }
426
+
427
+ trackEvent(AnalyticsEvent.SLASH_COMMAND_USED, analyticsPayload)
428
+
429
+ // The command handler (via defineCommand/defineCommandWithArgs factories)
430
+ // is responsible for validating and handling args
431
+ return await commandDef.handler(params, parsedCommand.args)
432
+ }
433
+ }
434
+
435
+ // Regular message or unknown slash command - send to agent
436
+
437
+ // Block sending if attachments are still processing
438
+ if (hasProcessingImages() || hasProcessingFiles()) {
439
+ showClipboardMessage('processing attachments...', {
440
+ durationMs: 2000,
441
+ })
442
+ return
443
+ }
444
+
445
+ saveToHistory(trimmed)
446
+ setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
447
+
448
+ if (
449
+ isStreaming ||
450
+ streamMessageIdRef.current ||
451
+ isChainInProgressRef.current
452
+ ) {
453
+ const pendingAttachmentsForQueue = capturePendingAttachments()
454
+ // Pass a copy of pending attachments to the queue
455
+ addToQueue(trimmed, pendingAttachmentsForQueue)
456
+
457
+ setInputFocused(true)
458
+ inputRef.current?.focus()
459
+ return
460
+ }
461
+
462
+ // Unknown slash command - show error
463
+ if (isSlashCommand(trimmed)) {
464
+ // Track invalid/unknown command (only log command name, not full input for privacy)
465
+ const attemptedCmd = trimmed.slice(1).split(/\s+/)[0]?.toLowerCase() || ''
466
+ trackEvent(AnalyticsEvent.INVALID_COMMAND, {
467
+ attemptedCommand: attemptedCmd,
468
+ inputLength: trimmed.length,
469
+ agentMode,
470
+ })
471
+
472
+ setMessages((prev) => [
473
+ ...prev,
474
+ getUserMessage(trimmed),
475
+ getSystemMessage(`Command not found: ${JSON.stringify(trimmed)}`),
476
+ ])
477
+ return
478
+ }
479
+
480
+ sendMessage({ content: trimmed, agentMode })
481
+
482
+ setTimeout(() => {
483
+ scrollToLatest()
484
+ }, 0)
485
+
486
+ return
487
+ }
@@ -0,0 +1,26 @@
1
+ import { useChatStore } from '../state/chat-store'
2
+ import { getAuthToken } from '../utils/auth'
3
+ import { getSystemMessage } from '../utils/message-history'
4
+
5
+ import type { PostUserMessageFn } from '../types/contracts/send-message'
6
+
7
+ export async function handleUsageCommand(): Promise<{
8
+ postUserMessage: PostUserMessageFn
9
+ }> {
10
+ const authToken = getAuthToken()
11
+
12
+ if (!authToken) {
13
+ const postUserMessage: PostUserMessageFn = (prev) => [
14
+ ...prev,
15
+ getSystemMessage('Please log in first to view your usage.'),
16
+ ]
17
+ return { postUserMessage }
18
+ }
19
+
20
+ // Show the usage banner - the useUsageQuery hook will automatically fetch
21
+ // the data when the banner becomes visible
22
+ useChatStore.getState().setInputMode('usage')
23
+
24
+ const postUserMessage: PostUserMessageFn = (prev) => prev
25
+ return { postUserMessage }
26
+ }
@@ -0,0 +1,83 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import { getAdDisplayLabel, getInlineAdLayout } from '../ad-banner'
4
+
5
+ describe('ad banner display label', () => {
6
+ test('uses the display domain when the ad has a URL', () => {
7
+ expect(
8
+ getAdDisplayLabel({
9
+ title: 'Example Sponsor',
10
+ url: 'https://www.example.com/path',
11
+ }),
12
+ ).toEqual({ text: 'example.com', variant: 'domain' })
13
+ })
14
+
15
+ test('uses the ad title when the ad has no URL', () => {
16
+ expect(
17
+ getAdDisplayLabel({
18
+ title: 'Example Sponsor',
19
+ url: '',
20
+ }),
21
+ ).toEqual({ text: 'Example Sponsor', variant: 'title' })
22
+ })
23
+ })
24
+
25
+ describe('inline ad layout', () => {
26
+ const ad = {
27
+ adText:
28
+ 'Deploy frontends globally with zero config and preview every pull request.',
29
+ title: 'Vercel',
30
+ url: 'https://www.vercel.com/products',
31
+ }
32
+
33
+ test('fits the compact copy and sponsor within the card interior', () => {
34
+ const width = 60
35
+ const layout = getInlineAdLayout(ad, width)
36
+ const header = `${layout.title} Ad`
37
+ const detail = `${layout.description} ${layout.label} ↗`
38
+
39
+ expect(header.length).toBeLessThanOrEqual(width - 4)
40
+ expect(detail.length).toBeLessThanOrEqual(width - 4)
41
+ expect(layout.title).toBe('Vercel')
42
+ expect(layout.label).toBe('vercel.com')
43
+ expect(layout.description.endsWith('…')).toBe(true)
44
+ })
45
+
46
+ test('truncates long labels without starving narrow cards', () => {
47
+ const layout = getInlineAdLayout(
48
+ {
49
+ ...ad,
50
+ url: 'https://www.extraordinarily-long-sponsor-domain.example',
51
+ },
52
+ 48,
53
+ )
54
+
55
+ expect(layout.label).toBe('extraordinari…')
56
+ expect(layout.description.length).toBeGreaterThan(0)
57
+ expect(`${layout.title} Ad`.length).toBeLessThanOrEqual(44)
58
+ })
59
+
60
+ test('prioritizes copy over the destination on very narrow cards', () => {
61
+ const width = 47
62
+ const layout = getInlineAdLayout(ad, width)
63
+
64
+ expect(layout.label).toBe('')
65
+ expect(layout.description.length).toBe(width - 4)
66
+ expect(layout.description.endsWith('…')).toBe(true)
67
+ })
68
+
69
+ test('uses the full detail row when no destination domain is available', () => {
70
+ const layout = getInlineAdLayout(
71
+ {
72
+ adText: 'A Carbon ad whose tracked destination is intentionally hidden.',
73
+ title: 'Example Sponsor',
74
+ url: '',
75
+ },
76
+ 40,
77
+ )
78
+
79
+ expect(layout.title).toBe('Example Sponsor')
80
+ expect(layout.label).toBe('')
81
+ expect(layout.description.length).toBe(36)
82
+ })
83
+ })
@@ -0,0 +1,91 @@
1
+ import { afterEach, beforeAll, describe, expect, test } from 'bun:test'
2
+ import { createTestRenderer } from '@opentui/core/testing'
3
+ import { createRoot, flushSync } from '@opentui/react'
4
+ import React from 'react'
5
+
6
+ import { LandingHeadingRow } from '../anvil-landing-screen'
7
+ import { initializeThemeStore } from '../../hooks/use-theme'
8
+ import { getAnvilStreakLine } from '../../utils/anvil-streak-line'
9
+
10
+ let cleanupRenderer: (() => void) | undefined
11
+
12
+ beforeAll(() => {
13
+ initializeThemeStore()
14
+ })
15
+
16
+ afterEach(() => {
17
+ cleanupRenderer?.()
18
+ cleanupRenderer = undefined
19
+ })
20
+
21
+ /**
22
+ * Mirrors the landing screen's containers around the heading row: a
23
+ * shrink-to-fit column inside a centered, maxWidth-capped column. That shape
24
+ * is what broke the row — with nothing wider on screen the column collapses
25
+ * to the row's own width, leaving space-between no space to hand out.
26
+ */
27
+ const mountHeadingRow = async (streak: number, contentMaxWidth: number) => {
28
+ const setup = await createTestRenderer({ width: contentMaxWidth + 4, height: 6 })
29
+ const root = createRoot(setup.renderer)
30
+ cleanupRenderer = () => {
31
+ flushSync(() => root.unmount())
32
+ setup.renderer.destroy()
33
+ }
34
+ flushSync(() =>
35
+ root.render(
36
+ <box style={{ flexDirection: 'column', alignItems: 'center', maxWidth: contentMaxWidth }}>
37
+ <box style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 0 }}>
38
+ <LandingHeadingRow
39
+ streakLine={getAnvilStreakLine(streak)}
40
+ marginBottom={1}
41
+ />
42
+ </box>
43
+ </box>,
44
+ ),
45
+ )
46
+ await setup.renderOnce()
47
+ return setup
48
+ }
49
+
50
+ const renderHeadingRow = async (streak: number, contentMaxWidth: number) =>
51
+ (await mountHeadingRow(streak, contentMaxWidth)).captureCharFrame()
52
+
53
+ describe('LandingHeadingRow', () => {
54
+ test('keeps the streak clear of the heading when nothing wider is on screen', async () => {
55
+ const frame = await renderHeadingRow(18, 60)
56
+ const line = frame
57
+ .split('\n')
58
+ .find((row) => row.includes('Start coding for free'))!
59
+
60
+ // The bug: "Start coding for free18 day streak".
61
+ expect(line).not.toContain('free18')
62
+ expect(line).toMatch(/Start coding for free {3,}18 day streak/)
63
+ })
64
+
65
+ test('draws the progress dots as filled/hollow circles', async () => {
66
+ const frame = await renderHeadingRow(18, 60)
67
+
68
+ expect(frame).toContain('●●●●●●●+')
69
+ })
70
+
71
+ test('leaves the row bare for a user with no streak yet', async () => {
72
+ const frame = await renderHeadingRow(0, 60)
73
+
74
+ expect(frame).toContain('Start coding for free')
75
+ expect(frame).not.toContain('day streak')
76
+ })
77
+
78
+ // The landing screen only hands over a streak that fits, but a resize lays
79
+ // the current tree out against the new width before React re-renders, so the
80
+ // row does get measured too narrow for a frame. As a space-between flex row
81
+ // that overflow segfaulted the native renderer — it has to survive as text.
82
+ test('survives being laid out narrower than its content', async () => {
83
+ const setup = await mountHeadingRow(18, 60)
84
+
85
+ for (const width of [34, 20, 12]) {
86
+ setup.resize(width, 6)
87
+ await setup.renderOnce()
88
+ expect(setup.captureCharFrame()).toContain('Start')
89
+ }
90
+ })
91
+ })