@alumbwe/anvil 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (529) hide show
  1. package/README.md +84 -0
  2. package/bin/anvil +2 -0
  3. package/bin/tree-sitter.wasm +0 -0
  4. package/package.json +67 -0
  5. package/src/__tests__/README.md +256 -0
  6. package/src/__tests__/bash-mode.test.ts +442 -0
  7. package/src/__tests__/cli-args.test.ts +181 -0
  8. package/src/__tests__/cli-harness.test.ts +52 -0
  9. package/src/__tests__/e2e/first-time-login.test.ts +173 -0
  10. package/src/__tests__/e2e/logout-relogin-flow.test.ts +110 -0
  11. package/src/__tests__/e2e/returning-user-auth.test.ts +113 -0
  12. package/src/__tests__/e2e-cli.test.ts +183 -0
  13. package/src/__tests__/helpers/launcher-disconnect-fixture.cjs +176 -0
  14. package/src/__tests__/helpers/mock-api-client.ts +64 -0
  15. package/src/__tests__/helpers/renderer-cleanup-fixture.tsx +89 -0
  16. package/src/__tests__/helpers/terminal-watchdog-fixture.ts +75 -0
  17. package/src/__tests__/home-directory-detection.test.ts +213 -0
  18. package/src/__tests__/integration/api-integration.test.ts +288 -0
  19. package/src/__tests__/integration/credentials-storage.test.ts +486 -0
  20. package/src/__tests__/integration/local-agents.test.ts +1187 -0
  21. package/src/__tests__/integration/login-flow-instrumentation.test.ts +134 -0
  22. package/src/__tests__/integration/login-polling-working.test.ts +285 -0
  23. package/src/__tests__/integration/queue-editing.test.tsx +261 -0
  24. package/src/__tests__/integration/usage-refresh-on-completion.test.ts +168 -0
  25. package/src/__tests__/integration-tmux.test.ts +188 -0
  26. package/src/__tests__/launcher-avx2-fallback.test.ts +630 -0
  27. package/src/__tests__/launcher-disconnect.test.ts +104 -0
  28. package/src/__tests__/mocks/hover-toggle-controller.ts +71 -0
  29. package/src/__tests__/open-tui-native-bundle.test.ts +378 -0
  30. package/src/__tests__/path-completion.test.ts +243 -0
  31. package/src/__tests__/project-files-chat-id.test.ts +34 -0
  32. package/src/__tests__/release/proxy-http-get.test.ts +667 -0
  33. package/src/__tests__/release/wrapper-safety.test.ts +412 -0
  34. package/src/__tests__/renderer-cleanup.test.ts +174 -0
  35. package/src/__tests__/rerender-perf.integration.test.ts +283 -0
  36. package/src/__tests__/terminal-reset-sequences.test.ts +36 -0
  37. package/src/__tests__/terminal-watchdog.test.ts +388 -0
  38. package/src/__tests__/test-utils.ts +271 -0
  39. package/src/__tests__/tmux-poc.ts +150 -0
  40. package/src/__tests__/unit/agent-mode-toggle.test.ts +137 -0
  41. package/src/__tests__/unit/copy-button.test.ts +157 -0
  42. package/src/__tests__/unit/create-run-config.test.ts +66 -0
  43. package/src/__tests__/unit/publish-confirmation.test.ts +70 -0
  44. package/src/__tests__/unit/segmented-control.test.ts +152 -0
  45. package/src/__tests__/utils/env.test.ts +169 -0
  46. package/src/__tests__/utils/project-picker.test.ts +39 -0
  47. package/src/agents/bundled-agents.generated.d.ts +14 -0
  48. package/src/agents/bundled-agents.generated.ts +3692 -0
  49. package/src/app.tsx +399 -0
  50. package/src/chat.tsx +1698 -0
  51. package/src/cli-args.ts +134 -0
  52. package/src/commands/__tests__/bash-command.test.ts +445 -0
  53. package/src/commands/__tests__/command-args.test.ts +349 -0
  54. package/src/commands/__tests__/copy-conversation-clipboard.test.ts +108 -0
  55. package/src/commands/__tests__/copy-conversation.test.ts +226 -0
  56. package/src/commands/__tests__/diagnostics-command.test.ts +35 -0
  57. package/src/commands/__tests__/freebuff-command-aliases.test.ts +56 -0
  58. package/src/commands/__tests__/image.test.ts +99 -0
  59. package/src/commands/__tests__/init.test.ts +456 -0
  60. package/src/commands/__tests__/new-command-rotates-chat.test.ts +91 -0
  61. package/src/commands/__tests__/process-diagnostics.test.ts +54 -0
  62. package/src/commands/__tests__/prompt-builders.test.ts +28 -0
  63. package/src/commands/__tests__/router-input.test.ts +298 -0
  64. package/src/commands/ads.ts +44 -0
  65. package/src/commands/command-registry.ts +726 -0
  66. package/src/commands/copy-conversation.ts +392 -0
  67. package/src/commands/help.ts +13 -0
  68. package/src/commands/image.ts +20 -0
  69. package/src/commands/init.ts +121 -0
  70. package/src/commands/process-diagnostics.ts +120 -0
  71. package/src/commands/prompt-builders.ts +100 -0
  72. package/src/commands/publish.ts +203 -0
  73. package/src/commands/router-utils.ts +80 -0
  74. package/src/commands/router.ts +487 -0
  75. package/src/commands/usage.ts +26 -0
  76. package/src/components/__tests__/ad-banner.test.tsx +83 -0
  77. package/src/components/__tests__/chat-input-bar.test.tsx +130 -0
  78. package/src/components/__tests__/freebuff-landing-heading-row.test.tsx +91 -0
  79. package/src/components/__tests__/freebuff-model-selector.test.tsx +519 -0
  80. package/src/components/__tests__/freebuff-offer-invariants.test.ts +54 -0
  81. package/src/components/__tests__/freebuff-takeover-prompt.test.tsx +212 -0
  82. package/src/components/__tests__/grid-layout.integration.test.tsx +304 -0
  83. package/src/components/__tests__/grid-layout.test.tsx +1051 -0
  84. package/src/components/__tests__/message-block.completion.test.tsx +104 -0
  85. package/src/components/__tests__/message-block.streaming.test.tsx +78 -0
  86. package/src/components/__tests__/message-with-agents.test.tsx +579 -0
  87. package/src/components/__tests__/multiline-input.test.tsx +1132 -0
  88. package/src/components/__tests__/queue-panel.test.tsx +398 -0
  89. package/src/components/__tests__/selectable-list.test.ts +232 -0
  90. package/src/components/__tests__/status-bar.test.tsx +44 -0
  91. package/src/components/__tests__/status-indicator.test.tsx +204 -0
  92. package/src/components/__tests__/terminal-thinking-orb.test.tsx +111 -0
  93. package/src/components/__tests__/user-error-banner.test.tsx +102 -0
  94. package/src/components/ad-banner.tsx +323 -0
  95. package/src/components/agent-checklist.tsx +350 -0
  96. package/src/components/agent-mode-toggle.tsx +241 -0
  97. package/src/components/anvil-mark.tsx +52 -0
  98. package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +432 -0
  99. package/src/components/ask-user/__tests__/validation.test.ts +213 -0
  100. package/src/components/ask-user/components/accordion-question.tsx +153 -0
  101. package/src/components/ask-user/components/custom-answer-input.tsx +66 -0
  102. package/src/components/ask-user/components/options-list.tsx +133 -0
  103. package/src/components/ask-user/components/question-header.tsx +80 -0
  104. package/src/components/ask-user/components/question-option.tsx +89 -0
  105. package/src/components/ask-user/constants.ts +38 -0
  106. package/src/components/ask-user/index.tsx +631 -0
  107. package/src/components/ask-user/utils/validation.ts +85 -0
  108. package/src/components/attachment-card.tsx +65 -0
  109. package/src/components/blocks/agent-block-grid.tsx +58 -0
  110. package/src/components/blocks/agent-branch-item.tsx +311 -0
  111. package/src/components/blocks/agent-branch-wrapper.tsx +493 -0
  112. package/src/components/blocks/agent-list-branch.tsx +77 -0
  113. package/src/components/blocks/ask-user-branch.tsx +81 -0
  114. package/src/components/blocks/block-helpers.ts +11 -0
  115. package/src/components/blocks/blocks-renderer.tsx +289 -0
  116. package/src/components/blocks/content-with-markdown.tsx +33 -0
  117. package/src/components/blocks/image-block.tsx +128 -0
  118. package/src/components/blocks/implementor-row.tsx +429 -0
  119. package/src/components/blocks/single-block.tsx +201 -0
  120. package/src/components/blocks/thinking-block.tsx +67 -0
  121. package/src/components/blocks/tool-block-group.tsx +57 -0
  122. package/src/components/blocks/tool-branch.tsx +206 -0
  123. package/src/components/blocks/user-content-copy.tsx +157 -0
  124. package/src/components/bottom-banner.tsx +136 -0
  125. package/src/components/build-mode-buttons.tsx +110 -0
  126. package/src/components/button.tsx +66 -0
  127. package/src/components/chat-header.tsx +77 -0
  128. package/src/components/chat-history-screen.tsx +413 -0
  129. package/src/components/chat-input-bar.tsx +523 -0
  130. package/src/components/clickable-title-box.tsx +66 -0
  131. package/src/components/clickable.tsx +119 -0
  132. package/src/components/collapse-button.tsx +34 -0
  133. package/src/components/copy-button.tsx +203 -0
  134. package/src/components/elapsed-timer.tsx +55 -0
  135. package/src/components/error-boundary.tsx +55 -0
  136. package/src/components/feedback-container.tsx +181 -0
  137. package/src/components/feedback-icon-button.tsx +89 -0
  138. package/src/components/feedback-input-mode.tsx +344 -0
  139. package/src/components/file-attachment-card.tsx +98 -0
  140. package/src/components/freebuff-active-session-summary.tsx +63 -0
  141. package/src/components/freebuff-landing-screen.tsx +865 -0
  142. package/src/components/freebuff-model-selector.tsx +1087 -0
  143. package/src/components/freebuff-referral-banner.tsx +604 -0
  144. package/src/components/freebuff-superseded-screen.tsx +62 -0
  145. package/src/components/grid-layout.tsx +80 -0
  146. package/src/components/help-banner.tsx +127 -0
  147. package/src/components/highlighted-text.tsx +52 -0
  148. package/src/components/image-card.tsx +167 -0
  149. package/src/components/image-thumbnail.tsx +114 -0
  150. package/src/components/input-cursor.tsx +77 -0
  151. package/src/components/input-mode-banner.tsx +57 -0
  152. package/src/components/load-previous-button.tsx +48 -0
  153. package/src/components/login-modal.tsx +475 -0
  154. package/src/components/message-block.tsx +346 -0
  155. package/src/components/message-footer.tsx +258 -0
  156. package/src/components/message-with-agents.tsx +506 -0
  157. package/src/components/mode-divider.tsx +43 -0
  158. package/src/components/multiline-input.tsx +1241 -0
  159. package/src/components/out-of-credits-banner.tsx +160 -0
  160. package/src/components/pending-attachments-banner.tsx +116 -0
  161. package/src/components/pending-bash-message.tsx +59 -0
  162. package/src/components/progress-bar.tsx +81 -0
  163. package/src/components/project-picker-screen.tsx +485 -0
  164. package/src/components/publish-confirmation.tsx +493 -0
  165. package/src/components/publish-container.tsx +687 -0
  166. package/src/components/queue-panel.tsx +310 -0
  167. package/src/components/raised-pill.tsx +95 -0
  168. package/src/components/renderers/plan-box.tsx +59 -0
  169. package/src/components/review-screen.tsx +112 -0
  170. package/src/components/scroll-to-bottom-button.tsx +34 -0
  171. package/src/components/segmented-control.tsx +187 -0
  172. package/src/components/selectable-list.tsx +248 -0
  173. package/src/components/selected-chips.tsx +80 -0
  174. package/src/components/separator.tsx +40 -0
  175. package/src/components/session-ended-banner.tsx +226 -0
  176. package/src/components/shimmer-text.tsx +248 -0
  177. package/src/components/status-bar.tsx +313 -0
  178. package/src/components/subscription-limit-banner.tsx +206 -0
  179. package/src/components/suggested-prompts.tsx +184 -0
  180. package/src/components/suggestion-menu.tsx +215 -0
  181. package/src/components/terminal-command-display.tsx +151 -0
  182. package/src/components/terminal-link.tsx +97 -0
  183. package/src/components/terminal-thinking-orb.tsx +95 -0
  184. package/src/components/text-attachment-card.tsx +77 -0
  185. package/src/components/thinking.tsx +109 -0
  186. package/src/components/tools/__tests__/apply-patch.test.tsx +84 -0
  187. package/src/components/tools/__tests__/code-search.test.tsx +45 -0
  188. package/src/components/tools/__tests__/gravity-index.test.ts +76 -0
  189. package/src/components/tools/__tests__/render-ui.test.tsx +92 -0
  190. package/src/components/tools/__tests__/run-terminal-command.test.ts +233 -0
  191. package/src/components/tools/apply-patch.tsx +95 -0
  192. package/src/components/tools/code-search.tsx +45 -0
  193. package/src/components/tools/composio.tsx +152 -0
  194. package/src/components/tools/diff-viewer.tsx +76 -0
  195. package/src/components/tools/glob.tsx +61 -0
  196. package/src/components/tools/gravity-index.tsx +77 -0
  197. package/src/components/tools/list-directory.tsx +61 -0
  198. package/src/components/tools/read-docs.tsx +37 -0
  199. package/src/components/tools/read-files.tsx +91 -0
  200. package/src/components/tools/read-subtree.tsx +44 -0
  201. package/src/components/tools/read-url.tsx +33 -0
  202. package/src/components/tools/registry.ts +126 -0
  203. package/src/components/tools/render-ui.tsx +122 -0
  204. package/src/components/tools/run-terminal-command.tsx +79 -0
  205. package/src/components/tools/skill.tsx +29 -0
  206. package/src/components/tools/str-replace.tsx +77 -0
  207. package/src/components/tools/suggest-followups.tsx +369 -0
  208. package/src/components/tools/task-completed.tsx +16 -0
  209. package/src/components/tools/tool-call-item.tsx +273 -0
  210. package/src/components/tools/types.ts +55 -0
  211. package/src/components/tools/web-search.tsx +33 -0
  212. package/src/components/tools/write-file.tsx +16 -0
  213. package/src/components/tools/write-todos.tsx +95 -0
  214. package/src/components/top-banner.tsx +178 -0
  215. package/src/components/usage-banner.tsx +244 -0
  216. package/src/components/user-error-banner.tsx +56 -0
  217. package/src/components/validation-error-popover.tsx +192 -0
  218. package/src/contexts/__tests__/chat-runtime-context.test.tsx +149 -0
  219. package/src/contexts/chat-runtime-context.tsx +240 -0
  220. package/src/data/slash-commands.test.ts +24 -0
  221. package/src/data/slash-commands.ts +249 -0
  222. package/src/entry.ts +12 -0
  223. package/src/hooks/__tests__/holds-live-freebuff-slot.test.ts +49 -0
  224. package/src/hooks/__tests__/session-fetch-signal.test.ts +63 -0
  225. package/src/hooks/__tests__/use-activity-query.test.ts +1000 -0
  226. package/src/hooks/__tests__/use-ask-user-bridge.test.ts +176 -0
  227. package/src/hooks/__tests__/use-auth-query.test.ts +21 -0
  228. package/src/hooks/__tests__/use-chat-input-remount.test.tsx +66 -0
  229. package/src/hooks/__tests__/use-clipboard.test.tsx +73 -0
  230. package/src/hooks/__tests__/use-connection-status.test.ts +112 -0
  231. package/src/hooks/__tests__/use-directory-browser.test.ts +203 -0
  232. package/src/hooks/__tests__/use-gravity-ad.test.ts +73 -0
  233. package/src/hooks/__tests__/use-grid-layout.test.ts +349 -0
  234. package/src/hooks/__tests__/use-input-history.test.ts +699 -0
  235. package/src/hooks/__tests__/use-message-queue-editing.test.tsx +209 -0
  236. package/src/hooks/__tests__/use-message-queue.test.tsx +71 -0
  237. package/src/hooks/__tests__/use-path-tab-completion.test.ts +315 -0
  238. package/src/hooks/__tests__/use-queue-controls.test.ts +68 -0
  239. package/src/hooks/__tests__/use-queue-ui.test.tsx +83 -0
  240. package/src/hooks/__tests__/use-searchable-list.test.ts +359 -0
  241. package/src/hooks/__tests__/use-send-message-timer.test.ts +146 -0
  242. package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +361 -0
  243. package/src/hooks/__tests__/use-suggestion-engine.test.ts +485 -0
  244. package/src/hooks/__tests__/use-terminal-focus.test.ts +66 -0
  245. package/src/hooks/__tests__/use-terminal-layout.test.ts +675 -0
  246. package/src/hooks/__tests__/use-timeout.test.ts +330 -0
  247. package/src/hooks/__tests__/use-usage-query.test.ts +502 -0
  248. package/src/hooks/__tests__/use-user-details-query.test.ts +195 -0
  249. package/src/hooks/helpers/__tests__/send-message.test.ts +1918 -0
  250. package/src/hooks/helpers/send-message.ts +654 -0
  251. package/src/hooks/stream-state.ts +83 -0
  252. package/src/hooks/use-activity-query.ts +668 -0
  253. package/src/hooks/use-agent-validation.ts +74 -0
  254. package/src/hooks/use-ask-user-bridge.ts +92 -0
  255. package/src/hooks/use-auth-query.ts +253 -0
  256. package/src/hooks/use-auth-state.ts +152 -0
  257. package/src/hooks/use-chat-input.ts +105 -0
  258. package/src/hooks/use-chat-keyboard.ts +321 -0
  259. package/src/hooks/use-chat-messages.ts +254 -0
  260. package/src/hooks/use-chat-state.ts +165 -0
  261. package/src/hooks/use-chat-streaming.ts +180 -0
  262. package/src/hooks/use-chat-ui.ts +131 -0
  263. package/src/hooks/use-clipboard.ts +151 -0
  264. package/src/hooks/use-connection-status.ts +142 -0
  265. package/src/hooks/use-directory-browser.ts +88 -0
  266. package/src/hooks/use-elapsed-time.ts +116 -0
  267. package/src/hooks/use-event.ts +34 -0
  268. package/src/hooks/use-exit-handler.ts +66 -0
  269. package/src/hooks/use-fetch-login-url.ts +65 -0
  270. package/src/hooks/use-fingerprint.ts +61 -0
  271. package/src/hooks/use-freebuff-ctrl-c-exit.ts +23 -0
  272. package/src/hooks/use-freebuff-session-progress.ts +34 -0
  273. package/src/hooks/use-freebuff-session.ts +806 -0
  274. package/src/hooks/use-freebuff-streak-query.ts +61 -0
  275. package/src/hooks/use-gravity-ad.ts +656 -0
  276. package/src/hooks/use-grid-layout.ts +73 -0
  277. package/src/hooks/use-input-history.ts +173 -0
  278. package/src/hooks/use-login-keyboard-handlers.ts +75 -0
  279. package/src/hooks/use-login-polling.ts +114 -0
  280. package/src/hooks/use-logo.tsx +161 -0
  281. package/src/hooks/use-message-queue.ts +387 -0
  282. package/src/hooks/use-now.ts +20 -0
  283. package/src/hooks/use-path-tab-completion.ts +88 -0
  284. package/src/hooks/use-publish-mutation.ts +60 -0
  285. package/src/hooks/use-queue-controls.ts +68 -0
  286. package/src/hooks/use-queue-ui.ts +74 -0
  287. package/src/hooks/use-scroll-management.ts +184 -0
  288. package/src/hooks/use-searchable-list.ts +98 -0
  289. package/src/hooks/use-send-message.ts +773 -0
  290. package/src/hooks/use-sheen-animation.tsx +88 -0
  291. package/src/hooks/use-subscription-query.ts +71 -0
  292. package/src/hooks/use-suggestion-engine.ts +783 -0
  293. package/src/hooks/use-terminal-breakpoints.ts +77 -0
  294. package/src/hooks/use-terminal-dimensions.ts +46 -0
  295. package/src/hooks/use-terminal-focus.ts +29 -0
  296. package/src/hooks/use-terminal-layout.ts +189 -0
  297. package/src/hooks/use-theme.tsx +140 -0
  298. package/src/hooks/use-timeout.ts +85 -0
  299. package/src/hooks/use-update-preference.ts +66 -0
  300. package/src/hooks/use-usage-monitor.ts +55 -0
  301. package/src/hooks/use-usage-query.ts +124 -0
  302. package/src/hooks/use-user-details-query.ts +93 -0
  303. package/src/hooks/use-why-did-you-update.ts +177 -0
  304. package/src/index.tsx +441 -0
  305. package/src/init/__tests__/init-direnv.test.ts +526 -0
  306. package/src/init/init-app.ts +34 -0
  307. package/src/init/init-direnv.ts +133 -0
  308. package/src/login/constants.ts +60 -0
  309. package/src/login/login-flow.ts +204 -0
  310. package/src/login/plain-login.ts +108 -0
  311. package/src/login/utils.ts +183 -0
  312. package/src/native/ripgrep.ts +80 -0
  313. package/src/polyfills/bun-strip-ansi.ts +12 -0
  314. package/src/pre-init/tree-sitter-wasm.ts +96 -0
  315. package/src/project-files.ts +102 -0
  316. package/src/smoke/terminal-command-broker.ts +439 -0
  317. package/src/state/__tests__/chat-store-focus.test.ts +22 -0
  318. package/src/state/__tests__/feedback-store.test.ts +257 -0
  319. package/src/state/chat-history-store.ts +30 -0
  320. package/src/state/chat-store.ts +544 -0
  321. package/src/state/feedback-store.ts +160 -0
  322. package/src/state/freebuff-model-store.ts +42 -0
  323. package/src/state/freebuff-session-store.ts +53 -0
  324. package/src/state/login-store.ts +150 -0
  325. package/src/state/message-block-store.ts +149 -0
  326. package/src/state/publish-store.ts +146 -0
  327. package/src/state/queue-panel-store.ts +26 -0
  328. package/src/state/review-store.ts +24 -0
  329. package/src/testing/env.ts +46 -0
  330. package/src/types/chat-state.ts +15 -0
  331. package/src/types/chat.ts +241 -0
  332. package/src/types/contracts/send-message.ts +12 -0
  333. package/src/types/env.ts +98 -0
  334. package/src/types/freebuff-session.ts +17 -0
  335. package/src/types/function-params.ts +28 -0
  336. package/src/types/react19-compat.d.ts +19 -0
  337. package/src/types/store.ts +111 -0
  338. package/src/types/theme-system.ts +144 -0
  339. package/src/types/utils.ts +3 -0
  340. package/src/utils/__tests__/active-run.test.ts +140 -0
  341. package/src/utils/__tests__/activity-tracker.test.ts +440 -0
  342. package/src/utils/__tests__/agent-display.test.ts +139 -0
  343. package/src/utils/__tests__/analytics-client.test.ts +262 -0
  344. package/src/utils/__tests__/anonymous-id.test.ts +74 -0
  345. package/src/utils/__tests__/anvil-api.test.ts +578 -0
  346. package/src/utils/__tests__/arrays.test.ts +264 -0
  347. package/src/utils/__tests__/bash-context-processor.test.ts +38 -0
  348. package/src/utils/__tests__/block-processor.test.ts +810 -0
  349. package/src/utils/__tests__/chat-history.test.ts +203 -0
  350. package/src/utils/__tests__/chat-input-key-intercept.test.ts +40 -0
  351. package/src/utils/__tests__/chat-meta.test.ts +130 -0
  352. package/src/utils/__tests__/clipboard-linux.test.ts +359 -0
  353. package/src/utils/__tests__/clipboard.test.ts +910 -0
  354. package/src/utils/__tests__/code-search-summary.test.ts +84 -0
  355. package/src/utils/__tests__/collapse-helpers.test.ts +1109 -0
  356. package/src/utils/__tests__/error-handling.test.ts +658 -0
  357. package/src/utils/__tests__/exit-cleanly.test.ts +110 -0
  358. package/src/utils/__tests__/feedback-helpers.test.ts +444 -0
  359. package/src/utils/__tests__/feedback-submission.test.ts +26 -0
  360. package/src/utils/__tests__/fetch-usage.test.ts +355 -0
  361. package/src/utils/__tests__/fingerprint.test.ts +144 -0
  362. package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +7 -0
  363. package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +3 -0
  364. package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +33 -0
  365. package/src/utils/__tests__/format-elapsed-time.test.ts +103 -0
  366. package/src/utils/__tests__/format-timeout.test.ts +87 -0
  367. package/src/utils/__tests__/freebuff-instance-owner.test.ts +69 -0
  368. package/src/utils/__tests__/freebuff-model-navigation.test.ts +102 -0
  369. package/src/utils/__tests__/freebuff-premium-reset.test.ts +79 -0
  370. package/src/utils/__tests__/freebuff-referral-cache.test.ts +116 -0
  371. package/src/utils/__tests__/freebuff-session-api.test.ts +215 -0
  372. package/src/utils/__tests__/freebuff-session-display.test.ts +23 -0
  373. package/src/utils/__tests__/freebuff-streak-line.test.ts +217 -0
  374. package/src/utils/__tests__/image-dimensions.test.ts +249 -0
  375. package/src/utils/__tests__/image-processor.test.ts +209 -0
  376. package/src/utils/__tests__/implementor-helpers.test.ts +1458 -0
  377. package/src/utils/__tests__/keyboard-actions.test.ts +660 -0
  378. package/src/utils/__tests__/layout-helpers.test.ts +36 -0
  379. package/src/utils/__tests__/lazy-response-ads.test.ts +167 -0
  380. package/src/utils/__tests__/log-shipper.test.ts +53 -0
  381. package/src/utils/__tests__/markdown-renderer.test.tsx +421 -0
  382. package/src/utils/__tests__/message-block-helpers.test.ts +1263 -0
  383. package/src/utils/__tests__/message-updater.test.ts +645 -0
  384. package/src/utils/__tests__/osc-timeout-scenarios.test.ts +172 -0
  385. package/src/utils/__tests__/pending-attachments.test.ts +277 -0
  386. package/src/utils/__tests__/polling-backoff.test.ts +51 -0
  387. package/src/utils/__tests__/queue-panel-actions.test.ts +148 -0
  388. package/src/utils/__tests__/run-state-storage.test.ts +919 -0
  389. package/src/utils/__tests__/safe-json.test.ts +77 -0
  390. package/src/utils/__tests__/sdk-event-handlers.test.ts +685 -0
  391. package/src/utils/__tests__/send-message-helpers.test.ts +1898 -0
  392. package/src/utils/__tests__/send-message-timer.test.ts +64 -0
  393. package/src/utils/__tests__/settings.test.ts +99 -0
  394. package/src/utils/__tests__/strings.test.ts +192 -0
  395. package/src/utils/__tests__/terminal-color-detection.test.ts +425 -0
  396. package/src/utils/__tests__/terminal-command-broker.test.ts +528 -0
  397. package/src/utils/__tests__/terminal-enter-detection.test.ts +77 -0
  398. package/src/utils/__tests__/terminal-io.test.ts +137 -0
  399. package/src/utils/__tests__/terminal-protocol-controller.test.ts +119 -0
  400. package/src/utils/__tests__/text-layout.test.ts +82 -0
  401. package/src/utils/__tests__/theme-platform-detection.test.ts +125 -0
  402. package/src/utils/__tests__/think-tag-parser.test.ts +159 -0
  403. package/src/utils/__tests__/trace-writer.test.ts +223 -0
  404. package/src/utils/__tests__/trim-chat-logs.test.ts +78 -0
  405. package/src/utils/__tests__/usage-banner-state.test.ts +298 -0
  406. package/src/utils/__tests__/validation-error-formatting.test.ts +126 -0
  407. package/src/utils/__tests__/windows-terminal-health.test.ts +101 -0
  408. package/src/utils/__tests__/write-file-atomic.test.ts +127 -0
  409. package/src/utils/active-run.ts +131 -0
  410. package/src/utils/activity-tracker.ts +66 -0
  411. package/src/utils/agent-display.ts +87 -0
  412. package/src/utils/agent-helpers.ts +27 -0
  413. package/src/utils/agent-id-utils.ts +14 -0
  414. package/src/utils/ai-message-id.ts +12 -0
  415. package/src/utils/analytics.ts +361 -0
  416. package/src/utils/anonymous-id.ts +87 -0
  417. package/src/utils/anvil-api.ts +612 -0
  418. package/src/utils/anvil-client.ts +217 -0
  419. package/src/utils/arrays.ts +117 -0
  420. package/src/utils/auth.ts +258 -0
  421. package/src/utils/bash-context-processor.ts +47 -0
  422. package/src/utils/bash-messages.ts +108 -0
  423. package/src/utils/block-margins.ts +35 -0
  424. package/src/utils/block-operations.ts +523 -0
  425. package/src/utils/block-processor.ts +213 -0
  426. package/src/utils/chat-history.ts +253 -0
  427. package/src/utils/chat-input-key-intercept.ts +52 -0
  428. package/src/utils/chat-meta.ts +90 -0
  429. package/src/utils/chat-scroll-accel.ts +151 -0
  430. package/src/utils/clipboard-image.ts +588 -0
  431. package/src/utils/clipboard.ts +397 -0
  432. package/src/utils/code-search-summary.ts +70 -0
  433. package/src/utils/collapse-helpers.ts +256 -0
  434. package/src/utils/config-dir.ts +24 -0
  435. package/src/utils/constants.ts +190 -0
  436. package/src/utils/create-event-handler-state.ts +73 -0
  437. package/src/utils/create-run-config.ts +121 -0
  438. package/src/utils/detect-shell.ts +116 -0
  439. package/src/utils/directory-browser.ts +72 -0
  440. package/src/utils/engagement.ts +55 -0
  441. package/src/utils/env.ts +92 -0
  442. package/src/utils/error-handling.ts +255 -0
  443. package/src/utils/error-messages.ts +62 -0
  444. package/src/utils/exit-cleanly.ts +86 -0
  445. package/src/utils/feedback-helpers.ts +103 -0
  446. package/src/utils/feedback-submission.ts +22 -0
  447. package/src/utils/fetch-usage.ts +76 -0
  448. package/src/utils/fingerprint.ts +245 -0
  449. package/src/utils/format-elapsed-time.ts +33 -0
  450. package/src/utils/format-session-units.ts +6 -0
  451. package/src/utils/format-timeout.ts +28 -0
  452. package/src/utils/format-validation-errors-for-message.ts +84 -0
  453. package/src/utils/freebuff-agent-selection.ts +33 -0
  454. package/src/utils/freebuff-instance-owner.ts +66 -0
  455. package/src/utils/freebuff-model-navigation.ts +50 -0
  456. package/src/utils/freebuff-premium-reset.ts +55 -0
  457. package/src/utils/freebuff-referral-cache.ts +51 -0
  458. package/src/utils/freebuff-session-api.ts +243 -0
  459. package/src/utils/freebuff-session-display.ts +21 -0
  460. package/src/utils/freebuff-streak-line.ts +93 -0
  461. package/src/utils/git.ts +17 -0
  462. package/src/utils/helpers.ts +47 -0
  463. package/src/utils/image-display.ts +67 -0
  464. package/src/utils/image-handler.ts +325 -0
  465. package/src/utils/image-processor.ts +133 -0
  466. package/src/utils/image-thumbnail.ts +78 -0
  467. package/src/utils/implementor-helpers.ts +787 -0
  468. package/src/utils/input-modes.ts +174 -0
  469. package/src/utils/keyboard-actions.ts +392 -0
  470. package/src/utils/keypad-keys.ts +47 -0
  471. package/src/utils/layout-helpers.ts +33 -0
  472. package/src/utils/lazy-response-ads.ts +1 -0
  473. package/src/utils/local-agent-registry.ts +474 -0
  474. package/src/utils/log-shipper.ts +126 -0
  475. package/src/utils/logger.ts +327 -0
  476. package/src/utils/markdown-renderer.tsx +1090 -0
  477. package/src/utils/math.ts +3 -0
  478. package/src/utils/message-block-helpers.ts +684 -0
  479. package/src/utils/message-history.ts +143 -0
  480. package/src/utils/message-tree-utils.ts +67 -0
  481. package/src/utils/message-updater.ts +279 -0
  482. package/src/utils/open-file.ts +146 -0
  483. package/src/utils/open-url.ts +63 -0
  484. package/src/utils/path-completion.ts +111 -0
  485. package/src/utils/path-helpers.ts +38 -0
  486. package/src/utils/pending-attachments.ts +360 -0
  487. package/src/utils/polling-backoff.ts +59 -0
  488. package/src/utils/project-picker.ts +12 -0
  489. package/src/utils/queue-panel-actions.ts +70 -0
  490. package/src/utils/recent-projects.ts +156 -0
  491. package/src/utils/renderer-cleanup.ts +233 -0
  492. package/src/utils/response-ad-positions.ts +1 -0
  493. package/src/utils/run-state-storage.ts +603 -0
  494. package/src/utils/safe-json.ts +153 -0
  495. package/src/utils/sdk-event-handlers.ts +571 -0
  496. package/src/utils/send-message-helpers.ts +210 -0
  497. package/src/utils/send-message-timer.ts +110 -0
  498. package/src/utils/settings.ts +226 -0
  499. package/src/utils/skill-registry.ts +94 -0
  500. package/src/utils/spawn-agent-matcher.ts +55 -0
  501. package/src/utils/status-indicator-state.ts +107 -0
  502. package/src/utils/stream-chunk-processor.ts +57 -0
  503. package/src/utils/strings.ts +271 -0
  504. package/src/utils/subscription.ts +31 -0
  505. package/src/utils/syntax-highlighter.tsx +19 -0
  506. package/src/utils/terminal-color-detection.ts +474 -0
  507. package/src/utils/terminal-command-broker.ts +467 -0
  508. package/src/utils/terminal-enter-detection.ts +63 -0
  509. package/src/utils/terminal-images.ts +229 -0
  510. package/src/utils/terminal-io.ts +85 -0
  511. package/src/utils/terminal-protocol-controller.ts +173 -0
  512. package/src/utils/terminal-reset-sequences.ts +33 -0
  513. package/src/utils/terminal-title.ts +84 -0
  514. package/src/utils/terminal-watchdog.ts +366 -0
  515. package/src/utils/text-layout.ts +149 -0
  516. package/src/utils/theme-config.ts +141 -0
  517. package/src/utils/theme-system.ts +1206 -0
  518. package/src/utils/think-tag-parser.ts +95 -0
  519. package/src/utils/time-format.ts +21 -0
  520. package/src/utils/trace-writer.ts +162 -0
  521. package/src/utils/ui-constants.ts +69 -0
  522. package/src/utils/update-checker.ts +110 -0
  523. package/src/utils/usage-banner-state.ts +146 -0
  524. package/src/utils/validation-error-formatting.ts +86 -0
  525. package/src/utils/validation-error-helpers.ts +19 -0
  526. package/src/utils/windows-terminal-health.ts +124 -0
  527. package/src/utils/word-wrap-utils.ts +53 -0
  528. package/src/utils/write-file-atomic.ts +54 -0
  529. package/src/utils/yield-to-event-loop.ts +9 -0
