@alumbwe/anvil 1.0.18 → 1.0.20

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 (494) hide show
  1. package/README.md +28 -30
  2. package/bin/anvil +2 -0
  3. package/package.json +71 -75
  4. package/src/agents/bundled-agents.generated.d.ts +14 -0
  5. package/src/agents/bundled-agents.generated.ts +3760 -0
  6. package/src/app.tsx +399 -0
  7. package/src/chat.tsx +1698 -0
  8. package/src/cli-args.ts +134 -0
  9. package/src/commands/__tests__/anvil-command-aliases.test.ts +56 -0
  10. package/src/commands/__tests__/bash-command.test.ts +445 -0
  11. package/src/commands/__tests__/command-args.test.ts +349 -0
  12. package/src/commands/__tests__/copy-conversation-clipboard.test.ts +108 -0
  13. package/src/commands/__tests__/copy-conversation.test.ts +226 -0
  14. package/src/commands/__tests__/diagnostics-command.test.ts +35 -0
  15. package/src/commands/__tests__/image.test.ts +99 -0
  16. package/src/commands/__tests__/init.test.ts +456 -0
  17. package/src/commands/__tests__/new-command-rotates-chat.test.ts +91 -0
  18. package/src/commands/__tests__/process-diagnostics.test.ts +54 -0
  19. package/src/commands/__tests__/prompt-builders.test.ts +28 -0
  20. package/src/commands/__tests__/router-input.test.ts +298 -0
  21. package/src/commands/ads.ts +44 -0
  22. package/src/commands/command-registry.ts +726 -0
  23. package/src/commands/copy-conversation.ts +392 -0
  24. package/src/commands/help.ts +13 -0
  25. package/src/commands/image.ts +20 -0
  26. package/src/commands/init.ts +121 -0
  27. package/src/commands/process-diagnostics.ts +120 -0
  28. package/src/commands/prompt-builders.ts +100 -0
  29. package/src/commands/publish.ts +203 -0
  30. package/src/commands/router-utils.ts +80 -0
  31. package/src/commands/router.ts +487 -0
  32. package/src/commands/usage.ts +26 -0
  33. package/src/components/__tests__/ad-banner.test.tsx +83 -0
  34. package/src/components/__tests__/anvil-landing-heading-row.test.tsx +91 -0
  35. package/src/components/__tests__/anvil-model-selector.test.tsx +519 -0
  36. package/src/components/__tests__/anvil-offer-invariants.test.ts +54 -0
  37. package/src/components/__tests__/anvil-takeover-prompt.test.tsx +212 -0
  38. package/src/components/__tests__/chat-input-bar.test.tsx +130 -0
  39. package/src/components/__tests__/grid-layout.integration.test.tsx +304 -0
  40. package/src/components/__tests__/grid-layout.test.tsx +1051 -0
  41. package/src/components/__tests__/message-block.completion.test.tsx +104 -0
  42. package/src/components/__tests__/message-block.streaming.test.tsx +78 -0
  43. package/src/components/__tests__/message-with-agents.test.tsx +579 -0
  44. package/src/components/__tests__/multiline-input.test.tsx +1132 -0
  45. package/src/components/__tests__/queue-panel.test.tsx +398 -0
  46. package/src/components/__tests__/selectable-list.test.ts +232 -0
  47. package/src/components/__tests__/status-bar.test.tsx +44 -0
  48. package/src/components/__tests__/status-indicator.test.tsx +204 -0
  49. package/src/components/__tests__/terminal-thinking-orb.test.tsx +111 -0
  50. package/src/components/__tests__/user-error-banner.test.tsx +102 -0
  51. package/src/components/ad-banner.tsx +323 -0
  52. package/src/components/agent-checklist.tsx +350 -0
  53. package/src/components/agent-mode-toggle.tsx +241 -0
  54. package/src/components/anvil-active-session-summary.tsx +63 -0
  55. package/src/components/anvil-landing-screen.tsx +865 -0
  56. package/src/components/anvil-mark.tsx +52 -0
  57. package/src/components/anvil-model-selector.tsx +1087 -0
  58. package/src/components/anvil-referral-banner.tsx +604 -0
  59. package/src/components/anvil-superseded-screen.tsx +62 -0
  60. package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +432 -0
  61. package/src/components/ask-user/__tests__/validation.test.ts +213 -0
  62. package/src/components/ask-user/components/accordion-question.tsx +153 -0
  63. package/src/components/ask-user/components/custom-answer-input.tsx +66 -0
  64. package/src/components/ask-user/components/options-list.tsx +133 -0
  65. package/src/components/ask-user/components/question-header.tsx +80 -0
  66. package/src/components/ask-user/components/question-option.tsx +89 -0
  67. package/src/components/ask-user/constants.ts +38 -0
  68. package/src/components/ask-user/index.tsx +631 -0
  69. package/src/components/ask-user/utils/validation.ts +85 -0
  70. package/src/components/attachment-card.tsx +65 -0
  71. package/src/components/blocks/agent-block-grid.tsx +58 -0
  72. package/src/components/blocks/agent-branch-item.tsx +311 -0
  73. package/src/components/blocks/agent-branch-wrapper.tsx +493 -0
  74. package/src/components/blocks/agent-list-branch.tsx +77 -0
  75. package/src/components/blocks/ask-user-branch.tsx +81 -0
  76. package/src/components/blocks/block-helpers.ts +11 -0
  77. package/src/components/blocks/blocks-renderer.tsx +289 -0
  78. package/src/components/blocks/content-with-markdown.tsx +33 -0
  79. package/src/components/blocks/image-block.tsx +128 -0
  80. package/src/components/blocks/implementor-row.tsx +429 -0
  81. package/src/components/blocks/single-block.tsx +201 -0
  82. package/src/components/blocks/thinking-block.tsx +67 -0
  83. package/src/components/blocks/tool-block-group.tsx +57 -0
  84. package/src/components/blocks/tool-branch.tsx +206 -0
  85. package/src/components/blocks/user-content-copy.tsx +157 -0
  86. package/src/components/bottom-banner.tsx +136 -0
  87. package/src/components/build-mode-buttons.tsx +110 -0
  88. package/src/components/button.tsx +66 -0
  89. package/src/components/chat-header.tsx +77 -0
  90. package/src/components/chat-history-screen.tsx +413 -0
  91. package/src/components/chat-input-bar.tsx +523 -0
  92. package/src/components/clickable-title-box.tsx +66 -0
  93. package/src/components/clickable.tsx +119 -0
  94. package/src/components/collapse-button.tsx +34 -0
  95. package/src/components/copy-button.tsx +203 -0
  96. package/src/components/elapsed-timer.tsx +55 -0
  97. package/src/components/error-boundary.tsx +55 -0
  98. package/src/components/feedback-container.tsx +181 -0
  99. package/src/components/feedback-icon-button.tsx +89 -0
  100. package/src/components/feedback-input-mode.tsx +344 -0
  101. package/src/components/file-attachment-card.tsx +98 -0
  102. package/src/components/grid-layout.tsx +80 -0
  103. package/src/components/help-banner.tsx +127 -0
  104. package/src/components/highlighted-text.tsx +52 -0
  105. package/src/components/image-card.tsx +167 -0
  106. package/src/components/image-thumbnail.tsx +114 -0
  107. package/src/components/input-cursor.tsx +77 -0
  108. package/src/components/input-mode-banner.tsx +57 -0
  109. package/src/components/load-previous-button.tsx +48 -0
  110. package/src/components/login-modal.tsx +475 -0
  111. package/src/components/message-block.tsx +346 -0
  112. package/src/components/message-footer.tsx +258 -0
  113. package/src/components/message-with-agents.tsx +506 -0
  114. package/src/components/mode-divider.tsx +43 -0
  115. package/src/components/multiline-input.tsx +1241 -0
  116. package/src/components/out-of-credits-banner.tsx +160 -0
  117. package/src/components/pending-attachments-banner.tsx +116 -0
  118. package/src/components/pending-bash-message.tsx +59 -0
  119. package/src/components/progress-bar.tsx +81 -0
  120. package/src/components/project-picker-screen.tsx +485 -0
  121. package/src/components/publish-confirmation.tsx +493 -0
  122. package/src/components/publish-container.tsx +687 -0
  123. package/src/components/queue-panel.tsx +310 -0
  124. package/src/components/raised-pill.tsx +95 -0
  125. package/src/components/renderers/plan-box.tsx +59 -0
  126. package/src/components/review-screen.tsx +112 -0
  127. package/src/components/scroll-to-bottom-button.tsx +34 -0
  128. package/src/components/segmented-control.tsx +187 -0
  129. package/src/components/selectable-list.tsx +248 -0
  130. package/src/components/selected-chips.tsx +80 -0
  131. package/src/components/separator.tsx +40 -0
  132. package/src/components/session-ended-banner.tsx +226 -0
  133. package/src/components/shimmer-text.tsx +248 -0
  134. package/src/components/status-bar.tsx +313 -0
  135. package/src/components/subscription-limit-banner.tsx +206 -0
  136. package/src/components/suggested-prompts.tsx +184 -0
  137. package/src/components/suggestion-menu.tsx +215 -0
  138. package/src/components/terminal-command-display.tsx +151 -0
  139. package/src/components/terminal-link.tsx +97 -0
  140. package/src/components/terminal-thinking-orb.tsx +95 -0
  141. package/src/components/text-attachment-card.tsx +77 -0
  142. package/src/components/thinking.tsx +109 -0
  143. package/src/components/tools/__tests__/apply-patch.test.tsx +84 -0
  144. package/src/components/tools/__tests__/code-search.test.tsx +45 -0
  145. package/src/components/tools/__tests__/gravity-index.test.ts +76 -0
  146. package/src/components/tools/__tests__/render-ui.test.tsx +92 -0
  147. package/src/components/tools/__tests__/run-terminal-command.test.ts +233 -0
  148. package/src/components/tools/apply-patch.tsx +95 -0
  149. package/src/components/tools/code-search.tsx +45 -0
  150. package/src/components/tools/composio.tsx +152 -0
  151. package/src/components/tools/diff-viewer.tsx +76 -0
  152. package/src/components/tools/glob.tsx +61 -0
  153. package/src/components/tools/gravity-index.tsx +77 -0
  154. package/src/components/tools/list-directory.tsx +61 -0
  155. package/src/components/tools/read-docs.tsx +37 -0
  156. package/src/components/tools/read-files.tsx +91 -0
  157. package/src/components/tools/read-subtree.tsx +44 -0
  158. package/src/components/tools/read-url.tsx +33 -0
  159. package/src/components/tools/registry.ts +126 -0
  160. package/src/components/tools/render-ui.tsx +122 -0
  161. package/src/components/tools/run-terminal-command.tsx +79 -0
  162. package/src/components/tools/skill.tsx +29 -0
  163. package/src/components/tools/str-replace.tsx +77 -0
  164. package/src/components/tools/suggest-followups.tsx +369 -0
  165. package/src/components/tools/task-completed.tsx +16 -0
  166. package/src/components/tools/tool-call-item.tsx +273 -0
  167. package/src/components/tools/types.ts +55 -0
  168. package/src/components/tools/web-search.tsx +33 -0
  169. package/src/components/tools/write-file.tsx +16 -0
  170. package/src/components/tools/write-todos.tsx +95 -0
  171. package/src/components/top-banner.tsx +178 -0
  172. package/src/components/usage-banner.tsx +244 -0
  173. package/src/components/user-error-banner.tsx +56 -0
  174. package/src/components/validation-error-popover.tsx +192 -0
  175. package/src/contexts/__tests__/chat-runtime-context.test.tsx +149 -0
  176. package/src/contexts/chat-runtime-context.tsx +240 -0
  177. package/src/data/slash-commands.test.ts +24 -0
  178. package/src/data/slash-commands.ts +249 -0
  179. package/src/entry.ts +12 -0
  180. package/src/hooks/__tests__/holds-live-anvil-slot.test.ts +49 -0
  181. package/src/hooks/__tests__/session-fetch-signal.test.ts +63 -0
  182. package/src/hooks/__tests__/use-activity-query.test.ts +1000 -0
  183. package/src/hooks/__tests__/use-ask-user-bridge.test.ts +176 -0
  184. package/src/hooks/__tests__/use-auth-query.test.ts +21 -0
  185. package/src/hooks/__tests__/use-chat-input-remount.test.tsx +66 -0
  186. package/src/hooks/__tests__/use-clipboard.test.tsx +73 -0
  187. package/src/hooks/__tests__/use-connection-status.test.ts +112 -0
  188. package/src/hooks/__tests__/use-directory-browser.test.ts +203 -0
  189. package/src/hooks/__tests__/use-gravity-ad.test.ts +73 -0
  190. package/src/hooks/__tests__/use-grid-layout.test.ts +349 -0
  191. package/src/hooks/__tests__/use-input-history.test.ts +699 -0
  192. package/src/hooks/__tests__/use-message-queue-editing.test.tsx +209 -0
  193. package/src/hooks/__tests__/use-message-queue.test.tsx +71 -0
  194. package/src/hooks/__tests__/use-path-tab-completion.test.ts +315 -0
  195. package/src/hooks/__tests__/use-queue-controls.test.ts +68 -0
  196. package/src/hooks/__tests__/use-queue-ui.test.tsx +83 -0
  197. package/src/hooks/__tests__/use-searchable-list.test.ts +359 -0
  198. package/src/hooks/__tests__/use-send-message-timer.test.ts +146 -0
  199. package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +361 -0
  200. package/src/hooks/__tests__/use-suggestion-engine.test.ts +485 -0
  201. package/src/hooks/__tests__/use-terminal-focus.test.ts +66 -0
  202. package/src/hooks/__tests__/use-terminal-layout.test.ts +675 -0
  203. package/src/hooks/__tests__/use-timeout.test.ts +330 -0
  204. package/src/hooks/__tests__/use-usage-query.test.ts +502 -0
  205. package/src/hooks/__tests__/use-user-details-query.test.ts +195 -0
  206. package/src/hooks/helpers/__tests__/send-message.test.ts +1918 -0
  207. package/src/hooks/helpers/send-message.ts +654 -0
  208. package/src/hooks/stream-state.ts +83 -0
  209. package/src/hooks/use-activity-query.ts +668 -0
  210. package/src/hooks/use-agent-validation.ts +74 -0
  211. package/src/hooks/use-anvil-ctrl-c-exit.ts +23 -0
  212. package/src/hooks/use-anvil-session-progress.ts +34 -0
  213. package/src/hooks/use-anvil-session.ts +806 -0
  214. package/src/hooks/use-anvil-streak-query.ts +61 -0
  215. package/src/hooks/use-ask-user-bridge.ts +92 -0
  216. package/src/hooks/use-auth-query.ts +262 -0
  217. package/src/hooks/use-auth-state.ts +152 -0
  218. package/src/hooks/use-chat-input.ts +105 -0
  219. package/src/hooks/use-chat-keyboard.ts +321 -0
  220. package/src/hooks/use-chat-messages.ts +254 -0
  221. package/src/hooks/use-chat-state.ts +165 -0
  222. package/src/hooks/use-chat-streaming.ts +180 -0
  223. package/src/hooks/use-chat-ui.ts +131 -0
  224. package/src/hooks/use-clipboard.ts +151 -0
  225. package/src/hooks/use-connection-status.ts +142 -0
  226. package/src/hooks/use-directory-browser.ts +88 -0
  227. package/src/hooks/use-elapsed-time.ts +116 -0
  228. package/src/hooks/use-event.ts +34 -0
  229. package/src/hooks/use-exit-handler.ts +66 -0
  230. package/src/hooks/use-fetch-login-url.ts +65 -0
  231. package/src/hooks/use-fingerprint.ts +61 -0
  232. package/src/hooks/use-gravity-ad.ts +656 -0
  233. package/src/hooks/use-grid-layout.ts +73 -0
  234. package/src/hooks/use-input-history.ts +173 -0
  235. package/src/hooks/use-login-keyboard-handlers.ts +75 -0
  236. package/src/hooks/use-login-polling.ts +114 -0
  237. package/src/hooks/use-logo.tsx +161 -0
  238. package/src/hooks/use-message-queue.ts +387 -0
  239. package/src/hooks/use-now.ts +20 -0
  240. package/src/hooks/use-path-tab-completion.ts +88 -0
  241. package/src/hooks/use-publish-mutation.ts +60 -0
  242. package/src/hooks/use-queue-controls.ts +68 -0
  243. package/src/hooks/use-queue-ui.ts +74 -0
  244. package/src/hooks/use-scroll-management.ts +184 -0
  245. package/src/hooks/use-searchable-list.ts +98 -0
  246. package/src/hooks/use-send-message.ts +773 -0
  247. package/src/hooks/use-sheen-animation.tsx +88 -0
  248. package/src/hooks/use-subscription-query.ts +71 -0
  249. package/src/hooks/use-suggestion-engine.ts +783 -0
  250. package/src/hooks/use-terminal-breakpoints.ts +77 -0
  251. package/src/hooks/use-terminal-dimensions.ts +46 -0
  252. package/src/hooks/use-terminal-focus.ts +29 -0
  253. package/src/hooks/use-terminal-layout.ts +189 -0
  254. package/src/hooks/use-theme.tsx +140 -0
  255. package/src/hooks/use-timeout.ts +85 -0
  256. package/src/hooks/use-update-preference.ts +66 -0
  257. package/src/hooks/use-usage-monitor.ts +55 -0
  258. package/src/hooks/use-usage-query.ts +124 -0
  259. package/src/hooks/use-user-details-query.ts +93 -0
  260. package/src/hooks/use-why-did-you-update.ts +177 -0
  261. package/src/index.tsx +473 -0
  262. package/src/init/__tests__/init-direnv.test.ts +526 -0
  263. package/src/init/init-app.ts +34 -0
  264. package/src/init/init-direnv.ts +133 -0
  265. package/src/login/constants.ts +60 -0
  266. package/src/login/login-flow.ts +204 -0
  267. package/src/login/plain-login.ts +108 -0
  268. package/src/login/utils.ts +183 -0
  269. package/src/native/ripgrep.ts +80 -0
  270. package/src/polyfills/bun-strip-ansi.ts +12 -0
  271. package/src/pre-init/tree-sitter-wasm.ts +96 -0
  272. package/src/project-files.ts +102 -0
  273. package/src/setup/key-setup.ts +295 -0
  274. package/src/setup/load-env.ts +106 -0
  275. package/src/state/__tests__/chat-store-focus.test.ts +22 -0
  276. package/src/state/__tests__/feedback-store.test.ts +257 -0
  277. package/src/state/anvil-model-store.ts +42 -0
  278. package/src/state/anvil-session-store.ts +53 -0
  279. package/src/state/chat-history-store.ts +30 -0
  280. package/src/state/chat-store.ts +544 -0
  281. package/src/state/feedback-store.ts +160 -0
  282. package/src/state/login-store.ts +150 -0
  283. package/src/state/message-block-store.ts +149 -0
  284. package/src/state/publish-store.ts +146 -0
  285. package/src/state/queue-panel-store.ts +26 -0
  286. package/src/state/review-store.ts +24 -0
  287. package/src/types/anvil-session.ts +17 -0
  288. package/src/types/chat-state.ts +15 -0
  289. package/src/types/chat.ts +241 -0
  290. package/src/types/contracts/send-message.ts +12 -0
  291. package/src/types/env.ts +98 -0
  292. package/src/types/function-params.ts +28 -0
  293. package/src/types/react19-compat.d.ts +19 -0
  294. package/src/types/store.ts +111 -0
  295. package/src/types/theme-system.ts +144 -0
  296. package/src/types/utils.ts +3 -0
  297. package/src/utils/__tests__/active-run.test.ts +140 -0
  298. package/src/utils/__tests__/activity-tracker.test.ts +440 -0
  299. package/src/utils/__tests__/agent-display.test.ts +139 -0
  300. package/src/utils/__tests__/analytics-client.test.ts +262 -0
  301. package/src/utils/__tests__/anonymous-id.test.ts +74 -0
  302. package/src/utils/__tests__/anvil-api.test.ts +578 -0
  303. package/src/utils/__tests__/anvil-instance-owner.test.ts +69 -0
  304. package/src/utils/__tests__/anvil-model-navigation.test.ts +102 -0
  305. package/src/utils/__tests__/anvil-premium-reset.test.ts +79 -0
  306. package/src/utils/__tests__/anvil-referral-cache.test.ts +116 -0
  307. package/src/utils/__tests__/anvil-session-api.test.ts +215 -0
  308. package/src/utils/__tests__/anvil-session-display.test.ts +23 -0
  309. package/src/utils/__tests__/anvil-streak-line.test.ts +217 -0
  310. package/src/utils/__tests__/arrays.test.ts +264 -0
  311. package/src/utils/__tests__/bash-context-processor.test.ts +38 -0
  312. package/src/utils/__tests__/block-processor.test.ts +810 -0
  313. package/src/utils/__tests__/chat-history.test.ts +203 -0
  314. package/src/utils/__tests__/chat-input-key-intercept.test.ts +40 -0
  315. package/src/utils/__tests__/chat-meta.test.ts +130 -0
  316. package/src/utils/__tests__/clipboard-linux.test.ts +359 -0
  317. package/src/utils/__tests__/clipboard.test.ts +910 -0
  318. package/src/utils/__tests__/code-search-summary.test.ts +84 -0
  319. package/src/utils/__tests__/collapse-helpers.test.ts +1109 -0
  320. package/src/utils/__tests__/error-handling.test.ts +658 -0
  321. package/src/utils/__tests__/exit-cleanly.test.ts +110 -0
  322. package/src/utils/__tests__/feedback-helpers.test.ts +444 -0
  323. package/src/utils/__tests__/feedback-submission.test.ts +26 -0
  324. package/src/utils/__tests__/fetch-usage.test.ts +355 -0
  325. package/src/utils/__tests__/fingerprint.test.ts +144 -0
  326. package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +7 -0
  327. package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +3 -0
  328. package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +33 -0
  329. package/src/utils/__tests__/format-elapsed-time.test.ts +103 -0
  330. package/src/utils/__tests__/format-timeout.test.ts +87 -0
  331. package/src/utils/__tests__/image-dimensions.test.ts +249 -0
  332. package/src/utils/__tests__/image-processor.test.ts +209 -0
  333. package/src/utils/__tests__/implementor-helpers.test.ts +1458 -0
  334. package/src/utils/__tests__/keyboard-actions.test.ts +660 -0
  335. package/src/utils/__tests__/layout-helpers.test.ts +36 -0
  336. package/src/utils/__tests__/lazy-response-ads.test.ts +167 -0
  337. package/src/utils/__tests__/log-shipper.test.ts +53 -0
  338. package/src/utils/__tests__/markdown-renderer.test.tsx +421 -0
  339. package/src/utils/__tests__/message-block-helpers.test.ts +1263 -0
  340. package/src/utils/__tests__/message-updater.test.ts +645 -0
  341. package/src/utils/__tests__/osc-timeout-scenarios.test.ts +172 -0
  342. package/src/utils/__tests__/pending-attachments.test.ts +277 -0
  343. package/src/utils/__tests__/polling-backoff.test.ts +51 -0
  344. package/src/utils/__tests__/queue-panel-actions.test.ts +148 -0
  345. package/src/utils/__tests__/run-state-storage.test.ts +919 -0
  346. package/src/utils/__tests__/safe-json.test.ts +77 -0
  347. package/src/utils/__tests__/sdk-event-handlers.test.ts +685 -0
  348. package/src/utils/__tests__/send-message-helpers.test.ts +1898 -0
  349. package/src/utils/__tests__/send-message-timer.test.ts +64 -0
  350. package/src/utils/__tests__/settings.test.ts +99 -0
  351. package/src/utils/__tests__/strings.test.ts +192 -0
  352. package/src/utils/__tests__/terminal-color-detection.test.ts +425 -0
  353. package/src/utils/__tests__/terminal-command-broker.test.ts +528 -0
  354. package/src/utils/__tests__/terminal-enter-detection.test.ts +77 -0
  355. package/src/utils/__tests__/terminal-io.test.ts +137 -0
  356. package/src/utils/__tests__/terminal-protocol-controller.test.ts +119 -0
  357. package/src/utils/__tests__/text-layout.test.ts +82 -0
  358. package/src/utils/__tests__/theme-platform-detection.test.ts +125 -0
  359. package/src/utils/__tests__/think-tag-parser.test.ts +159 -0
  360. package/src/utils/__tests__/trace-writer.test.ts +223 -0
  361. package/src/utils/__tests__/trim-chat-logs.test.ts +78 -0
  362. package/src/utils/__tests__/usage-banner-state.test.ts +298 -0
  363. package/src/utils/__tests__/validation-error-formatting.test.ts +126 -0
  364. package/src/utils/__tests__/windows-terminal-health.test.ts +101 -0
  365. package/src/utils/__tests__/write-file-atomic.test.ts +127 -0
  366. package/src/utils/active-run.ts +131 -0
  367. package/src/utils/activity-tracker.ts +66 -0
  368. package/src/utils/agent-display.ts +87 -0
  369. package/src/utils/agent-helpers.ts +27 -0
  370. package/src/utils/agent-id-utils.ts +14 -0
  371. package/src/utils/ai-message-id.ts +12 -0
  372. package/src/utils/analytics.ts +361 -0
  373. package/src/utils/anonymous-id.ts +87 -0
  374. package/src/utils/anvil-agent-selection.ts +33 -0
  375. package/src/utils/anvil-api.ts +612 -0
  376. package/src/utils/anvil-client.ts +217 -0
  377. package/src/utils/anvil-instance-owner.ts +66 -0
  378. package/src/utils/anvil-model-navigation.ts +50 -0
  379. package/src/utils/anvil-premium-reset.ts +55 -0
  380. package/src/utils/anvil-referral-cache.ts +51 -0
  381. package/src/utils/anvil-session-api.ts +243 -0
  382. package/src/utils/anvil-session-display.ts +21 -0
  383. package/src/utils/anvil-streak-line.ts +93 -0
  384. package/src/utils/arrays.ts +117 -0
  385. package/src/utils/auth.ts +258 -0
  386. package/src/utils/bash-context-processor.ts +47 -0
  387. package/src/utils/bash-messages.ts +108 -0
  388. package/src/utils/block-margins.ts +35 -0
  389. package/src/utils/block-operations.ts +523 -0
  390. package/src/utils/block-processor.ts +213 -0
  391. package/src/utils/chat-history.ts +253 -0
  392. package/src/utils/chat-input-key-intercept.ts +52 -0
  393. package/src/utils/chat-meta.ts +90 -0
  394. package/src/utils/chat-scroll-accel.ts +151 -0
  395. package/src/utils/clipboard-image.ts +588 -0
  396. package/src/utils/clipboard.ts +397 -0
  397. package/src/utils/code-search-summary.ts +70 -0
  398. package/src/utils/collapse-helpers.ts +256 -0
  399. package/src/utils/config-dir.ts +24 -0
  400. package/src/utils/constants.ts +190 -0
  401. package/src/utils/create-event-handler-state.ts +73 -0
  402. package/src/utils/create-run-config.ts +121 -0
  403. package/src/utils/detect-shell.ts +116 -0
  404. package/src/utils/directory-browser.ts +72 -0
  405. package/src/utils/engagement.ts +55 -0
  406. package/src/utils/env.ts +92 -0
  407. package/src/utils/error-handling.ts +255 -0
  408. package/src/utils/error-messages.ts +62 -0
  409. package/src/utils/exit-cleanly.ts +86 -0
  410. package/src/utils/feedback-helpers.ts +103 -0
  411. package/src/utils/feedback-submission.ts +22 -0
  412. package/src/utils/fetch-usage.ts +76 -0
  413. package/src/utils/fingerprint.ts +245 -0
  414. package/src/utils/format-elapsed-time.ts +33 -0
  415. package/src/utils/format-session-units.ts +6 -0
  416. package/src/utils/format-timeout.ts +28 -0
  417. package/src/utils/format-validation-errors-for-message.ts +84 -0
  418. package/src/utils/git.ts +17 -0
  419. package/src/utils/helpers.ts +47 -0
  420. package/src/utils/image-display.ts +67 -0
  421. package/src/utils/image-handler.ts +325 -0
  422. package/src/utils/image-processor.ts +133 -0
  423. package/src/utils/image-thumbnail.ts +78 -0
  424. package/src/utils/implementor-helpers.ts +787 -0
  425. package/src/utils/input-modes.ts +174 -0
  426. package/src/utils/keyboard-actions.ts +392 -0
  427. package/src/utils/keypad-keys.ts +47 -0
  428. package/src/utils/layout-helpers.ts +33 -0
  429. package/src/utils/lazy-response-ads.ts +1 -0
  430. package/src/utils/local-agent-registry.ts +474 -0
  431. package/src/utils/log-shipper.ts +126 -0
  432. package/src/utils/logger.ts +327 -0
  433. package/src/utils/markdown-renderer.tsx +1090 -0
  434. package/src/utils/math.ts +3 -0
  435. package/src/utils/message-block-helpers.ts +684 -0
  436. package/src/utils/message-history.ts +143 -0
  437. package/src/utils/message-tree-utils.ts +67 -0
  438. package/src/utils/message-updater.ts +279 -0
  439. package/src/utils/open-file.ts +146 -0
  440. package/src/utils/open-url.ts +63 -0
  441. package/src/utils/path-completion.ts +111 -0
  442. package/src/utils/path-helpers.ts +38 -0
  443. package/src/utils/pending-attachments.ts +360 -0
  444. package/src/utils/polling-backoff.ts +59 -0
  445. package/src/utils/project-picker.ts +12 -0
  446. package/src/utils/queue-panel-actions.ts +70 -0
  447. package/src/utils/recent-projects.ts +156 -0
  448. package/src/utils/renderer-cleanup.ts +233 -0
  449. package/src/utils/response-ad-positions.ts +1 -0
  450. package/src/utils/run-state-storage.ts +603 -0
  451. package/src/utils/safe-json.ts +153 -0
  452. package/src/utils/sdk-event-handlers.ts +571 -0
  453. package/src/utils/send-message-helpers.ts +210 -0
  454. package/src/utils/send-message-timer.ts +110 -0
  455. package/src/utils/settings.ts +226 -0
  456. package/src/utils/skill-registry.ts +94 -0
  457. package/src/utils/spawn-agent-matcher.ts +55 -0
  458. package/src/utils/status-indicator-state.ts +107 -0
  459. package/src/utils/stream-chunk-processor.ts +57 -0
  460. package/src/utils/strings.ts +271 -0
  461. package/src/utils/subscription.ts +31 -0
  462. package/src/utils/syntax-highlighter.tsx +19 -0
  463. package/src/utils/terminal-color-detection.ts +474 -0
  464. package/src/utils/terminal-command-broker.ts +467 -0
  465. package/src/utils/terminal-enter-detection.ts +63 -0
  466. package/src/utils/terminal-images.ts +229 -0
  467. package/src/utils/terminal-io.ts +85 -0
  468. package/src/utils/terminal-protocol-controller.ts +173 -0
  469. package/src/utils/terminal-reset-sequences.ts +33 -0
  470. package/src/utils/terminal-title.ts +84 -0
  471. package/src/utils/terminal-watchdog.ts +366 -0
  472. package/src/utils/text-layout.ts +149 -0
  473. package/src/utils/theme-config.ts +141 -0
  474. package/src/utils/theme-system.ts +1206 -0
  475. package/src/utils/think-tag-parser.ts +95 -0
  476. package/src/utils/time-format.ts +21 -0
  477. package/src/utils/trace-writer.ts +162 -0
  478. package/src/utils/ui-constants.ts +69 -0
  479. package/src/utils/update-checker.ts +110 -0
  480. package/src/utils/usage-banner-state.ts +146 -0
  481. package/src/utils/validation-error-formatting.ts +86 -0
  482. package/src/utils/validation-error-helpers.ts +19 -0
  483. package/src/utils/windows-terminal-health.ts +124 -0
  484. package/src/utils/word-wrap-utils.ts +53 -0
  485. package/src/utils/write-file-atomic.ts +54 -0
  486. package/src/utils/yield-to-event-loop.ts +9 -0
  487. package/bin/anvil.cjs +0 -40
  488. package/dist/bundled.js +0 -123450
  489. package/dist/bundled.js.map +0 -827
  490. package/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
  491. package/dist/vendor/ripgrep/arm64-linux/rg +0 -0
  492. package/dist/vendor/ripgrep/x64-darwin/rg +0 -0
  493. package/dist/vendor/ripgrep/x64-linux/rg +0 -0
  494. package/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
