@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,919 @@
1
+ import { describe, test, expect, afterAll, beforeEach, afterEach, mock } from 'bun:test'
2
+ import * as fs from 'fs'
3
+ import * as path from 'path'
4
+ import * as os from 'os'
5
+
6
+ import {
7
+ getAllToggleIdsFromMessages,
8
+ getRunStatePath,
9
+ getChatMessagesPath,
10
+ saveChatState,
11
+ loadMostRecentChatState,
12
+ clearChatState,
13
+ setChatDirOverrideForTesting,
14
+ setLiveChatStateProvider,
15
+ clearLiveChatStateProvider,
16
+ flushLiveChatState,
17
+ scheduleCheckpointSave,
18
+ settleCheckpointSave,
19
+ } from '../run-state-storage'
20
+ import type { ChatMessage, ContentBlock } from '../../types/chat'
21
+ import type { RunState } from '@anvil/sdk'
22
+
23
+ /** Every directory below hangs off a root this run owns. They were fixed
24
+ * `os.tmpdir()/anvil-test-*` paths that `beforeEach` deletes recursively,
25
+ * so an overlapping run wiped the chat state this one was still writing (4 of
26
+ * 10 overlapped runs failed; solo always passed). Only the parent is unique —
27
+ * the names below stay stable, which is what the assertions read. See
28
+ * docs/testing.md. */
29
+ const TEST_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-run-state-'))
30
+
31
+ afterAll(() => {
32
+ fs.rmSync(TEST_ROOT, { recursive: true, force: true })
33
+ })
34
+
35
+ // Mock the project-files module
36
+ const mockProjectDataDir = path.join(TEST_ROOT, 'anvil-test-project')
37
+ const mockCurrentChatDir = path.join(
38
+ mockProjectDataDir,
39
+ 'chats',
40
+ 'test-chat-123',
41
+ )
42
+
43
+ // Mock the module before importing
44
+ const originalGetProjectDataDir = () => mockProjectDataDir
45
+ const originalGetCurrentChatDir = () => mockCurrentChatDir
46
+
47
+ describe('run-state-storage', () => {
48
+ beforeEach(() => {
49
+ // Create test directories
50
+ if (fs.existsSync(mockProjectDataDir)) {
51
+ fs.rmSync(mockProjectDataDir, { recursive: true })
52
+ }
53
+ fs.mkdirSync(mockCurrentChatDir, { recursive: true })
54
+ })
55
+
56
+ afterEach(() => {
57
+ // Clean up test directories
58
+ if (fs.existsSync(mockProjectDataDir)) {
59
+ fs.rmSync(mockProjectDataDir, { recursive: true })
60
+ }
61
+ })
62
+
63
+ describe('getAllToggleIdsFromMessages', () => {
64
+ test('extracts agent IDs from messages', () => {
65
+ const messages: ChatMessage[] = [
66
+ {
67
+ id: 'msg-1',
68
+ variant: 'agent',
69
+ content: '',
70
+ timestamp: new Date().toISOString(),
71
+ blocks: [
72
+ {
73
+ type: 'agent',
74
+ agentId: 'agent-1',
75
+ agentName: 'TestAgent',
76
+ agentType: 'inline',
77
+ content: '',
78
+ status: 'complete',
79
+ blocks: [],
80
+ },
81
+ ],
82
+ },
83
+ ]
84
+
85
+ const ids = getAllToggleIdsFromMessages(messages)
86
+
87
+ expect(ids).toContain('agent-1')
88
+ })
89
+
90
+ test('extracts tool call IDs from messages', () => {
91
+ const messages: ChatMessage[] = [
92
+ {
93
+ id: 'msg-1',
94
+ variant: 'agent',
95
+ content: '',
96
+ timestamp: new Date().toISOString(),
97
+ blocks: [
98
+ {
99
+ type: 'tool',
100
+ toolCallId: 'tool-1',
101
+ toolName: 'glob',
102
+ input: {},
103
+ output: '',
104
+ },
105
+ ],
106
+ },
107
+ ]
108
+
109
+ const ids = getAllToggleIdsFromMessages(messages)
110
+
111
+ expect(ids).toContain('tool-1')
112
+ })
113
+
114
+ test('recursively extracts IDs from nested agent blocks', () => {
115
+ const messages: ChatMessage[] = [
116
+ {
117
+ id: 'msg-1',
118
+ variant: 'agent',
119
+ content: '',
120
+ timestamp: new Date().toISOString(),
121
+ blocks: [
122
+ {
123
+ type: 'agent',
124
+ agentId: 'parent-agent',
125
+ agentName: 'ParentAgent',
126
+ agentType: 'inline',
127
+ content: '',
128
+ status: 'complete',
129
+ blocks: [
130
+ {
131
+ type: 'tool',
132
+ toolCallId: 'nested-tool',
133
+ toolName: 'glob',
134
+ input: {},
135
+ output: '',
136
+ },
137
+ {
138
+ type: 'agent',
139
+ agentId: 'child-agent',
140
+ agentName: 'ChildAgent',
141
+ agentType: 'inline',
142
+ content: '',
143
+ status: 'complete',
144
+ blocks: [
145
+ {
146
+ type: 'tool',
147
+ toolCallId: 'deep-tool',
148
+ toolName: 'glob',
149
+ input: {},
150
+ output: '',
151
+ },
152
+ ],
153
+ },
154
+ ],
155
+ },
156
+ ],
157
+ },
158
+ ]
159
+
160
+ const ids = getAllToggleIdsFromMessages(messages)
161
+
162
+ expect(ids).toContain('parent-agent')
163
+ expect(ids).toContain('nested-tool')
164
+ expect(ids).toContain('child-agent')
165
+ expect(ids).toContain('deep-tool')
166
+ })
167
+
168
+ test('handles messages with no blocks', () => {
169
+ const messages: ChatMessage[] = [
170
+ {
171
+ id: 'msg-1',
172
+ variant: 'user',
173
+ content: '',
174
+ timestamp: new Date().toISOString(),
175
+ blocks: [],
176
+ },
177
+ ]
178
+
179
+ const ids = getAllToggleIdsFromMessages(messages)
180
+
181
+ expect(ids).toHaveLength(0)
182
+ })
183
+
184
+ test('handles empty messages array', () => {
185
+ const ids = getAllToggleIdsFromMessages([])
186
+ expect(ids).toHaveLength(0)
187
+ })
188
+
189
+ test('handles mixed block types in single message', () => {
190
+ const messages: ChatMessage[] = [
191
+ {
192
+ id: 'msg-1',
193
+ variant: 'agent',
194
+ content: '',
195
+ timestamp: new Date().toISOString(),
196
+ blocks: [
197
+ { type: 'text', content: 'Some text' },
198
+ {
199
+ type: 'agent',
200
+ agentId: 'agent-1',
201
+ agentName: 'TestAgent',
202
+ agentType: 'inline',
203
+ content: '',
204
+ status: 'complete',
205
+ blocks: [],
206
+ },
207
+ {
208
+ type: 'tool',
209
+ toolCallId: 'tool-1',
210
+ toolName: 'glob',
211
+ input: {},
212
+ output: '',
213
+ },
214
+ ],
215
+ },
216
+ ]
217
+
218
+ const ids = getAllToggleIdsFromMessages(messages)
219
+
220
+ expect(ids).toContain('agent-1')
221
+ expect(ids).toContain('tool-1')
222
+ expect(ids).toHaveLength(2)
223
+ })
224
+
225
+ test('does not deduplicate IDs (returns all occurrences)', () => {
226
+ const messages: ChatMessage[] = [
227
+ {
228
+ id: 'msg-1',
229
+ variant: 'agent',
230
+ content: '',
231
+ timestamp: new Date().toISOString(),
232
+ blocks: [
233
+ {
234
+ type: 'agent',
235
+ agentId: 'shared-id',
236
+ agentName: 'TestAgent',
237
+ agentType: 'inline',
238
+ content: '',
239
+ status: 'complete',
240
+ blocks: [],
241
+ },
242
+ ],
243
+ },
244
+ {
245
+ id: 'msg-2',
246
+ variant: 'agent',
247
+ content: '',
248
+ timestamp: new Date().toISOString(),
249
+ blocks: [
250
+ {
251
+ type: 'tool',
252
+ toolCallId: 'shared-id',
253
+ toolName: 'glob',
254
+ input: {},
255
+ output: '',
256
+ },
257
+ ],
258
+ },
259
+ ]
260
+
261
+ const ids = getAllToggleIdsFromMessages(messages)
262
+
263
+ // Current implementation returns all occurrences without deduplication
264
+ expect(ids.filter((id) => id === 'shared-id')).toHaveLength(2)
265
+ })
266
+ })
267
+
268
+ describe('getRunStatePath', () => {
269
+ test('returns path with correct filename', () => {
270
+ // We need to mock the internal functions
271
+ // This is a simplified test - in reality we'd need to mock the module
272
+ const testPath = path.join(mockCurrentChatDir, 'run-state.json')
273
+ expect(testPath).toContain('run-state.json')
274
+ })
275
+ })
276
+
277
+ describe('getChatMessagesPath', () => {
278
+ test('returns path with correct filename', () => {
279
+ const testPath = path.join(mockCurrentChatDir, 'chat-messages.json')
280
+ expect(testPath).toContain('chat-messages.json')
281
+ })
282
+ })
283
+
284
+ describe('file serialization format', () => {
285
+ test('run state JSON structure is preserved through serialization', () => {
286
+ const runState: RunState = {
287
+ output: {
288
+ type: 'error',
289
+ message: 'Test output',
290
+ },
291
+ } as unknown as RunState
292
+
293
+ const runStatePath = path.join(mockCurrentChatDir, 'run-state.json')
294
+ fs.writeFileSync(runStatePath, JSON.stringify(runState, null, 2))
295
+
296
+ const savedRunState = JSON.parse(fs.readFileSync(runStatePath, 'utf8'))
297
+ expect(savedRunState.output.type).toBe('error')
298
+ expect(savedRunState.output.message).toBe('Test output')
299
+ })
300
+
301
+ test('messages JSON structure is preserved through serialization', () => {
302
+ const messages: ChatMessage[] = [
303
+ {
304
+ id: 'msg-1',
305
+ variant: 'user',
306
+ content: 'Hello',
307
+ timestamp: new Date().toISOString(),
308
+ blocks: [{ type: 'text', content: 'Hello' }],
309
+ },
310
+ ]
311
+
312
+ const messagesPath = path.join(mockCurrentChatDir, 'chat-messages.json')
313
+ fs.writeFileSync(messagesPath, JSON.stringify(messages, null, 2))
314
+
315
+ const savedMessages = JSON.parse(fs.readFileSync(messagesPath, 'utf8'))
316
+ expect(savedMessages).toHaveLength(1)
317
+ expect(savedMessages[0].variant).toBe('user')
318
+ })
319
+
320
+ test('nested message structure is preserved through serialization', () => {
321
+ const messages: ChatMessage[] = [
322
+ {
323
+ id: 'msg-1',
324
+ variant: 'agent',
325
+ content: '',
326
+ timestamp: new Date().toISOString(),
327
+ blocks: [
328
+ {
329
+ type: 'agent',
330
+ agentId: 'nested-agent',
331
+ agentName: 'NestedAgent',
332
+ agentType: 'inline',
333
+ content: '',
334
+ status: 'complete',
335
+ blocks: [
336
+ { type: 'text', content: 'Nested content' },
337
+ {
338
+ type: 'tool',
339
+ toolCallId: 'tool-xyz',
340
+ toolName: 'glob',
341
+ input: {},
342
+ output: '',
343
+ },
344
+ ],
345
+ },
346
+ ],
347
+ },
348
+ ]
349
+
350
+ const messagesPath = path.join(mockCurrentChatDir, 'chat-messages.json')
351
+ fs.writeFileSync(messagesPath, JSON.stringify(messages, null, 2))
352
+
353
+ const savedMessages = JSON.parse(fs.readFileSync(messagesPath, 'utf8'))
354
+ expect(savedMessages[0].blocks[0].type).toBe('agent')
355
+ expect(savedMessages[0].blocks[0].blocks).toHaveLength(2)
356
+ })
357
+ })
358
+
359
+ describe('edge cases', () => {
360
+ test('handles empty blocks array', () => {
361
+ const messages: ChatMessage[] = [
362
+ {
363
+ id: 'msg-1',
364
+ variant: 'agent',
365
+ content: '',
366
+ timestamp: new Date().toISOString(),
367
+ blocks: [],
368
+ },
369
+ ]
370
+
371
+ const ids = getAllToggleIdsFromMessages(messages)
372
+ expect(ids).toHaveLength(0)
373
+ })
374
+
375
+ test('handles deeply nested structure', () => {
376
+ const deepBlock: ContentBlock = {
377
+ type: 'agent',
378
+ agentId: 'level-0',
379
+ agentName: 'Level0Agent',
380
+ agentType: 'inline',
381
+ content: '',
382
+ status: 'complete',
383
+ blocks: [
384
+ {
385
+ type: 'agent',
386
+ agentId: 'level-1',
387
+ agentName: 'Level1Agent',
388
+ agentType: 'inline',
389
+ content: '',
390
+ status: 'complete',
391
+ blocks: [
392
+ {
393
+ type: 'agent',
394
+ agentId: 'level-2',
395
+ agentName: 'Level2Agent',
396
+ agentType: 'inline',
397
+ content: '',
398
+ status: 'complete',
399
+ blocks: [
400
+ {
401
+ type: 'tool',
402
+ toolCallId: 'deep-tool',
403
+ toolName: 'glob',
404
+ input: {},
405
+ output: '',
406
+ },
407
+ ],
408
+ },
409
+ ],
410
+ },
411
+ ],
412
+ }
413
+
414
+ const messages: ChatMessage[] = [
415
+ {
416
+ id: 'msg-1',
417
+ variant: 'agent',
418
+ content: '',
419
+ timestamp: new Date().toISOString(),
420
+ blocks: [deepBlock],
421
+ },
422
+ ]
423
+
424
+ const ids = getAllToggleIdsFromMessages(messages)
425
+
426
+ expect(ids).toContain('level-0')
427
+ expect(ids).toContain('level-1')
428
+ expect(ids).toContain('level-2')
429
+ expect(ids).toContain('deep-tool')
430
+ })
431
+
432
+ test('preserves order of IDs as encountered', () => {
433
+ const messages: ChatMessage[] = [
434
+ {
435
+ id: 'msg-1',
436
+ variant: 'agent',
437
+ content: '',
438
+ timestamp: new Date().toISOString(),
439
+ blocks: [
440
+ {
441
+ type: 'agent',
442
+ agentId: 'first',
443
+ agentName: 'FirstAgent',
444
+ agentType: 'inline',
445
+ content: '',
446
+ status: 'complete',
447
+ blocks: [],
448
+ },
449
+ {
450
+ type: 'tool',
451
+ toolCallId: 'second',
452
+ toolName: 'glob',
453
+ input: {},
454
+ output: '',
455
+ },
456
+ {
457
+ type: 'agent',
458
+ agentId: 'third',
459
+ agentName: 'ThirdAgent',
460
+ agentType: 'inline',
461
+ content: '',
462
+ status: 'complete',
463
+ blocks: [],
464
+ },
465
+ ],
466
+ },
467
+ ]
468
+
469
+ const ids = getAllToggleIdsFromMessages(messages)
470
+
471
+ expect(ids[0]).toBe('first')
472
+ expect(ids[1]).toBe('second')
473
+ expect(ids[2]).toBe('third')
474
+ })
475
+ })
476
+ })
477
+
478
+ describe('live chat state provider', () => {
479
+ // Point persistence at a temp dir via the explicit test override — module
480
+ // seams (mock.module, HOME, spyOn on auth) are unreliable across bun test
481
+ // files and platforms.
482
+ const chatDir = path.join(TEST_ROOT, 'anvil-test-live-chatdir')
483
+
484
+ const testRunState = (marker: string): RunState =>
485
+ ({
486
+ output: { type: 'error', message: marker },
487
+ }) as unknown as RunState
488
+
489
+ const testMessages = (marker: string): ChatMessage[] => [
490
+ {
491
+ id: 'msg-1',
492
+ variant: 'user',
493
+ content: marker,
494
+ timestamp: new Date().toISOString(),
495
+ },
496
+ ]
497
+
498
+ const readSavedMessages = () =>
499
+ JSON.parse(
500
+ fs.readFileSync(path.join(chatDir, 'chat-messages.json'), 'utf8'),
501
+ ) as ChatMessage[]
502
+
503
+ beforeEach(() => {
504
+ fs.rmSync(chatDir, { recursive: true, force: true })
505
+ setChatDirOverrideForTesting(chatDir)
506
+ })
507
+
508
+ afterEach(() => {
509
+ clearLiveChatStateProvider('run-a')
510
+ clearLiveChatStateProvider('run-b')
511
+ setChatDirOverrideForTesting(undefined)
512
+ fs.rmSync(chatDir, { recursive: true, force: true })
513
+ })
514
+
515
+ test('flushLiveChatState persists the provided state', () => {
516
+ setLiveChatStateProvider('run-a', () => ({
517
+ runState: testRunState('checkpoint'),
518
+ messages: testMessages('in-flight prompt'),
519
+ }))
520
+
521
+ flushLiveChatState()
522
+
523
+ expect(readSavedMessages()[0].content).toBe('in-flight prompt')
524
+ })
525
+
526
+ test('flushLiveChatState is a no-op with no provider registered', () => {
527
+ flushLiveChatState()
528
+
529
+ expect(fs.existsSync(path.join(chatDir, 'chat-messages.json'))).toBe(false)
530
+ })
531
+
532
+ test('clearLiveChatStateProvider stops flushing', () => {
533
+ setLiveChatStateProvider('run-a', () => ({
534
+ runState: testRunState('checkpoint'),
535
+ messages: testMessages('in-flight prompt'),
536
+ }))
537
+ clearLiveChatStateProvider('run-a')
538
+
539
+ flushLiveChatState()
540
+
541
+ expect(fs.existsSync(path.join(chatDir, 'chat-messages.json'))).toBe(false)
542
+ })
543
+
544
+ test('a stale run cannot clear a newer run provider', () => {
545
+ setLiveChatStateProvider('run-a', () => ({
546
+ runState: testRunState('old'),
547
+ messages: testMessages('old prompt'),
548
+ }))
549
+ setLiveChatStateProvider('run-b', () => ({
550
+ runState: testRunState('new'),
551
+ messages: testMessages('new prompt'),
552
+ }))
553
+
554
+ // The old run settling late must not remove the new run's provider.
555
+ clearLiveChatStateProvider('run-a')
556
+ flushLiveChatState()
557
+
558
+ expect(readSavedMessages()[0].content).toBe('new prompt')
559
+ })
560
+
561
+ test('flushLiveChatState swallows provider errors', () => {
562
+ setLiveChatStateProvider('run-a', () => {
563
+ throw new Error('boom')
564
+ })
565
+
566
+ expect(() => flushLiveChatState()).not.toThrow()
567
+ })
568
+ })
569
+
570
+ describe('atomic save and resilient load', () => {
571
+ const chatDir = path.join(TEST_ROOT, 'anvil-test-resilient-chatdir')
572
+
573
+ const runState = { output: { type: 'error', message: 'x' } } as RunState
574
+ const messages: ChatMessage[] = [
575
+ {
576
+ id: 'msg-1',
577
+ variant: 'user',
578
+ content: 'the prompt',
579
+ timestamp: new Date().toISOString(),
580
+ },
581
+ ]
582
+
583
+ beforeEach(() => {
584
+ fs.rmSync(chatDir, { recursive: true, force: true })
585
+ setChatDirOverrideForTesting(chatDir)
586
+ })
587
+
588
+ afterEach(() => {
589
+ setChatDirOverrideForTesting(undefined)
590
+ fs.rmSync(chatDir, { recursive: true, force: true })
591
+ })
592
+
593
+ test('saveChatState leaves no .tmp files behind', () => {
594
+ saveChatState(runState, messages)
595
+
596
+ const leftovers = fs
597
+ .readdirSync(chatDir)
598
+ .filter((name) => name.endsWith('.tmp'))
599
+ expect(leftovers).toHaveLength(0)
600
+ expect(
601
+ JSON.parse(
602
+ fs.readFileSync(path.join(chatDir, 'chat-messages.json'), 'utf8'),
603
+ ),
604
+ ).toHaveLength(1)
605
+ })
606
+
607
+ test('torn run-state.json still restores the transcript', () => {
608
+ saveChatState(runState, messages)
609
+ fs.writeFileSync(
610
+ path.join(chatDir, 'run-state.json'),
611
+ '{"sessionState": {"trunc',
612
+ )
613
+
614
+ const loaded = loadMostRecentChatState()
615
+
616
+ expect(loaded).not.toBeNull()
617
+ expect(loaded!.messages[0].content).toBe('the prompt')
618
+ expect(loaded!.runState.output.type).toBe('error')
619
+ })
620
+
621
+ test('torn chat-messages.json still restores the run state', () => {
622
+ saveChatState(runState, messages)
623
+ fs.writeFileSync(path.join(chatDir, 'chat-messages.json'), '[{"id":')
624
+
625
+ const loaded = loadMostRecentChatState()
626
+
627
+ expect(loaded).not.toBeNull()
628
+ expect(loaded!.messages).toHaveLength(0)
629
+ expect((loaded!.runState.output as any).message).toBe('x')
630
+ })
631
+
632
+ test('returns null when both files are unreadable', () => {
633
+ saveChatState(runState, messages)
634
+ fs.writeFileSync(path.join(chatDir, 'run-state.json'), '{')
635
+ fs.writeFileSync(path.join(chatDir, 'chat-messages.json'), '[')
636
+
637
+ expect(loadMostRecentChatState()).toBeNull()
638
+ })
639
+ })
640
+
641
+ describe('scheduleCheckpointSave (async, coalescing)', () => {
642
+ const chatDir = path.join(TEST_ROOT, 'anvil-test-checkpoint-chatdir')
643
+
644
+ const runState = (marker: string) =>
645
+ ({ output: { type: 'error', message: marker } }) as unknown as RunState
646
+
647
+ const messages = (marker: string): ChatMessage[] => [
648
+ {
649
+ id: 'msg-1',
650
+ variant: 'user',
651
+ content: marker,
652
+ timestamp: new Date().toISOString(),
653
+ },
654
+ ]
655
+
656
+ const readSavedMessages = () =>
657
+ JSON.parse(
658
+ fs.readFileSync(path.join(chatDir, 'chat-messages.json'), 'utf8'),
659
+ ) as ChatMessage[]
660
+
661
+ beforeEach(() => {
662
+ fs.rmSync(chatDir, { recursive: true, force: true })
663
+ setChatDirOverrideForTesting(chatDir)
664
+ })
665
+
666
+ afterEach(async () => {
667
+ await settleCheckpointSave()
668
+ setChatDirOverrideForTesting(undefined)
669
+ fs.rmSync(chatDir, { recursive: true, force: true })
670
+ })
671
+
672
+ test('persists the scheduled state after settling', async () => {
673
+ scheduleCheckpointSave(runState('a'), messages('first'))
674
+
675
+ await settleCheckpointSave()
676
+
677
+ expect(readSavedMessages()[0].content).toBe('first')
678
+ })
679
+
680
+ test('does not write synchronously (deferred off the calling tick)', () => {
681
+ scheduleCheckpointSave(runState('a'), messages('deferred'))
682
+
683
+ // Nothing on disk yet: the write is scheduled for a later tick.
684
+ expect(fs.existsSync(path.join(chatDir, 'chat-messages.json'))).toBe(false)
685
+ })
686
+
687
+ test('coalesces a burst to the latest state', async () => {
688
+ scheduleCheckpointSave(runState('a'), messages('one'))
689
+ scheduleCheckpointSave(runState('b'), messages('two'))
690
+ scheduleCheckpointSave(runState('c'), messages('three'))
691
+
692
+ await settleCheckpointSave()
693
+
694
+ // Whatever intermediate states were dropped, the newest wins.
695
+ expect(readSavedMessages()[0].content).toBe('three')
696
+ })
697
+
698
+ test('an authoritative save after settling is the last write (no clobber)', async () => {
699
+ scheduleCheckpointSave(runState('a'), messages('checkpoint'))
700
+ // settle waits for the queued async write to flush, so the synchronous
701
+ // final save below is guaranteed to land last.
702
+ await settleCheckpointSave()
703
+
704
+ saveChatState(runState('final'), messages('authoritative'))
705
+ // Give any lingering async write a chance to (incorrectly) land on top.
706
+ await new Promise((r) => setImmediate(r))
707
+ await settleCheckpointSave()
708
+
709
+ expect(readSavedMessages()[0].content).toBe('authoritative')
710
+ })
711
+
712
+ test('settleCheckpointSave is safe with nothing scheduled', async () => {
713
+ await expect(settleCheckpointSave()).resolves.toBeUndefined()
714
+ })
715
+ })
716
+
717
+ describe('chat switches while saves are pending', () => {
718
+ // Regression tests for /history threads being clobbered: save paths used to
719
+ // be resolved at write time from the mutable current chat id, so a pending
720
+ // write from chat A could land in chat B's directory after a /new or
721
+ // /history resume rotated the id in between.
722
+ const chatDirA = path.join(TEST_ROOT, 'anvil-test-switch-chat-a')
723
+ const chatDirB = path.join(TEST_ROOT, 'anvil-test-switch-chat-b')
724
+
725
+ const runState = (marker: string) =>
726
+ ({ output: { type: 'error', message: marker } }) as unknown as RunState
727
+
728
+ const messages = (marker: string): ChatMessage[] => [
729
+ {
730
+ id: 'msg-1',
731
+ variant: 'user',
732
+ content: marker,
733
+ timestamp: new Date().toISOString(),
734
+ },
735
+ ]
736
+
737
+ const messagesFileIn = (dir: string) => path.join(dir, 'chat-messages.json')
738
+
739
+ beforeEach(() => {
740
+ fs.rmSync(chatDirA, { recursive: true, force: true })
741
+ fs.rmSync(chatDirB, { recursive: true, force: true })
742
+ setChatDirOverrideForTesting(chatDirA)
743
+ })
744
+
745
+ afterEach(async () => {
746
+ await settleCheckpointSave()
747
+ clearLiveChatStateProvider('run-a')
748
+ setChatDirOverrideForTesting(undefined)
749
+ fs.rmSync(chatDirA, { recursive: true, force: true })
750
+ fs.rmSync(chatDirB, { recursive: true, force: true })
751
+ })
752
+
753
+ test('a checkpoint scheduled before a chat switch writes to the original chat dir', async () => {
754
+ scheduleCheckpointSave(runState('a'), messages('chat A transcript'))
755
+
756
+ // Simulate /new or a /history resume rotating the current chat while the
757
+ // async checkpoint write is still queued.
758
+ setChatDirOverrideForTesting(chatDirB)
759
+ await settleCheckpointSave()
760
+
761
+ const saved = JSON.parse(
762
+ fs.readFileSync(messagesFileIn(chatDirA), 'utf8'),
763
+ ) as ChatMessage[]
764
+ expect(saved[0].content).toBe('chat A transcript')
765
+ expect(fs.existsSync(messagesFileIn(chatDirB))).toBe(false)
766
+ })
767
+
768
+ test('checkpoints for different chats do not displace each other', async () => {
769
+ // Chat A's final flush is queued, then the newly-active chat B schedules
770
+ // its own checkpoint before the queue drains. Both must be written —
771
+ // coalescing is per chat, not global.
772
+ scheduleCheckpointSave(runState('a'), messages('chat A final'), chatDirA)
773
+ scheduleCheckpointSave(runState('b'), messages('chat B first'), chatDirB)
774
+
775
+ await settleCheckpointSave()
776
+
777
+ const savedA = JSON.parse(
778
+ fs.readFileSync(messagesFileIn(chatDirA), 'utf8'),
779
+ ) as ChatMessage[]
780
+ const savedB = JSON.parse(
781
+ fs.readFileSync(messagesFileIn(chatDirB), 'utf8'),
782
+ ) as ChatMessage[]
783
+ expect(savedA[0].content).toBe('chat A final')
784
+ expect(savedB[0].content).toBe('chat B first')
785
+ })
786
+
787
+ test('saveChatState with an explicit chatDir ignores a later chat switch', () => {
788
+ setChatDirOverrideForTesting(chatDirB)
789
+
790
+ // chatDirA was captured while chat A was current (run start).
791
+ saveChatState(runState('a'), messages('chat A final'), chatDirA)
792
+
793
+ const saved = JSON.parse(
794
+ fs.readFileSync(messagesFileIn(chatDirA), 'utf8'),
795
+ ) as ChatMessage[]
796
+ expect(saved[0].content).toBe('chat A final')
797
+ expect(fs.existsSync(messagesFileIn(chatDirB))).toBe(false)
798
+ })
799
+
800
+ test('flushLiveChatState after a chat switch writes nothing', () => {
801
+ setLiveChatStateProvider('run-a', () => ({
802
+ runState: runState('a'),
803
+ // After the switch the store holds the NEW chat's messages; flushing
804
+ // them into chat A's directory would replace A's transcript.
805
+ messages: messages('chat B messages'),
806
+ }))
807
+
808
+ setChatDirOverrideForTesting(chatDirB)
809
+ flushLiveChatState()
810
+
811
+ expect(fs.existsSync(messagesFileIn(chatDirA))).toBe(false)
812
+ expect(fs.existsSync(messagesFileIn(chatDirB))).toBe(false)
813
+ })
814
+
815
+ test('flushLiveChatState drains queued checkpoints synchronously on exit', () => {
816
+ // A chat switch aborted the run and queued its final checkpoint, then the
817
+ // process exits before the async drain runs. The exit flush must write
818
+ // the queued checkpoint (to chat A) even though the current chat is B.
819
+ scheduleCheckpointSave(runState('a'), messages('aborted turn'), chatDirA)
820
+ setChatDirOverrideForTesting(chatDirB)
821
+
822
+ flushLiveChatState()
823
+
824
+ const saved = JSON.parse(
825
+ fs.readFileSync(messagesFileIn(chatDirA), 'utf8'),
826
+ ) as ChatMessage[]
827
+ expect(saved[0].content).toBe('aborted turn')
828
+ expect(fs.existsSync(messagesFileIn(chatDirB))).toBe(false)
829
+ })
830
+
831
+ test('flushLiveChatState still writes to the run chat dir when unswitched', () => {
832
+ setLiveChatStateProvider('run-a', () => ({
833
+ runState: runState('a'),
834
+ messages: messages('in-flight prompt'),
835
+ }))
836
+
837
+ flushLiveChatState()
838
+
839
+ const saved = JSON.parse(
840
+ fs.readFileSync(messagesFileIn(chatDirA), 'utf8'),
841
+ ) as ChatMessage[]
842
+ expect(saved[0].content).toBe('in-flight prompt')
843
+ })
844
+ })
845
+
846
+ describe('poisoned payload persistence', () => {
847
+ const chatDir = path.join(TEST_ROOT, 'anvil-test-poisoned-chatdir')
848
+
849
+ const messages: ChatMessage[] = [
850
+ {
851
+ id: 'msg-1',
852
+ variant: 'user',
853
+ content: 'the prompt',
854
+ timestamp: new Date().toISOString(),
855
+ },
856
+ ]
857
+
858
+ beforeEach(() => {
859
+ fs.rmSync(chatDir, { recursive: true, force: true })
860
+ fs.mkdirSync(chatDir, { recursive: true })
861
+ setChatDirOverrideForTesting(chatDir)
862
+ })
863
+
864
+ afterEach(() => {
865
+ setChatDirOverrideForTesting(undefined)
866
+ fs.rmSync(chatDir, { recursive: true, force: true })
867
+ })
868
+
869
+ test('cyclic run state still persists (cycles broken) alongside messages', () => {
870
+ const cyclicRunState: any = { output: { type: 'error', message: 'x' } }
871
+ cyclicRunState.self = cyclicRunState
872
+
873
+ saveChatState(cyclicRunState as RunState, messages)
874
+
875
+ const savedRunState = JSON.parse(
876
+ fs.readFileSync(path.join(chatDir, 'run-state.json'), 'utf8'),
877
+ )
878
+ expect(savedRunState.self).toBe('[Circular]')
879
+ expect(savedRunState.output.message).toBe('x')
880
+
881
+ const savedMessages = JSON.parse(
882
+ fs.readFileSync(path.join(chatDir, 'chat-messages.json'), 'utf8'),
883
+ ) as ChatMessage[]
884
+ expect(savedMessages[0].content).toBe('the prompt')
885
+ })
886
+
887
+ test('cyclic tool output in messages does not block the transcript save', () => {
888
+ const cyclicOutput: any = { status: 'ok' }
889
+ cyclicOutput.self = cyclicOutput
890
+ const poisonedMessages: ChatMessage[] = [
891
+ {
892
+ id: 'msg-1',
893
+ variant: 'agent',
894
+ content: '',
895
+ timestamp: new Date().toISOString(),
896
+ blocks: [
897
+ {
898
+ type: 'tool',
899
+ toolCallId: 'tc-1',
900
+ toolName: 'run_terminal_command' as any,
901
+ input: {},
902
+ outputRaw: cyclicOutput,
903
+ },
904
+ ],
905
+ },
906
+ ]
907
+
908
+ saveChatState(
909
+ { output: { type: 'error', message: 'x' } } as RunState,
910
+ poisonedMessages,
911
+ )
912
+
913
+ const savedMessages = JSON.parse(
914
+ fs.readFileSync(path.join(chatDir, 'chat-messages.json'), 'utf8'),
915
+ ) as ChatMessage[]
916
+ const block = savedMessages[0].blocks?.[0] as any
917
+ expect(block.outputRaw.self).toBe('[Circular]')
918
+ })
919
+ })