@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,271 @@
1
+ import { execSync } from 'child_process'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ /**
6
+ * Check if tmux is available and usable on the system.
7
+ * This checks both that tmux is installed AND that it can actually run
8
+ * (e.g., the tmux server socket directory exists and is accessible).
9
+ *
10
+ * Returns false on CI unless ANVIL_RUN_TMUX_TESTS=1 explicitly opts into
11
+ * running deterministic tmux tests in a known terminal environment.
12
+ */
13
+ export function isTmuxAvailable(): boolean {
14
+ // Skip on CI - tmux integration tests need a real terminal environment
15
+ if (
16
+ (process.env.CI === 'true' || process.env.CI === '1') &&
17
+ process.env.ANVIL_RUN_TMUX_TESTS !== '1'
18
+ ) {
19
+ return false
20
+ }
21
+
22
+ try {
23
+ // First check if tmux is installed
24
+ execSync('which tmux', { stdio: 'pipe' })
25
+ // Then verify tmux can actually run by creating and killing a test session
26
+ // This will fail if tmux server can't start (e.g., no socket directory on CI)
27
+ execSync('tmux new-session -d -s __anvil_tmux_check__ && tmux kill-session -t __anvil_tmux_check__', {
28
+ stdio: 'pipe',
29
+ timeout: 5000,
30
+ })
31
+ return true
32
+ } catch {
33
+ return false
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check if the SDK is built by checking for the dist directory
39
+ */
40
+ export function isSDKBuilt(): boolean {
41
+ try {
42
+ const sdkDistDir = path.join(__dirname, '../../../sdk/dist')
43
+ const possibleArtifacts = ['index.js', 'index.mjs', 'index.cjs']
44
+ return possibleArtifacts.some((file) =>
45
+ fs.existsSync(path.join(sdkDistDir, file)),
46
+ )
47
+ } catch {
48
+ return false
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Sleep utility for async delays
54
+ */
55
+ export const sleep = (ms: number) =>
56
+ new Promise((resolve) => setTimeout(resolve, ms))
57
+
58
+ let cachedEnv: Record<string, string> | null = null
59
+
60
+ const TEST_CLIENT_ENV_DEFAULTS: Record<string, string> = {
61
+ NEXT_PUBLIC_CB_ENVIRONMENT: 'test',
62
+ NEXT_PUBLIC_ANVIL_APP_URL: 'http://localhost:3000',
63
+ NEXT_PUBLIC_SUPPORT_EMAIL: 'support@codebuff.com',
64
+ NEXT_PUBLIC_POSTHOG_API_KEY: 'test-posthog-key',
65
+ NEXT_PUBLIC_POSTHOG_HOST_URL: 'https://us.i.posthog.com',
66
+ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_test_placeholder',
67
+ NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL:
68
+ 'https://billing.stripe.com/p/login/test_placeholder',
69
+ NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION_ID: 'test-verification',
70
+ NEXT_PUBLIC_WEB_PORT: '3000',
71
+ }
72
+ const TEST_SERVER_ENV_DEFAULTS: Record<string, string> = {
73
+ OPEN_ROUTER_API_KEY: 'test',
74
+ OPENAI_API_KEY: 'test',
75
+ ANTHROPIC_API_KEY: 'test',
76
+ SERPER_API_KEY: 'test',
77
+ GRAVITY_API_KEY: 'test',
78
+ GRAVITY_CAPI_KEY: 'test',
79
+ PORT: '4242',
80
+ DATABASE_URL: 'postgres://user:pass@localhost:5432/db',
81
+ ANVIL_GITHUB_ID: 'test-id',
82
+ ANVIL_GITHUB_SECRET: 'test-secret',
83
+ NEXTAUTH_SECRET: 'test-secret',
84
+ STRIPE_SECRET_KEY: 'sk_test_dummy',
85
+ STRIPE_WEBHOOK_SECRET_KEY: 'whsec_dummy',
86
+ STRIPE_TEAM_FEE_PRICE_ID: 'price_test',
87
+ LOOPS_API_KEY: 'test',
88
+ DISCORD_PUBLIC_KEY: 'test',
89
+ DISCORD_BOT_TOKEN: 'test',
90
+ DISCORD_APPLICATION_ID: 'test',
91
+ }
92
+
93
+ function ensureCliEnvDefaults(): void {
94
+ if (!process.env.NODE_ENV) {
95
+ process.env.NODE_ENV = 'test'
96
+ }
97
+ if (!process.env.BUN_ENV) {
98
+ process.env.BUN_ENV = 'test'
99
+ }
100
+ if (process.env.CI !== 'true' && process.env.CI !== '1') {
101
+ process.env.CI = 'true'
102
+ }
103
+
104
+ for (const [key, value] of Object.entries(TEST_CLIENT_ENV_DEFAULTS)) {
105
+ if (!process.env[key]) {
106
+ process.env[key] = value
107
+ }
108
+ }
109
+
110
+ for (const [key, value] of Object.entries(TEST_SERVER_ENV_DEFAULTS)) {
111
+ if (!process.env[key]) {
112
+ process.env[key] = value
113
+ }
114
+ }
115
+ }
116
+
117
+ function loadCliEnv(): Record<string, string> {
118
+ if (cachedEnv) {
119
+ return cachedEnv
120
+ }
121
+
122
+ try {
123
+ ensureCliEnvDefaults()
124
+ // NOTE: Inline require() is used for lazy loading - the env module depends on
125
+ // Infisical secrets which may not be available at module load time in test environments
126
+ const { env } = require('../../../packages/internal/src/env') as {
127
+ env: Record<string, unknown>
128
+ }
129
+
130
+ cachedEnv = Object.entries(env).reduce<Record<string, string>>(
131
+ (acc, [key, value]) => {
132
+ if (value !== undefined && value !== null) {
133
+ acc[key] = String(value)
134
+ }
135
+ return acc
136
+ },
137
+ {},
138
+ )
139
+
140
+ return cachedEnv
141
+ } catch (error) {
142
+ const message =
143
+ error instanceof Error
144
+ ? error.message
145
+ : 'unknown error loading environment'
146
+ throw new Error(
147
+ `Failed to load CLI environment via packages/internal/src/env: ${message}. ` +
148
+ 'Run commands via "infisical run -- bun …" or export the required variables.',
149
+ )
150
+ }
151
+ }
152
+
153
+ export function ensureCliTestEnv(): void {
154
+ loadCliEnv()
155
+ }
156
+
157
+ export function getDefaultCliEnv(): Record<string, string> {
158
+ return { ...loadCliEnv() }
159
+ }
160
+
161
+ /**
162
+ * Parsed re-render log entry from useWhyDidYouUpdate hook
163
+ */
164
+ export interface RerenderLogEntry {
165
+ timestamp: string
166
+ componentName: string
167
+ messageId: string
168
+ renderCount: number
169
+ changedProps: string[]
170
+ }
171
+
172
+ /**
173
+ * Aggregated re-render analysis results
174
+ */
175
+ export interface RerenderAnalysis {
176
+ totalRerenders: number
177
+ rerendersByMessage: Map<string, number>
178
+ propChangeFrequency: Map<string, number>
179
+ maxRerenderPerMessage: number
180
+ }
181
+
182
+ /**
183
+ * Parse re-render logs from the CLI debug log file
184
+ */
185
+ export function parseRerenderLogs(logPath: string): RerenderLogEntry[] {
186
+ const entries: RerenderLogEntry[] = []
187
+
188
+ try {
189
+ const content = fs.readFileSync(logPath, 'utf-8')
190
+ const lines = content.split('\n').filter((line) => line.trim())
191
+
192
+ for (const line of lines) {
193
+ try {
194
+ const parsed = JSON.parse(line)
195
+
196
+ // Check if this is a re-render log entry
197
+ if (
198
+ parsed.msg &&
199
+ typeof parsed.msg === 'string' &&
200
+ parsed.msg.includes('render #')
201
+ ) {
202
+ // Extract component name from msg like "MessageBlock render #2 [user-123]: 2 props changed"
203
+ const msgMatch = parsed.msg.match(
204
+ /^(\w+) render #(\d+) \[([^\]]+)\]/,
205
+ )
206
+ if (msgMatch && parsed.data) {
207
+ entries.push({
208
+ timestamp: parsed.timestamp,
209
+ componentName: msgMatch[1],
210
+ messageId: parsed.data.id || msgMatch[3],
211
+ renderCount: parseInt(msgMatch[2], 10),
212
+ changedProps: parsed.data.changedProps || [],
213
+ })
214
+ }
215
+ }
216
+ } catch {
217
+ // Skip malformed lines
218
+ }
219
+ }
220
+ } catch {
221
+ // File doesn't exist or can't be read
222
+ }
223
+
224
+ return entries
225
+ }
226
+
227
+ /**
228
+ * Analyze re-render logs and return aggregated statistics
229
+ */
230
+ export function analyzeRerenders(entries: RerenderLogEntry[]): RerenderAnalysis {
231
+ const rerendersByMessage = new Map<string, number>()
232
+ const propChangeFrequency = new Map<string, number>()
233
+
234
+ for (const entry of entries) {
235
+ // Count re-renders per message
236
+ const currentCount = rerendersByMessage.get(entry.messageId) || 0
237
+ rerendersByMessage.set(entry.messageId, currentCount + 1)
238
+
239
+ // Count how often each prop changes
240
+ for (const prop of entry.changedProps) {
241
+ const propCount = propChangeFrequency.get(prop) || 0
242
+ propChangeFrequency.set(prop, propCount + 1)
243
+ }
244
+ }
245
+
246
+ // Find max re-renders for any single message
247
+ let maxRerenderPerMessage = 0
248
+ for (const count of rerendersByMessage.values()) {
249
+ if (count > maxRerenderPerMessage) {
250
+ maxRerenderPerMessage = count
251
+ }
252
+ }
253
+
254
+ return {
255
+ totalRerenders: entries.length,
256
+ rerendersByMessage,
257
+ propChangeFrequency,
258
+ maxRerenderPerMessage,
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Clear the CLI debug log file
264
+ */
265
+ export function clearCliDebugLog(logPath: string): void {
266
+ try {
267
+ fs.writeFileSync(logPath, '')
268
+ } catch {
269
+ // Ignore errors
270
+ }
271
+ }
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Proof of Concept: tmux-based CLI testing
5
+ *
6
+ * This script demonstrates how to:
7
+ * 1. Create a tmux session
8
+ * 2. Run the CLI in that session
9
+ * 3. Send commands to the CLI
10
+ * 4. Capture and verify output
11
+ * 5. Clean up the session
12
+ */
13
+
14
+ import { spawn } from 'child_process'
15
+
16
+ import stripAnsi from 'strip-ansi'
17
+
18
+ import { isTmuxAvailable, sleep } from './test-utils'
19
+
20
+ // Utility to run tmux commands
21
+ function tmux(args: string[]): Promise<string> {
22
+ return new Promise((resolve, reject) => {
23
+ const proc = spawn('tmux', args, { stdio: 'pipe' })
24
+ let stdout = ''
25
+ let stderr = ''
26
+
27
+ proc.stdout?.on('data', (data) => {
28
+ stdout += data.toString()
29
+ })
30
+
31
+ proc.stderr?.on('data', (data) => {
32
+ stderr += data.toString()
33
+ })
34
+
35
+ proc.on('close', (code) => {
36
+ if (code === 0) {
37
+ resolve(stdout)
38
+ } else {
39
+ reject(new Error(`tmux command failed: ${stderr}`))
40
+ }
41
+ })
42
+ })
43
+ }
44
+
45
+ // Capture pane content
46
+ async function capturePane(sessionName: string): Promise<string> {
47
+ return await tmux(['capture-pane', '-t', sessionName, '-p'])
48
+ }
49
+
50
+ // Main test function
51
+ async function testCLIWithTmux() {
52
+ const sessionName = 'anvil-test-' + Date.now()
53
+
54
+ console.log('🚀 Starting tmux-based CLI test...')
55
+ console.log(`📦 Session: ${sessionName}`)
56
+
57
+ // 1. Check if tmux is installed
58
+ if (!isTmuxAvailable()) {
59
+ console.error('❌ tmux not found')
60
+ console.error('\n📦 Installation:')
61
+ console.error(' macOS: brew install tmux')
62
+ console.error(' Ubuntu: sudo apt-get install tmux')
63
+ console.error(' Windows: Use WSL and run sudo apt-get install tmux')
64
+ console.error(
65
+ '\nℹ️ This is just a proof-of-concept. See the documentation for alternatives.',
66
+ )
67
+ process.exit(1)
68
+ }
69
+
70
+ try {
71
+ const version = await tmux(['-V'])
72
+ console.log(`✅ tmux is installed: ${version.trim()}`)
73
+
74
+ // 2. Create new detached tmux session running the CLI
75
+ console.log('\n📺 Creating tmux session...')
76
+ await tmux([
77
+ 'new-session',
78
+ '-d',
79
+ '-s',
80
+ sessionName,
81
+ '-x',
82
+ '120', // width
83
+ '-y',
84
+ '30', // height
85
+ 'bun',
86
+ 'run',
87
+ 'src/entry.ts',
88
+ '--help',
89
+ ])
90
+ console.log('✅ Session created')
91
+
92
+ // 3. Wait for CLI to start
93
+ await sleep(1000)
94
+
95
+ // 4. Capture initial output
96
+ console.log('\n📸 Capturing initial output...')
97
+ const initialOutput = await capturePane(sessionName)
98
+ const cleanOutput = stripAnsi(initialOutput)
99
+
100
+ console.log('\n--- Output ---')
101
+ console.log(cleanOutput)
102
+ console.log('--- End Output ---\n')
103
+
104
+ // 5. Verify output contains expected text
105
+ const checks = [
106
+ { text: '--agent', pass: cleanOutput.includes('--agent') },
107
+ { text: 'Usage:', pass: cleanOutput.includes('Usage:') },
108
+ { text: '--help', pass: cleanOutput.includes('--help') },
109
+ ]
110
+
111
+ console.log('🔍 Verification:')
112
+ checks.forEach(({ text, pass }) => {
113
+ console.log(
114
+ ` ${pass ? '✅' : '❌'} Contains "${text}"${pass ? '' : ' - NOT FOUND'}`,
115
+ )
116
+ })
117
+
118
+ const allPassed = checks.every((c) => c.pass)
119
+ console.log(
120
+ `\n${allPassed ? '🎉 All checks passed!' : '⚠️ Some checks failed'}`,
121
+ )
122
+
123
+ // 6. Example: Send interactive command (commented out for --help test)
124
+ /*
125
+ console.log('\n⌨️ Sending test command...')
126
+ await sendKeys(sessionName, 'hello world')
127
+ await sendKeys(sessionName, 'Enter')
128
+ await sleep(2000)
129
+
130
+ const responseOutput = await capturePane(sessionName)
131
+ console.log('\n--- Response ---')
132
+ console.log(stripAnsi(responseOutput))
133
+ console.log('--- End Response ---')
134
+ */
135
+ } catch (error) {
136
+ console.error('\n❌ Test failed:', error)
137
+ } finally {
138
+ // 7. Cleanup: kill the tmux session
139
+ console.log('\n🧹 Cleaning up...')
140
+ try {
141
+ await tmux(['kill-session', '-t', sessionName])
142
+ console.log('✅ Session cleaned up')
143
+ } catch (e) {
144
+ console.log('⚠️ Session may have already exited')
145
+ }
146
+ }
147
+ }
148
+
149
+ // Run the test
150
+ testCLIWithTmux().catch(console.error)
@@ -0,0 +1,137 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
2
+
3
+ import {
4
+ buildExpandedSegments,
5
+ resolveAgentModeClick,
6
+ OPEN_DELAY_MS,
7
+ CLOSE_DELAY_MS,
8
+ REOPEN_SUPPRESS_MS,
9
+ } from '../../components/agent-mode-toggle'
10
+ import { createHoverToggleControllerForTest } from '../mocks/hover-toggle-controller'
11
+
12
+ import type { AgentMode } from '../../utils/constants'
13
+
14
+ describe('AgentModeToggle - buildExpandedSegments', () => {
15
+ const modes: AgentMode[] = ['DEFAULT', 'MAX', 'PLAN']
16
+
17
+ for (const mode of modes) {
18
+ test(`returns segments with active indicator for ${mode}`, () => {
19
+ const segs = buildExpandedSegments(mode)
20
+ // 4 mode options (DEFAULT, LITE, MAX, PLAN) + 1 active indicator
21
+ expect(segs.length).toBe(5)
22
+
23
+ // Current mode is disabled among the choices
24
+ const current = segs.find((s) => s.id === mode)
25
+ expect(current?.disabled).toBe(true)
26
+
27
+ // Active indicator has expected id and flags
28
+ const active = segs.find((s) => s.id === `active-${mode}`)
29
+ expect(active).toBeTruthy()
30
+ expect(active?.isSelected).toBe(true)
31
+ expect(active?.defaultHighlighted).toBe(true)
32
+ })
33
+ }
34
+ })
35
+
36
+ describe('AgentModeToggle - resolveAgentModeClick', () => {
37
+ test('clicking active indicator returns closeActive', () => {
38
+ const action = resolveAgentModeClick('DEFAULT', 'active-DEFAULT', true)
39
+ expect(action).toEqual({ type: 'closeActive' })
40
+ })
41
+
42
+ test('with onSelectMode provided, clicking different mode selects it', () => {
43
+ const action = resolveAgentModeClick('DEFAULT', 'MAX', true)
44
+ expect(action).toEqual({ type: 'selectMode', mode: 'MAX' })
45
+ })
46
+
47
+ test('without onSelectMode, clicking different mode toggles', () => {
48
+ const action = resolveAgentModeClick('DEFAULT', 'PLAN', false)
49
+ expect(action).toEqual({ type: 'toggleMode', mode: 'PLAN' })
50
+ })
51
+ })
52
+
53
+ // Extended Date.now type with test helper method
54
+ interface MockDateNow {
55
+ (): number
56
+ set: (v: number) => void
57
+ }
58
+
59
+ describe('useHoverToggle timing (controller)', () => {
60
+ let originalSetTimeout: typeof setTimeout
61
+ let originalClearTimeout: typeof clearTimeout
62
+ let originalNow: typeof Date.now
63
+
64
+ let timers: { id: number; ms: number; fn: () => void; active: boolean }[]
65
+ let nextId: number
66
+
67
+ const runAll = () => {
68
+ for (const t of timers) {
69
+ if (t.active) t.fn()
70
+ }
71
+ timers = []
72
+ }
73
+
74
+ beforeEach(() => {
75
+ timers = []
76
+ nextId = 1
77
+ originalSetTimeout = setTimeout
78
+ originalClearTimeout = clearTimeout
79
+ originalNow = Date.now
80
+
81
+ let now = 1_000
82
+ const mockDateNow: MockDateNow = Object.assign(
83
+ () => now,
84
+ { set: (v: number) => { now = v } }
85
+ )
86
+ Date.now = mockDateNow
87
+
88
+ globalThis.setTimeout = ((fn: () => void, ms?: number) => {
89
+ const id = nextId++
90
+ timers.push({ id, ms: Number(ms ?? 0), fn, active: true })
91
+ return id as unknown as ReturnType<typeof setTimeout>
92
+ }) as typeof setTimeout
93
+
94
+ globalThis.clearTimeout = ((id?: ReturnType<typeof clearTimeout>) => {
95
+ const rec = timers.find((t) => t.id === (id as unknown as number))
96
+ if (rec) rec.active = false
97
+ }) as typeof clearTimeout
98
+ })
99
+
100
+ afterEach(() => {
101
+ globalThis.setTimeout = originalSetTimeout
102
+ globalThis.clearTimeout = originalClearTimeout
103
+ Date.now = originalNow
104
+ })
105
+
106
+ test('scheduleOpen waits OPEN_DELAY_MS then opens', () => {
107
+ const ctl = createHoverToggleControllerForTest()
108
+ expect(ctl.isOpen).toBe(false)
109
+ ctl.scheduleOpen()
110
+ expect(timers.length).toBe(1)
111
+ expect(timers[0].ms).toBe(OPEN_DELAY_MS)
112
+ runAll()
113
+ expect(ctl.isOpen).toBe(true)
114
+ })
115
+
116
+ test('scheduleClose waits CLOSE_DELAY_MS then closes', () => {
117
+ const ctl = createHoverToggleControllerForTest()
118
+ ctl.openNow()
119
+ expect(ctl.isOpen).toBe(true)
120
+ ctl.scheduleClose()
121
+ expect(timers.length).toBe(1)
122
+ expect(timers[0].ms).toBe(CLOSE_DELAY_MS)
123
+ runAll()
124
+ expect(ctl.isOpen).toBe(false)
125
+ })
126
+
127
+ test('closeNow(true) suppresses reopen until time passes', () => {
128
+ const ctl = createHoverToggleControllerForTest()
129
+ ctl.closeNow(true)
130
+ ctl.scheduleOpen()
131
+ expect(timers.length).toBe(0)
132
+ ;(Date.now as MockDateNow).set(1_000 + REOPEN_SUPPRESS_MS + 1)
133
+ ctl.scheduleOpen()
134
+ expect(timers.length).toBe(1)
135
+ expect(timers[0].ms).toBe(OPEN_DELAY_MS)
136
+ })
137
+ })
@@ -0,0 +1,157 @@
1
+ import { createMockTimers } from '@anvil/common/testing/mocks/timers'
2
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
3
+
4
+ import {
5
+ getCopyIconText,
6
+ copyButtonHandlers,
7
+ COPIED_RESET_DELAY_MS,
8
+ COPY_ICON_COLLAPSED,
9
+ COPY_ICON_EXPANDED,
10
+ COPY_ICON_COPIED,
11
+ } from '../../components/copy-button'
12
+ import { initializeThemeStore } from '../../hooks/use-theme'
13
+
14
+ import type { MockTimers } from '@anvil/common/testing/mocks/timers'
15
+
16
+ // Initialize theme before tests
17
+ initializeThemeStore()
18
+
19
+ /**
20
+ * Tests for CopyButton component logic.
21
+ *
22
+ * These tests use the exported utilities from copy-button.tsx:
23
+ * - getCopyIconText: determines what text to display
24
+ * - copyButtonHandlers: pure functions for state transitions
25
+ * - COPIED_RESET_DELAY_MS: the timeout constant
26
+ */
27
+
28
+ describe('CopyButton - CopyIcon text rendering', () => {
29
+ describe('with leadingSpace=true (default)', () => {
30
+ test('renders collapsed icon when not hovered or copied', () => {
31
+ const text = getCopyIconText(false, false, true)
32
+ expect(text).toBe(' ⎘')
33
+ })
34
+
35
+ test('renders expanded text when hovered', () => {
36
+ const text = getCopyIconText(false, true, true)
37
+ expect(text).toBe(' [⎘ copy]')
38
+ })
39
+
40
+ test('renders copied text when copied', () => {
41
+ const text = getCopyIconText(true, false, true)
42
+ expect(text).toBe(' [✔ copied]')
43
+ })
44
+
45
+ test('renders copied text even when hovered (copied takes priority)', () => {
46
+ const text = getCopyIconText(true, true, true)
47
+ expect(text).toBe(' [✔ copied]')
48
+ })
49
+ })
50
+
51
+ describe('with leadingSpace=false', () => {
52
+ test('renders collapsed icon without leading space', () => {
53
+ const text = getCopyIconText(false, false, false)
54
+ expect(text).toBe('⎘')
55
+ })
56
+
57
+ test('renders expanded text without leading space', () => {
58
+ const text = getCopyIconText(false, true, false)
59
+ expect(text).toBe('[⎘ copy]')
60
+ })
61
+
62
+ test('renders copied text without leading space', () => {
63
+ const text = getCopyIconText(true, false, false)
64
+ expect(text).toBe('[✔ copied]')
65
+ })
66
+ })
67
+ })
68
+
69
+ describe('CopyButton - copyButtonHandlers (from component)', () => {
70
+ describe('handleMouseOver', () => {
71
+ test('returns true (should hover) when not copied', () => {
72
+ expect(copyButtonHandlers.handleMouseOver(false)).toBe(true)
73
+ })
74
+
75
+ test('returns false (block hover) when copied', () => {
76
+ expect(copyButtonHandlers.handleMouseOver(true)).toBe(false)
77
+ })
78
+ })
79
+
80
+ describe('handleMouseOut', () => {
81
+ test('always returns false to clear hover', () => {
82
+ expect(copyButtonHandlers.handleMouseOut()).toBe(false)
83
+ })
84
+ })
85
+
86
+ describe('handleCopy', () => {
87
+ test('returns copied=true and clears hover', () => {
88
+ const result = copyButtonHandlers.handleCopy()
89
+ expect(result).toEqual({ isCopied: true, isHovered: false })
90
+ })
91
+ })
92
+ })
93
+
94
+ describe('CopyButton - exported constants', () => {
95
+ test('COPIED_RESET_DELAY_MS is 2000ms', () => {
96
+ expect(COPIED_RESET_DELAY_MS).toBe(2000)
97
+ })
98
+
99
+ test('icon constants are defined', () => {
100
+ expect(COPY_ICON_COLLAPSED).toBe('⎘')
101
+ expect(COPY_ICON_EXPANDED).toBe('[⎘ copy]')
102
+ expect(COPY_ICON_COPIED).toBe('[✔ copied]')
103
+ })
104
+ })
105
+
106
+ describe('CopyButton - copied state reset timing', () => {
107
+ let mockTimers: MockTimers
108
+
109
+ beforeEach(() => {
110
+ mockTimers = createMockTimers()
111
+ mockTimers.install()
112
+ })
113
+
114
+ afterEach(() => {
115
+ mockTimers.restore()
116
+ })
117
+
118
+ test('uses the exported COPIED_RESET_DELAY_MS constant (2000ms)', () => {
119
+ let isCopied = false
120
+
121
+ // Simulate handleCopy using the exported constant
122
+ const handleCopy = () => {
123
+ const newState = copyButtonHandlers.handleCopy()
124
+ isCopied = newState.isCopied
125
+ setTimeout(() => {
126
+ isCopied = false
127
+ }, COPIED_RESET_DELAY_MS)
128
+ }
129
+
130
+ handleCopy()
131
+ expect(isCopied).toBe(true)
132
+ expect(mockTimers.getPendingCount()).toBe(1)
133
+ const nextTimer = mockTimers.getNext()
134
+ expect(nextTimer?.ms).toBe(COPIED_RESET_DELAY_MS)
135
+
136
+ mockTimers.runAll()
137
+ expect(isCopied).toBe(false)
138
+ })
139
+
140
+ test('multiple rapid clicks only create one active timer', () => {
141
+ let currentTimerId: number | null = null
142
+
143
+ const handleCopy = () => {
144
+ if (currentTimerId !== null) {
145
+ clearTimeout(currentTimerId)
146
+ }
147
+ copyButtonHandlers.handleCopy()
148
+ currentTimerId = setTimeout(() => {}, COPIED_RESET_DELAY_MS) as unknown as number
149
+ }
150
+
151
+ handleCopy()
152
+ handleCopy()
153
+ handleCopy()
154
+
155
+ expect(mockTimers.getPendingCount()).toBe(1)
156
+ })
157
+ })