@@ -0,0 +1,226 @@
1
+ import {
2
+ FALLBACK_ANVIL_MODEL_ID,
3
+ isAnvilPremiumModelId,
4
+ SUPPORTED_ANVIL_MODELS,
5
+ type AnvilModelOption,
6
+ } from '@anvil/common/constants/anvil-models'
7
+ import { getRateLimitsByModel } from '@anvil/common/types/anvil-session'
8
+ import { TextAttributes } from '@opentui/core'
9
+ import { useKeyboard } from '@opentui/react'
10
+ import React, { useCallback, useState } from 'react'
11
+
12
+ import { Button } from './button'
13
+ import {
14
+ refreshAnvilSession,
15
+ returnToAnvilLanding,
16
+ } from '../hooks/use-anvil-session'
17
+ import { useTheme } from '../hooks/use-theme'
18
+ import { useAnvilModelStore } from '../state/anvil-model-store'
19
+ import { useAnvilSessionStore } from '../state/anvil-session-store'
20
+ import { formatSessionUnits } from '../utils/format-session-units'
21
+ import { isPlainEnterKey } from '../utils/terminal-enter-detection'
22
+ import { BORDER_CHARS } from '../utils/ui-constants'
23
+
24
+ import type { KeyEvent } from '@opentui/core'
25
+
26
+ interface SessionEndedBannerProps {
27
+ /** True while an agent request is still streaming under the server-side
28
+ * grace window. Swaps the Enter-to-rejoin affordance for a "let it
29
+ * finish" hint so the user doesn't abort their in-flight work. */
30
+ isStreaming: boolean
31
+ }
32
+
33
+ /**
34
+ * Replaces the chat input when the anvil session has ended. Captures
35
+ * Enter to start a new same-chat session. Esc returns to model selection
36
+ * once no in-flight work needs the global stream-interrupt handler.
37
+ */
38
+ export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
39
+ isStreaming,
40
+ }) => {
41
+ const theme = useTheme()
42
+ const [pendingAction, setPendingAction] = useState<
43
+ 'landing' | 'same-chat' | null
44
+ >(null)
45
+
46
+ // All premium models share one daily pool; the server replicates the same
47
+ // snapshot under each premium model id, so the first entry has the right
48
+ // count.
49
+ const premiumQuota = useAnvilSessionStore(
50
+ (s) => Object.values(getRateLimitsByModel(s.session) ?? {})[0] ?? null,
51
+ )
52
+ const isQuotaExhausted = premiumQuota
53
+ ? premiumQuota.recentCount >= premiumQuota.limit
54
+ : false
55
+ const accessTier = useAnvilSessionStore((s) =>
56
+ s.session && 'accessTier' in s.session ? s.session.accessTier : 'full',
57
+ )
58
+ const quotaLabel = accessTier === 'limited' ? 'sessions' : 'premium sessions'
59
+ const bannerTitle = premiumQuota
60
+ ? `Session ended · ${formatSessionUnits(premiumQuota.recentCount)} of ${premiumQuota.limit} ${quotaLabel} used today`
61
+ : 'Session ended'
62
+ const landingButtonLabel = 'Change model'
63
+ const landingPendingLabel = 'Opening model selection…'
64
+
65
+ // With the shared premium pool spent, rejoining on the still-selected
66
+ // premium model would be rejected server-side and dead-end on the terminal
67
+ // rate-limited screen. Continue on the unlimited fallback (DeepSeek V4
68
+ // Flash) instead — the same flip the landing picker's recommendation makes.
69
+ // Limited tier is excluded: its models all share the one exhausted pool, so
70
+ // there is nothing to flip to.
71
+ const selectedModel = useAnvilModelStore((s) => s.selectedModel)
72
+ const continueOnFallback =
73
+ isQuotaExhausted &&
74
+ accessTier !== 'limited' &&
75
+ isAnvilPremiumModelId(selectedModel)
76
+ const fallbackModel: AnvilModelOption | undefined =
77
+ SUPPORTED_ANVIL_MODELS.find((m) => m.id === FALLBACK_ANVIL_MODEL_ID)
78
+ const fallbackModelName = fallbackModel?.displayName ?? 'DeepSeek V4 Flash'
79
+ // Remind the user of the fallback's data-collection policy before they
80
+ // continue on it — the landing picker shows this caveat on the model row,
81
+ // but this banner is a one-keypress continue, so surface it here too.
82
+ const fallbackWarning = fallbackModel?.warning
83
+
84
+ // While a request is still streaming, restart is disabled: it would
85
+ // unmount <Chat> and abort the in-flight agent run. The promise is "we
86
+ // let the agent finish" — honoring that means Enter does nothing until
87
+ // the stream ends or the user hits Esc.
88
+ const canRestart = !isStreaming && pendingAction === null
89
+ const pickNewModel = useCallback(() => {
90
+ if (!canRestart) return
91
+ setPendingAction('landing')
92
+ // Drop back to the landing picker (status: 'none') so the user picks a
93
+ // model and hits Enter again to commit, instead of silently starting a
94
+ // new session. app.tsx swaps us into <AnvilLandingScreen> on the
95
+ // transition, unmounting this banner — no need to clear the pending state on
96
+ // success.
97
+ returnToAnvilLanding({ resetChat: true }).catch(() =>
98
+ setPendingAction(null),
99
+ )
100
+ }, [canRestart])
101
+
102
+ const startSameChatSession = useCallback(() => {
103
+ if (!canRestart) return
104
+ setPendingAction('same-chat')
105
+ if (continueOnFallback) {
106
+ // In-memory flip only (like the server-driven model flips): today's
107
+ // exhausted pool must not overwrite the user's saved preference. The
108
+ // rejoin POST reads the store at tick time, so it picks this up.
109
+ useAnvilModelStore
110
+ .getState()
111
+ .setSelectedModel(FALLBACK_ANVIL_MODEL_ID)
112
+ }
113
+ // Re-POST with the currently selected model and keep the chat/run state
114
+ // intact so the next prompt continues the same conversation.
115
+ refreshAnvilSession().catch(() => setPendingAction(null))
116
+ }, [canRestart, continueOnFallback])
117
+
118
+ useKeyboard(
119
+ useCallback(
120
+ (key: KeyEvent) => {
121
+ if (!canRestart) return
122
+ if (isPlainEnterKey(key)) {
123
+ key.preventDefault?.()
124
+ startSameChatSession()
125
+ return
126
+ }
127
+ if (key.name === 'escape') {
128
+ key.preventDefault?.()
129
+ pickNewModel()
130
+ }
131
+ },
132
+ [startSameChatSession, pickNewModel, canRestart],
133
+ ),
134
+ )
135
+
136
+ return (
137
+ <box
138
+ title={bannerTitle}
139
+ titleAlignment="center"
140
+ style={{
141
+ width: '100%',
142
+ borderStyle: 'single',
143
+ // Amber border doubles as the "you've hit the cap" signal now that
144
+ // the quota count lives in the title (which can't carry per-char
145
+ // color); muted otherwise.
146
+ borderColor: isQuotaExhausted ? theme.secondary : theme.muted,
147
+ customBorderChars: BORDER_CHARS,
148
+ paddingLeft: 1,
149
+ paddingRight: 1,
150
+ paddingTop: 0,
151
+ paddingBottom: 0,
152
+ flexDirection: 'column',
153
+ gap: 0,
154
+ }}
155
+ >
156
+ {isStreaming ? (
157
+ <text style={{ fg: theme.muted, wrapMode: 'word' }}>
158
+ Agent is wrapping up. Rejoin the wait room after it's finished.
159
+ </text>
160
+ ) : (
161
+ <box
162
+ style={{
163
+ width: '100%',
164
+ flexDirection: 'row',
165
+ alignItems: 'center',
166
+ gap: 2,
167
+ }}
168
+ >
169
+ <Button onClick={startSameChatSession}>
170
+ <text
171
+ style={{
172
+ fg:
173
+ pendingAction === 'same-chat'
174
+ ? theme.muted
175
+ : theme.foreground,
176
+ }}
177
+ attributes={TextAttributes.BOLD}
178
+ >
179
+ {pendingAction === 'same-chat'
180
+ ? 'Starting…'
181
+ : continueOnFallback
182
+ ? `Press Enter to continue with ${fallbackModelName}`
183
+ : 'Press Enter to continue in a new session'}
184
+ </text>
185
+ </Button>
186
+ <box style={{ flexGrow: 1 }} />
187
+ <Button
188
+ onClick={pickNewModel}
189
+ style={{
190
+ borderStyle: 'single',
191
+ borderColor:
192
+ pendingAction === 'landing' ? theme.muted : theme.border,
193
+ customBorderChars: BORDER_CHARS,
194
+ paddingLeft: 1,
195
+ paddingRight: 1,
196
+ }}
197
+ border={['top', 'bottom', 'left', 'right']}
198
+ >
199
+ <text
200
+ style={{
201
+ fg:
202
+ pendingAction === 'landing'
203
+ ? theme.muted
204
+ : theme.foreground,
205
+ }}
206
+ >
207
+ {pendingAction === 'landing' ? (
208
+ landingPendingLabel
209
+ ) : (
210
+ <>
211
+ {landingButtonLabel}
212
+ <span fg={theme.muted}>{' Esc'}</span>
213
+ </>
214
+ )}
215
+ </text>
216
+ </Button>
217
+ </box>
218
+ )}
219
+ {!isStreaming && continueOnFallback && fallbackWarning && (
220
+ <text style={{ fg: theme.secondary, wrapMode: 'word' }}>
221
+ {`${fallbackModelName} ${fallbackWarning.toLowerCase()}.`}
222
+ </text>
223
+ )}
224
+ </box>
225
+ )
226
+ }
@@ -0,0 +1,248 @@
1
+ import { TextAttributes } from '@opentui/core'
2
+ import React, { useEffect, useMemo, useState } from 'react'
3
+
4
+ import { useTheme } from '../hooks/use-theme'
5
+
6
+ const clamp = (value: number, min: number, max: number): number =>
7
+ Math.min(max, Math.max(min, value))
8
+
9
+ const normalizeHex = (hex: string): string | null => {
10
+ const trimmed = hex.trim()
11
+ const withoutHash = trimmed.startsWith('#') ? trimmed.slice(1) : trimmed
12
+ if (withoutHash.length === 3) {
13
+ return withoutHash
14
+ .split('')
15
+ .map((char) => char + char)
16
+ .join('')
17
+ }
18
+ if (withoutHash.length === 6) {
19
+ return withoutHash
20
+ }
21
+ return null
22
+ }
23
+
24
+ const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => {
25
+ const normalized = normalizeHex(hex)
26
+ if (!normalized) return null
27
+ const r = parseInt(normalized.slice(0, 2), 16) / 255
28
+ const g = parseInt(normalized.slice(2, 4), 16) / 255
29
+ const b = parseInt(normalized.slice(4, 6), 16) / 255
30
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null
31
+ return { r, g, b }
32
+ }
33
+
34
+ const rgbToHsl = (
35
+ r: number,
36
+ g: number,
37
+ b: number,
38
+ ): { h: number; s: number; l: number } => {
39
+ const max = Math.max(r, g, b)
40
+ const min = Math.min(r, g, b)
41
+ const l = (max + min) / 2
42
+
43
+ let h = 0
44
+ let s = 0
45
+
46
+ if (max !== min) {
47
+ const d = max - min
48
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
49
+ switch (max) {
50
+ case r:
51
+ h = (g - b) / d + (g < b ? 6 : 0)
52
+ break
53
+ case g:
54
+ h = (b - r) / d + 2
55
+ break
56
+ default:
57
+ h = (r - g) / d + 4
58
+ }
59
+ h /= 6
60
+ }
61
+
62
+ return { h, s, l }
63
+ }
64
+
65
+ const hueToRgb = (p: number, q: number, t: number): number => {
66
+ let temp = t
67
+ if (temp < 0) temp += 1
68
+ if (temp > 1) temp -= 1
69
+ if (temp < 1 / 6) return p + (q - p) * 6 * temp
70
+ if (temp < 1 / 2) return q
71
+ if (temp < 2 / 3) return p + (q - p) * (2 / 3 - temp) * 6
72
+ return p
73
+ }
74
+
75
+ const hslToRgb = (
76
+ h: number,
77
+ s: number,
78
+ l: number,
79
+ ): { r: number; g: number; b: number } => {
80
+ if (s === 0) {
81
+ return { r: l, g: l, b: l }
82
+ }
83
+
84
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s
85
+ const p = 2 * l - q
86
+
87
+ return {
88
+ r: hueToRgb(p, q, h + 1 / 3),
89
+ g: hueToRgb(p, q, h),
90
+ b: hueToRgb(p, q, h - 1 / 3),
91
+ }
92
+ }
93
+
94
+ const rgbToHex = (r: number, g: number, b: number): string => {
95
+ const toHex = (value: number) =>
96
+ Math.round(clamp(value, 0, 1) * 255)
97
+ .toString(16)
98
+ .padStart(2, '0')
99
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`
100
+ }
101
+
102
+ const generatePaletteFromPrimary = (
103
+ primaryColor: string,
104
+ size: number,
105
+ fallbackColor: string,
106
+ ): string[] => {
107
+ const baseRgb = hexToRgb(primaryColor)
108
+ if (!baseRgb) {
109
+ // If we can't parse the color, return a simple palette using the fallback
110
+ return Array.from({ length: size }, () => fallbackColor)
111
+ }
112
+
113
+ const { h, s, l } = rgbToHsl(baseRgb.r, baseRgb.g, baseRgb.b)
114
+ const palette: string[] = []
115
+ const paletteSize = Math.max(6, Math.min(24, size))
116
+ const lightnessRange = 0.22
117
+
118
+ for (let i = 0; i < paletteSize; i++) {
119
+ const ratio = paletteSize === 1 ? 0.5 : i / (paletteSize - 1)
120
+ const offset = (0.5 - ratio) * 2 * lightnessRange
121
+ const adjustedLightness = clamp(l + offset, 0.08, 0.92)
122
+ // Keep full saturation for vibrant colors, only vary lightness
123
+ const adjustedSaturation = s
124
+ const { r, g, b } = hslToRgb(h, adjustedSaturation, adjustedLightness)
125
+ palette.push(rgbToHex(r, g, b))
126
+ }
127
+
128
+ return palette
129
+ }
130
+
131
+ export const ShimmerText = ({
132
+ text,
133
+ interval = 180,
134
+ colors,
135
+ primaryColor,
136
+ }: {
137
+ text: string
138
+ interval?: number
139
+ colors?: string[]
140
+ primaryColor?: string
141
+ }) => {
142
+ const theme = useTheme()
143
+ const [pulse, setPulse] = useState<number>(0)
144
+ const chars = text.split('')
145
+ const numChars = chars.length
146
+
147
+ useEffect(() => {
148
+ const pulseInterval = setInterval(() => {
149
+ setPulse((prev) => (prev + 1) % numChars)
150
+ }, interval)
151
+
152
+ return () => clearInterval(pulseInterval)
153
+ }, [interval, numChars])
154
+
155
+ const generateColors = (length: number, colorPalette: string[]): string[] => {
156
+ if (length === 0) return []
157
+ if (colorPalette.length === 0) {
158
+ return Array.from({ length }, () => theme.muted)
159
+ }
160
+ if (colorPalette.length === 1) {
161
+ return Array.from({ length }, () => colorPalette[0])
162
+ }
163
+ const generatedColors: string[] = []
164
+ for (let i = 0; i < length; i++) {
165
+ const ratio = length === 1 ? 0 : i / (length - 1)
166
+ const colorIndex = Math.min(
167
+ colorPalette.length - 1,
168
+ Math.floor(ratio * (colorPalette.length - 1)),
169
+ )
170
+ generatedColors.push(colorPalette[colorIndex])
171
+ }
172
+ return generatedColors
173
+ }
174
+
175
+ const palette = useMemo(() => {
176
+ if (colors && colors.length > 0) {
177
+ return colors
178
+ }
179
+ if (primaryColor) {
180
+ const paletteSize = Math.max(8, Math.min(20, Math.ceil(numChars * 1.5)))
181
+ return generatePaletteFromPrimary(primaryColor, paletteSize, theme.muted)
182
+ }
183
+ // Use theme shimmer color as default
184
+ const paletteSize = Math.max(8, Math.min(20, Math.ceil(numChars * 1.5)))
185
+ return generatePaletteFromPrimary(theme.info, paletteSize, theme.muted)
186
+ }, [colors, primaryColor, numChars, theme.info, theme.muted])
187
+
188
+ const generateAttributes = (length: number): number[] => {
189
+ const attributes: number[] = []
190
+ for (let i = 0; i < length; i++) {
191
+ const ratio = length <= 1 ? 0 : i / (length - 1)
192
+ if (ratio < 0.23) {
193
+ attributes.push(TextAttributes.BOLD)
194
+ } else if (ratio < 0.69) {
195
+ attributes.push(TextAttributes.NONE)
196
+ } else {
197
+ attributes.push(TextAttributes.DIM)
198
+ }
199
+ }
200
+ return attributes
201
+ }
202
+
203
+ const generatedColors = useMemo(
204
+ () => generateColors(numChars, palette),
205
+ [numChars, palette],
206
+ )
207
+ const attributes = useMemo(() => generateAttributes(numChars), [numChars])
208
+
209
+ const parts: { text: string; color: string; attr: number }[] = []
210
+ let currentColor = generatedColors[0]
211
+ let currentAttr = attributes[0]
212
+ let currentText = ''
213
+
214
+ chars.forEach((char, index) => {
215
+ const phase = (pulse - index + numChars) % numChars
216
+ const charColor = generatedColors[phase]
217
+ const charAttr = attributes[phase]
218
+
219
+ if (charColor === currentColor && charAttr === currentAttr) {
220
+ currentText += char
221
+ } else {
222
+ if (currentText) {
223
+ parts.push({
224
+ text: currentText,
225
+ color: currentColor,
226
+ attr: currentAttr,
227
+ })
228
+ }
229
+ currentText = char
230
+ currentColor = charColor
231
+ currentAttr = charAttr
232
+ }
233
+ })
234
+
235
+ if (currentText) {
236
+ parts.push({ text: currentText, color: currentColor, attr: currentAttr })
237
+ }
238
+
239
+ return (
240
+ <>
241
+ {parts.map((part, index) => (
242
+ <span key={index} fg={part.color} attributes={part.attr}>
243
+ {part.text}
244
+ </span>
245
+ ))}
246
+ </>
247
+ )
248
+ }