@@ -0,0 +1,33 @@
1
+ import {
2
+ getFreebuffBase3RootAgentIdForModel,
3
+ getFreebuffRootAgentIdForModel,
4
+ } from '@anvil/common/constants/free-agents'
5
+
6
+ import { getSelectedFreebuffModel } from '../state/freebuff-model-store'
7
+ import {
8
+ AGENT_MODE_TO_ID,
9
+ CLI_HARNESS,
10
+ IS_FREEBUFF,
11
+ type AgentMode,
12
+ } from './constants'
13
+
14
+ /**
15
+ * Freebuff is locked to LITE (chat-store's setAgentMode is a no-op when
16
+ * IS_FREEBUFF), so this is effectively "which root does the selected model
17
+ * run". Both harnesses have a root per picker model; CLI_HARNESS picks the
18
+ * family. It is currently base3; keeping both branches live preserves the
19
+ * release-based rollback path for the CLI.
20
+ */
21
+ export function getFreebuffCliAgentIdForModel(model: string): string {
22
+ return CLI_HARNESS === 'base3'
23
+ ? getFreebuffBase3RootAgentIdForModel(model)
24
+ : getFreebuffRootAgentIdForModel(model)
25
+ }
26
+
27
+ export function getAgentIdForMode(agentMode: AgentMode): string {
28
+ if (IS_FREEBUFF && agentMode === 'LITE') {
29
+ return getFreebuffCliAgentIdForModel(getSelectedFreebuffModel())
30
+ }
31
+
32
+ return AGENT_MODE_TO_ID[agentMode]
33
+ }
@@ -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 FreebuffInstanceOwner {
8
+ instanceId: string
9
+ pid: number
10
+ }
11
+
12
+ const OWNER_FILE = 'freebuff-instance-owner.json'
13
+
14
+ const getOwnerPath = (): string => path.join(getConfigDir(), OWNER_FILE)
15
+
16
+ function readOwner(): FreebuffInstanceOwner | null {
17
+ try {
18
+ const raw = fs.readFileSync(getOwnerPath(), 'utf8')
19
+ const parsed = JSON.parse(raw) as Partial<FreebuffInstanceOwner>
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 recordFreebuffInstanceOwner(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
+ '[freebuff-session] Failed to record local owner',
56
+ )
57
+ }
58
+ }
59
+
60
+ export function isFreebuffInstanceOwnedByDeadLocalProcess(
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 FreebuffModelNavigationDirection = '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 nextFreebuffModelId(params: {
13
+ modelIds: readonly string[]
14
+ focusedId: string
15
+ direction: FreebuffModelNavigationDirection
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 freebuffModelNavigationDirectionForKey(key: {
28
+ name?: string
29
+ shift?: boolean
30
+ sequence?: string
31
+ raw?: string
32
+ }): FreebuffModelNavigationDirection | 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 { FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE } from '@anvil/common/constants/freebuff-models'
2
+ import { getZonedDayBounds } from '@anvil/common/util/zoned-time'
3
+
4
+ import type { FreebuffSessionRateLimitByModel } from '@anvil/common/types/freebuff-session'
5
+
6
+ export function getFreebuffPremiumResetAt(params: {
7
+ rateLimitsByModel?: FreebuffSessionRateLimitByModel
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
+ FREEBUFF_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 formatFreebuffPremiumResetCountdown(
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/freebuff-session'
2
+
3
+ import type { FreebuffAccessTier } from '@anvil/common/constants/freebuff-models'
4
+ import type { FreebuffReferralInfo } from '@anvil/common/types/freebuff-session'
5
+ import type { FreebuffSessionResponse } from '../types/freebuff-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<FreebuffAccessTier, FreebuffReferralInfo>
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: FreebuffSessionResponse | 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: FreebuffAccessTier | undefined,
44
+ ): FreebuffReferralInfo | 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
+ FREEBUFF_COMPACT_SESSION_HEADER,
4
+ FREEBUFF_INSTANCE_HEADER,
5
+ FREEBUFF_MODEL_HEADER,
6
+ } from '@anvil/common/constants/freebuff-models'
7
+
8
+ import { useFreebuffSessionStore } from '../state/freebuff-session-store'
9
+ import { getAuthTokenDetails } from './auth'
10
+ import { IS_FREEBUFF } from './constants'
11
+
12
+ import type { FreebuffSessionResponse } from '../types/freebuff-session'
13
+ import type { FreebuffSessionServerResponse } from '@anvil/common/types/freebuff-session'
14
+
15
+ const SESSION_FETCH_TIMEOUT_MS = 20_000
16
+ export type FreebuffSessionMethod = 'POST' | 'GET' | 'DELETE'
17
+
18
+ export class FreebuffSessionRequestError 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 = 'FreebuffSessionRequestError'
27
+ }
28
+ }
29
+
30
+ export function isFreebuffSessionTimeoutError(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 FreebuffSessionFailureDisposition = '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 classifyFreebuffSessionRequestFailure(
47
+ method: Extract<FreebuffSessionMethod, 'POST' | 'GET'>,
48
+ error: unknown,
49
+ ): FreebuffSessionFailureDisposition {
50
+ if (method === 'POST') {
51
+ if (!(error instanceof FreebuffSessionRequestError)) 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 FreebuffSessionRequestError)) 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/freebuff/session`
101
+ }
102
+
103
+ export async function callFreebuffSession(
104
+ method: FreebuffSessionMethod,
105
+ token: string,
106
+ opts: {
107
+ instanceId?: string
108
+ model?: string
109
+ signal?: AbortSignal
110
+ compact?: boolean
111
+ } = {},
112
+ ): Promise<FreebuffSessionServerResponse> {
113
+ const headers: Record<string, string> = { Authorization: `Bearer ${token}` }
114
+ if (method === 'GET' && opts.instanceId) {
115
+ headers[FREEBUFF_INSTANCE_HEADER] = opts.instanceId
116
+ }
117
+ if (method === 'GET' && opts.compact) {
118
+ headers[FREEBUFF_COMPACT_SESSION_HEADER] = '1'
119
+ }
120
+ if (method === 'POST' && opts.model) {
121
+ headers[FREEBUFF_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 FreebuffSessionServerResponse | 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 FreebuffSessionServerResponse | 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 FreebuffSessionServerResponse | 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 FreebuffSessionRequestError(
182
+ `freebuff 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 FreebuffSessionServerResponse
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: FreebuffSessionResponse | null,
197
+ next: FreebuffSessionServerResponse,
198
+ ): FreebuffSessionServerResponse | 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 holdsLiveFreebuffSlot(
215
+ current: FreebuffSessionResponse | 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 releaseFreebuffSlot(): Promise<void> {
226
+ const current = useFreebuffSessionStore.getState().session
227
+ if (!holdsLiveFreebuffSlot(current)) return
228
+
229
+ const { token } = getAuthTokenDetails()
230
+ if (!token) return
231
+
232
+ try {
233
+ await callFreebuffSession('DELETE', token)
234
+ } catch {
235
+ // The server-side sweep is the backstop.
236
+ }
237
+ }
238
+
239
+ /** Release the Freebuff slot on exit paths that skip React unmount. */
240
+ export async function endFreebuffSessionBestEffort(): Promise<void> {
241
+ if (!IS_FREEBUFF) return
242
+ await releaseFreebuffSlot()
243
+ }
@@ -0,0 +1,21 @@
1
+ export const FREEBUFF_COUNTDOWN_VISIBLE_MS = 5 * 60_000
2
+
3
+ export const formatFreebuffSessionCountdown = (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 formatFreebuffSessionRemaining = (ms: number): string => {
12
+ if (ms <= 0) return 'expiring…'
13
+ if (ms < FREEBUFF_COUNTDOWN_VISIBLE_MS) {
14
+ return `${formatFreebuffSessionCountdown(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
+ }
@@ -0,0 +1,93 @@
1
+ // The label/dots/perk-note logic is shared with Freebuff Desktop and lives in
2
+ // common; this module re-exports it and adds the terminal rendering and layout
3
+ // gating only the CLI needs.
4
+ export {
5
+ FREEBUFF_STREAK_WEEK,
6
+ getFreebuffStreakBonusNote,
7
+ } from '@anvil/common/util/freebuff-streak-line'
8
+ export type { FreebuffStreakLine } from '@anvil/common/util/freebuff-streak-line'
9
+
10
+ import {
11
+ FREEBUFF_STREAK_WEEK,
12
+ getFreebuffStreakBonusNote,
13
+ getFreebuffStreakLine as getSharedFreebuffStreakLine,
14
+ } from '@anvil/common/util/freebuff-streak-line'
15
+
16
+ import type { FreebuffStreakLine } from '@anvil/common/util/freebuff-streak-line'
17
+
18
+ const FREEBUFF_STREAK_BONUS_MIN_HEIGHT = 30
19
+
20
+ /** Columns between the count label and its progress dots. */
21
+ export const FREEBUFF_STREAK_LABEL_GAP = 2
22
+
23
+ /** Columns kept clear between the heading and the streak when they share a
24
+ * row. The heading row is laid out space-between inside a shrink-to-fit
25
+ * column, so when the row is the widest child there is no free space to
26
+ * distribute and the two would otherwise render flush against each other
27
+ * ("Start coding for free18 day streak"). This is the floor, and the same
28
+ * number decides whether they may share a row at all. */
29
+ export const FREEBUFF_STREAK_INLINE_GAP = 3
30
+
31
+ /** Progress glyphs for a terminal — the same ●/○ pair the shared module and
32
+ * the desktop app use. Bullet and middle dot were tried here because U+25CF
33
+ * is missing from a few terminal fonts and lands as a tofu box, but • and ·
34
+ * differ only in size: at a glance a partial week and a full one look alike,
35
+ * which is the whole point of the row. Filled-vs-hollow reads instantly, and
36
+ * the CLI already bets on ●/○ for its agent status indicators, so a font that
37
+ * can't draw them is already visibly broken elsewhere.
38
+ *
39
+ * If a font ever fails these, █/░ (Block Elements) is the fallback pair: the
40
+ * ASCII logo and the progress bar are built from them, so anything that
41
+ * renders the CLI at all renders those. */
42
+ const TERMINAL_DOT_CHARS = { filled: '●', empty: '○' }
43
+
44
+ /** The streak line as the CLI draws it. */
45
+ export function getFreebuffStreakLine(
46
+ streak: number,
47
+ ): FreebuffStreakLine | null {
48
+ return getSharedFreebuffStreakLine(streak, TERMINAL_DOT_CHARS)
49
+ }
50
+
51
+ /** Rendered width of the streak, e.g. "18 day streak ●●●●●●●+". */
52
+ export function getFreebuffStreakInlineWidth(line: FreebuffStreakLine): number {
53
+ return line.label.length + FREEBUFF_STREAK_LABEL_GAP + line.dots.length
54
+ }
55
+
56
+ /** What a user with no streak yet is about to earn. The empty slot is measured
57
+ * against it so the row doesn't move on day one. */
58
+ const DAY_ONE_LINE = getFreebuffStreakLine(1)!
59
+
60
+ /** Whether the heading and the streak can share a row with the inline gap left
61
+ * clear between them. A streak long enough to widen its own label (or a
62
+ * narrow terminal) pushes the streak onto its own line instead of letting the
63
+ * two collide. */
64
+ export function fitsFreebuffStreakOnHeadingRow(params: {
65
+ /** null when the user has no streak yet — measured as day one. */
66
+ line: FreebuffStreakLine | null
67
+ headingWidth: number
68
+ availableWidth: number
69
+ }): boolean {
70
+ return (
71
+ params.headingWidth +
72
+ FREEBUFF_STREAK_INLINE_GAP +
73
+ getFreebuffStreakInlineWidth(params.line ?? DAY_ONE_LINE) <=
74
+ params.availableWidth
75
+ )
76
+ }
77
+
78
+ /** Returns the earned perk note only when the landing layout can show it
79
+ * without crowding the picker or wrapping onto additional rows. */
80
+ export function getFreebuffStreakBonusNoteForLayout(params: {
81
+ streak: number
82
+ accessTier: 'full' | 'limited'
83
+ terminalHeight: number
84
+ availableWidth: number
85
+ }): string | null {
86
+ if (params.streak < FREEBUFF_STREAK_WEEK) return null
87
+ if (params.terminalHeight < FREEBUFF_STREAK_BONUS_MIN_HEIGHT) return null
88
+
89
+ const note = getFreebuffStreakBonusNote(params)
90
+ if (!note || note.length > params.availableWidth) return null
91
+
92
+ return note
93
+ }
@@ -0,0 +1,17 @@
1
+ import { existsSync } from 'fs'
2
+ import { dirname, join } from 'path'
3
+
4
+ export function findGitRoot(params: { cwd: string }): string | null {
5
+ const { cwd } = params
6
+
7
+ let currentDir = cwd
8
+
9
+ while (currentDir !== dirname(currentDir)) {
10
+ if (existsSync(join(currentDir, '.git'))) {
11
+ return currentDir
12
+ }
13
+ currentDir = dirname(currentDir)
14
+ }
15
+
16
+ return null
17
+ }
@@ -0,0 +1,47 @@
1
+ let timestampFormatter: Intl.DateTimeFormat | null = null
2
+ export function initTimestampFormatter() {
3
+ if (timestampFormatter) return
4
+ try {
5
+ timestampFormatter = new Intl.DateTimeFormat(undefined, {
6
+ hour: '2-digit',
7
+ minute: '2-digit',
8
+ })
9
+ } catch {
10
+ timestampFormatter = null
11
+ }
12
+ }
13
+
14
+ export function formatTimestamp(date = new Date()): string {
15
+ if (timestampFormatter) {
16
+ return timestampFormatter.format(date)
17
+ }
18
+ return date.toLocaleTimeString([], {
19
+ hour: '2-digit',
20
+ minute: '2-digit',
21
+ })
22
+ }
23
+
24
+ export function formatQueuedPreview(
25
+ messages: Array<{ content: string }>,
26
+ maxChars: number = 60,
27
+ ): string {
28
+ if (messages.length === 0) return ''
29
+
30
+ const latestMessage = messages[messages.length - 1].content
31
+ const singleLine = latestMessage.replace(/\s+/g, ' ').trim()
32
+ if (!singleLine) return ''
33
+
34
+ const countSuffix = messages.length > 1 ? ` (+ ${messages.length - 1})` : ''
35
+ const prefix = '↑ '
36
+ const suffix = ' ↑'
37
+ const availableChars =
38
+ maxChars - prefix.length - suffix.length - countSuffix.length
39
+
40
+ let messagePreview = singleLine
41
+ if (singleLine.length > availableChars) {
42
+ messagePreview =
43
+ singleLine.slice(0, Math.max(0, availableChars - 3)) + '...'
44
+ }
45
+
46
+ return `${prefix}${messagePreview}${countSuffix}${suffix}`
47
+ }