@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,215 @@
1
+ import React, { useEffect, useState } from 'react'
2
+
3
+ import { Button } from './button'
4
+ import { HighlightedSubsequenceText } from './highlighted-text'
5
+ import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
6
+ import { useTheme } from '../hooks/use-theme'
7
+
8
+ export interface SuggestionItem {
9
+ id: string
10
+ label: string
11
+ labelHighlightIndices?: number[] | null
12
+ description: string
13
+ descriptionHighlightIndices?: number[] | null
14
+ }
15
+
16
+ interface SuggestionMenuProps {
17
+ items: SuggestionItem[]
18
+ selectedIndex: number
19
+ maxVisible: number
20
+ prefix?: string
21
+ onItemClick?: (index: number) => void
22
+ /** Muted hint line rendered below the suggestions */
23
+ footer?: string
24
+ }
25
+
26
+ export const SuggestionMenu = ({
27
+ items,
28
+ selectedIndex,
29
+ maxVisible,
30
+ prefix = '/',
31
+ onItemClick,
32
+ footer,
33
+ }: SuggestionMenuProps) => {
34
+ const theme = useTheme()
35
+ const { terminalWidth } = useTerminalDimensions()
36
+ const screenPadding = 4
37
+ const menuWidth = Math.max(10, terminalWidth - screenPadding * 2)
38
+
39
+ // Hover state: only highlight on hover after user has moved mouse
40
+ const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
41
+ const [hasHoveredSinceOpen, setHasHoveredSinceOpen] = useState(false)
42
+
43
+ // Reset hover state when items change (new menu session)
44
+ useEffect(() => {
45
+ setHasHoveredSinceOpen(false)
46
+ setHoveredIndex(null)
47
+ }, [items])
48
+
49
+ if (items.length === 0) {
50
+ return null
51
+ }
52
+
53
+ const effectivePrefix = prefix ?? ''
54
+
55
+ const clampedSelected = Math.min(
56
+ Math.max(selectedIndex, 0),
57
+ Math.max(items.length - 1, 0),
58
+ )
59
+ const visibleCount = Math.min(Math.max(maxVisible, 1), items.length)
60
+
61
+ const maxStart = Math.max(items.length - visibleCount, 0)
62
+ const idealStart = clampedSelected - Math.floor((visibleCount - 1) / 2)
63
+ const start = Math.max(0, Math.min(idealStart, maxStart))
64
+ const visibleItems = items.slice(start, start + visibleCount)
65
+
66
+ // Calculate max label length for alignment
67
+ const maxLabelLength = Math.max(
68
+ ...visibleItems.map(
69
+ (item) => effectivePrefix.length + item.label.length,
70
+ ),
71
+ )
72
+
73
+ // Find the longest description to determine if we can use same-line layout
74
+ const maxDescriptionLength = Math.max(
75
+ ...visibleItems.map((item) => item.description.length),
76
+ )
77
+
78
+ // Check if all items can fit on same line with aligned descriptions
79
+ const minWidthForSameLine = maxLabelLength + 2 + maxDescriptionLength
80
+ const useSameLine = menuWidth >= minWidthForSameLine
81
+
82
+ const renderSuggestionItem = (item: SuggestionItem, idx: number) => {
83
+ const absoluteIndex = start + idx
84
+ const isSelected = absoluteIndex === clampedSelected
85
+ const isHovered = hasHoveredSinceOpen && absoluteIndex === hoveredIndex
86
+ const isHighlighted = isSelected || isHovered
87
+ const labelLength = effectivePrefix.length + item.label.length
88
+ const textColor = isHighlighted ? theme.foreground : theme.inputFg
89
+ const descriptionColor = isHighlighted ? theme.foreground : theme.muted
90
+ const highlightColor = theme.primary
91
+
92
+ const handleClick = onItemClick ? () => onItemClick(absoluteIndex) : undefined
93
+ const handleMouseOver = () => {
94
+ setHoveredIndex(absoluteIndex)
95
+ setHasHoveredSinceOpen(true)
96
+ }
97
+
98
+ if (useSameLine) {
99
+ // Calculate padding to align descriptions
100
+ const paddingLength = maxLabelLength - labelLength
101
+ const padding = ' '.repeat(paddingLength)
102
+ // Wide terminal: description on same line with 2-space gap
103
+ return (
104
+ <Button
105
+ key={item.id}
106
+ onClick={handleClick}
107
+ onMouseOver={handleMouseOver}
108
+ style={{
109
+ flexDirection: 'column',
110
+ gap: 0,
111
+ paddingLeft: 1,
112
+ paddingRight: 1,
113
+ paddingTop: 0,
114
+ paddingBottom: 0,
115
+ backgroundColor: isHighlighted ? theme.surfaceHover : theme.background,
116
+ width: '100%',
117
+ }}
118
+ >
119
+ <text
120
+ style={{
121
+ fg: textColor,
122
+ marginBottom: 0,
123
+ }}
124
+ >
125
+ <span fg={theme.primary}>{effectivePrefix}</span>
126
+ <HighlightedSubsequenceText
127
+ text={item.label}
128
+ indices={item.labelHighlightIndices}
129
+ color={textColor}
130
+ highlightColor={highlightColor}
131
+ />
132
+ <span>{padding} </span>
133
+ <HighlightedSubsequenceText
134
+ text={item.description}
135
+ indices={item.descriptionHighlightIndices}
136
+ color={descriptionColor}
137
+ highlightColor={highlightColor}
138
+ />
139
+ </text>
140
+ </Button>
141
+ )
142
+ } else {
143
+ // Narrow terminal: description on next line
144
+ return (
145
+ <Button
146
+ key={item.id}
147
+ onClick={handleClick}
148
+ onMouseOver={handleMouseOver}
149
+ style={{
150
+ flexDirection: 'column',
151
+ gap: 0,
152
+ paddingLeft: 1,
153
+ paddingRight: 1,
154
+ paddingTop: 0,
155
+ paddingBottom: 0,
156
+ backgroundColor: isHighlighted ? theme.surfaceHover : theme.background,
157
+ width: '100%',
158
+ }}
159
+ >
160
+ <text
161
+ style={{
162
+ fg: textColor,
163
+ marginBottom: 0,
164
+ }}
165
+ >
166
+ <span fg={theme.primary}>{effectivePrefix}</span>
167
+ <HighlightedSubsequenceText
168
+ text={item.label}
169
+ indices={item.labelHighlightIndices}
170
+ color={textColor}
171
+ highlightColor={highlightColor}
172
+ />
173
+ </text>
174
+ <text
175
+ style={{
176
+ fg: descriptionColor,
177
+ marginBottom: 0,
178
+ marginLeft: 2,
179
+ }}
180
+ >
181
+ <HighlightedSubsequenceText
182
+ text={item.description}
183
+ indices={item.descriptionHighlightIndices}
184
+ color={descriptionColor}
185
+ highlightColor={highlightColor}
186
+ />
187
+ </text>
188
+ </Button>
189
+ )
190
+ }
191
+ }
192
+
193
+ return (
194
+ <box
195
+ style={{
196
+ flexDirection: 'column',
197
+ gap: 0,
198
+ paddingLeft: 0,
199
+ paddingRight: 0,
200
+ paddingTop: 0,
201
+ paddingBottom: 0,
202
+ backgroundColor: theme.surface,
203
+ width: '100%',
204
+ }}
205
+ onMouseOut={() => setHoveredIndex(null)}
206
+ >
207
+ {visibleItems.map(renderSuggestionItem)}
208
+ {footer ? (
209
+ <box style={{ paddingLeft: 1, paddingRight: 1 }}>
210
+ <text style={{ fg: theme.muted, wrapMode: 'word' }}>{footer}</text>
211
+ </box>
212
+ ) : null}
213
+ </box>
214
+ )
215
+ }
@@ -0,0 +1,151 @@
1
+ import { TextAttributes } from '@opentui/core'
2
+ import { useState } from 'react'
3
+
4
+ import { Button } from './button'
5
+ import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
6
+ import { useTheme } from '../hooks/use-theme'
7
+ import { formatTimeout } from '../utils/format-timeout'
8
+ import { getLastNVisualLines } from '../utils/text-layout'
9
+
10
+ interface TerminalCommandDisplayProps {
11
+ command: string
12
+ output: string | null
13
+ /** Whether to show an expandable "Show more" button for long output */
14
+ expandable?: boolean
15
+ /** Max lines to show before truncation (default 5 for expandable, 10 for non-expandable) */
16
+ maxVisibleLines?: number
17
+ /** Whether command is still running */
18
+ isRunning?: boolean
19
+ /** Working directory where the command was run */
20
+ cwd?: string
21
+ /** Timeout in seconds for the command */
22
+ timeoutSeconds?: number
23
+ /** Optional width override for wrapping calculations */
24
+ availableWidth?: number
25
+ }
26
+
27
+ /**
28
+ * Shared component for displaying terminal command with output.
29
+ * Used in both the ghost message (pending bash) and message history.
30
+ */
31
+ export const TerminalCommandDisplay = ({
32
+ command,
33
+ output,
34
+ expandable = true,
35
+ maxVisibleLines,
36
+ isRunning = false,
37
+ timeoutSeconds,
38
+ availableWidth,
39
+ }: TerminalCommandDisplayProps) => {
40
+ const theme = useTheme()
41
+ const { separatorWidth } = useTerminalDimensions()
42
+ const [isExpanded, setIsExpanded] = useState(false)
43
+
44
+ // Default max lines depends on whether expandable
45
+ const defaultMaxLines = expandable ? 5 : 10
46
+ const maxLines = maxVisibleLines ?? defaultMaxLines
47
+
48
+ // Format timeout display - show when provided and not the default (30s)
49
+ const DEFAULT_TIMEOUT_SECONDS = 30
50
+ const timeoutLabel =
51
+ timeoutSeconds !== undefined && timeoutSeconds !== DEFAULT_TIMEOUT_SECONDS
52
+ ? formatTimeout(timeoutSeconds)
53
+ : null
54
+
55
+ // Command header - shared between output and no-output cases
56
+ const commandHeader = (
57
+ <text style={{ wrapMode: 'word' }}>
58
+ <span fg={theme.success}>$ </span>
59
+ <span fg={theme.foreground} attributes={TextAttributes.BOLD}>
60
+ {command}
61
+ </span>
62
+ {timeoutLabel && (
63
+ <span fg={theme.muted} attributes={TextAttributes.DIM}>
64
+ {' '}({timeoutLabel})
65
+ </span>
66
+ )}
67
+ </text>
68
+ )
69
+
70
+ // No output case
71
+ if (!output) {
72
+ return (
73
+ <box style={{ flexDirection: 'column', gap: 0, width: '100%' }}>
74
+ {commandHeader}
75
+ {/* Running indicator */}
76
+ {isRunning && <text fg={theme.muted}>...</text>}
77
+ </box>
78
+ )
79
+ }
80
+
81
+ // With output - calculate visual lines
82
+ const width = Math.max(10, availableWidth ?? separatorWidth)
83
+ const allLines = output.split('\n')
84
+
85
+ // Calculate total visual lines across all output lines
86
+ let totalVisualLines = 0
87
+ const visualLinesByOriginalLine: string[][] = []
88
+
89
+ for (const line of allLines) {
90
+ const { lines: wrappedLines } = getLastNVisualLines(line, width, Infinity)
91
+ visualLinesByOriginalLine.push(wrappedLines)
92
+ totalVisualLines += wrappedLines.length
93
+ }
94
+
95
+ const hasMoreLines = totalVisualLines > maxLines
96
+ const hiddenLinesCount = totalVisualLines - maxLines
97
+
98
+ // Build display output
99
+ let displayOutput: string
100
+ if (isExpanded || !hasMoreLines) {
101
+ displayOutput = output
102
+ } else {
103
+ // Take first N visual lines
104
+ const displayLines: string[] = []
105
+ let count = 0
106
+
107
+ for (const wrappedLines of visualLinesByOriginalLine) {
108
+ for (const line of wrappedLines) {
109
+ if (count >= maxLines) break
110
+ displayLines.push(line)
111
+ count++
112
+ }
113
+ if (count >= maxLines) break
114
+ }
115
+
116
+ displayOutput = displayLines.join('\n')
117
+ }
118
+
119
+ return (
120
+ <box style={{ flexDirection: 'column', gap: 0, width: '100%' }}>
121
+ {commandHeader}
122
+ {/* Output */}
123
+ <box style={{ flexDirection: 'column', gap: 0, width: '100%' }}>
124
+ {hasMoreLines && !expandable && (
125
+ <text fg={theme.muted} attributes={TextAttributes.DIM}>
126
+ ... ({hiddenLinesCount} more lines above)
127
+ </text>
128
+ )}
129
+ <text fg={theme.muted} style={{ wrapMode: 'word' }}>
130
+ {displayOutput}
131
+ </text>
132
+ {hasMoreLines && expandable && (
133
+ <Button
134
+ style={{ marginTop: 0 }}
135
+ onClick={() => setIsExpanded(!isExpanded)}
136
+ >
137
+ <text
138
+ fg={theme.secondary}
139
+ style={{ wrapMode: 'word' }}
140
+ attributes={TextAttributes.UNDERLINE}
141
+ >
142
+ {isExpanded
143
+ ? 'Show less'
144
+ : `Show ${hiddenLinesCount} more ${hiddenLinesCount === 1 ? 'line' : 'lines'}`}
145
+ </text>
146
+ </Button>
147
+ )}
148
+ </box>
149
+ </box>
150
+ )
151
+ }
@@ -0,0 +1,97 @@
1
+ import React, { useCallback, useMemo, useState } from 'react'
2
+
3
+ import { Button } from './button'
4
+ import { useTheme } from '../hooks/use-theme'
5
+
6
+ type FormatLinesFn = (text: string, maxWidth?: number) => string[]
7
+
8
+ export interface TerminalLinkProps {
9
+ text: string
10
+ maxWidth?: number
11
+ formatLines?: FormatLinesFn
12
+ color?: string
13
+ activeColor?: string
14
+ underlineOnHover?: boolean
15
+ isActive?: boolean
16
+ onActivate?: () => void | Promise<any>
17
+ containerStyle?: Record<string, unknown>
18
+ lineWrap?: boolean
19
+ inline?: boolean
20
+ }
21
+
22
+ const defaultFormatLines: FormatLinesFn = (text) => [text]
23
+ const PREFIXES_TO_STRIP = ['https://', 'http://']
24
+
25
+ export const TerminalLink: React.FC<TerminalLinkProps> = ({
26
+ text,
27
+ maxWidth,
28
+ formatLines = defaultFormatLines,
29
+ color,
30
+ activeColor,
31
+ underlineOnHover = true,
32
+ isActive = false,
33
+ onActivate,
34
+ containerStyle,
35
+ lineWrap = false,
36
+ inline = false,
37
+ }) => {
38
+ const theme = useTheme()
39
+
40
+ // Use theme colors as defaults if not provided
41
+ const linkColor = color ?? theme.link
42
+ const linkActiveColor = activeColor ?? theme.success
43
+ const [isHovered, setIsHovered] = useState(false)
44
+
45
+ const displayLines = useMemo(() => {
46
+ let displayText = text.trim()
47
+ for (const prefix of PREFIXES_TO_STRIP) {
48
+ if (displayText.startsWith(prefix)) {
49
+ displayText = displayText.slice(prefix.length)
50
+ }
51
+ }
52
+
53
+ const formatted = formatLines(displayText, maxWidth)
54
+ if (formatted.length <= 1) {
55
+ return formatted
56
+ }
57
+ return formatted.filter((line) => line.trim().length > 0)
58
+ }, [formatLines, maxWidth, text])
59
+
60
+ const displayColor = isActive ? linkActiveColor : linkColor
61
+ const shouldUnderline = underlineOnHover && isHovered
62
+
63
+ const handleActivate = useCallback(() => {
64
+ if (onActivate) {
65
+ onActivate()
66
+ }
67
+ }, [onActivate])
68
+
69
+ // For inline mode, render without hover/click support (spans don't support mouse events)
70
+ if (inline) {
71
+ return <span fg={displayColor}>{text}</span>
72
+ }
73
+
74
+ return (
75
+ <Button
76
+ style={{
77
+ flexDirection: 'column',
78
+ alignItems: 'flex-start',
79
+ width: '100%',
80
+ gap: 0,
81
+ ...containerStyle,
82
+ }}
83
+ onMouseOver={() => setIsHovered(true)}
84
+ onMouseOut={() => setIsHovered(false)}
85
+ onClick={handleActivate}
86
+ >
87
+ {displayLines.map((line: string, index: number) => {
88
+ const coloredText = <span fg={displayColor}>{line}</span>
89
+ return (
90
+ <text key={index} style={{ wrapMode: lineWrap ? 'word' : 'none' }}>
91
+ {shouldUnderline ? <u>{coloredText}</u> : coloredText}
92
+ </text>
93
+ )
94
+ })}
95
+ </Button>
96
+ )
97
+ }
@@ -0,0 +1,95 @@
1
+ import { TextAttributes } from '@opentui/core'
2
+ import React, { useEffect, useState } from 'react'
3
+
4
+ import { useTheme } from '../hooks/use-theme'
5
+
6
+ /**
7
+ * A terminal-native take on the `thinking-orbs` aesthetic: a small dotted
8
+ * "thought orb" built from Unicode particles, animated in place.
9
+ *
10
+ * The real `thinking-orbs` package renders a web `<canvas>` and cannot paint
11
+ * inside the OpenTUI terminal renderer (no DOM), so this component recreates
12
+ * the same visual language — sparse orbiting dots on a transparent field —
13
+ * with text cells. Each frame advances a few dots around a 2×2 mini-orb, and
14
+ * the dot count/look changes with the state, mirroring the npm package's
15
+ * nine-verb idea:
16
+ *
17
+ * - waiting: two dots, slow orbit (the model is thinking)
18
+ * - working: three dots, steady orbit (it is doing the work)
19
+ * - searching: one sweeping dot (scanning)
20
+ * - connecting: dots blink in (wiring up)
21
+ * - retrying: a single pulsing dot (hiccup)
22
+ *
23
+ * With `variant="spark"` it swaps to a rotating spark glyph — ✻ → ✺ → ✹ → ✸ —
24
+ * the same "worked for 39s" rotating marker used by Command Code's loading
25
+ * states, rendered as a single crisp cell.
26
+ */
27
+
28
+ type OrbState =
29
+ | 'waiting'
30
+ | 'working'
31
+ | 'searching'
32
+ | 'connecting'
33
+ | 'retrying'
34
+
35
+ const ORB_FRAMES: Record<string, string[]> = {
36
+ // Braille 2×4 dot cells read as a tiny orbiting particle field.
37
+ waiting: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'],
38
+ working: ['⠁', '⠉', '⠙', '⠸', '⠼', '⠴', '⠦', '⠇'],
39
+ searching: ['⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'],
40
+ connecting: ['⡀', '⠄', '⠂', '⠁', '⠉', '⠘', '⠨', '⢀'],
41
+ retrying: ['⠄', '⡆', '⣶', '⣿', '⣶', '⡆', '⠄', '⠂'],
42
+ }
43
+
44
+ // Rotating spark glyphs — the Command Code-style rotating marker.
45
+ const SPARK_FRAMES = ['✻', '✺', '✹', '✸', '✹', '✺']
46
+
47
+ const ORB_INTERVAL_MS: Record<OrbState, number> = {
48
+ waiting: 140,
49
+ working: 110,
50
+ searching: 130,
51
+ connecting: 150,
52
+ retrying: 180,
53
+ }
54
+
55
+ interface TerminalThinkingOrbProps {
56
+ /** Which animation to show. @default 'working' */
57
+ state?: OrbState
58
+ /** Spark (✻ rotation) or dotted braille particle field. @default 'dots' */
59
+ variant?: 'dots' | 'spark'
60
+ /** Animation speed multiplier on top of the state's baked speed. @default 1 */
61
+ speed?: number
62
+ /** Color override; defaults to theme.primary (violet). */
63
+ color?: string
64
+ }
65
+
66
+ export const TerminalThinkingOrb = ({
67
+ state = 'working',
68
+ variant = 'dots',
69
+ speed = 1,
70
+ color,
71
+ }: TerminalThinkingOrbProps) => {
72
+ const theme = useTheme()
73
+ const [frame, setFrame] = useState(0)
74
+
75
+ const frames =
76
+ variant === 'spark' ? SPARK_FRAMES : (ORB_FRAMES[state] ?? ORB_FRAMES.working)
77
+ const interval = Math.max(
78
+ 40,
79
+ (variant === 'spark' ? 140 : ORB_INTERVAL_MS[state] ?? 110) / speed,
80
+ )
81
+
82
+ useEffect(() => {
83
+ const id = setInterval(() => {
84
+ setFrame((prev) => (prev + 1) % frames.length)
85
+ }, interval)
86
+ return () => clearInterval(id)
87
+ }, [interval, frames.length])
88
+
89
+ return (
90
+ <span fg={color ?? theme.primary} attributes={TextAttributes.BOLD}>
91
+ {frames[frame]}
92
+ </span>
93
+ )
94
+ }
95
+
@@ -0,0 +1,77 @@
1
+ import { AttachmentCard } from './attachment-card'
2
+ import { useTheme } from '../hooks/use-theme'
3
+
4
+ import type { PendingTextAttachment } from '../types/store'
5
+
6
+ const TEXT_CARD_WIDTH = 24
7
+ const MAX_PREVIEW_LINES = 2
8
+ // Available width for text: card width - 2 (border) - 2 (padding) = 20 chars per line
9
+ const TEXT_CONTENT_WIDTH = TEXT_CARD_WIDTH - 4
10
+
11
+ interface TextAttachmentCardProps {
12
+ attachment: PendingTextAttachment | { preview: string; charCount: number }
13
+ onRemove?: () => void
14
+ showRemoveButton?: boolean
15
+ }
16
+
17
+ export const TextAttachmentCard = ({
18
+ attachment,
19
+ onRemove,
20
+ showRemoveButton = true,
21
+ }: TextAttachmentCardProps) => {
22
+ const theme = useTheme()
23
+
24
+ // Preview is already processed (newlines replaced with spaces)
25
+ // Show as much as fits in the available space (width × lines)
26
+ const maxPreviewChars = TEXT_CONTENT_WIDTH * MAX_PREVIEW_LINES
27
+ const displayPreview =
28
+ attachment.preview.slice(0, maxPreviewChars) +
29
+ (attachment.preview.length > maxPreviewChars ? '…' : '')
30
+
31
+ return (
32
+ <AttachmentCard
33
+ width={TEXT_CARD_WIDTH}
34
+ onRemove={onRemove}
35
+ showRemoveButton={showRemoveButton}
36
+ >
37
+ {/* Preview area */}
38
+ <box
39
+ style={{
40
+ paddingLeft: 1,
41
+ paddingRight: 1,
42
+ height: 3,
43
+ justifyContent: 'center',
44
+ }}
45
+ >
46
+ <text
47
+ style={{
48
+ fg: theme.foreground,
49
+ wrapMode: 'word',
50
+ }}
51
+ >
52
+ {displayPreview || '(empty)'}
53
+ </text>
54
+ </box>
55
+
56
+ {/* Footer with icon and char count */}
57
+ <box
58
+ style={{
59
+ paddingLeft: 1,
60
+ paddingRight: 1,
61
+ flexDirection: 'row',
62
+ gap: 1,
63
+ }}
64
+ >
65
+ <text style={{ fg: theme.info }}>📄</text>
66
+ <text
67
+ style={{
68
+ fg: theme.muted,
69
+ wrapMode: 'none',
70
+ }}
71
+ >
72
+ {attachment.charCount.toLocaleString()} chars
73
+ </text>
74
+ </box>
75
+ </AttachmentCard>
76
+ )
77
+ }