@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,217 @@
1
+ import { API_KEY_ENV_VAR } from '@anvil/common/old-constants'
2
+ import { AskUserBridge } from '@anvil/common/utils/ask-user-bridge'
3
+ import { AnvilClient } from '@anvil/sdk'
4
+
5
+ import { getAuthTokenDetails } from './auth'
6
+ import { getCliEnv, getSystemProcessEnv } from './env'
7
+ import { loadAgentDefinitions } from './local-agent-registry'
8
+ import { logger } from './logger'
9
+ import { createTraceWriter } from './trace-writer'
10
+ import { terminalCommandBroker } from './terminal-command-broker'
11
+ import { getRgPath } from '../native/ripgrep'
12
+ import { getProjectRoot } from '../project-files'
13
+
14
+ import type { ClientToolCall } from '@anvil/common/tools/list'
15
+
16
+ let clientInstance: AnvilClient | null = null
17
+
18
+ /**
19
+ * Recursively removes undefined values from an object to ensure clean JSON serialization.
20
+ * This prevents issues with APIs that don't accept explicit undefined values.
21
+ */
22
+ function removeUndefinedValues<T>(obj: T): T {
23
+ if (obj === null || obj === undefined) {
24
+ return obj
25
+ }
26
+ if (Array.isArray(obj)) {
27
+ return obj.map(removeUndefinedValues) as T
28
+ }
29
+ if (typeof obj === 'object') {
30
+ const result: Record<string, unknown> = {}
31
+ for (const [key, value] of Object.entries(obj)) {
32
+ if (value !== undefined) {
33
+ result[key] = removeUndefinedValues(value)
34
+ }
35
+ }
36
+ return result as T
37
+ }
38
+ return obj
39
+ }
40
+
41
+ /**
42
+ * Reset the cached AnvilClient instance.
43
+ * This should be called after login to ensure the client is re-initialized with new credentials.
44
+ */
45
+ export function resetAnvilClient(): void {
46
+ clientInstance = null
47
+ }
48
+
49
+ export async function getAnvilClient(): Promise<AnvilClient | null> {
50
+ if (!clientInstance) {
51
+ const { token: apiKey } = getAuthTokenDetails()
52
+
53
+ if (!apiKey) {
54
+ logger.warn(
55
+ {},
56
+ `No authentication token found. Please run the login flow or set ${API_KEY_ENV_VAR}.`,
57
+ )
58
+ return null
59
+ }
60
+
61
+ const projectRoot = getProjectRoot()
62
+
63
+ // Set up ripgrep path for SDK to use
64
+ const env = getCliEnv()
65
+ if (env.ANVIL_IS_BINARY) {
66
+ try {
67
+ const rgPath = await getRgPath()
68
+ // Note: We still set process.env here because SDK reads from it
69
+ getSystemProcessEnv().ANVIL_RG_PATH = rgPath
70
+ } catch (error) {
71
+ logger.error(error, 'Failed to set up ripgrep binary for SDK')
72
+ }
73
+ }
74
+
75
+ try {
76
+ const agentDefinitions = loadAgentDefinitions()
77
+ clientInstance = new AnvilClient({
78
+ apiKey,
79
+ cwd: projectRoot,
80
+ agentDefinitions,
81
+ logger,
82
+ traceWriter: createTraceWriter(),
83
+ terminalCommandBroker,
84
+ overrideTools: {
85
+ ask_user: async (input: ClientToolCall<'ask_user'>['input']) => {
86
+ const askUserResponse = await AskUserBridge.request(
87
+ 'cli-override',
88
+ input.questions,
89
+ )
90
+ const response = askUserResponse as {
91
+ answers?: Array<{ questionIndex: number; selectedOption: string }>
92
+ skipped?: boolean
93
+ }
94
+ return [
95
+ {
96
+ type: 'json',
97
+ value: removeUndefinedValues(response),
98
+ },
99
+ ]
100
+ },
101
+ },
102
+ })
103
+ } catch (error) {
104
+ logger.error(error, 'Failed to initialize AnvilClient')
105
+ return null
106
+ }
107
+ }
108
+
109
+ return clientInstance
110
+ }
111
+
112
+ export function getToolDisplayInfo(toolName: string): {
113
+ name: string
114
+ type: string
115
+ } {
116
+ const TOOL_NAME_OVERRIDES: Record<string, string> = {
117
+ list_directory: 'List Directories',
118
+ }
119
+
120
+ const capitalizeWords = (str: string) => {
121
+ return str.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())
122
+ }
123
+
124
+ return {
125
+ name: TOOL_NAME_OVERRIDES[toolName] ?? capitalizeWords(toolName),
126
+ type: 'tool',
127
+ }
128
+ }
129
+
130
+ function toYaml(obj: any, indent = 0): string {
131
+ const spaces = ' '.repeat(indent)
132
+
133
+ if (obj === null || obj === undefined) {
134
+ return 'null'
135
+ }
136
+
137
+ if (typeof obj === 'string') {
138
+ if (obj.includes('\n')) {
139
+ const lines = obj.split('\n')
140
+ return (
141
+ '|\n' + lines.map((line) => ' '.repeat(indent + 1) + line).join('\n')
142
+ )
143
+ }
144
+ return obj.includes(':') || obj.includes('#') ? `"${obj}"` : obj
145
+ }
146
+
147
+ if (typeof obj === 'number' || typeof obj === 'boolean') {
148
+ return String(obj)
149
+ }
150
+
151
+ if (Array.isArray(obj)) {
152
+ if (obj.length === 0) return '[]'
153
+ return (
154
+ '\n' +
155
+ obj
156
+ .map((item) => spaces + '- ' + toYaml(item, indent + 1).trimStart())
157
+ .join('\n')
158
+ )
159
+ }
160
+
161
+ if (typeof obj === 'object') {
162
+ const entries = Object.entries(obj)
163
+ if (entries.length === 0) return '{}'
164
+
165
+ return entries
166
+ .map(([key, value]) => {
167
+ const yamlValue = toYaml(value, indent + 1)
168
+ if (
169
+ typeof value === 'object' &&
170
+ value !== null &&
171
+ !Array.isArray(value) &&
172
+ Object.keys(value).length > 0
173
+ ) {
174
+ return `${spaces}${key}:\n${yamlValue}`
175
+ }
176
+ if (typeof value === 'string' && value.includes('\n')) {
177
+ return `${spaces}${key}: ${yamlValue}`
178
+ }
179
+ return `${spaces}${key}: ${yamlValue}`
180
+ })
181
+ .join('\n')
182
+ }
183
+
184
+ return String(obj)
185
+ }
186
+
187
+ export function formatToolOutput(output: unknown): string {
188
+ if (!output) return ''
189
+
190
+ if (Array.isArray(output)) {
191
+ return output
192
+ .map((item) => {
193
+ if (item.type === 'json') {
194
+ // Handle errorMessage in the value object
195
+ if (
196
+ item.value &&
197
+ typeof item.value === 'object' &&
198
+ 'errorMessage' in item.value
199
+ ) {
200
+ return String(item.value.errorMessage)
201
+ }
202
+ return toYaml(item.value)
203
+ }
204
+ if (item.type === 'text') {
205
+ return item.text || ''
206
+ }
207
+ return String(item)
208
+ })
209
+ .join('\n')
210
+ }
211
+
212
+ if (typeof output === 'string') {
213
+ return output
214
+ }
215
+
216
+ return toYaml(output)
217
+ }
@@ -0,0 +1,66 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ import { getConfigDir } from './auth'
5
+ import { logger } from './logger'
6
+
7
+ interface AnvilInstanceOwner {
8
+ instanceId: string
9
+ pid: number
10
+ }
11
+
12
+ const OWNER_FILE = 'anvil-instance-owner.json'
13
+
14
+ const getOwnerPath = (): string => path.join(getConfigDir(), OWNER_FILE)
15
+
16
+ function readOwner(): AnvilInstanceOwner | null {
17
+ try {
18
+ const raw = fs.readFileSync(getOwnerPath(), 'utf8')
19
+ const parsed = JSON.parse(raw) as Partial<AnvilInstanceOwner>
20
+ if (
21
+ typeof parsed.instanceId !== 'string' ||
22
+ typeof parsed.pid !== 'number'
23
+ ) {
24
+ return null
25
+ }
26
+ return {
27
+ instanceId: parsed.instanceId,
28
+ pid: parsed.pid,
29
+ }
30
+ } catch {
31
+ return null
32
+ }
33
+ }
34
+
35
+ function isProcessRunning(pid: number): boolean {
36
+ if (!Number.isInteger(pid) || pid <= 0) return false
37
+ try {
38
+ process.kill(pid, 0)
39
+ return true
40
+ } catch (error) {
41
+ return (error as NodeJS.ErrnoException).code === 'EPERM'
42
+ }
43
+ }
44
+
45
+ export function recordAnvilInstanceOwner(instanceId: string): void {
46
+ try {
47
+ fs.mkdirSync(getConfigDir(), { recursive: true })
48
+ fs.writeFileSync(
49
+ getOwnerPath(),
50
+ JSON.stringify({ instanceId, pid: process.pid }, null, 2),
51
+ )
52
+ } catch (error) {
53
+ logger.debug(
54
+ { error: error instanceof Error ? error.message : String(error) },
55
+ '[anvil-session] Failed to record local owner',
56
+ )
57
+ }
58
+ }
59
+
60
+ export function isAnvilInstanceOwnedByDeadLocalProcess(
61
+ instanceId: string,
62
+ ): boolean {
63
+ const owner = readOwner()
64
+ if (!owner || owner.instanceId !== instanceId) return false
65
+ return !isProcessRunning(owner.pid)
66
+ }
@@ -0,0 +1,50 @@
1
+ export type AnvilModelNavigationDirection = 'forward' | 'backward'
2
+
3
+ const FORWARD_KEY_NAMES = new Set(['right', 'down'])
4
+ const BACKWARD_KEY_NAMES = new Set(['left', 'up'])
5
+ const FORWARD_TAB_SEQUENCES = new Set(['\t', '\x1b[9u'])
6
+ const BACKWARD_TAB_SEQUENCES = new Set([
7
+ '\x1b[Z',
8
+ '\x1b[9;2u',
9
+ '\x1b[27;2;9~',
10
+ ])
11
+
12
+ export function nextAnvilModelId(params: {
13
+ modelIds: readonly string[]
14
+ focusedId: string
15
+ direction: AnvilModelNavigationDirection
16
+ }): string | null {
17
+ const { modelIds, focusedId, direction } = params
18
+ if (modelIds.length === 0) return null
19
+
20
+ const currentIdx = modelIds.indexOf(focusedId)
21
+ if (currentIdx === -1) return modelIds[0] ?? null
22
+
23
+ const step = direction === 'forward' ? 1 : -1
24
+ return modelIds[(currentIdx + step + modelIds.length) % modelIds.length]
25
+ }
26
+
27
+ export function anvilModelNavigationDirectionForKey(key: {
28
+ name?: string
29
+ shift?: boolean
30
+ sequence?: string
31
+ raw?: string
32
+ }): AnvilModelNavigationDirection | null {
33
+ const name = (key.name ?? '').toLowerCase()
34
+ const sequence = key.sequence ?? key.raw ?? ''
35
+
36
+ if (FORWARD_KEY_NAMES.has(name)) return 'forward'
37
+ if (BACKWARD_KEY_NAMES.has(name)) return 'backward'
38
+
39
+ if (
40
+ (name === 'tab' && Boolean(key.shift)) ||
41
+ BACKWARD_TAB_SEQUENCES.has(sequence)
42
+ ) {
43
+ return 'backward'
44
+ }
45
+ if (name === 'tab' || FORWARD_TAB_SEQUENCES.has(sequence)) {
46
+ return 'forward'
47
+ }
48
+
49
+ return null
50
+ }
@@ -0,0 +1,55 @@
1
+ import { ANVIL_PREMIUM_SESSION_RESET_TIMEZONE } from '@anvil/common/constants/anvil-models'
2
+ import { getZonedDayBounds } from '@anvil/common/util/zoned-time'
3
+
4
+ import type { AnvilSessionRateLimitByModel } from '@anvil/common/types/anvil-session'
5
+
6
+ export function getAnvilPremiumResetAt(params: {
7
+ rateLimitsByModel?: AnvilSessionRateLimitByModel
8
+ nowMs: number
9
+ }): Date {
10
+ const { rateLimitsByModel, nowMs } = params
11
+ const serverResetAt = rateLimitsByModel
12
+ ? Object.values(rateLimitsByModel)[0]?.resetAt
13
+ : undefined
14
+ const parsedServerResetAt = serverResetAt ? new Date(serverResetAt) : null
15
+
16
+ if (
17
+ parsedServerResetAt &&
18
+ Number.isFinite(parsedServerResetAt.getTime())
19
+ ) {
20
+ return parsedServerResetAt
21
+ }
22
+
23
+ return getZonedDayBounds(
24
+ new Date(nowMs),
25
+ ANVIL_PREMIUM_SESSION_RESET_TIMEZONE,
26
+ ).resetsAt
27
+ }
28
+
29
+ /**
30
+ * Human "resets in …" countdown. Daily pools stop at hours (`withDays` off);
31
+ * a multi-day window (e.g. a lapsed reset) sets `withDays` so it reads as "2d 5h"
32
+ * instead of a 100-hour figure.
33
+ */
34
+ export function formatAnvilPremiumResetCountdown(
35
+ resetAt: Date,
36
+ nowMs: number,
37
+ { withDays = false }: { withDays?: boolean } = {},
38
+ ): string {
39
+ const diffMs = resetAt.getTime() - nowMs
40
+ if (!Number.isFinite(diffMs) || diffMs <= 0) return 'now'
41
+
42
+ const totalMinutes = Math.max(1, Math.floor(diffMs / 60_000))
43
+ if (withDays) {
44
+ const days = Math.floor(totalMinutes / (60 * 24))
45
+ if (days > 0) {
46
+ const hours = Math.floor((totalMinutes % (60 * 24)) / 60)
47
+ return hours === 0 ? `${days}d` : `${days}d ${hours}h`
48
+ }
49
+ }
50
+ const hours = Math.floor(totalMinutes / 60)
51
+ const minutes = totalMinutes % 60
52
+
53
+ if (hours === 0) return `${minutes}m`
54
+ return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`
55
+ }
@@ -0,0 +1,51 @@
1
+ import { getReferralInfo } from '@anvil/common/types/anvil-session'
2
+
3
+ import type { AnvilAccessTier } from '@anvil/common/constants/anvil-models'
4
+ import type { AnvilReferralInfo } from '@anvil/common/types/anvil-session'
5
+ import type { AnvilSessionResponse } from '../types/anvil-session'
6
+
7
+ /**
8
+ * Process-wide cache of the most recent referral block the server sent for
9
+ * each access tier.
10
+ *
11
+ * The server only attaches `referral` to `none` (landing) responses — once the
12
+ * user joins (queued/active) or ends a session (ended) it's dropped from the
13
+ * payload. That breaks the return-to-landing flow: after a session ends the
14
+ * slot DELETE leaves an `ended` row, so the landing GET sees `ended` (no
15
+ * referral) until that row is swept, which would otherwise blank the GLM
16
+ * referral banner for the whole visit. Caching the last-known block lets the
17
+ * picker re-render it immediately; a later clean `none` GET refreshes it.
18
+ */
19
+ let referralByAccessTier: Partial<
20
+ Record<AnvilAccessTier, AnvilReferralInfo>
21
+ > = {}
22
+
23
+ /** Remember the referral block whenever a response includes one, so it can be
24
+ * carried across the join → end → return-to-landing round-trip. Active/ended
25
+ * responses intentionally omit it and keep the prior value; an authoritative
26
+ * `none` response without it clears stale metadata for that tier. */
27
+ export function rememberReferral(session: AnvilSessionResponse | null): void {
28
+ const referral = getReferralInfo(session)
29
+ const accessTier =
30
+ session && 'accessTier' in session ? session.accessTier : undefined
31
+ // Full- and limited-tier referral blocks have different meanings. Never
32
+ // cache one without the tier needed to interpret it safely.
33
+ if (!accessTier) return
34
+ if (referral) {
35
+ referralByAccessTier[accessTier] = referral
36
+ } else if (session?.status === 'none') {
37
+ delete referralByAccessTier[accessTier]
38
+ }
39
+ }
40
+
41
+ /** The last referral block seen for this tier, if any. */
42
+ export function getCachedReferral(
43
+ accessTier: AnvilAccessTier | undefined,
44
+ ): AnvilReferralInfo | undefined {
45
+ return accessTier ? referralByAccessTier[accessTier] : undefined
46
+ }
47
+
48
+ /** Clear account-scoped referral metadata when its session owner unmounts. */
49
+ export function clearReferralCache(): void {
50
+ referralByAccessTier = {}
51
+ }
@@ -0,0 +1,243 @@
1
+ import { env } from '@anvil/common/env'
2
+ import {
3
+ ANVIL_COMPACT_SESSION_HEADER,
4
+ ANVIL_INSTANCE_HEADER,
5
+ ANVIL_MODEL_HEADER,
6
+ } from '@anvil/common/constants/anvil-models'
7
+
8
+ import { useAnvilSessionStore } from '../state/anvil-session-store'
9
+ import { getAuthTokenDetails } from './auth'
10
+ import { IS_ANVIL } from './constants'
11
+
12
+ import type { AnvilSessionResponse } from '../types/anvil-session'
13
+ import type { AnvilSessionServerResponse } from '@anvil/common/types/anvil-session'
14
+
15
+ const SESSION_FETCH_TIMEOUT_MS = 20_000
16
+ export type AnvilSessionMethod = 'POST' | 'GET' | 'DELETE'
17
+
18
+ export class AnvilSessionRequestError extends Error {
19
+ constructor(
20
+ message: string,
21
+ readonly statusCode: number,
22
+ readonly retryAfterMs?: number,
23
+ readonly errorCode?: string,
24
+ ) {
25
+ super(message)
26
+ this.name = 'AnvilSessionRequestError'
27
+ }
28
+ }
29
+
30
+ export function isAnvilSessionTimeoutError(error: unknown): boolean {
31
+ return (
32
+ error instanceof Error &&
33
+ (error.name === 'TimeoutError' || /timeout|timed out/i.test(error.message))
34
+ )
35
+ }
36
+
37
+ export type AnvilSessionFailureDisposition = 'retry' | 'stop' | 'unknown'
38
+
39
+ /** How the poll loop should handle a failed request.
40
+ *
41
+ * A POST without a response may already have rotated the active instance, so
42
+ * repeating it is unsafe without protocol-level idempotency. HTTP 408, 429,
43
+ * and 503 responses are the exception: edge rejection or admission shedding
44
+ * produces them before the session mutation can commit. GET is read-only and
45
+ * can retry transient failures normally. */
46
+ export function classifyAnvilSessionRequestFailure(
47
+ method: Extract<AnvilSessionMethod, 'POST' | 'GET'>,
48
+ error: unknown,
49
+ ): AnvilSessionFailureDisposition {
50
+ if (method === 'POST') {
51
+ if (!(error instanceof AnvilSessionRequestError)) return 'unknown'
52
+ // These responses are produced before the session mutation can commit:
53
+ // 408/429 come from an edge or unparsed response (the endpoint's typed
54
+ // rate-limit responses are returned above), and a 503 means no handler was
55
+ // available or admission shed the request. Retrying them cannot repeat a
56
+ // successful takeover.
57
+ if ([408, 429, 503].includes(error.statusCode)) {
58
+ return 'retry'
59
+ }
60
+ return error.statusCode >= 400 && error.statusCode < 500
61
+ ? 'stop'
62
+ : 'unknown'
63
+ }
64
+
65
+ if (!(error instanceof AnvilSessionRequestError)) return 'retry'
66
+ return error.statusCode === 408 ||
67
+ error.statusCode === 429 ||
68
+ error.statusCode >= 500
69
+ ? 'retry'
70
+ : 'stop'
71
+ }
72
+
73
+ export function parseRetryAfterMs(
74
+ value: string | null,
75
+ nowMs = Date.now(),
76
+ ): number | undefined {
77
+ if (!value) return undefined
78
+ const seconds = Number(value)
79
+ if (Number.isFinite(seconds) && seconds >= 0) {
80
+ const milliseconds = seconds * 1_000
81
+ return Number.isFinite(milliseconds) ? Math.ceil(milliseconds) : undefined
82
+ }
83
+ const dateMs = Date.parse(value)
84
+ return Number.isFinite(dateMs) ? Math.max(0, dateMs - nowMs) : undefined
85
+ }
86
+
87
+ /** Combine the caller's abort signal with a per-request timeout. */
88
+ export function sessionFetchSignal(
89
+ signal: AbortSignal | undefined,
90
+ timeoutMs: number = SESSION_FETCH_TIMEOUT_MS,
91
+ ): AbortSignal {
92
+ const timeout = AbortSignal.timeout(timeoutMs)
93
+ return signal ? AbortSignal.any([signal, timeout]) : timeout
94
+ }
95
+
96
+ function sessionEndpoint(): string {
97
+ const base = (
98
+ env.NEXT_PUBLIC_ANVIL_APP_URL || 'https://codebuff.com'
99
+ ).replace(/\/$/, '')
100
+ return `${base}/api/v1/anvil/session`
101
+ }
102
+
103
+ export async function callAnvilSession(
104
+ method: AnvilSessionMethod,
105
+ token: string,
106
+ opts: {
107
+ instanceId?: string
108
+ model?: string
109
+ signal?: AbortSignal
110
+ compact?: boolean
111
+ } = {},
112
+ ): Promise<AnvilSessionServerResponse> {
113
+ const headers: Record<string, string> = { Authorization: `Bearer ${token}` }
114
+ if (method === 'GET' && opts.instanceId) {
115
+ headers[ANVIL_INSTANCE_HEADER] = opts.instanceId
116
+ }
117
+ if (method === 'GET' && opts.compact) {
118
+ headers[ANVIL_COMPACT_SESSION_HEADER] = '1'
119
+ }
120
+ if (method === 'POST' && opts.model) {
121
+ headers[ANVIL_MODEL_HEADER] = opts.model
122
+ }
123
+
124
+ const response = await fetch(sessionEndpoint(), {
125
+ method,
126
+ headers,
127
+ signal: sessionFetchSignal(opts.signal),
128
+ })
129
+
130
+ if (response.status === 404) {
131
+ return { status: 'none' }
132
+ }
133
+
134
+ if (response.status === 403) {
135
+ const body = (await response
136
+ .json()
137
+ .catch(() => null)) as AnvilSessionServerResponse | null
138
+ if (
139
+ body &&
140
+ (body.status === 'country_blocked' || body.status === 'banned')
141
+ ) {
142
+ return body
143
+ }
144
+ }
145
+
146
+ if (response.status === 409 && method === 'POST') {
147
+ const body = (await response
148
+ .json()
149
+ .catch(() => null)) as AnvilSessionServerResponse | null
150
+ if (
151
+ body &&
152
+ (body.status === 'model_locked' || body.status === 'model_unavailable')
153
+ ) {
154
+ return body
155
+ }
156
+ }
157
+
158
+ if (response.status === 429 && method === 'POST') {
159
+ const body = (await response
160
+ .json()
161
+ .catch(() => null)) as AnvilSessionServerResponse | null
162
+ if (
163
+ body &&
164
+ (body.status === 'rate_limited' ||
165
+ body.status === 'spend_limited' ||
166
+ body.status === 'ip_capped')
167
+ ) {
168
+ return body
169
+ }
170
+ }
171
+
172
+ if (!response.ok) {
173
+ const text = await response.text().catch(() => '')
174
+ let errorCode: string | undefined
175
+ try {
176
+ const body = JSON.parse(text) as { error?: unknown }
177
+ if (typeof body.error === 'string') errorCode = body.error
178
+ } catch {
179
+ // Non-JSON errors have no machine-readable code.
180
+ }
181
+ throw new AnvilSessionRequestError(
182
+ `anvil session ${method} failed: ${response.status} ${text.slice(0, 200)}`,
183
+ response.status,
184
+ parseRetryAfterMs(response.headers.get('retry-after')),
185
+ errorCode,
186
+ )
187
+ }
188
+
189
+ return (await response.json()) as AnvilSessionServerResponse
190
+ }
191
+
192
+ /** A compact poll omits quota fields that were already returned by admission.
193
+ * Keep that snapshot only for the same active session; null tells the poller
194
+ * to fetch one full response before compacting again. */
195
+ export function mergeCompactActiveSession(
196
+ current: AnvilSessionResponse | null,
197
+ next: AnvilSessionServerResponse,
198
+ ): AnvilSessionServerResponse | null {
199
+ if (
200
+ current?.status !== 'active' ||
201
+ next.status !== 'active' ||
202
+ current.instanceId !== next.instanceId ||
203
+ current.model !== next.model
204
+ ) {
205
+ return null
206
+ }
207
+ return {
208
+ ...next,
209
+ rateLimit: next.rateLimit ?? current.rateLimit,
210
+ rateLimitsByModel: next.rateLimitsByModel ?? current.rateLimitsByModel,
211
+ }
212
+ }
213
+
214
+ export function holdsLiveAnvilSlot(
215
+ current: AnvilSessionResponse | null,
216
+ ): boolean {
217
+ if (!current) return false
218
+ return (
219
+ current.status === 'active' ||
220
+ (current.status === 'ended' && Boolean(current.instanceId))
221
+ )
222
+ }
223
+
224
+ /** Best-effort DELETE of the caller's session row when it holds a live slot. */
225
+ export async function releaseAnvilSlot(): Promise<void> {
226
+ const current = useAnvilSessionStore.getState().session
227
+ if (!holdsLiveAnvilSlot(current)) return
228
+
229
+ const { token } = getAuthTokenDetails()
230
+ if (!token) return
231
+
232
+ try {
233
+ await callAnvilSession('DELETE', token)
234
+ } catch {
235
+ // The server-side sweep is the backstop.
236
+ }
237
+ }
238
+
239
+ /** Release the Anvil slot on exit paths that skip React unmount. */
240
+ export async function endAnvilSessionBestEffort(): Promise<void> {
241
+ if (!IS_ANVIL) return
242
+ await releaseAnvilSlot()
243
+ }
@@ -0,0 +1,21 @@
1
+ export const ANVIL_COUNTDOWN_VISIBLE_MS = 5 * 60_000
2
+
3
+ export const formatAnvilSessionCountdown = (ms: number): string => {
4
+ if (ms <= 0) return 'expiring…'
5
+ const totalSeconds = Math.ceil(ms / 1000)
6
+ const m = Math.floor(totalSeconds / 60)
7
+ const s = totalSeconds % 60
8
+ return `${m}:${s.toString().padStart(2, '0')}`
9
+ }
10
+
11
+ export const formatAnvilSessionRemaining = (ms: number): string => {
12
+ if (ms <= 0) return 'expiring…'
13
+ if (ms < ANVIL_COUNTDOWN_VISIBLE_MS) {
14
+ return `${formatAnvilSessionCountdown(ms)} left`
15
+ }
16
+ const totalMinutes = Math.ceil(ms / 60_000)
17
+ if (totalMinutes < 60) return `${totalMinutes}m left`
18
+ const hours = Math.floor(totalMinutes / 60)
19
+ const minutes = totalMinutes % 60
20
+ return minutes === 0 ? `${hours}h left` : `${hours}h ${minutes}m left`
21
+ }