@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,313 @@
1
+ import { getFreebuffModel } from '@anvil/common/constants/freebuff-models'
2
+ import { TextAttributes } from '@opentui/core'
3
+ import React, { useEffect, useState } from 'react'
4
+
5
+ import { Button } from './button'
6
+ import { ScrollToBottomButton } from './scroll-to-bottom-button'
7
+ import { ShimmerText } from './shimmer-text'
8
+ import { TerminalThinkingOrb } from './terminal-thinking-orb'
9
+
10
+ import { useFreebuffSessionProgress } from '../hooks/use-freebuff-session-progress'
11
+ import { useTheme } from '../hooks/use-theme'
12
+ import { formatElapsedTime } from '../utils/format-elapsed-time'
13
+ import {
14
+ FREEBUFF_COUNTDOWN_VISIBLE_MS,
15
+ formatFreebuffSessionCountdown,
16
+ formatFreebuffSessionRemaining,
17
+ } from '../utils/freebuff-session-display'
18
+
19
+ import type { FreebuffSessionResponse } from '../types/freebuff-session'
20
+ import type { StatusIndicatorState } from '../utils/status-indicator-state'
21
+
22
+ /** A small status-bar action button with hover-bold styling. */
23
+ const StatusActionButton = ({
24
+ children,
25
+ onClick,
26
+ }: {
27
+ children: React.ReactNode
28
+ onClick: () => void
29
+ }) => {
30
+ const theme = useTheme()
31
+ const [hovered, setHovered] = useState(false)
32
+
33
+ return (
34
+ <Button
35
+ style={{ paddingLeft: 1, paddingRight: 1 }}
36
+ onClick={onClick}
37
+ onMouseOver={() => setHovered(true)}
38
+ onMouseOut={() => setHovered(false)}
39
+ >
40
+ <text>
41
+ <span
42
+ fg={theme.secondary}
43
+ attributes={hovered ? TextAttributes.BOLD : TextAttributes.NONE}
44
+ >
45
+ {children}
46
+ </span>
47
+ </text>
48
+ </Button>
49
+ )
50
+ }
51
+
52
+ const SHIMMER_INTERVAL_MS = 160
53
+
54
+ interface StatusBarProps {
55
+ timerStartTime: number | null
56
+ isAtBottom: boolean
57
+ scrollToLatest: () => void
58
+ statusIndicatorState: StatusIndicatorState
59
+ onStop?: () => void
60
+ onEndSession?: () => void
61
+ freebuffSession: FreebuffSessionResponse | null
62
+ }
63
+
64
+ export const StatusBar = ({
65
+ timerStartTime,
66
+ isAtBottom,
67
+ scrollToLatest,
68
+ statusIndicatorState,
69
+ onStop,
70
+ onEndSession,
71
+ freebuffSession,
72
+ }: StatusBarProps) => {
73
+ const theme = useTheme()
74
+ const [elapsedSeconds, setElapsedSeconds] = useState(0)
75
+
76
+ // Show timer when actively working (streaming or waiting for response) or paused (ask_user)
77
+ // This uses statusIndicatorState as the single source of truth for "is the LLM working?"
78
+ const shouldShowTimer =
79
+ statusIndicatorState?.kind === 'waiting' ||
80
+ statusIndicatorState?.kind === 'streaming' ||
81
+ statusIndicatorState?.kind === 'paused'
82
+
83
+ useEffect(() => {
84
+ if (!timerStartTime || !shouldShowTimer) {
85
+ setElapsedSeconds(0)
86
+ return
87
+ }
88
+
89
+ // When paused, don't update the timer - just keep the frozen value
90
+ if (statusIndicatorState?.kind === 'paused') {
91
+ // Calculate current elapsed time once and freeze it
92
+ const now = Date.now()
93
+ const elapsed = Math.floor((now - timerStartTime) / 1000)
94
+ setElapsedSeconds(elapsed)
95
+ return
96
+ }
97
+
98
+ const updateElapsed = () => {
99
+ const now = Date.now()
100
+ const elapsed = Math.floor((now - timerStartTime) / 1000)
101
+ setElapsedSeconds(elapsed)
102
+ }
103
+
104
+ updateElapsed()
105
+ const interval = setInterval(updateElapsed, 1000)
106
+
107
+ return () => clearInterval(interval)
108
+ }, [timerStartTime, shouldShowTimer, statusIndicatorState?.kind])
109
+
110
+ const sessionProgress = useFreebuffSessionProgress(freebuffSession)
111
+ const isUnlimited =
112
+ freebuffSession?.status === 'active' && !freebuffSession.rateLimit
113
+
114
+ const renderStatusIndicator = () => {
115
+ switch (statusIndicatorState.kind) {
116
+ case 'ctrlC':
117
+ return <span fg={theme.secondary}>Press Ctrl-C again to exit</span>
118
+
119
+ case 'clipboard':
120
+ // Use green color for feedback success messages
121
+ const isFeedbackSuccess =
122
+ statusIndicatorState.message.includes('Feedback sent')
123
+ return (
124
+ <span fg={isFeedbackSuccess ? theme.success : theme.primary}>
125
+ {statusIndicatorState.message}
126
+ </span>
127
+ )
128
+
129
+ case 'reconnected':
130
+ return <span fg={theme.success}>Back online</span>
131
+
132
+ case 'retrying':
133
+ return (
134
+ <span>
135
+ <TerminalThinkingOrb state="retrying" variant="spark" color={theme.warning} />{' '}
136
+ <ShimmerText text="hiccup — retrying..." primaryColor={theme.warning} />
137
+ </span>
138
+ )
139
+
140
+ case 'capacityWait':
141
+ return (
142
+ <span>
143
+ <TerminalThinkingOrb state="waiting" variant="spark" color={theme.warning} />{' '}
144
+ <ShimmerText
145
+ text="peak traffic — you're in line, starting soon..."
146
+ primaryColor={theme.warning}
147
+ />
148
+ </span>
149
+ )
150
+
151
+ case 'connecting':
152
+ return (
153
+ <span>
154
+ <TerminalThinkingOrb state="connecting" variant="spark" />{' '}
155
+ <ShimmerText text="dialing in..." />
156
+ </span>
157
+ )
158
+
159
+ case 'waiting':
160
+ return (
161
+ <span>
162
+ <TerminalThinkingOrb state="waiting" variant="spark" />{' '}
163
+ <ShimmerText
164
+ text="warming up..."
165
+ interval={SHIMMER_INTERVAL_MS}
166
+ primaryColor={theme.secondary}
167
+ />
168
+ </span>
169
+ )
170
+
171
+ case 'streaming':
172
+ return (
173
+ <span>
174
+ <TerminalThinkingOrb state="working" variant="spark" />{' '}
175
+ <ShimmerText
176
+ text="on it..."
177
+ interval={SHIMMER_INTERVAL_MS}
178
+ primaryColor={theme.secondary}
179
+ />
180
+ </span>
181
+ )
182
+
183
+ case 'paused':
184
+ return null
185
+
186
+ case 'idle':
187
+ if (sessionProgress !== null) {
188
+ const isUrgent =
189
+ sessionProgress.remainingMs < FREEBUFF_COUNTDOWN_VISIBLE_MS
190
+ const modelName =
191
+ freebuffSession?.status === 'active'
192
+ ? getFreebuffModel(freebuffSession.model).displayName
193
+ : null
194
+ return (
195
+ <span
196
+ fg={
197
+ isUnlimited
198
+ ? theme.secondary
199
+ : isUrgent
200
+ ? theme.warning
201
+ : theme.secondary
202
+ }
203
+ >
204
+ {modelName ? `${modelName} · ` : ''}
205
+ {isUnlimited
206
+ ? 'unlimited'
207
+ : formatFreebuffSessionRemaining(sessionProgress.remainingMs)}
208
+ </span>
209
+ )
210
+ }
211
+ return null
212
+ }
213
+ }
214
+
215
+ const renderElapsedTime = () => {
216
+ if (!shouldShowTimer || elapsedSeconds === 0) {
217
+ return null
218
+ }
219
+
220
+ return (
221
+ <span fg={theme.secondary}>
222
+ worked for {formatElapsedTime(elapsedSeconds)}
223
+ </span>
224
+ )
225
+ }
226
+
227
+ const statusIndicatorContent = renderStatusIndicator()
228
+ const elapsedTimeContent = renderElapsedTime()
229
+
230
+ // Show gray background when there's status indicator, timer, or when the
231
+ // freebuff session fill is visible (otherwise the fill would float over
232
+ // transparent space).
233
+ const hasContent =
234
+ statusIndicatorContent || elapsedTimeContent || sessionProgress !== null
235
+
236
+ return (
237
+ <box
238
+ style={{
239
+ width: '100%',
240
+ flexDirection: 'row',
241
+ alignItems: 'center',
242
+ paddingLeft: 1,
243
+ paddingRight: 1,
244
+ gap: 1,
245
+ backgroundColor: hasContent ? theme.surface : 'transparent',
246
+ }}
247
+ >
248
+ {sessionProgress !== null && (
249
+ <box
250
+ style={{
251
+ position: 'absolute',
252
+ left: 0,
253
+ top: 0,
254
+ bottom: 0,
255
+ // Fill anchors left and shrinks as time passes — the draining
256
+ // bar is the countdown; no separate numeric readout needed.
257
+ width: `${sessionProgress.fraction * 100}%`,
258
+ backgroundColor: theme.surfaceHover,
259
+ }}
260
+ />
261
+ )}
262
+ <box
263
+ style={{
264
+ flexGrow: 1,
265
+ flexShrink: 1,
266
+ flexBasis: 0,
267
+ }}
268
+ >
269
+ <text style={{ wrapMode: 'none' }}>{statusIndicatorContent}</text>
270
+ </box>
271
+
272
+ <box style={{ flexShrink: 0 }}>
273
+ {!isAtBottom && <ScrollToBottomButton onClick={scrollToLatest} />}
274
+ </box>
275
+
276
+ <box
277
+ style={{
278
+ flexGrow: 1,
279
+ flexShrink: 1,
280
+ flexBasis: 0,
281
+ flexDirection: 'row',
282
+ justifyContent: 'flex-end',
283
+ alignItems: 'center',
284
+ gap: 1,
285
+ }}
286
+ >
287
+ <text style={{ wrapMode: 'none' }}>{elapsedTimeContent}</text>
288
+ {onStop &&
289
+ (statusIndicatorState.kind === 'waiting' ||
290
+ statusIndicatorState.kind === 'streaming') && (
291
+ <StatusActionButton onClick={onStop}>■ Esc</StatusActionButton>
292
+ )}
293
+ {onEndSession &&
294
+ statusIndicatorState.kind === 'idle' &&
295
+ freebuffSession?.status === 'active' && (
296
+ <StatusActionButton onClick={onEndSession}>
297
+ ✕ End session
298
+ </StatusActionButton>
299
+ )}
300
+ {sessionProgress !== null &&
301
+ sessionProgress.remainingMs < FREEBUFF_COUNTDOWN_VISIBLE_MS &&
302
+ statusIndicatorState.kind !== 'idle' &&
303
+ !isUnlimited && (
304
+ <text style={{ wrapMode: 'none' }}>
305
+ <span fg={theme.warning} attributes={TextAttributes.BOLD}>
306
+ {formatFreebuffSessionCountdown(sessionProgress.remainingMs)}
307
+ </span>
308
+ </text>
309
+ )}
310
+ </box>
311
+ </box>
312
+ )
313
+ }
@@ -0,0 +1,206 @@
1
+ import { SUBSCRIPTION_TIERS } from '@anvil/common/constants/subscription-plans'
2
+ import { IS_FREEBUFF } from '../utils/constants'
3
+ import { safeOpen } from '../utils/open-url'
4
+ import React from 'react'
5
+
6
+ import { Button } from './button'
7
+ import { ProgressBar } from './progress-bar'
8
+ import { useSubscriptionQuery } from '../hooks/use-subscription-query'
9
+ import { useTheme } from '../hooks/use-theme'
10
+ import { useUpdatePreference } from '../hooks/use-update-preference'
11
+ import { useUsageQuery } from '../hooks/use-usage-query'
12
+ import { WEBSITE_URL } from '../login/constants'
13
+ import { useChatStore } from '../state/chat-store'
14
+ import { formatResetTime } from '../utils/time-format'
15
+ import { BORDER_CHARS } from '../utils/ui-constants'
16
+
17
+ export const SubscriptionLimitBanner = () => {
18
+ if (IS_FREEBUFF) return null
19
+
20
+ const setInputMode = useChatStore((state) => state.setInputMode)
21
+ const theme = useTheme()
22
+
23
+ const { data: subscriptionData } = useSubscriptionQuery({
24
+ refetchInterval: 15 * 1000,
25
+ })
26
+
27
+ const { data: usageData } = useUsageQuery({
28
+ enabled: true,
29
+ refetchInterval: 30 * 1000,
30
+ })
31
+
32
+ const rateLimit = subscriptionData?.hasSubscription ? subscriptionData.rateLimit : undefined
33
+ const remainingBalance = usageData?.remainingBalance ?? 0
34
+ const hasAlaCarteCredits = remainingBalance > 0
35
+
36
+ // Determine if user can upgrade (not on highest tier)
37
+ const maxTier = Math.max(...Object.keys(SUBSCRIPTION_TIERS).map(Number))
38
+ const currentTier = subscriptionData?.hasSubscription ? subscriptionData.subscription.tier : 0
39
+ const canUpgrade = currentTier < maxTier
40
+
41
+ const fallbackToALaCarte = subscriptionData?.fallbackToALaCarte ?? false
42
+ const updatePreference = useUpdatePreference()
43
+
44
+ const handleToggleFallbackToALaCarte = () => {
45
+ updatePreference.mutate({ fallbackToALaCarte: !fallbackToALaCarte })
46
+ }
47
+
48
+ if (!subscriptionData || !rateLimit?.limited) {
49
+ return null
50
+ }
51
+
52
+ const { reason, weeklyPercentUsed, weeklyResetsAt: weeklyResetsAtStr, blockResetsAt: blockResetsAtStr } = rateLimit
53
+ const isWeeklyLimit = reason === 'weekly_limit'
54
+ const isBlockExhausted = reason === 'block_exhausted'
55
+ const weeklyRemaining = 100 - weeklyPercentUsed
56
+ const weeklyResetsAt = weeklyResetsAtStr ? new Date(weeklyResetsAtStr) : null
57
+ const blockResetsAt = blockResetsAtStr ? new Date(blockResetsAtStr) : null
58
+
59
+ const handleContinueWithCredits = () => {
60
+ setInputMode('default')
61
+ }
62
+
63
+ const handleBuyCredits = () => {
64
+ safeOpen(WEBSITE_URL + '/usage')
65
+ }
66
+
67
+ const handleUpgrade = () => {
68
+ safeOpen(WEBSITE_URL + '/subscribe')
69
+ }
70
+
71
+ const borderColor = isWeeklyLimit ? theme.error : theme.warning
72
+
73
+ return (
74
+ <box
75
+ style={{
76
+ width: '100%',
77
+ borderStyle: 'single',
78
+ borderColor,
79
+ customBorderChars: BORDER_CHARS,
80
+ paddingLeft: 1,
81
+ paddingRight: 1,
82
+ paddingTop: 0,
83
+ paddingBottom: 0,
84
+ flexDirection: 'column',
85
+ gap: 0,
86
+ }}
87
+ >
88
+ <box
89
+ style={{
90
+ flexDirection: 'column',
91
+ justifyContent: 'center',
92
+ minHeight: 3,
93
+ gap: 0,
94
+ }}
95
+ >
96
+ {isWeeklyLimit ? (
97
+ <>
98
+ <text style={{ fg: theme.error, marginBottom: 1 }}>
99
+ 🛑 Weekly limit reached
100
+ </text>
101
+ <text style={{ fg: theme.muted }}>
102
+ You've used all {rateLimit.weeklyLimit.toLocaleString()} credits for this week.
103
+ </text>
104
+ {weeklyResetsAt && (
105
+ <text style={{ fg: theme.muted }}>
106
+ Weekly usage resets in {formatResetTime(weeklyResetsAt)}
107
+ </text>
108
+ )}
109
+ </>
110
+ ) : isBlockExhausted ? (
111
+ <>
112
+ <text style={{ fg: theme.warning, marginBottom: 1 }}>
113
+ 5 hour limit reached
114
+ </text>
115
+ {blockResetsAt && (
116
+ <text style={{ fg: theme.muted }}>
117
+ New session starts in {formatResetTime(blockResetsAt)}
118
+ </text>
119
+ )}
120
+ </>
121
+ ) : (
122
+ <text style={{ fg: theme.warning }}>
123
+ Subscription limit reached
124
+ </text>
125
+ )}
126
+
127
+ <box style={{ flexDirection: 'row', alignItems: 'center', gap: 1, marginTop: 0 }}>
128
+ <text style={{ fg: theme.muted }}>Weekly:</text>
129
+ <ProgressBar value={weeklyRemaining} width={12} showPercentage={false} />
130
+ <text style={{ fg: theme.muted }}>{weeklyPercentUsed}% used</text>
131
+ </box>
132
+
133
+ {hasAlaCarteCredits ? (
134
+ <box style={{ flexDirection: 'column', gap: 1, marginTop: 1 }}>
135
+ {fallbackToALaCarte ? (
136
+ <>
137
+ <text style={{ fg: theme.muted }}>
138
+ ✓ Credit spending enabled. You can continue using your credits.
139
+ </text>
140
+ <box style={{ flexDirection: 'row', gap: 2 }}>
141
+ <Button onClick={handleContinueWithCredits}>
142
+ <text style={{ fg: theme.background, bg: theme.foreground }}>
143
+ {' '}Continue with credits ({remainingBalance.toLocaleString()}){' '}
144
+ </text>
145
+ </Button>
146
+ {canUpgrade ? (
147
+ <Button onClick={handleUpgrade}>
148
+ <text style={{ fg: theme.background, bg: theme.foreground }}>{' '}Upgrade Plan ↗{' '}</text>
149
+ </Button>
150
+ ) : (
151
+ <Button onClick={handleBuyCredits}>
152
+ <text style={{ fg: theme.background, bg: theme.muted }}>{' '}Buy Credits ↗{' '}</text>
153
+ </Button>
154
+ )}
155
+ </box>
156
+ <Button onClick={handleToggleFallbackToALaCarte} disabled={updatePreference.isPending}>
157
+ <text style={{ fg: theme.muted }}>
158
+ {updatePreference.isPending ? '[updating...]' : '[disable credit spending]'}
159
+ </text>
160
+ </Button>
161
+ </>
162
+ ) : (
163
+ <>
164
+ <text style={{ fg: theme.warning }}>
165
+ Credit spending is disabled. Enable it to continue.
166
+ </text>
167
+ <box style={{ flexDirection: 'row', gap: 2 }}>
168
+ <Button onClick={handleToggleFallbackToALaCarte} disabled={updatePreference.isPending}>
169
+ <text style={{ fg: theme.background, bg: theme.foreground }}>
170
+ {updatePreference.isPending ? ' Enabling... ' : ' Enable Credit Spending '}
171
+ </text>
172
+ </Button>
173
+ {canUpgrade ? (
174
+ <Button onClick={handleUpgrade}>
175
+ <text style={{ fg: theme.background, bg: theme.muted }}>{' '}Upgrade Plan ↗{' '}</text>
176
+ </Button>
177
+ ) : (
178
+ <Button onClick={handleBuyCredits}>
179
+ <text style={{ fg: theme.background, bg: theme.muted }}>{' '}Buy Credits ↗{' '}</text>
180
+ </Button>
181
+ )}
182
+ </box>
183
+ <text style={{ fg: theme.muted }}>
184
+ You have {remainingBalance.toLocaleString()} credits available.
185
+ </text>
186
+ </>
187
+ )}
188
+ </box>
189
+ ) : (
190
+ <box style={{ flexDirection: 'row', gap: 2, marginTop: 1 }}>
191
+ <text style={{ fg: theme.muted }}>No a-la-carte credits available.</text>
192
+ {canUpgrade ? (
193
+ <Button onClick={handleUpgrade}>
194
+ <text style={{ fg: theme.background, bg: theme.muted }}>{' '}Upgrade Plan ↗{' '}</text>
195
+ </Button>
196
+ ) : (
197
+ <Button onClick={handleBuyCredits}>
198
+ <text style={{ fg: theme.background, bg: theme.muted }}>{' '}Buy Credits ↗{' '}</text>
199
+ </Button>
200
+ )}
201
+ </box>
202
+ )}
203
+ </box>
204
+ </box>
205
+ )
206
+ }
@@ -0,0 +1,184 @@
1
+ import { TextAttributes } from '@opentui/core'
2
+ import { useCallback, 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
+
8
+ /** Minimum terminal width to reveal the full prompt preview on hover */
9
+ const MIN_WIDTH_FOR_DESCRIPTION = 80
10
+ /** Gap between the label column and the hover preview so they never touch */
11
+ const LABEL_DESCRIPTION_GAP = 2
12
+
13
+ export interface SuggestedPrompt {
14
+ /** Short text shown on the chip */
15
+ label: string
16
+ /** Full prompt submitted to the agent when clicked */
17
+ prompt: string
18
+ }
19
+
20
+ /** Metadata passed alongside the prompt when a suggestion is chosen */
21
+ export interface SuggestedPromptSelection {
22
+ label: string
23
+ index: number
24
+ }
25
+
26
+ /**
27
+ * First-time onboarding prompts. Kept short and broadly useful so a brand-new
28
+ * user can get a feel for what the agent does with a single click.
29
+ */
30
+ export const DEFAULT_SUGGESTED_PROMPTS: SuggestedPrompt[] = [
31
+ {
32
+ label: 'Explain this codebase',
33
+ prompt:
34
+ 'Give me a high-level overview of how this codebase is structured and what the main parts do.',
35
+ },
36
+ {
37
+ label: 'Find opportunities to refactor',
38
+ prompt:
39
+ 'Look through my codebase for opportunities to refactor and simplify, and suggest the highest-impact ones.',
40
+ },
41
+ {
42
+ label: 'Improve my test coverage',
43
+ prompt:
44
+ 'Analyze my test coverage and tell me where adding tests would have the most impact.',
45
+ },
46
+ ]
47
+
48
+ interface SuggestedPromptLineProps {
49
+ prompt: SuggestedPrompt
50
+ index: number
51
+ isHovered: boolean
52
+ onSelect: (prompt: string, selection: SuggestedPromptSelection) => void
53
+ onHover: (label: string | null) => void
54
+ /** Fixed width of the label column so hover previews align across rows */
55
+ labelColumnWidth: number
56
+ }
57
+
58
+ const SuggestedPromptLine = ({
59
+ prompt,
60
+ index,
61
+ isHovered,
62
+ onSelect,
63
+ onHover,
64
+ labelColumnWidth,
65
+ }: SuggestedPromptLineProps) => {
66
+ const theme = useTheme()
67
+ const { terminalWidth } = useTerminalDimensions()
68
+
69
+ const handleClick = useCallback(
70
+ () => onSelect(prompt.prompt, { label: prompt.label, index }),
71
+ [onSelect, prompt.prompt, prompt.label, index],
72
+ )
73
+ const handleMouseOver = useCallback(
74
+ () => onHover(prompt.label),
75
+ [onHover, prompt.label],
76
+ )
77
+ const handleMouseOut = useCallback(() => onHover(null), [onHover])
78
+
79
+ const iconColor = isHovered ? theme.primary : theme.muted
80
+ const labelColor = isHovered ? theme.primary : theme.foreground
81
+
82
+ // On hover, reveal the full prompt that will be sent so the short chip label
83
+ // doesn't hide what actually happens. Only when there's room for it.
84
+ const showDescription =
85
+ isHovered && terminalWidth >= MIN_WIDTH_FOR_DESCRIPTION
86
+ const labelLength = '→ '.length + prompt.label.length
87
+ const paddingSpaces = showDescription
88
+ ? ' '.repeat(Math.max(0, labelColumnWidth - labelLength))
89
+ : ''
90
+ const truncatedPrompt = showDescription
91
+ ? (() => {
92
+ const availableWidth = Math.max(0, terminalWidth - labelColumnWidth - 4)
93
+ return prompt.prompt.length > availableWidth
94
+ ? prompt.prompt.slice(0, availableWidth - 1) + '…'
95
+ : prompt.prompt
96
+ })()
97
+ : ''
98
+
99
+ return (
100
+ <box style={{ flexDirection: 'row', width: '100%' }}>
101
+ {/* Only the label itself is clickable */}
102
+ <Button
103
+ onClick={handleClick}
104
+ onMouseOver={handleMouseOver}
105
+ onMouseOut={handleMouseOut}
106
+ style={{
107
+ flexShrink: 0,
108
+ flexGrow: 0,
109
+ backgroundColor: isHovered ? theme.surface : undefined,
110
+ }}
111
+ >
112
+ <text style={{ wrapMode: 'none' }}>
113
+ <span fg={iconColor}>→</span>
114
+ <span
115
+ fg={labelColor}
116
+ attributes={isHovered ? TextAttributes.BOLD : undefined}
117
+ >
118
+ {' '}
119
+ {prompt.label}
120
+ </span>
121
+ </text>
122
+ </Button>
123
+ {/* Non-clickable preview of the full prompt, aligned via padding */}
124
+ {showDescription && (
125
+ <box style={{ flexGrow: 1 }}>
126
+ <text style={{ wrapMode: 'none' }}>
127
+ <span fg={theme.muted} attributes={TextAttributes.ITALIC}>
128
+ {paddingSpaces}
129
+ {truncatedPrompt}
130
+ </span>
131
+ </text>
132
+ </box>
133
+ )}
134
+ </box>
135
+ )
136
+ }
137
+
138
+ interface SuggestedPromptsProps {
139
+ onSelect: (prompt: string, selection: SuggestedPromptSelection) => void
140
+ /** Limit the number of suggestions shown (e.g. on short terminals) */
141
+ maxItems?: number
142
+ prompts?: SuggestedPrompt[]
143
+ }
144
+
145
+ /**
146
+ * A small block of clickable starter prompts shown to first-time users on an
147
+ * empty chat, just above the input box. Clicking one submits it immediately.
148
+ */
149
+ export const SuggestedPrompts = ({
150
+ onSelect,
151
+ maxItems,
152
+ prompts = DEFAULT_SUGGESTED_PROMPTS,
153
+ }: SuggestedPromptsProps) => {
154
+ const theme = useTheme()
155
+ const [hoveredLabel, setHoveredLabel] = useState<string | null>(null)
156
+
157
+ const items =
158
+ maxItems != null ? prompts.slice(0, Math.max(0, maxItems)) : prompts
159
+
160
+ if (items.length === 0) return null
161
+
162
+ // Shared label-column width so the hover previews line up across rows, plus
163
+ // a gap so the preview never touches even the longest label.
164
+ const labelColumnWidth =
165
+ Math.max(...items.map((p) => '→ '.length + p.label.length)) +
166
+ LABEL_DESCRIPTION_GAP
167
+
168
+ return (
169
+ <box style={{ flexDirection: 'column', paddingLeft: 1, paddingBottom: 1 }}>
170
+ <text style={{ fg: theme.muted }}>Try one of these:</text>
171
+ {items.map((prompt, index) => (
172
+ <SuggestedPromptLine
173
+ key={prompt.label}
174
+ prompt={prompt}
175
+ index={index}
176
+ isHovered={hoveredLabel === prompt.label}
177
+ onSelect={onSelect}
178
+ onHover={setHoveredLabel}
179
+ labelColumnWidth={labelColumnWidth}
180
+ />
181
+ ))}
182
+ </box>
183
+ )
184
+ }