@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,80 @@
1
+ import React, { memo, type ReactNode } from 'react'
2
+
3
+ import { useGridLayout } from '../hooks/use-grid-layout'
4
+ import { MIN_COLUMN_WIDTH } from '../utils/layout-helpers'
5
+
6
+ export interface GridLayoutProps<T> {
7
+ items: T[]
8
+ availableWidth: number
9
+ getItemKey: (item: T) => string
10
+ renderItem: (item: T, index: number, columnWidth: number) => ReactNode
11
+ footer?: ReactNode
12
+ marginTop?: number
13
+ }
14
+
15
+ function GridLayoutInner<T>({
16
+ items,
17
+ availableWidth,
18
+ getItemKey,
19
+ renderItem,
20
+ footer,
21
+ marginTop = 0,
22
+ }: GridLayoutProps<T>): ReactNode {
23
+ const { columns, columnWidth, columnGroups } = useGridLayout(items, availableWidth)
24
+
25
+ if (items.length === 0) return null
26
+
27
+ // Unified structure for both single and multi-column layouts
28
+ // Using a consistent DOM structure prevents reconciliation issues during resize transitions
29
+ const isMultiColumn = columns > 1
30
+
31
+ return (
32
+ <box
33
+ style={{
34
+ flexDirection: 'column',
35
+ gap: isMultiColumn ? 1 : 0,
36
+ width: '100%',
37
+ marginTop,
38
+ }}
39
+ >
40
+ <box
41
+ style={{
42
+ flexDirection: 'row',
43
+ gap: isMultiColumn ? 1 : 0,
44
+ width: '100%',
45
+ alignItems: 'flex-start',
46
+ }}
47
+ >
48
+ {columnGroups.map((columnItems, colIdx) => {
49
+ const columnKey = columnItems[0]
50
+ ? getItemKey(columnItems[0])
51
+ : `col-${colIdx}`
52
+ return (
53
+ <box
54
+ key={columnKey}
55
+ style={{
56
+ flexDirection: 'column',
57
+ gap: 0,
58
+ flexGrow: 1,
59
+ flexShrink: 1,
60
+ flexBasis: 0,
61
+ // Use MIN_COLUMN_WIDTH instead of 0 to prevent columns from collapsing
62
+ // to zero during resize transitions (prevents 2→1 column transition bug)
63
+ minWidth: MIN_COLUMN_WIDTH,
64
+ }}
65
+ >
66
+ {columnItems.map((item, idx) => (
67
+ <box key={getItemKey(item)} style={{ width: '100%' }}>
68
+ {renderItem(item, idx, columnWidth)}
69
+ </box>
70
+ ))}
71
+ </box>
72
+ )
73
+ })}
74
+ </box>
75
+ {footer}
76
+ </box>
77
+ )
78
+ }
79
+
80
+ export const GridLayout = memo(GridLayoutInner) as typeof GridLayoutInner
@@ -0,0 +1,127 @@
1
+ import React from 'react'
2
+
3
+ import { BottomBanner } from './bottom-banner'
4
+ import { useSubscriptionQuery } from '../hooks/use-subscription-query'
5
+ import { useTheme } from '../hooks/use-theme'
6
+ import { IS_FREEBUFF } from '../utils/constants'
7
+ import { useChatStore } from '../state/chat-store'
8
+
9
+ const HELP_TIMEOUT = 60 * 1000 // 60 seconds
10
+
11
+ /** Section header component for consistent styling */
12
+ const SectionHeader = ({ children }: { children: React.ReactNode }) => {
13
+ const theme = useTheme()
14
+ return <text style={{ fg: theme.muted }}>{children}</text>
15
+ }
16
+
17
+ /** Keyboard shortcut item */
18
+ const Shortcut = ({
19
+ keys,
20
+ action,
21
+ }: {
22
+ keys: string
23
+ action: string
24
+ }) => {
25
+ const theme = useTheme()
26
+ return (
27
+ <box style={{ flexDirection: 'row', gap: 1 }}>
28
+ <text style={{ fg: theme.foreground }}>{keys}</text>
29
+ <text style={{ fg: theme.muted }}>{action}</text>
30
+ </box>
31
+ )
32
+ }
33
+
34
+ /** Help banner showing keyboard shortcuts and tips in an organized layout. */
35
+ export const HelpBanner = () => {
36
+ const setInputMode = useChatStore((state) => state.setInputMode)
37
+ const theme = useTheme()
38
+ const { data: subscriptionData } = useSubscriptionQuery()
39
+ const hasSubscription = subscriptionData?.hasSubscription ?? false
40
+
41
+ // Auto-hide after timeout
42
+ React.useEffect(() => {
43
+ const timer = setTimeout(() => {
44
+ setInputMode('default')
45
+ }, HELP_TIMEOUT)
46
+ return () => clearTimeout(timer)
47
+ }, [setInputMode])
48
+
49
+ return (
50
+ <BottomBanner
51
+ borderColorKey="info"
52
+ onClose={() => setInputMode('default')}
53
+ >
54
+ <box style={{ flexDirection: 'column', gap: 1, flexGrow: 1 }}>
55
+ {/* Shortcuts Section */}
56
+ <box style={{ flexDirection: 'column', gap: 0 }}>
57
+ <SectionHeader>Shortcuts</SectionHeader>
58
+ <box style={{ flexDirection: 'row', flexWrap: 'wrap', columnGap: 2, paddingLeft: 2 }}>
59
+ <Shortcut keys="Ctrl+C / Esc" action="stop" />
60
+ <Shortcut keys="Ctrl+J / Opt+Enter" action="newline" />
61
+ <Shortcut keys="↑↓" action="history" />
62
+ <Shortcut keys="Ctrl+T" action="collapse/expand agents" />
63
+ <Shortcut keys="Ctrl+Q" action="edit queued messages" />
64
+ </box>
65
+ </box>
66
+
67
+ {/* Features Section */}
68
+ <box style={{ flexDirection: 'column', gap: 0 }}>
69
+ <SectionHeader>Features</SectionHeader>
70
+ <box style={{ flexDirection: 'row', flexWrap: 'wrap', columnGap: 2, paddingLeft: 2 }}>
71
+ <Shortcut keys="/" action="commands" />
72
+ <Shortcut keys="@files" action="mention" />
73
+ <Shortcut keys="@agents" action="use agent" />
74
+ <Shortcut keys="!bash" action="run command" />
75
+ </box>
76
+ </box>
77
+
78
+ {/* Tips Section */}
79
+ <box style={{ flexDirection: 'column', gap: 0 }}>
80
+ <SectionHeader>Tips</SectionHeader>
81
+ <box style={{ flexDirection: 'column', paddingLeft: 2 }}>
82
+ {IS_FREEBUFF && (
83
+ <text style={{ fg: theme.muted }}>
84
+ Try workflow: /interview → /plan → implement → /review
85
+ </text>
86
+ )}
87
+ <text style={{ fg: theme.muted }}>
88
+ Use @ to reference agents to spawn or files to read
89
+ </text>
90
+ <text style={{ fg: theme.muted }}>
91
+ Drag to select text — it copies automatically (or click ⎘ on a
92
+ message)
93
+ </text>
94
+ <text style={{ fg: theme.muted }}>
95
+ Esc to cancel the current response
96
+ </text>
97
+ </box>
98
+ </box>
99
+
100
+ {/* Credits Section — hidden in Freebuff */}
101
+ {!IS_FREEBUFF && (
102
+ <box style={{ flexDirection: 'column', gap: 0 }}>
103
+ <SectionHeader>Credits</SectionHeader>
104
+ <box style={{ flexDirection: 'column', paddingLeft: 2 }}>
105
+ <box style={{ flexDirection: 'row', flexWrap: 'wrap', columnGap: 1 }}>
106
+ <text style={{ fg: theme.foreground }}>1 credit = 1 cent</text>
107
+ <text style={{ fg: theme.muted }}>·</text>
108
+ <text style={{ fg: theme.foreground }}>/subscribe</text>
109
+ <text style={{ fg: theme.muted }}>·</text>
110
+ <text style={{ fg: theme.foreground }}>/usage</text>
111
+ {!hasSubscription && (
112
+ <>
113
+ <text style={{ fg: theme.muted }}>·</text>
114
+ <text style={{ fg: theme.foreground }}>/ads:enable</text>
115
+ </>
116
+ )}
117
+ </box>
118
+ <text style={{ fg: theme.muted }}>
119
+ Subscribe for the best credit rates — /subscribe
120
+ </text>
121
+ </box>
122
+ </box>
123
+ )}
124
+ </box>
125
+ </BottomBanner>
126
+ )
127
+ }
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+
3
+ interface HighlightedSubsequenceTextProps {
4
+ text: string
5
+ indices?: number[] | null
6
+ color: string
7
+ highlightColor: string
8
+ }
9
+
10
+ export const HighlightedSubsequenceText = ({
11
+ text,
12
+ indices,
13
+ color,
14
+ highlightColor,
15
+ }: HighlightedSubsequenceTextProps) => {
16
+ if (!indices || indices.length === 0) {
17
+ return <span fg={color}>{text}</span>
18
+ }
19
+
20
+ const parts: React.ReactNode[] = []
21
+ const highlightSet = new Set(indices)
22
+ let currentSegment = ''
23
+ let isHighlighted = false
24
+
25
+ for (let i = 0; i < text.length; i++) {
26
+ const shouldHighlight = highlightSet.has(i)
27
+
28
+ if (shouldHighlight !== isHighlighted) {
29
+ if (currentSegment) {
30
+ parts.push(
31
+ <span key={parts.length} fg={isHighlighted ? highlightColor : color}>
32
+ {currentSegment}
33
+ </span>,
34
+ )
35
+ }
36
+ currentSegment = text[i]
37
+ isHighlighted = shouldHighlight
38
+ } else {
39
+ currentSegment += text[i]
40
+ }
41
+ }
42
+
43
+ if (currentSegment) {
44
+ parts.push(
45
+ <span key={parts.length} fg={isHighlighted ? highlightColor : color}>
46
+ {currentSegment}
47
+ </span>,
48
+ )
49
+ }
50
+
51
+ return <>{parts}</>
52
+ }
@@ -0,0 +1,167 @@
1
+ import fs from 'fs'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ import { AttachmentCard } from './attachment-card'
6
+ import { ImageThumbnail } from './image-thumbnail'
7
+ import { useTheme } from '../hooks/use-theme'
8
+ import {
9
+ supportsInlineImages,
10
+ renderInlineImage,
11
+ } from '../utils/terminal-images'
12
+
13
+ // Image card display constants
14
+ const MAX_FILENAME_LENGTH = 16
15
+ const IMAGE_CARD_WIDTH = 18
16
+ const THUMBNAIL_WIDTH = 14
17
+ const THUMBNAIL_HEIGHT = 3
18
+ const INLINE_IMAGE_WIDTH = 4
19
+ const INLINE_IMAGE_HEIGHT = 3
20
+
21
+ const truncateFilename = (filename: string): string => {
22
+ if (filename.length <= MAX_FILENAME_LENGTH) {
23
+ return filename
24
+ }
25
+ const lastDot = filename.lastIndexOf('.')
26
+ const ext = lastDot !== -1 ? filename.slice(lastDot) : ''
27
+ const baseName = lastDot !== -1 ? filename.slice(0, lastDot) : filename
28
+ const maxBaseLength = MAX_FILENAME_LENGTH - ext.length - 1 // -1 for ellipsis
29
+ return baseName.slice(0, maxBaseLength) + '…' + ext
30
+ }
31
+
32
+ export interface ImageCardImage {
33
+ path: string
34
+ filename: string
35
+ status?: 'processing' | 'ready' | 'error' // Defaults to 'ready' if not provided
36
+ note?: string // Display note: 'compressed' | error message
37
+ processedImage?: {
38
+ base64: string
39
+ mediaType: string
40
+ }
41
+ }
42
+
43
+ interface ImageCardProps {
44
+ image: ImageCardImage
45
+ onRemove?: () => void
46
+ showRemoveButton?: boolean
47
+ }
48
+
49
+ export const ImageCard = ({
50
+ image,
51
+ onRemove,
52
+ showRemoveButton = true,
53
+ }: ImageCardProps) => {
54
+ const theme = useTheme()
55
+ const [thumbnailSequence, setThumbnailSequence] = useState<string | null>(
56
+ null,
57
+ )
58
+ const canShowInlineImages = supportsInlineImages()
59
+
60
+ // Load thumbnail if terminal supports inline images (iTerm2/Kitty)
61
+ useEffect(() => {
62
+ if (!canShowInlineImages) return
63
+ // Skip loading while image is processing or has error to avoid race condition and unnecessary failed reads
64
+ if ((image.status ?? 'ready') !== 'ready') return
65
+
66
+ let cancelled = false
67
+
68
+ const loadThumbnail = async () => {
69
+ try {
70
+ let base64Data: string | undefined
71
+
72
+ if (image.processedImage) {
73
+ base64Data = image.processedImage.base64
74
+ } else if (!image.path.startsWith('clipboard:')) {
75
+ const imageData = fs.readFileSync(image.path)
76
+ base64Data = imageData.toString('base64')
77
+ }
78
+
79
+ if (base64Data) {
80
+ const sequence = renderInlineImage(base64Data, {
81
+ width: INLINE_IMAGE_WIDTH,
82
+ height: INLINE_IMAGE_HEIGHT,
83
+ filename: image.filename,
84
+ })
85
+ if (!cancelled) {
86
+ setThumbnailSequence(sequence)
87
+ }
88
+ } else {
89
+ if (!cancelled) {
90
+ setThumbnailSequence(null)
91
+ }
92
+ }
93
+ } catch {
94
+ // Failed to load image, will show icon fallback
95
+ if (!cancelled) {
96
+ setThumbnailSequence(null)
97
+ }
98
+ }
99
+ }
100
+
101
+ loadThumbnail()
102
+
103
+ return () => {
104
+ cancelled = true
105
+ }
106
+ }, [image, image.filename, canShowInlineImages])
107
+
108
+ const truncatedName = truncateFilename(image.filename)
109
+
110
+ return (
111
+ <AttachmentCard
112
+ width={IMAGE_CARD_WIDTH}
113
+ onRemove={onRemove}
114
+ showRemoveButton={showRemoveButton}
115
+ >
116
+ {/* Thumbnail or icon area */}
117
+ <box
118
+ style={{
119
+ height: THUMBNAIL_HEIGHT,
120
+ justifyContent: 'center',
121
+ alignItems: 'center',
122
+ }}
123
+ >
124
+ {thumbnailSequence ? (
125
+ <text>{thumbnailSequence}</text>
126
+ ) : (
127
+ <ImageThumbnail
128
+ image={image}
129
+ width={THUMBNAIL_WIDTH}
130
+ height={THUMBNAIL_HEIGHT}
131
+ fallback={<text style={{ fg: theme.info }}>🖼️</text>}
132
+ />
133
+ )}
134
+ </box>
135
+
136
+ {/* Filename - full width */}
137
+ <box
138
+ style={{
139
+ paddingLeft: 1,
140
+ paddingRight: 1,
141
+ flexDirection: 'column',
142
+ }}
143
+ >
144
+ <text
145
+ style={{
146
+ fg: theme.foreground,
147
+ wrapMode: 'none',
148
+ }}
149
+ >
150
+ {truncatedName}
151
+ </text>
152
+ {((image.status ?? 'ready') === 'processing' || image.note) && (
153
+ <text
154
+ style={{
155
+ fg: theme.muted,
156
+ wrapMode: 'none',
157
+ }}
158
+ >
159
+ {(image.status ?? 'ready') === 'processing'
160
+ ? 'processing…'
161
+ : image.note}
162
+ </text>
163
+ )}
164
+ </box>
165
+ </AttachmentCard>
166
+ )
167
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Image Thumbnail Component
3
+ * Renders a small image preview using colored Unicode half-blocks
4
+ * Uses OpenTUI's native fg/backgroundColor styling instead of ANSI escape sequences
5
+ */
6
+
7
+ import React, { useEffect, useState, memo } from 'react'
8
+
9
+ import { type ImageCardImage } from './image-card'
10
+ import {
11
+ extractThumbnailColors,
12
+ rgbToHex,
13
+ type ThumbnailData,
14
+ } from '../utils/image-thumbnail'
15
+
16
+ interface ImageThumbnailProps {
17
+ image: ImageCardImage
18
+ width: number // Width in cells
19
+ height: number // Height in rows (each row uses half-blocks for 2 pixel rows)
20
+ fallback?: React.ReactNode
21
+ }
22
+
23
+ /**
24
+ * Renders an image as colored blocks using Unicode half-blocks (▀)
25
+ * Each character cell displays 2 vertical pixels by using:
26
+ * - Foreground color for top pixel
27
+ * - Background color for bottom pixel
28
+ * - ▀ (upper half block) character
29
+ */
30
+ export const ImageThumbnail = memo(({
31
+ image,
32
+ width,
33
+ height,
34
+ fallback,
35
+ }: ImageThumbnailProps) => {
36
+ const [thumbnailData, setThumbnailData] = useState<ThumbnailData | null>(null)
37
+
38
+ useEffect(() => {
39
+ // Skip loading while image is processing or has error to avoid race condition and unnecessary failed reads
40
+ if ((image.status ?? 'ready') !== 'ready') return
41
+
42
+ let cancelled = false
43
+
44
+ const loadThumbnail = async () => {
45
+ let data: ThumbnailData | null = null
46
+ try {
47
+ if (image.processedImage) {
48
+ const imageBuffer = Buffer.from(image.processedImage.base64, 'base64')
49
+ data = await extractThumbnailColors(imageBuffer, width, height)
50
+ } else if (!image.path.startsWith('clipboard:')) {
51
+ data = await extractThumbnailColors(image.path, width, height)
52
+ }
53
+ } catch {
54
+ // Ignore errors, will show fallback
55
+ }
56
+
57
+ if (!cancelled) {
58
+ setThumbnailData(data)
59
+ }
60
+ }
61
+
62
+ loadThumbnail()
63
+
64
+ return () => {
65
+ cancelled = true
66
+ }
67
+ }, [image, width, height])
68
+
69
+ if (!thumbnailData) {
70
+ return <>{fallback}</>
71
+ }
72
+
73
+ // Render the thumbnail using half-blocks
74
+ // Each row of our output combines 2 pixel rows from the image
75
+ const rows: React.ReactNode[] = []
76
+
77
+ for (let rowIndex = 0; rowIndex < thumbnailData.height; rowIndex += 2) {
78
+ const topRow = thumbnailData.pixels[rowIndex]
79
+ const bottomRow = thumbnailData.pixels[rowIndex + 1] || topRow // Use top row if no bottom
80
+
81
+ const cells: React.ReactNode[] = []
82
+
83
+ for (let col = 0; col < thumbnailData.width; col++) {
84
+ const topPixel = topRow[col]
85
+ const bottomPixel = bottomRow[col]
86
+
87
+ const fgColor = rgbToHex(topPixel.r, topPixel.g, topPixel.b)
88
+ const bgColor = rgbToHex(bottomPixel.r, bottomPixel.g, bottomPixel.b)
89
+
90
+ cells.push(
91
+ <box
92
+ key={col}
93
+ style={{
94
+ backgroundColor: bgColor,
95
+ }}
96
+ >
97
+ <text style={{ fg: fgColor }}>▀</text>
98
+ </box>
99
+ )
100
+ }
101
+
102
+ rows.push(
103
+ <box key={rowIndex} style={{ flexDirection: 'row' }}>
104
+ {cells}
105
+ </box>
106
+ )
107
+ }
108
+
109
+ return (
110
+ <box style={{ flexDirection: 'column' }}>
111
+ {rows}
112
+ </box>
113
+ )
114
+ })
@@ -0,0 +1,77 @@
1
+ import { TextAttributes } from '@opentui/core'
2
+ import React, { useEffect, useRef, useState } from 'react'
3
+
4
+ interface InputCursorProps {
5
+ visible: boolean
6
+ focused: boolean
7
+ shouldBlink?: boolean
8
+ char?: string
9
+ color?: string
10
+ blinkDelay?: number
11
+ blinkInterval?: number
12
+ bold?: boolean
13
+ }
14
+
15
+ export const InputCursor: React.FC<InputCursorProps> = ({
16
+ visible,
17
+ focused,
18
+ shouldBlink = true,
19
+ char = '▍',
20
+ color,
21
+ blinkDelay = 500,
22
+ blinkInterval = 500, // Faster blinking
23
+ bold = true,
24
+ }) => {
25
+ // false = normal/visible, true = invisible
26
+ const [isInvisible, setIsInvisible] = useState(false)
27
+ const blinkIntervalRef = useRef<NodeJS.Timeout | null>(null)
28
+
29
+ // Handle blinking (toggle visible/invisible) when idle
30
+ useEffect(() => {
31
+ // Clear any existing interval
32
+ if (blinkIntervalRef.current) {
33
+ clearInterval(blinkIntervalRef.current)
34
+ blinkIntervalRef.current = null
35
+ }
36
+
37
+ // Reset cursor to visible
38
+ setIsInvisible(false)
39
+
40
+ // Only blink if shouldBlink is enabled, focused, and visible
41
+ if (!shouldBlink || !focused || !visible) return
42
+
43
+ // Set up idle detection
44
+ const idleTimer = setTimeout(() => {
45
+ // Start blinking interval (toggle between visible and invisible)
46
+ blinkIntervalRef.current = setInterval(() => {
47
+ setIsInvisible((prev) => !prev)
48
+ }, blinkInterval)
49
+ }, blinkDelay)
50
+
51
+ return () => {
52
+ clearTimeout(idleTimer)
53
+ if (blinkIntervalRef.current) {
54
+ clearInterval(blinkIntervalRef.current)
55
+ blinkIntervalRef.current = null
56
+ }
57
+ }
58
+ }, [visible, focused, shouldBlink, blinkDelay, blinkInterval])
59
+
60
+ if (!visible || !focused) {
61
+ return null
62
+ }
63
+
64
+ // When invisible, return a space to maintain layout
65
+ if (isInvisible) {
66
+ return <span> </span>
67
+ }
68
+
69
+ return (
70
+ <span
71
+ {...(color ? { fg: color } : undefined)}
72
+ {...(bold ? { attributes: TextAttributes.BOLD } : undefined)}
73
+ >
74
+ {char}
75
+ </span>
76
+ )
77
+ }
@@ -0,0 +1,57 @@
1
+ import React from 'react'
2
+ import { IS_FREEBUFF } from '../utils/constants'
3
+
4
+ import { HelpBanner } from './help-banner'
5
+ import { PendingAttachmentsBanner } from './pending-attachments-banner'
6
+ import { SubscriptionLimitBanner } from './subscription-limit-banner'
7
+ import { UsageBanner } from './usage-banner'
8
+ import { useChatStore } from '../state/chat-store'
9
+
10
+ /**
11
+ * Registry mapping input modes to their banner components.
12
+ *
13
+ * To add a new banner:
14
+ * 1. Create the banner component using BottomBanner
15
+ * 2. Add an entry here mapping the input mode to a render function
16
+ *
17
+ * Render functions receive context (like showTime) and return the component.
18
+ */
19
+ const BANNER_REGISTRY: Record<
20
+ string,
21
+ (ctx: { showTime: number }) => React.ReactNode
22
+ > = {
23
+ default: () => <PendingAttachmentsBanner />,
24
+ image: () => <PendingAttachmentsBanner />,
25
+ ...(IS_FREEBUFF ? {} : { usage: ({ showTime }: { showTime: number }) => <UsageBanner showTime={showTime} /> }),
26
+ help: () => <HelpBanner />,
27
+ ...(IS_FREEBUFF ? {} : { subscriptionLimit: () => <SubscriptionLimitBanner /> }),
28
+ }
29
+
30
+ /**
31
+ * Banner component that shows contextual information below the input box.
32
+ * Shows mode-specific banners based on the current input mode.
33
+ *
34
+ * Uses a registry pattern for easy extensibility - add new banners by
35
+ * updating BANNER_REGISTRY above.
36
+ */
37
+ export const InputModeBanner = () => {
38
+ const inputMode = useChatStore((state) => state.inputMode)
39
+
40
+ const [usageBannerShowTime, setUsageBannerShowTime] = React.useState(() =>
41
+ Date.now(),
42
+ )
43
+
44
+ React.useEffect(() => {
45
+ if (inputMode === 'usage') {
46
+ setUsageBannerShowTime(Date.now())
47
+ }
48
+ }, [inputMode])
49
+
50
+ const renderBanner = BANNER_REGISTRY[inputMode]
51
+
52
+ if (!renderBanner) {
53
+ return null
54
+ }
55
+
56
+ return <>{renderBanner({ showTime: usageBannerShowTime })}</>
57
+ }