@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,96 @@
1
+ // Find tree-sitter.wasm so the SDK's tree-sitter parser singleton can load
2
+ // it at runtime. Must be the very first import in `index.tsx`: subsequent
3
+ // imports (the SDK / code-map) eagerly construct the parser, and its init
4
+ // reads what we publish here on `globalThis` and via the env var.
5
+ //
6
+ // Final approach after several attempts to embed the wasm into the bun
7
+ // --compile binary all failed on Windows (the bytes ended up in the
8
+ // binary, but every JS-level retrieval mechanism — `with { type: 'file' }`
9
+ // import binding, base64 string literals, chunked base64 in a generated
10
+ // module, function-export wrappers — was either tree-shaken, transformed
11
+ // by the minifier, or otherwise stripped):
12
+ //
13
+ // ship tree-sitter.wasm as a sibling file next to the binary.
14
+ //
15
+ // It's 200KB, the npm tarball already contains the binary; adding one
16
+ // more file is trivial. The build script copies the wasm into `cli/bin/`
17
+ // after compile, the release workflow tarballs both, and the freebuff /
18
+ // anvil downloader extracts both into the same directory. At runtime,
19
+ // `process.execPath` plus a relative file lookup gets us the wasm with
20
+ // zero bundler involvement.
21
+
22
+ import { existsSync, readFileSync } from 'fs'
23
+ import { dirname, isAbsolute, join, resolve } from 'path'
24
+
25
+ // Where to look for the sibling tree-sitter.wasm. We can't just use
26
+ // `dirname(process.execPath)`: at pre-init time inside a bun --compile
27
+ // binary on Windows, `process.execPath` returns the *bunfs* internal
28
+ // path (`B:\~BUN\root\<binary>.exe`) rather than the on-disk path of
29
+ // the .exe the user invoked. By the time main() runs it switches to
30
+ // the disk path, but pre-init has long since bailed out.
31
+ //
32
+ // Try several sources in order; the first whose sibling .wasm exists
33
+ // wins. argv[0] is normally the path the binary was invoked with —
34
+ // always a real disk path, never bunfs. execPath is kept as a fallback
35
+ // for environments where argv[0] is something exotic.
36
+ const candidates = (
37
+ [process.argv[0], process.execPath] as Array<string | undefined>
38
+ )
39
+ .filter((p): p is string => typeof p === 'string' && p.length > 0)
40
+ .map((p) => (isAbsolute(p) ? p : resolve(p)))
41
+ .map((p) => join(dirname(p), 'tree-sitter.wasm'))
42
+
43
+ const siblingPath = candidates.find((p) => existsSync(p))
44
+
45
+ // Pre-init diagnostic — only fires when --smoke-tree-sitter is set so we
46
+ // don't spam every run. We need to see what argv[0] / execPath looked
47
+ // like at this exact phase on Windows: the round-7 main() diag showed
48
+ // disk paths, but pre-init silently bailed, meaning module-init time
49
+ // gives different values. argv[0] alone wasn't enough to fix it.
50
+ if (process.argv.includes('--smoke-tree-sitter')) {
51
+ console.error(
52
+ `[pre-init diag] argv[0]=${process.argv[0]}\n` +
53
+ `[pre-init diag] execPath=${process.execPath}\n` +
54
+ `[pre-init diag] candidates=${JSON.stringify(candidates)}\n` +
55
+ `[pre-init diag] resolved siblingPath=${siblingPath ?? '<none>'}\n`,
56
+ )
57
+ }
58
+
59
+ if (siblingPath) {
60
+ // Tell init-node.ts (in code-map / the SDK bundle) where the wasm
61
+ // is. The locateFile callback there will hand this path to
62
+ // emscripten, which fs.readFile's it.
63
+ process.env.ANVIL_TREE_SITTER_WASM_PATH = siblingPath
64
+
65
+ // Also publish on globalThis so the smoke handler in index.tsx can
66
+ // read it without touching process.env (which is gated by the env
67
+ // architecture check outside the allowlisted pre-init files).
68
+ ;(
69
+ globalThis as { __ANVIL_TREE_SITTER_WASM_PATH__?: string }
70
+ ).__ANVIL_TREE_SITTER_WASM_PATH__ = siblingPath
71
+
72
+ // Also try the synchronous-bytes path: hand the bytes straight to
73
+ // Parser.init({ wasmBinary }) so the SDK doesn't need to round-trip
74
+ // through emscripten's path resolution. Both channels feed the same
75
+ // tree-sitter init; whichever one trips first wins.
76
+ try {
77
+ const buf = readFileSync(siblingPath)
78
+ ;(
79
+ globalThis as { __ANVIL_TREE_SITTER_WASM_BINARY__?: Uint8Array }
80
+ ).__ANVIL_TREE_SITTER_WASM_BINARY__ = new Uint8Array(
81
+ buf.buffer,
82
+ buf.byteOffset,
83
+ buf.byteLength,
84
+ )
85
+ } catch (err) {
86
+ console.error(
87
+ '[tree-sitter pre-init] readFileSync failed for sibling wasm at',
88
+ siblingPath,
89
+ '—',
90
+ err instanceof Error ? err.message : String(err),
91
+ )
92
+ }
93
+ }
94
+
95
+ // `--smoke-tree-sitter` is the deterministic CI gate. The handler lives at
96
+ // the top of main() in cli/src/index.tsx (before parseArgs).
@@ -0,0 +1,102 @@
1
+ import { mkdirSync, readdirSync, statSync } from 'fs'
2
+ import path from 'path'
3
+
4
+ import { getConfigDir } from './utils/auth'
5
+
6
+ let projectRoot: string | undefined
7
+ let currentChatId: string | undefined
8
+
9
+ function ensureChatDirectory(dir: string) {
10
+ mkdirSync(dir, { recursive: true })
11
+ }
12
+
13
+ export function setProjectRoot(dir: string) {
14
+ projectRoot = dir
15
+ return projectRoot
16
+ }
17
+
18
+ export function getProjectRoot() {
19
+ if (!projectRoot) {
20
+ throw new Error('Project root not set')
21
+ }
22
+ return projectRoot
23
+ }
24
+
25
+ export function tryGetProjectRoot() {
26
+ return projectRoot
27
+ }
28
+
29
+ export function getCurrentChatId() {
30
+ if (!currentChatId) {
31
+ currentChatId = new Date().toISOString().replace(/:/g, '-')
32
+ }
33
+ return currentChatId
34
+ }
35
+
36
+ export function setCurrentChatId(chatId: string) {
37
+ currentChatId = chatId
38
+ return currentChatId
39
+ }
40
+
41
+ export function startNewChat() {
42
+ currentChatId = new Date().toISOString().replace(/:/g, '-')
43
+ return currentChatId
44
+ }
45
+
46
+ // Get the project-specific data directory
47
+ export function getProjectDataDir(): string {
48
+ const root = getProjectRoot()
49
+ if (!root) {
50
+ throw new Error('Project root not set')
51
+ }
52
+
53
+ const baseName = path.basename(root)
54
+ const baseDir = path.join(getConfigDir(), 'projects', baseName)
55
+
56
+ return baseDir
57
+ }
58
+
59
+ /**
60
+ * Find the most recent chat directory based on modification time
61
+ * Returns null if no chat directories exist
62
+ */
63
+ export function getMostRecentChatDir(): string | null {
64
+ try {
65
+ const chatsDir = path.join(getProjectDataDir(), 'chats')
66
+ if (!statSync(chatsDir, { throwIfNoEntry: false })) {
67
+ return null
68
+ }
69
+
70
+ const chatDirs = readdirSync(chatsDir)
71
+ .map((name) => {
72
+ const fullPath = path.join(chatsDir, name)
73
+ try {
74
+ const stat = statSync(fullPath)
75
+ return { name, fullPath, mtime: stat.mtime }
76
+ } catch {
77
+ return null
78
+ }
79
+ })
80
+ .filter(
81
+ (item): item is { name: string; fullPath: string; mtime: Date } =>
82
+ item !== null && statSync(item.fullPath).isDirectory(),
83
+ )
84
+
85
+ if (chatDirs.length === 0) {
86
+ return null
87
+ }
88
+
89
+ // Sort by modification time, most recent first
90
+ chatDirs.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
91
+ return chatDirs[0].fullPath
92
+ } catch {
93
+ return null
94
+ }
95
+ }
96
+
97
+ export function getCurrentChatDir(): string {
98
+ const chatId = getCurrentChatId()
99
+ const dir = path.join(getProjectDataDir(), 'chats', chatId)
100
+ ensureChatDirectory(dir)
101
+ return dir
102
+ }
@@ -0,0 +1,439 @@
1
+ import { runTerminalCommand } from '@anvil/sdk'
2
+ import { createCliRenderer } from '@opentui/core'
3
+ import { existsSync, mkdirSync, writeFileSync } from 'fs'
4
+ import path from 'path'
5
+
6
+ import { installProcessCleanupHandlers } from '../utils/renderer-cleanup'
7
+ import { startTerminalWatchdog } from '../utils/terminal-watchdog'
8
+ import {
9
+ createTerminalCommandBroker,
10
+ terminalCommandBroker,
11
+ } from '../utils/terminal-command-broker'
12
+ import { installTerminalProtocolController } from '../utils/terminal-protocol-controller'
13
+ import { writeTerminalControlSync } from '../utils/terminal-io'
14
+
15
+ import type { CliRenderer } from '@opentui/core'
16
+ import type { AnvilToolOutput } from '@anvil/sdk'
17
+
18
+ const WAIT_INTERVAL_MS = 25
19
+
20
+ type TerminalResult =
21
+ AnvilToolOutput<'run_terminal_command'>[number]['value']
22
+
23
+ type SmokeResult = {
24
+ ok: boolean
25
+ platform: NodeJS.Platform
26
+ stdinIsTTY: boolean
27
+ stdoutIsTTY: boolean
28
+ simpleCommand?: { stdout: string; stderr: string; exitCode: number | null }
29
+ repeatedCommands?: { completed: number }
30
+ overlap?: {
31
+ focusStates: boolean[]
32
+ controlWrites: string[]
33
+ mouseStayedEnabledDuringOverlap: boolean
34
+ mouseStayedEnabledAfterCancellation: boolean
35
+ mouseStayedEnabledAfterCompletion: boolean
36
+ firstCancellationMessage: string
37
+ secondStdout: string
38
+ consoleReaderStdout: string
39
+ }
40
+ brokerFailure?: { message: string; commandStarted: boolean }
41
+ error?: string
42
+ stack?: string
43
+ }
44
+
45
+ function assertSmoke(condition: unknown, message: string): asserts condition {
46
+ if (!condition) throw new Error(message)
47
+ }
48
+
49
+ function shellQuote(value: string): string {
50
+ return `'${value.replace(/'/g, `'\\''`)}'`
51
+ }
52
+
53
+ function toBashPath(value: string): string {
54
+ return value.replace(/\\/g, '/')
55
+ }
56
+
57
+ function asTerminalResult(
58
+ output: AnvilToolOutput<'run_terminal_command'>,
59
+ ): TerminalResult {
60
+ assertSmoke(output.length === 1, 'terminal command returned no result')
61
+ return output[0].value
62
+ }
63
+
64
+ function readString(value: TerminalResult, key: 'stdout' | 'stderr'): string {
65
+ if (key === 'stdout') {
66
+ return 'stdout' in value && typeof value.stdout === 'string'
67
+ ? value.stdout
68
+ : ''
69
+ }
70
+ return 'stderr' in value && typeof value.stderr === 'string'
71
+ ? value.stderr
72
+ : ''
73
+ }
74
+
75
+ function readExitCode(value: TerminalResult): number | null {
76
+ return 'exitCode' in value && typeof value.exitCode === 'number'
77
+ ? value.exitCode
78
+ : null
79
+ }
80
+
81
+ function readMessage(value: TerminalResult): string {
82
+ return 'message' in value && typeof value.message === 'string'
83
+ ? value.message
84
+ : ''
85
+ }
86
+
87
+ async function waitFor(
88
+ condition: () => boolean,
89
+ timeoutMs: number,
90
+ failureMessage: string,
91
+ ): Promise<void> {
92
+ const deadline = Date.now() + timeoutMs
93
+ while (!condition()) {
94
+ if (Date.now() >= deadline) throw new Error(failureMessage)
95
+ await Bun.sleep(WAIT_INTERVAL_MS)
96
+ }
97
+ }
98
+
99
+ function writeResult(resultPath: string, result: SmokeResult): void {
100
+ mkdirSync(path.dirname(resultPath), { recursive: true })
101
+ writeFileSync(resultPath, JSON.stringify(result, null, 2))
102
+ }
103
+
104
+ async function destroyRenderer(renderer: CliRenderer): Promise<void> {
105
+ if (renderer.isDestroyed) return
106
+ await new Promise<void>((resolve, reject) => {
107
+ const onDestroy = () => {
108
+ clearTimeout(timeout)
109
+ resolve()
110
+ }
111
+ const timeout = setTimeout(() => {
112
+ renderer.removeListener('destroy', onDestroy)
113
+ renderer.isDestroyed
114
+ ? resolve()
115
+ : reject(new Error('OpenTUI renderer did not finish destroying'))
116
+ }, 2_000)
117
+ renderer.once('destroy', onDestroy)
118
+ renderer.destroy()
119
+ })
120
+ }
121
+
122
+ /**
123
+ * `markerPath` is written before the CONIN$ probe so the harness knows the descendant is alive and
124
+ * can start injecting terminal reports for a leaked console to pick up. It cannot also stand for
125
+ * "the probe finished" — it is written while the outcome is still unknown.
126
+ *
127
+ * `verdictPath` is written once the probe has resolved, down both branches, and is what makes the
128
+ * abort deterministic. Waiting a fixed interval after the ready marker instead raced PowerShell's
129
+ * startup on a loaded runner and aborted before the verdict reached stdout.
130
+ *
131
+ * The flush is load-bearing: the broker force-kills on Windows (`taskkill /f`), so a line still
132
+ * sitting in PowerShell's own buffer is lost outright rather than drained from the pipe.
133
+ */
134
+ function createConsoleReaderScript(
135
+ markerPath: string,
136
+ verdictPath: string,
137
+ ): string {
138
+ const quotedMarker = markerPath.replace(/'/g, "''")
139
+ const quotedVerdict = verdictPath.replace(/'/g, "''")
140
+ return [
141
+ `$ErrorActionPreference = 'Stop'`,
142
+ `[System.IO.File]::WriteAllText('${quotedMarker}', 'ready')`,
143
+ `try {`,
144
+ ` $stream = [System.IO.File]::Open('CONIN$', [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)`,
145
+ ` $buffer = New-Object byte[] 4096`,
146
+ ` $count = $stream.Read($buffer, 0, $buffer.Length)`,
147
+ ` if ($count -gt 0) {`,
148
+ ` $hex = [System.BitConverter]::ToString($buffer, 0, $count).Replace('-', '')`,
149
+ ` [Console]::Out.WriteLine('CONSOLE_LEAK_HEX:' + $hex)`,
150
+ ` }`,
151
+ `} catch {`,
152
+ ` [Console]::Out.WriteLine('CONSOLE_UNAVAILABLE')`,
153
+ `}`,
154
+ `[Console]::Out.Flush()`,
155
+ `[System.IO.File]::WriteAllText('${quotedVerdict}', 'done')`,
156
+ `Start-Sleep -Seconds 20`,
157
+ ].join('\r\n')
158
+ }
159
+
160
+ export async function runPackagedTerminalBrokerSmoke({
161
+ resultPath,
162
+ exchangeDir,
163
+ }: {
164
+ resultPath: string
165
+ exchangeDir: string
166
+ }): Promise<number> {
167
+ const result: SmokeResult = {
168
+ ok: false,
169
+ platform: process.platform,
170
+ stdinIsTTY: Boolean(process.stdin.isTTY),
171
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
172
+ }
173
+ let renderer: CliRenderer | null = null
174
+
175
+ try {
176
+ assertSmoke(process.platform === 'win32', 'broker smoke requires Windows')
177
+ assertSmoke(
178
+ process.stdin.isTTY && process.stdout.isTTY,
179
+ 'broker smoke requires a native Windows console',
180
+ )
181
+
182
+ mkdirSync(exchangeDir, { recursive: true })
183
+ const harnessReadyPath = path.join(exchangeDir, 'broker-ready')
184
+ const reportsSentPath = path.join(exchangeDir, 'reports-sent')
185
+ const consoleReaderReadyPath = path.join(
186
+ exchangeDir,
187
+ 'console-reader-ready',
188
+ )
189
+ const consoleReaderVerdictPath = path.join(
190
+ exchangeDir,
191
+ 'console-reader-verdict',
192
+ )
193
+ const consoleReaderScriptPath = path.join(exchangeDir, 'console-reader.ps1')
194
+ const forbiddenSpawnPath = path.join(exchangeDir, 'broker-failure-spawned')
195
+ writeFileSync(
196
+ consoleReaderScriptPath,
197
+ createConsoleReaderScript(
198
+ consoleReaderReadyPath,
199
+ consoleReaderVerdictPath,
200
+ ),
201
+ )
202
+
203
+ startTerminalWatchdog()
204
+ renderer = await createCliRenderer({
205
+ backgroundColor: 'transparent',
206
+ exitOnCtrlC: false,
207
+ screenMode: 'alternate-screen',
208
+ })
209
+ installProcessCleanupHandlers(renderer)
210
+
211
+ const controlWrites: string[] = []
212
+ const controller = installTerminalProtocolController(renderer, {
213
+ writeControl: (sequence) => {
214
+ controlWrites.push(sequence)
215
+ return writeTerminalControlSync(sequence)
216
+ },
217
+ })
218
+ renderer.once('destroy', () => controller.dispose())
219
+ const focusStates: boolean[] = []
220
+ const unsubscribeFocus = controller.subscribeToFocus({
221
+ onFocusChange: (focused) => focusStates.push(focused),
222
+ })
223
+
224
+ const simple = asTerminalResult(
225
+ await runTerminalCommand({
226
+ command: `printf 'COMMAND_OK'; printf 'COMMAND_ERR' >&2`,
227
+ process_type: 'SYNC',
228
+ cwd: process.cwd(),
229
+ timeout_seconds: 10,
230
+ terminalCommandBroker,
231
+ }),
232
+ )
233
+ const simpleStdout = readString(simple, 'stdout')
234
+ const simpleStderr = readString(simple, 'stderr')
235
+ const simpleExitCode = readExitCode(simple)
236
+ assertSmoke(simpleStdout === 'COMMAND_OK', 'simple command stdout was lost')
237
+ assertSmoke(
238
+ simpleStderr === 'COMMAND_ERR',
239
+ 'simple command stderr was lost',
240
+ )
241
+ assertSmoke(
242
+ simpleExitCode === 0,
243
+ 'simple command did not exit successfully',
244
+ )
245
+ result.simpleCommand = {
246
+ stdout: simpleStdout,
247
+ stderr: simpleStderr,
248
+ exitCode: simpleExitCode,
249
+ }
250
+
251
+ const overlapWriteStart = controlWrites.length
252
+ const firstAbort = new AbortController()
253
+ const readerCommand =
254
+ `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ` +
255
+ shellQuote(toBashPath(consoleReaderScriptPath))
256
+ const firstRun = runTerminalCommand({
257
+ command: readerCommand,
258
+ process_type: 'SYNC',
259
+ cwd: process.cwd(),
260
+ timeout_seconds: 30,
261
+ signal: firstAbort.signal,
262
+ terminalCommandBroker,
263
+ })
264
+ const secondRun = runTerminalCommand({
265
+ command: `sleep 5; printf 'SECOND_DONE'`,
266
+ process_type: 'SYNC',
267
+ cwd: process.cwd(),
268
+ timeout_seconds: 15,
269
+ terminalCommandBroker,
270
+ })
271
+
272
+ const mouseStayedEnabledDuringOverlap = renderer.useMouse === true
273
+ assertSmoke(
274
+ mouseStayedEnabledDuringOverlap,
275
+ 'brokers changed mouse reporting',
276
+ )
277
+ assertSmoke(
278
+ controlWrites.length === overlapWriteStart,
279
+ 'brokers changed focus reporting',
280
+ )
281
+ await waitFor(
282
+ () => existsSync(consoleReaderReadyPath),
283
+ 10_000,
284
+ 'console reader descendant did not start',
285
+ )
286
+ writeFileSync(harnessReadyPath, 'ready')
287
+ await waitFor(
288
+ () => existsSync(reportsSentPath),
289
+ 10_000,
290
+ 'native Windows harness did not inject terminal reports',
291
+ )
292
+ await waitFor(
293
+ () => focusStates.includes(false) && focusStates.includes(true),
294
+ 5_000,
295
+ 'OpenTUI did not receive focus activity while commands ran',
296
+ )
297
+ // Only abort once the descendant has actually resolved its CONIN$ probe and flushed the
298
+ // verdict. This sits after the reports-sent wait deliberately: a descendant that DID inherit a
299
+ // console is blocked in $stream.Read until those reports arrive, and only then writes its leak
300
+ // line and the verdict. A descendant with no console fails File.Open immediately, so if the
301
+ // verdict never lands the open must have succeeded — which is itself the leak this asserts
302
+ // against, hence the wording below rather than a bare timeout.
303
+ // 10s to match the waits above, and because the abort has a real budget: the script exits on
304
+ // its own after Start-Sleep -Seconds 20 and firstRun times out at 30, either of which would
305
+ // turn the cancellation assertion below into a confusing failure. In practice the verdict is
306
+ // already on disk by the time this runs — a console-free open throws at once.
307
+ await waitFor(
308
+ () => existsSync(consoleReaderVerdictPath),
309
+ 10_000,
310
+ 'console reader never resolved its CONIN$ probe — an open that blocks instead of failing means the descendant did have a Windows console',
311
+ )
312
+
313
+ firstAbort.abort()
314
+ const first = asTerminalResult(await firstRun)
315
+ const firstCancellationMessage = readMessage(first)
316
+ assertSmoke(
317
+ firstCancellationMessage.includes('aborted by the user'),
318
+ 'first overlapping command did not report cancellation',
319
+ )
320
+ const mouseStayedEnabledAfterCancellation = renderer.useMouse === true
321
+ assertSmoke(
322
+ mouseStayedEnabledAfterCancellation,
323
+ 'broker cancellation changed mouse reporting',
324
+ )
325
+
326
+ const second = asTerminalResult(await secondRun)
327
+ const secondStdout = readString(second, 'stdout')
328
+ assertSmoke(secondStdout === 'SECOND_DONE', 'overlapping command was lost')
329
+ const mouseStayedEnabledAfterCompletion = renderer.useMouse === true
330
+ const overlapWrites = controlWrites.slice(overlapWriteStart)
331
+ assertSmoke(
332
+ overlapWrites.length === 0,
333
+ 'completion changed focus reporting',
334
+ )
335
+
336
+ const consoleReaderStdout = readString(first, 'stdout')
337
+ assertSmoke(
338
+ !consoleReaderStdout.includes('CONSOLE_LEAK_HEX:'),
339
+ 'a command descendant read terminal reports from CONIN$',
340
+ )
341
+ assertSmoke(
342
+ consoleReaderStdout.includes('CONSOLE_UNAVAILABLE'),
343
+ 'the command descendant still had a Windows console',
344
+ )
345
+ result.overlap = {
346
+ focusStates,
347
+ controlWrites: overlapWrites,
348
+ mouseStayedEnabledDuringOverlap,
349
+ mouseStayedEnabledAfterCancellation,
350
+ mouseStayedEnabledAfterCompletion,
351
+ firstCancellationMessage,
352
+ secondStdout,
353
+ consoleReaderStdout,
354
+ }
355
+
356
+ // The Windows failure that motivated this gate appeared only after users
357
+ // had run several agent commands. Exercise repeated packaged-broker stdio
358
+ // setup after the driver has completed its time-sensitive report injection.
359
+ const repeatedCommandCount = 64
360
+ for (let index = 0; index < repeatedCommandCount; index++) {
361
+ const marker = `REPEATED_${index}`
362
+ const repeated = asTerminalResult(
363
+ await runTerminalCommand({
364
+ command: `printf '${marker}'`,
365
+ process_type: 'SYNC',
366
+ cwd: process.cwd(),
367
+ timeout_seconds: 10,
368
+ terminalCommandBroker,
369
+ }),
370
+ )
371
+ assertSmoke(
372
+ readString(repeated, 'stdout') === marker,
373
+ `repeated broker command ${index} lost its output`,
374
+ )
375
+ assertSmoke(
376
+ readExitCode(repeated) === 0,
377
+ `repeated broker command ${index} failed`,
378
+ )
379
+ }
380
+ result.repeatedCommands = { completed: repeatedCommandCount }
381
+
382
+ let failureMessage = ''
383
+ try {
384
+ await runTerminalCommand({
385
+ command: `printf 'spawned' > ${shellQuote(toBashPath(forbiddenSpawnPath))}`,
386
+ process_type: 'SYNC',
387
+ cwd: process.cwd(),
388
+ timeout_seconds: 10,
389
+ terminalCommandBroker: createTerminalCommandBroker({
390
+ invocation: () => ({
391
+ executable: path.join(
392
+ exchangeDir,
393
+ `missing-terminal-broker-${crypto.randomUUID()}.exe`,
394
+ ),
395
+ args: [],
396
+ }),
397
+ }),
398
+ })
399
+ throw new Error('command unexpectedly started without its broker')
400
+ } catch (error) {
401
+ failureMessage = error instanceof Error ? error.message : String(error)
402
+ }
403
+ result.brokerFailure = {
404
+ message: failureMessage,
405
+ commandStarted: existsSync(forbiddenSpawnPath),
406
+ }
407
+ assertSmoke(
408
+ failureMessage.includes('Restart Freebuff'),
409
+ 'broker failure did not include actionable recovery guidance',
410
+ )
411
+ assertSmoke(
412
+ !existsSync(forbiddenSpawnPath),
413
+ 'command spawned after broker startup failed',
414
+ )
415
+
416
+ unsubscribeFocus()
417
+ controller.dispose()
418
+ await destroyRenderer(renderer)
419
+ result.ok = true
420
+ writeResult(resultPath, result)
421
+ return 0
422
+ } catch (error) {
423
+ result.error = error instanceof Error ? error.message : String(error)
424
+ if (error instanceof Error && error.stack) result.stack = error.stack
425
+ if (renderer && !renderer.isDestroyed) {
426
+ try {
427
+ await destroyRenderer(renderer)
428
+ } catch (cleanupError) {
429
+ result.error += `; cleanup failed: ${errorMessage(cleanupError)}`
430
+ }
431
+ }
432
+ writeResult(resultPath, result)
433
+ return 1
434
+ }
435
+ }
436
+
437
+ function errorMessage(error: unknown): string {
438
+ return error instanceof Error ? error.message : String(error)
439
+ }
@@ -0,0 +1,22 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+
3
+ import { useChatStore } from '../chat-store'
4
+
5
+ afterEach(() => {
6
+ const store = useChatStore.getState()
7
+ store.setInputFocused(true)
8
+ store.setIsFocusSupported(false)
9
+ })
10
+
11
+ describe('chat store focus state', () => {
12
+ test('chat reset preserves terminal focus and detected capabilities', () => {
13
+ const store = useChatStore.getState()
14
+ store.setInputFocused(false)
15
+ store.setIsFocusSupported(true)
16
+
17
+ store.reset()
18
+
19
+ expect(useChatStore.getState().inputFocused).toBe(false)
20
+ expect(useChatStore.getState().isFocusSupported).toBe(true)
21
+ })
22
+ })