@adecore/agents-react 0.0.1 → 0.17.0-beta.1

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 (513) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +53 -0
  3. package/dist/agents/AccountDot.d.ts +4 -0
  4. package/dist/agents/AccountDot.js +8 -0
  5. package/dist/agents/AgentIcon.d.ts +6 -0
  6. package/dist/agents/AgentIcon.js +20 -0
  7. package/dist/agents/ModelOptionControl.d.ts +15 -0
  8. package/dist/agents/ModelOptionControl.js +33 -0
  9. package/dist/agents/ProviderLogo.d.ts +8 -0
  10. package/dist/agents/ProviderLogo.js +8 -0
  11. package/dist/agents/account-limits.d.ts +7 -0
  12. package/dist/agents/account-limits.js +40 -0
  13. package/dist/agents/accounts.d.ts +22 -0
  14. package/dist/agents/accounts.js +113 -0
  15. package/dist/agents/model-name.d.ts +12 -0
  16. package/dist/agents/model-name.js +56 -0
  17. package/dist/agents/model-options.d.ts +5 -0
  18. package/dist/agents/model-options.js +15 -0
  19. package/dist/agents/provider-paths.d.ts +2 -0
  20. package/dist/agents/provider-paths.js +8 -0
  21. package/dist/agents/status-look.d.ts +13 -0
  22. package/dist/agents/status-look.js +24 -0
  23. package/dist/chat/account-choice.d.ts +11 -0
  24. package/dist/chat/account-choice.js +30 -0
  25. package/dist/chat/actions.d.ts +6 -0
  26. package/dist/chat/actions.js +68 -0
  27. package/dist/chat/activity.d.ts +5 -0
  28. package/dist/chat/activity.js +6 -0
  29. package/dist/chat/attachments.d.ts +23 -0
  30. package/dist/chat/attachments.js +86 -0
  31. package/dist/chat/bookmarks.d.ts +14 -0
  32. package/dist/chat/bookmarks.js +73 -0
  33. package/dist/chat/chat-client.d.ts +55 -0
  34. package/dist/chat/chat-client.js +279 -0
  35. package/dist/chat/chat-references.d.ts +5 -0
  36. package/dist/chat/chat-references.js +5 -0
  37. package/dist/chat/drafts.d.ts +29 -0
  38. package/dist/chat/drafts.js +116 -0
  39. package/dist/chat/forks.d.ts +2 -0
  40. package/dist/chat/forks.js +28 -0
  41. package/dist/chat/guards.d.ts +12 -0
  42. package/dist/chat/guards.js +69 -0
  43. package/dist/chat/logic/ansi.d.ts +13 -0
  44. package/dist/chat/logic/ansi.js +185 -0
  45. package/dist/chat/logic/answers.d.ts +1 -0
  46. package/dist/chat/logic/answers.js +8 -0
  47. package/dist/chat/logic/bookmarks.d.ts +5 -0
  48. package/dist/chat/logic/bookmarks.js +33 -0
  49. package/dist/chat/logic/context-usage.d.ts +12 -0
  50. package/dist/chat/logic/context-usage.js +21 -0
  51. package/dist/chat/logic/fork.d.ts +45 -0
  52. package/dist/chat/logic/fork.js +148 -0
  53. package/dist/chat/logic/handback.d.ts +2 -0
  54. package/dist/chat/logic/handback.js +8 -0
  55. package/dist/chat/logic/json.d.ts +1 -0
  56. package/dist/chat/logic/json.js +4 -0
  57. package/dist/chat/logic/limit.d.ts +7 -0
  58. package/dist/chat/logic/limit.js +34 -0
  59. package/dist/chat/logic/resume-compaction.d.ts +14 -0
  60. package/dist/chat/logic/resume-compaction.js +47 -0
  61. package/dist/chat/logic/scrubber.d.ts +50 -0
  62. package/dist/chat/logic/scrubber.js +220 -0
  63. package/dist/chat/logic/thread-cards.d.ts +3 -0
  64. package/dist/chat/logic/thread-cards.js +59 -0
  65. package/dist/chat/logic/timeline-copy.d.ts +4 -0
  66. package/dist/chat/logic/timeline-copy.js +59 -0
  67. package/dist/chat/logic/timeline-target.d.ts +12 -0
  68. package/dist/chat/logic/timeline-target.js +34 -0
  69. package/dist/chat/logic/timeline.d.ts +115 -0
  70. package/dist/chat/logic/timeline.js +343 -0
  71. package/dist/chat/logic/tool-catalog.d.ts +10 -0
  72. package/dist/chat/logic/tool-catalog.js +44 -0
  73. package/dist/chat/logic/tools.d.ts +15 -0
  74. package/dist/chat/logic/tools.js +82 -0
  75. package/dist/chat/logic/welcome.d.ts +2 -0
  76. package/dist/chat/logic/welcome.js +10 -0
  77. package/dist/chat/logic/workflow.d.ts +14 -0
  78. package/dist/chat/logic/workflow.js +47 -0
  79. package/dist/chat/mentions.d.ts +65 -0
  80. package/dist/chat/mentions.js +184 -0
  81. package/dist/chat/persisted-json.d.ts +5 -0
  82. package/dist/chat/persisted-json.js +58 -0
  83. package/dist/chat/preferences.d.ts +27 -0
  84. package/dist/chat/preferences.js +139 -0
  85. package/dist/chat/quote.d.ts +2 -0
  86. package/dist/chat/quote.js +25 -0
  87. package/dist/chat/recent-messages.d.ts +17 -0
  88. package/dist/chat/recent-messages.js +36 -0
  89. package/dist/chat/resume-compaction-dismissals.d.ts +3 -0
  90. package/dist/chat/resume-compaction-dismissals.js +14 -0
  91. package/dist/chat/runtime-modes.d.ts +4 -0
  92. package/dist/chat/runtime-modes.js +15 -0
  93. package/dist/chat/stash.d.ts +24 -0
  94. package/dist/chat/stash.js +91 -0
  95. package/dist/chat/subagent-conversation.d.ts +41 -0
  96. package/dist/chat/subagent-conversation.js +167 -0
  97. package/dist/chat/subagent-list.d.ts +19 -0
  98. package/dist/chat/subagent-list.js +106 -0
  99. package/dist/chat/subagent-support.d.ts +6 -0
  100. package/dist/chat/subagent-support.js +14 -0
  101. package/dist/chat/subagent-view.d.ts +35 -0
  102. package/dist/chat/subagent-view.js +96 -0
  103. package/dist/chat/thumbnails.d.ts +10 -0
  104. package/dist/chat/thumbnails.js +89 -0
  105. package/dist/chat/timeline-flash.d.ts +12 -0
  106. package/dist/chat/timeline-flash.js +16 -0
  107. package/dist/chat/timeline-scroll.d.ts +27 -0
  108. package/dist/chat/timeline-scroll.js +152 -0
  109. package/dist/chat/ui/AccountPill.d.ts +3 -0
  110. package/dist/chat/ui/AccountPill.js +23 -0
  111. package/dist/chat/ui/AnsiOutput.d.ts +5 -0
  112. package/dist/chat/ui/AnsiOutput.js +9 -0
  113. package/dist/chat/ui/BookmarkMarker.d.ts +8 -0
  114. package/dist/chat/ui/BookmarkMarker.js +57 -0
  115. package/dist/chat/ui/BookmarkSubmenu.d.ts +3 -0
  116. package/dist/chat/ui/BookmarkSubmenu.js +33 -0
  117. package/dist/chat/ui/ChatActivity.d.ts +7 -0
  118. package/dist/chat/ui/ChatActivity.js +84 -0
  119. package/dist/chat/ui/ChatReferenceChip.d.ts +4 -0
  120. package/dist/chat/ui/ChatReferenceChip.js +16 -0
  121. package/dist/chat/ui/CodeBlock.d.ts +4 -0
  122. package/dist/chat/ui/CodeBlock.js +132 -0
  123. package/dist/chat/ui/Composer.d.ts +15 -0
  124. package/dist/chat/ui/Composer.js +856 -0
  125. package/dist/chat/ui/ComposerInput.d.ts +29 -0
  126. package/dist/chat/ui/ComposerInput.js +127 -0
  127. package/dist/chat/ui/DiffPool.d.ts +5 -0
  128. package/dist/chat/ui/DiffPool.js +26 -0
  129. package/dist/chat/ui/EditDiff.d.ts +4 -0
  130. package/dist/chat/ui/EditDiff.js +21 -0
  131. package/dist/chat/ui/FadingWords.d.ts +3 -0
  132. package/dist/chat/ui/FadingWords.js +5 -0
  133. package/dist/chat/ui/ImageView.d.ts +6 -0
  134. package/dist/chat/ui/ImageView.js +81 -0
  135. package/dist/chat/ui/LimitState.d.ts +8 -0
  136. package/dist/chat/ui/LimitState.js +112 -0
  137. package/dist/chat/ui/Markdown.d.ts +18 -0
  138. package/dist/chat/ui/Markdown.js +159 -0
  139. package/dist/chat/ui/MessageActions.d.ts +9 -0
  140. package/dist/chat/ui/MessageActions.js +53 -0
  141. package/dist/chat/ui/Pickers.d.ts +21 -0
  142. package/dist/chat/ui/Pickers.js +111 -0
  143. package/dist/chat/ui/PromptComposer.d.ts +13 -0
  144. package/dist/chat/ui/PromptComposer.js +56 -0
  145. package/dist/chat/ui/QuoteButton.d.ts +7 -0
  146. package/dist/chat/ui/QuoteButton.js +74 -0
  147. package/dist/chat/ui/ResumeCompactionDock.d.ts +5 -0
  148. package/dist/chat/ui/ResumeCompactionDock.js +14 -0
  149. package/dist/chat/ui/RunSettings.d.ts +28 -0
  150. package/dist/chat/ui/RunSettings.js +153 -0
  151. package/dist/chat/ui/Scrubber.d.ts +17 -0
  152. package/dist/chat/ui/Scrubber.js +118 -0
  153. package/dist/chat/ui/SubagentControls.d.ts +11 -0
  154. package/dist/chat/ui/SubagentControls.js +36 -0
  155. package/dist/chat/ui/SubagentInfo.d.ts +4 -0
  156. package/dist/chat/ui/SubagentInfo.js +34 -0
  157. package/dist/chat/ui/SubagentStopButton.d.ts +7 -0
  158. package/dist/chat/ui/SubagentStopButton.js +34 -0
  159. package/dist/chat/ui/SubagentTimeline.d.ts +4 -0
  160. package/dist/chat/ui/SubagentTimeline.js +107 -0
  161. package/dist/chat/ui/Timeline.d.ts +6 -0
  162. package/dist/chat/ui/Timeline.js +424 -0
  163. package/dist/chat/ui/TimelineMenu.d.ts +12 -0
  164. package/dist/chat/ui/TimelineMenu.js +54 -0
  165. package/dist/chat/ui/UnifiedDiff.d.ts +11 -0
  166. package/dist/chat/ui/UnifiedDiff.js +66 -0
  167. package/dist/chat/ui/UploadThumb.d.ts +4 -0
  168. package/dist/chat/ui/UploadThumb.js +11 -0
  169. package/dist/chat/ui/Welcome.d.ts +4 -0
  170. package/dist/chat/ui/Welcome.js +16 -0
  171. package/dist/chat/ui/chips.d.ts +4 -0
  172. package/dist/chat/ui/chips.js +11 -0
  173. package/dist/chat/ui/code-lines.d.ts +18 -0
  174. package/dist/chat/ui/code-lines.js +46 -0
  175. package/dist/chat/ui/code-streaming.d.ts +1 -0
  176. package/dist/chat/ui/code-streaming.js +3 -0
  177. package/dist/chat/ui/code-theme.d.ts +3 -0
  178. package/dist/chat/ui/code-theme.js +12 -0
  179. package/dist/chat/ui/composer/chips.d.ts +6 -0
  180. package/dist/chat/ui/composer/chips.js +31 -0
  181. package/dist/chat/ui/composer/editor.d.ts +11 -0
  182. package/dist/chat/ui/composer/editor.js +98 -0
  183. package/dist/chat/ui/composer/insert.d.ts +2 -0
  184. package/dist/chat/ui/composer/insert.js +19 -0
  185. package/dist/chat/ui/composer/keys.d.ts +30 -0
  186. package/dist/chat/ui/composer/keys.js +100 -0
  187. package/dist/chat/ui/diff-theme.d.ts +2 -0
  188. package/dist/chat/ui/diff-theme.js +20 -0
  189. package/dist/chat/ui/file-links.d.ts +5 -0
  190. package/dist/chat/ui/file-links.js +20 -0
  191. package/dist/chat/ui/find-reveal.d.ts +7 -0
  192. package/dist/chat/ui/find-reveal.js +13 -0
  193. package/dist/chat/ui/full-diff.d.ts +6 -0
  194. package/dist/chat/ui/full-diff.js +19 -0
  195. package/dist/chat/ui/icons.d.ts +4 -0
  196. package/dist/chat/ui/icons.js +20 -0
  197. package/dist/chat/ui/markdown-blocks.d.ts +6 -0
  198. package/dist/chat/ui/markdown-blocks.js +103 -0
  199. package/dist/chat/ui/quote-selection.d.ts +3 -0
  200. package/dist/chat/ui/quote-selection.js +46 -0
  201. package/dist/chat/ui/rehype-chips.d.ts +23 -0
  202. package/dist/chat/ui/rehype-chips.js +32 -0
  203. package/dist/chat/ui/rehype-fade.d.ts +23 -0
  204. package/dist/chat/ui/rehype-fade.js +36 -0
  205. package/dist/chat/ui/remark-html-as-text.d.ts +8 -0
  206. package/dist/chat/ui/remark-html-as-text.js +26 -0
  207. package/dist/chat/ui/reply-context.d.ts +5 -0
  208. package/dist/chat/ui/reply-context.js +2 -0
  209. package/dist/chat/ui/reveal.d.ts +13 -0
  210. package/dist/chat/ui/reveal.js +114 -0
  211. package/dist/chat/ui/rows/ForksRow.d.ts +4 -0
  212. package/dist/chat/ui/rows/ForksRow.js +38 -0
  213. package/dist/chat/ui/rows/MessageRows.d.ts +40 -0
  214. package/dist/chat/ui/rows/MessageRows.js +204 -0
  215. package/dist/chat/ui/rows/Rows.d.ts +12 -0
  216. package/dist/chat/ui/rows/Rows.js +61 -0
  217. package/dist/chat/ui/rows/SubagentRow.d.ts +17 -0
  218. package/dist/chat/ui/rows/SubagentRow.js +101 -0
  219. package/dist/chat/ui/rows/TaskRow.d.ts +8 -0
  220. package/dist/chat/ui/rows/TaskRow.js +63 -0
  221. package/dist/chat/ui/rows/WorkRows.d.ts +47 -0
  222. package/dist/chat/ui/rows/WorkRows.js +168 -0
  223. package/dist/chat/ui/rows/WorkflowRow.d.ts +8 -0
  224. package/dist/chat/ui/rows/WorkflowRow.js +48 -0
  225. package/dist/chat/ui/rows/row-rhythm.d.ts +6 -0
  226. package/dist/chat/ui/rows/row-rhythm.js +34 -0
  227. package/dist/chat/ui/useToggleSet.d.ts +6 -0
  228. package/dist/chat/ui/useToggleSet.js +25 -0
  229. package/dist/host.d.ts +195 -0
  230. package/dist/host.js +55 -0
  231. package/dist/lazy.d.ts +6 -0
  232. package/dist/lazy.js +57 -0
  233. package/dist/locales/en/agent-chat.json +446 -0
  234. package/dist/locales/en/agent-prompts.json +42 -0
  235. package/dist/locales/en/agent-providers.json +131 -0
  236. package/dist/locales/en/agent-usage.json +131 -0
  237. package/dist/locales/nl/agent-chat.json +446 -0
  238. package/dist/locales/nl/agent-prompts.json +42 -0
  239. package/dist/locales/nl/agent-providers.json +131 -0
  240. package/dist/locales/nl/agent-usage.json +131 -0
  241. package/dist/locales.d.ts +5 -0
  242. package/dist/locales.js +20 -0
  243. package/dist/mounted-registry.d.ts +7 -0
  244. package/dist/mounted-registry.js +39 -0
  245. package/dist/port-transport.d.ts +6 -0
  246. package/dist/port-transport.js +129 -0
  247. package/dist/prompts/logic/focus.d.ts +2 -0
  248. package/dist/prompts/logic/focus.js +23 -0
  249. package/dist/prompts/logic/keys.d.ts +42 -0
  250. package/dist/prompts/logic/keys.js +94 -0
  251. package/dist/prompts/logic/prompts.d.ts +34 -0
  252. package/dist/prompts/logic/prompts.fixtures.d.ts +5 -0
  253. package/dist/prompts/logic/prompts.fixtures.js +79 -0
  254. package/dist/prompts/logic/prompts.js +32 -0
  255. package/dist/prompts/logic/subjects.d.ts +50 -0
  256. package/dist/prompts/logic/subjects.js +63 -0
  257. package/dist/prompts/logic/useFocusAfterAnswer.d.ts +6 -0
  258. package/dist/prompts/logic/useFocusAfterAnswer.js +28 -0
  259. package/dist/prompts/logic/usePromptSession.d.ts +24 -0
  260. package/dist/prompts/logic/usePromptSession.js +87 -0
  261. package/dist/prompts/ui/ApprovalBody.d.ts +13 -0
  262. package/dist/prompts/ui/ApprovalBody.js +34 -0
  263. package/dist/prompts/ui/PromptActions.d.ts +27 -0
  264. package/dist/prompts/ui/PromptActions.js +18 -0
  265. package/dist/prompts/ui/PromptCard.d.ts +16 -0
  266. package/dist/prompts/ui/PromptCard.js +50 -0
  267. package/dist/prompts/ui/PromptView.d.ts +2 -0
  268. package/dist/prompts/ui/PromptView.js +47 -0
  269. package/dist/prompts/ui/QuestionBody.d.ts +9 -0
  270. package/dist/prompts/ui/QuestionBody.js +87 -0
  271. package/dist/providers/AccountColors.d.ts +5 -0
  272. package/dist/providers/AccountColors.js +8 -0
  273. package/dist/providers/AccountDetail.d.ts +12 -0
  274. package/dist/providers/AccountDetail.js +69 -0
  275. package/dist/providers/AccountVariables.d.ts +8 -0
  276. package/dist/providers/AccountVariables.js +43 -0
  277. package/dist/providers/AddAccountForm.d.ts +10 -0
  278. package/dist/providers/AddAccountForm.js +54 -0
  279. package/dist/providers/CliDetail.d.ts +12 -0
  280. package/dist/providers/CliDetail.js +54 -0
  281. package/dist/providers/ProvidersPane.d.ts +9 -0
  282. package/dist/providers/ProvidersPane.js +86 -0
  283. package/dist/providers/account-actions.d.ts +7 -0
  284. package/dist/providers/account-actions.js +57 -0
  285. package/dist/providers/parts.d.ts +9 -0
  286. package/dist/providers/parts.js +18 -0
  287. package/dist/providers/provider-abilities.d.ts +2 -0
  288. package/dist/providers/provider-abilities.js +7 -0
  289. package/dist/providers/variables.d.ts +13 -0
  290. package/dist/providers/variables.js +63 -0
  291. package/dist/scope.d.ts +11 -0
  292. package/dist/scope.js +10 -0
  293. package/dist/settings/sections.d.ts +13 -0
  294. package/dist/settings/sections.js +32 -0
  295. package/dist/state/chats.d.ts +44 -0
  296. package/dist/state/chats.js +199 -0
  297. package/dist/state/provider-accounts.d.ts +17 -0
  298. package/dist/state/provider-accounts.js +67 -0
  299. package/dist/state/providers.d.ts +17 -0
  300. package/dist/state/providers.js +33 -0
  301. package/dist/state/usage.d.ts +44 -0
  302. package/dist/state/usage.js +121 -0
  303. package/dist/storage.d.ts +20 -0
  304. package/dist/storage.js +89 -0
  305. package/dist/theme.css +196 -0
  306. package/dist/transport.d.ts +24 -0
  307. package/dist/transport.js +18 -0
  308. package/dist/ui/PlainTextarea.d.ts +2 -0
  309. package/dist/ui/PlainTextarea.js +5 -0
  310. package/dist/usage/LimitsList.d.ts +18 -0
  311. package/dist/usage/LimitsList.js +73 -0
  312. package/dist/usage/UsageBreakdown.d.ts +9 -0
  313. package/dist/usage/UsageBreakdown.js +67 -0
  314. package/dist/usage/UsageChart.d.ts +10 -0
  315. package/dist/usage/UsageChart.js +47 -0
  316. package/dist/usage/UsageDialog.d.ts +6 -0
  317. package/dist/usage/UsageDialog.js +12 -0
  318. package/dist/usage/UsageLimits.d.ts +1 -0
  319. package/dist/usage/UsageLimits.js +57 -0
  320. package/dist/usage/UsageLimitsCard.d.ts +4 -0
  321. package/dist/usage/UsageLimitsCard.js +26 -0
  322. package/dist/usage/UsagePage.d.ts +7 -0
  323. package/dist/usage/UsagePage.js +154 -0
  324. package/dist/usage/UsagePane.d.ts +3 -0
  325. package/dist/usage/UsagePane.js +21 -0
  326. package/dist/usage/UsageSummary.d.ts +12 -0
  327. package/dist/usage/UsageSummary.js +15 -0
  328. package/dist/usage/UsageTiles.d.ts +7 -0
  329. package/dist/usage/UsageTiles.js +19 -0
  330. package/dist/usage/format.d.ts +13 -0
  331. package/dist/usage/format.js +55 -0
  332. package/dist/usage/limit-groups.d.ts +22 -0
  333. package/dist/usage/limit-groups.js +86 -0
  334. package/dist/usage/limits.d.ts +4 -0
  335. package/dist/usage/limits.js +58 -0
  336. package/dist/usage/money.d.ts +1 -0
  337. package/dist/usage/money.js +13 -0
  338. package/dist/usage/summary.d.ts +34 -0
  339. package/dist/usage/summary.js +94 -0
  340. package/package.json +895 -3
  341. package/src/agents/AccountDot.tsx +14 -0
  342. package/src/agents/AgentIcon.tsx +26 -0
  343. package/src/agents/ModelOptionControl.tsx +68 -0
  344. package/src/agents/ProviderLogo.tsx +20 -0
  345. package/src/agents/account-limits.ts +61 -0
  346. package/src/agents/accounts.ts +137 -0
  347. package/src/agents/model-name.ts +70 -0
  348. package/src/agents/model-options.ts +21 -0
  349. package/src/agents/provider-paths.ts +10 -0
  350. package/src/agents/status-look.ts +43 -0
  351. package/src/chat/account-choice.ts +44 -0
  352. package/src/chat/actions.ts +72 -0
  353. package/src/chat/activity.ts +8 -0
  354. package/src/chat/attachments.ts +118 -0
  355. package/src/chat/bookmarks.ts +87 -0
  356. package/src/chat/chat-client.ts +345 -0
  357. package/src/chat/chat-references.ts +11 -0
  358. package/src/chat/drafts.ts +165 -0
  359. package/src/chat/forks.ts +33 -0
  360. package/src/chat/guards.ts +84 -0
  361. package/src/chat/logic/ansi.ts +206 -0
  362. package/src/chat/logic/answers.ts +8 -0
  363. package/src/chat/logic/bookmarks.ts +42 -0
  364. package/src/chat/logic/context-usage.ts +36 -0
  365. package/src/chat/logic/fork.ts +200 -0
  366. package/src/chat/logic/handback.ts +10 -0
  367. package/src/chat/logic/json.ts +4 -0
  368. package/src/chat/logic/limit.ts +46 -0
  369. package/src/chat/logic/resume-compaction.ts +71 -0
  370. package/src/chat/logic/scrubber.ts +289 -0
  371. package/src/chat/logic/thread-cards.ts +64 -0
  372. package/src/chat/logic/timeline-copy.ts +65 -0
  373. package/src/chat/logic/timeline-target.ts +52 -0
  374. package/src/chat/logic/timeline.ts +419 -0
  375. package/src/chat/logic/tool-catalog.ts +62 -0
  376. package/src/chat/logic/tools.ts +102 -0
  377. package/src/chat/logic/welcome.ts +13 -0
  378. package/src/chat/logic/workflow.ts +66 -0
  379. package/src/chat/mentions.ts +232 -0
  380. package/src/chat/persisted-json.ts +67 -0
  381. package/src/chat/preferences.ts +179 -0
  382. package/src/chat/quote.ts +26 -0
  383. package/src/chat/recent-messages.ts +59 -0
  384. package/src/chat/resume-compaction-dismissals.ts +17 -0
  385. package/src/chat/runtime-modes.ts +18 -0
  386. package/src/chat/stash.ts +122 -0
  387. package/src/chat/subagent-conversation.ts +203 -0
  388. package/src/chat/subagent-list.ts +141 -0
  389. package/src/chat/subagent-support.ts +21 -0
  390. package/src/chat/subagent-view.ts +137 -0
  391. package/src/chat/thumbnails.ts +108 -0
  392. package/src/chat/timeline-flash.ts +33 -0
  393. package/src/chat/timeline-scroll.ts +199 -0
  394. package/src/chat/ui/AccountPill.tsx +31 -0
  395. package/src/chat/ui/AnsiOutput.tsx +18 -0
  396. package/src/chat/ui/BookmarkMarker.tsx +108 -0
  397. package/src/chat/ui/BookmarkSubmenu.tsx +53 -0
  398. package/src/chat/ui/ChatActivity.tsx +198 -0
  399. package/src/chat/ui/ChatReferenceChip.tsx +28 -0
  400. package/src/chat/ui/CodeBlock.tsx +175 -0
  401. package/src/chat/ui/Composer.tsx +1326 -0
  402. package/src/chat/ui/ComposerInput.tsx +187 -0
  403. package/src/chat/ui/DiffPool.tsx +36 -0
  404. package/src/chat/ui/EditDiff.tsx +30 -0
  405. package/src/chat/ui/FadingWords.tsx +18 -0
  406. package/src/chat/ui/ImageView.tsx +159 -0
  407. package/src/chat/ui/LimitState.tsx +165 -0
  408. package/src/chat/ui/Markdown.tsx +258 -0
  409. package/src/chat/ui/MessageActions.tsx +88 -0
  410. package/src/chat/ui/Pickers.tsx +272 -0
  411. package/src/chat/ui/PromptComposer.tsx +119 -0
  412. package/src/chat/ui/QuoteButton.tsx +103 -0
  413. package/src/chat/ui/ResumeCompactionDock.tsx +28 -0
  414. package/src/chat/ui/RunSettings.tsx +546 -0
  415. package/src/chat/ui/Scrubber.tsx +265 -0
  416. package/src/chat/ui/SubagentControls.tsx +66 -0
  417. package/src/chat/ui/SubagentInfo.tsx +67 -0
  418. package/src/chat/ui/SubagentStopButton.tsx +45 -0
  419. package/src/chat/ui/SubagentTimeline.tsx +170 -0
  420. package/src/chat/ui/Timeline.tsx +628 -0
  421. package/src/chat/ui/TimelineMenu.tsx +124 -0
  422. package/src/chat/ui/UnifiedDiff.tsx +99 -0
  423. package/src/chat/ui/UploadThumb.tsx +13 -0
  424. package/src/chat/ui/Welcome.tsx +22 -0
  425. package/src/chat/ui/chips.ts +15 -0
  426. package/src/chat/ui/code-lines.ts +60 -0
  427. package/src/chat/ui/code-streaming.ts +4 -0
  428. package/src/chat/ui/code-theme.ts +15 -0
  429. package/src/chat/ui/composer/chips.ts +43 -0
  430. package/src/chat/ui/composer/editor.ts +118 -0
  431. package/src/chat/ui/composer/insert.ts +22 -0
  432. package/src/chat/ui/composer/keys.ts +144 -0
  433. package/src/chat/ui/diff-theme.ts +23 -0
  434. package/src/chat/ui/file-links.ts +24 -0
  435. package/src/chat/ui/find-reveal.ts +23 -0
  436. package/src/chat/ui/full-diff.ts +25 -0
  437. package/src/chat/ui/icons.tsx +24 -0
  438. package/src/chat/ui/markdown-blocks.ts +123 -0
  439. package/src/chat/ui/quote-selection.ts +53 -0
  440. package/src/chat/ui/rehype-chips.ts +62 -0
  441. package/src/chat/ui/rehype-fade.ts +64 -0
  442. package/src/chat/ui/remark-html-as-text.ts +36 -0
  443. package/src/chat/ui/reply-context.ts +4 -0
  444. package/src/chat/ui/reveal.ts +135 -0
  445. package/src/chat/ui/rows/ForksRow.tsx +79 -0
  446. package/src/chat/ui/rows/MessageRows.tsx +415 -0
  447. package/src/chat/ui/rows/Rows.tsx +81 -0
  448. package/src/chat/ui/rows/SubagentRow.tsx +209 -0
  449. package/src/chat/ui/rows/TaskRow.tsx +91 -0
  450. package/src/chat/ui/rows/WorkRows.tsx +445 -0
  451. package/src/chat/ui/rows/WorkflowRow.tsx +120 -0
  452. package/src/chat/ui/rows/row-rhythm.ts +37 -0
  453. package/src/chat/ui/useToggleSet.ts +39 -0
  454. package/src/host.ts +324 -0
  455. package/src/lazy.tsx +64 -0
  456. package/src/locales/en/agent-chat.json +446 -0
  457. package/src/locales/en/agent-prompts.json +42 -0
  458. package/src/locales/en/agent-providers.json +131 -0
  459. package/src/locales/en/agent-usage.json +131 -0
  460. package/src/locales/nl/agent-chat.json +446 -0
  461. package/src/locales/nl/agent-prompts.json +42 -0
  462. package/src/locales/nl/agent-providers.json +131 -0
  463. package/src/locales/nl/agent-usage.json +131 -0
  464. package/src/locales.ts +33 -0
  465. package/src/mounted-registry.ts +45 -0
  466. package/src/port-transport.ts +146 -0
  467. package/src/prompts/logic/focus.ts +25 -0
  468. package/src/prompts/logic/keys.ts +116 -0
  469. package/src/prompts/logic/prompts.fixtures.ts +84 -0
  470. package/src/prompts/logic/prompts.ts +58 -0
  471. package/src/prompts/logic/subjects.ts +122 -0
  472. package/src/prompts/logic/useFocusAfterAnswer.ts +38 -0
  473. package/src/prompts/logic/usePromptSession.ts +114 -0
  474. package/src/prompts/ui/ApprovalBody.tsx +109 -0
  475. package/src/prompts/ui/PromptActions.tsx +109 -0
  476. package/src/prompts/ui/PromptCard.tsx +101 -0
  477. package/src/prompts/ui/PromptView.tsx +111 -0
  478. package/src/prompts/ui/QuestionBody.tsx +174 -0
  479. package/src/providers/AccountColors.tsx +8 -0
  480. package/src/providers/AccountDetail.tsx +209 -0
  481. package/src/providers/AccountVariables.tsx +126 -0
  482. package/src/providers/AddAccountForm.tsx +121 -0
  483. package/src/providers/CliDetail.tsx +157 -0
  484. package/src/providers/ProvidersPane.tsx +236 -0
  485. package/src/providers/account-actions.ts +67 -0
  486. package/src/providers/parts.tsx +25 -0
  487. package/src/providers/provider-abilities.ts +9 -0
  488. package/src/providers/variables.ts +87 -0
  489. package/src/scope.ts +30 -0
  490. package/src/settings/sections.ts +47 -0
  491. package/src/state/chats.ts +279 -0
  492. package/src/state/provider-accounts.ts +87 -0
  493. package/src/state/providers.ts +50 -0
  494. package/src/state/usage.ts +196 -0
  495. package/src/storage.ts +113 -0
  496. package/src/theme.css +196 -0
  497. package/src/transport.ts +50 -0
  498. package/src/ui/PlainTextarea.tsx +6 -0
  499. package/src/usage/LimitsList.tsx +232 -0
  500. package/src/usage/UsageBreakdown.tsx +210 -0
  501. package/src/usage/UsageChart.tsx +113 -0
  502. package/src/usage/UsageDialog.tsx +20 -0
  503. package/src/usage/UsageLimits.tsx +145 -0
  504. package/src/usage/UsageLimitsCard.tsx +36 -0
  505. package/src/usage/UsagePage.tsx +276 -0
  506. package/src/usage/UsagePane.tsx +52 -0
  507. package/src/usage/UsageSummary.tsx +50 -0
  508. package/src/usage/UsageTiles.tsx +34 -0
  509. package/src/usage/format.ts +71 -0
  510. package/src/usage/limit-groups.ts +119 -0
  511. package/src/usage/limits.ts +71 -0
  512. package/src/usage/money.ts +14 -0
  513. package/src/usage/summary.ts +137 -0
@@ -0,0 +1,258 @@
1
+ import { memo, useMemo, type ReactNode } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Zap } from 'lucide-react';
4
+ import ReactMarkdown, { type Components, type Options } from 'react-markdown';
5
+ import remarkBreaks from 'remark-breaks';
6
+ import remarkGfm from 'remark-gfm';
7
+ import { CHIP_IN_MESSAGE, MENTION_TONE, SKILL_TONE } from './chips';
8
+ import { CodeBlock } from './CodeBlock';
9
+ import { CodeStreamingContext } from './code-streaming';
10
+ import { splitMarkdownBlocks } from './markdown-blocks';
11
+ import { rehypeChips, type ChipOptions } from './rehype-chips';
12
+ import { rehypeFadeWords } from './rehype-fade';
13
+ import { remarkHtmlAsText } from './remark-html-as-text';
14
+ import type { FileRef } from '../../host';
15
+ import { openFileLink, useFileLinkCwd, useFileLinkTarget } from './file-links';
16
+ import { FileIcon, Icon } from '@adecore/ui';
17
+
18
+ function languageOf(className: string | undefined): string {
19
+ return /language-([\w-]+)/.exec(className ?? '')?.[1] ?? 'text';
20
+ }
21
+
22
+ const INLINE_CODE = 'rounded-sm bg-surface-sunken px-1 py-px font-mono text-code';
23
+
24
+ /*
25
+ * A path an agent wrote down, opened in the preview panel. It keeps the shape of the code chip it
26
+ * would otherwise be, so a sentence full of them does not start dancing; only the color says it can
27
+ * be clicked. `data-file-path` is what the thread's own right-click menu reads, so a link answers to
28
+ * both ways of opening it.
29
+ */
30
+ function FileLink({ target, className, children }: { target: FileRef; className?: string; children: ReactNode }) {
31
+ const cwd = useFileLinkCwd();
32
+ return (
33
+ // `select-text` because a thread is copied as often as it is clicked, and a button is not
34
+ // selectable on its own.
35
+ <button
36
+ type="button"
37
+ className={clsx('cursor-pointer select-text', className)}
38
+ data-file-path={target.path}
39
+ data-file-line={target.line}
40
+ onClick={() => openFileLink(cwd, target)}
41
+ >
42
+ {children}
43
+ </button>
44
+ );
45
+ }
46
+
47
+ /* Inline code, which is a file reference often enough that it is worth asking every time. */
48
+ function InlineCode({ text }: { text: string }) {
49
+ const target = useFileLinkTarget(text);
50
+ if (target === null) {
51
+ return <code className={INLINE_CODE}>{text}</code>;
52
+ }
53
+ return (
54
+ <FileLink target={target} className={`${INLINE_CODE} text-accent hover:underline`}>
55
+ {text}
56
+ </FileLink>
57
+ );
58
+ }
59
+
60
+ /* The path a link target spells, with the one scheme that still means a file on the host's machine
61
+ taken off. A href nobody encoded is left as it is, since decoding throws on a stray percent. */
62
+ function hrefPath(href: string): string {
63
+ const bare = href.replace(/^file:\/\//, '');
64
+ try {
65
+ return decodeURI(bare);
66
+ } catch {
67
+ return bare;
68
+ }
69
+ }
70
+
71
+ /* A markdown link, which is a file when its target names no scheme of its own. */
72
+ function MarkdownLink({ href, children }: { href?: string; children?: ReactNode }) {
73
+ const target = useFileLinkTarget(hrefPath(href ?? ''));
74
+ if (target !== null) {
75
+ return (
76
+ <FileLink target={target} className="text-accent underline underline-offset-2">
77
+ {children}
78
+ </FileLink>
79
+ );
80
+ }
81
+ return (
82
+ <a href={href} target="_blank" rel="noreferrer">
83
+ {children}
84
+ </a>
85
+ );
86
+ }
87
+
88
+ const components = {
89
+ pre({ children }: { children?: ReactNode }) {
90
+ return <>{children}</>;
91
+ },
92
+ code({ className, children }: { className?: string; children?: ReactNode }) {
93
+ const text = String(children ?? '');
94
+ // Fenced blocks end with a newline and carry a language; anything else is inline.
95
+ if (className?.startsWith('language-') || text.includes('\n')) {
96
+ return <CodeBlock code={text.replace(/\n$/, '')} lang={languageOf(className)} />;
97
+ }
98
+ return <InlineCode text={text} />;
99
+ },
100
+ a({ href, children }: { href?: string; children?: ReactNode }) {
101
+ return <MarkdownLink href={href}>{children}</MarkdownLink>;
102
+ },
103
+ table({ children }: { children?: ReactNode }) {
104
+ // A table wider than the column scrolls on its own instead of pushing the thread sideways.
105
+ return (
106
+ <div className="chat-table">
107
+ <table>{children}</table>
108
+ </div>
109
+ );
110
+ }
111
+ };
112
+
113
+ /* For text from outside this machine, where a path in inline code is not a file anybody meant to open here. */
114
+ const plainComponents = {
115
+ ...components,
116
+ code({ className, children }: { className?: string; children?: ReactNode }) {
117
+ const text = String(children ?? '');
118
+ if (className?.startsWith('language-') || text.includes('\n')) {
119
+ return <CodeBlock code={text.replace(/\n$/, '')} lang={languageOf(className)} />;
120
+ }
121
+ return <code className={INLINE_CODE}>{text}</code>;
122
+ },
123
+ a({ href, children }: { href?: string; children?: ReactNode }) {
124
+ return (
125
+ <a href={href} target="_blank" rel="noreferrer">
126
+ {children}
127
+ </a>
128
+ );
129
+ }
130
+ };
131
+
132
+ // A reply sits under the heading of its message, an `h3`, so its own headings start one level below
133
+ // that. Only `aria-level` moves. The tag stays, since the styles and a copy of the thread read it.
134
+ const REPLY_HEADING_OFFSET = 3;
135
+
136
+ function replyHeading(level: 1 | 2 | 3 | 4 | 5 | 6) {
137
+ const Tag = `h${level}` as const;
138
+ return function ReplyHeading({ children }: { children?: ReactNode }) {
139
+ return <Tag aria-level={Math.min(6, level + REPLY_HEADING_OFFSET)}>{children}</Tag>;
140
+ };
141
+ }
142
+
143
+ const replyComponents = {
144
+ ...components,
145
+ h1: replyHeading(1),
146
+ h2: replyHeading(2),
147
+ h3: replyHeading(3),
148
+ h4: replyHeading(4),
149
+ h5: replyHeading(5),
150
+ h6: replyHeading(6)
151
+ };
152
+
153
+ /* A picked file or skill in a sent message. The glyph stands in for the sigil the text still carries. */
154
+ function Chip({ kind, value }: { kind: string; value: string }) {
155
+ if (kind === 'skill') {
156
+ return (
157
+ <span className={clsx(CHIP_IN_MESSAGE, SKILL_TONE)}>
158
+ <Icon icon={Zap} size={14} className="shrink-0 opacity-85" />
159
+ <span className="truncate">{value}</span>
160
+ </span>
161
+ );
162
+ }
163
+ return (
164
+ // The path is what the thread's menu opens in the preview from here.
165
+ <span className={clsx(CHIP_IN_MESSAGE, MENTION_TONE)} data-file-path={value}>
166
+ <FileIcon path={value} size={14} />
167
+ <span className="truncate">{value}</span>
168
+ </span>
169
+ );
170
+ }
171
+
172
+ const messageComponents = {
173
+ ...replyComponents,
174
+ span({ children, 'data-chip': chip, 'data-value': value }: { children?: ReactNode; 'data-chip'?: string; 'data-value'?: string }) {
175
+ if (chip === undefined || value === undefined) {
176
+ return <span>{children}</span>;
177
+ }
178
+ return <Chip kind={chip} value={value} />;
179
+ }
180
+ };
181
+
182
+ // Module constants, so a render never hands react-markdown a fresh array and makes it parse again.
183
+ const PLUGINS = [remarkGfm];
184
+ const PLUGINS_WITH_BREAKS = [remarkGfm, remarkBreaks];
185
+ const MESSAGE_PLUGINS = [remarkGfm, remarkHtmlAsText, remarkBreaks];
186
+ const FADE_PLUGINS = [rehypeFadeWords];
187
+ const NO_PLUGINS: typeof FADE_PLUGINS = [];
188
+
189
+ /* One block of a reply. It renders no element of its own, so prose still sees the paragraphs as
190
+ direct children and keeps its first and last margins. */
191
+ const ReplyBlock = memo(function ReplyBlock({ text, fade, open }: { text: string; fade: boolean; open: boolean }) {
192
+ return (
193
+ // A context rather than a second set of components. A component that changed would mount the
194
+ // code block again the moment its fence closes, and it would lose the lines it already has.
195
+ <CodeStreamingContext.Provider value={open}>
196
+ <ReactMarkdown remarkPlugins={PLUGINS} rehypePlugins={fade ? FADE_PLUGINS : NO_PLUGINS} components={replyComponents}>
197
+ {text}
198
+ </ReactMarkdown>
199
+ </CodeStreamingContext.Provider>
200
+ );
201
+ });
202
+
203
+ /*
204
+ * A reply in a chat thread, parsed a block at a time so a delta only parses the block that grows.
205
+ * While it streams every new word fades in, and a fence that has not closed yet is highlighted a
206
+ * line at a time as it grows. `arriving` fades in each block as it is added instead, for a reply
207
+ * that is shown a block at a time.
208
+ */
209
+ export const ReplyMarkdown = memo(function ReplyMarkdown({ text, streaming, arriving = false }: { text: string; streaming: boolean; arriving?: boolean }) {
210
+ const blocks = useMemo(() => splitMarkdownBlocks(text), [text]);
211
+ return (
212
+ <div className="chat-markdown prose prose-sm max-w-none text-sm" data-arriving={arriving || undefined}>
213
+ {blocks.map((block, index) => (
214
+ // Blocks only ever grow at the end, so the place of a block is a stable key.
215
+ <ReplyBlock key={index} text={block.text} fade={streaming} open={streaming && block.openFence} />
216
+ ))}
217
+ </div>
218
+ );
219
+ });
220
+
221
+ /*
222
+ * A message a person sent. Enter meant a line break, a tag typed without backticks stays the text
223
+ * it was, and the files and skills picked in the composer are chips again.
224
+ */
225
+ export const MessageMarkdown = memo(function MessageMarkdown({ text, mentions, skills }: { text: string; mentions?: string[]; skills?: string[] }) {
226
+ const rehypePlugins = useMemo((): [typeof rehypeChips, ChipOptions][] => [[rehypeChips, { mentions, skills }]], [mentions, skills]);
227
+ return (
228
+ <div className="chat-markdown prose prose-sm max-w-none text-sm">
229
+ <ReactMarkdown remarkPlugins={MESSAGE_PLUGINS} rehypePlugins={rehypePlugins} components={messageComponents}>
230
+ {text}
231
+ </ReactMarkdown>
232
+ </div>
233
+ );
234
+ });
235
+
236
+ /* `breaks` is for person-authored notes; applying it to CLI markdown would change its layout. */
237
+ export const Markdown = memo(function Markdown({
238
+ text,
239
+ breaks = false,
240
+ fileLinks = true,
241
+ rehypePlugins,
242
+ componentOverrides
243
+ }: {
244
+ text: string;
245
+ breaks?: boolean;
246
+ fileLinks?: boolean;
247
+ rehypePlugins?: Options['rehypePlugins'];
248
+ componentOverrides?: Components;
249
+ }) {
250
+ const renderComponents = useMemo(() => ({ ...(fileLinks ? components : plainComponents), ...componentOverrides }), [fileLinks, componentOverrides]);
251
+ return (
252
+ <div className="chat-markdown prose prose-sm max-w-none text-sm">
253
+ <ReactMarkdown remarkPlugins={breaks ? PLUGINS_WITH_BREAKS : PLUGINS} rehypePlugins={rehypePlugins} components={renderComponents}>
254
+ {text}
255
+ </ReactMarkdown>
256
+ </div>
257
+ );
258
+ });
@@ -0,0 +1,88 @@
1
+ import { useEffect, useState } from 'react';
2
+ import clsx from 'clsx';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Bookmark, Check, Copy, GitFork } from 'lucide-react';
5
+ import { placeBookmark } from '../bookmarks';
6
+ import { forkRefusal } from '../logic/fork';
7
+ import type { TimelineRow } from '../logic/timeline';
8
+ import { markdownOf, messageTextOf } from '../logic/timeline-copy';
9
+ import { chatHost } from '../../host';
10
+ import { useChatScope } from '../../scope';
11
+ import { useChatRow, useChats } from '../../state/chats';
12
+ import { ButtonGroup, copyText, IconButton } from '@adecore/ui';
13
+
14
+ const COPIED_MS = 1500;
15
+
16
+ type MessageRow = Extract<TimelineRow, { kind: 'user' | 'assistant' }>;
17
+
18
+ /*
19
+ * Bookmark, fork and copy under a message, shown while the pointer or the focus is on a
20
+ * `group/message`. The row always takes its height, so the virtualizer never measures it again on a
21
+ * hover. Like the strip's card, it leaves out an action the message cannot take right now.
22
+ */
23
+ export function MessageActions({ chatId, row }: { chatId: string; row: MessageRow }) {
24
+ const { t } = useTranslation('agent-chat');
25
+ const scope = useChatScope();
26
+ const { fork } = chatHost();
27
+ const turnId = row.item.turnId;
28
+ const bookmark = useChatRow(chatId, (chat) => chat?.bookmarks?.find((candidate) => candidate.itemId === row.id) ?? null);
29
+ const canFork = useChatRow(chatId, (chat) => turnId !== null && forkRefusal(chat?.info ?? null, chat?.structure[turnId]) === null);
30
+ const streaming = useChatRow(chatId, (chat) => {
31
+ const item = chat?.items[row.id];
32
+ return item?.kind === 'assistant' && item.streaming;
33
+ });
34
+ const hasText = useChatRow(chatId, (chat) => {
35
+ const item = chat?.items[row.id];
36
+ return (item?.kind === 'user' || item?.kind === 'assistant') && item.text.trim() !== '';
37
+ });
38
+ const [copied, setCopied] = useState(false);
39
+
40
+ useEffect(() => {
41
+ if (!copied) {
42
+ return;
43
+ }
44
+ const timer = setTimeout(() => setCopied(false), COPIED_MS);
45
+ return () => clearTimeout(timer);
46
+ }, [copied]);
47
+
48
+ // The row was derived from the structure, which a delta leaves alone; the text to copy is the one held now.
49
+ const copy = (plain: boolean): void => {
50
+ const item = useChats.getState().byKey[scope.keyOf(chatId)]?.items[row.id];
51
+ const current = item?.kind === row.kind ? ({ ...row, item } as MessageRow) : row;
52
+ const text = (plain ? null : markdownOf(current)) ?? messageTextOf(current);
53
+ if (text !== null) {
54
+ copyText(text);
55
+ setCopied(true);
56
+ }
57
+ };
58
+
59
+ return (
60
+ <div
61
+ role="toolbar"
62
+ aria-label={t('timeline.actions.label')}
63
+ className={clsx(
64
+ 'flex h-7 items-center opacity-0 transition-opacity group-has-focus-visible/message:opacity-100 group-hover/message:opacity-100',
65
+ row.kind === 'user' ? '-mr-1.5 mt-1.5 justify-end' : '-ml-1.5'
66
+ )}
67
+ >
68
+ {!streaming && (
69
+ <ButtonGroup render={<span />}>
70
+ {bookmark === null && (
71
+ <IconButton icon={Bookmark} size="sm" label={t('bookmarks.add')} onClick={() => void placeBookmark(scope, chatId, row.id)} />
72
+ )}
73
+ {canFork && turnId !== null && fork !== null && (
74
+ <IconButton icon={GitFork} size="sm" label={t('timeline.menu.forkFromHere')} onClick={() => fork(chatId, turnId)} />
75
+ )}
76
+ {hasText && (
77
+ <IconButton
78
+ icon={copied ? Check : Copy}
79
+ size="sm"
80
+ label={copied ? t('timeline.actions.copied') : t('timeline.menu.copyMessage')}
81
+ onClick={(e) => copy(e.shiftKey)}
82
+ />
83
+ )}
84
+ </ButtonGroup>
85
+ )}
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,272 @@
1
+ import { useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Bookmark, ChevronDown, ChevronRight, Search, Trash2 } from 'lucide-react';
4
+ import type { AgentKind, ModelInfo, ModelSelection, ProviderInfo, UsageProvider } from '@adecore/agent-contracts';
5
+ import { AgentIcon } from '../../agents/AgentIcon';
6
+ import { ProviderLogo } from '../../agents/ProviderLogo';
7
+ import { agentChipOf, modelName } from '../../agents/model-name';
8
+ import { forgetStashed, STASH_SHORTCUT, useStash, type StashedPrompt } from '../stash';
9
+ import { Icon, IconButton, Menu, Popover, Tooltip } from '@adecore/ui';
10
+
11
+ const triggerClass =
12
+ 'flex h-7 shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-2 text-xs text-text-muted hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text';
13
+
14
+ const chipClass =
15
+ 'flex h-6 min-w-0 shrink items-center gap-1.5 whitespace-nowrap rounded-md bg-surface-hover px-2 text-xs text-text hover:bg-surface-active data-[popup-open]:bg-surface-active';
16
+
17
+ /* A model row, or the one row that folds the legacy models open. Both take a turn in the arrow keys. */
18
+ type PickerEntry = { kind: 'model'; provider: ProviderInfo; model: ModelInfo } | { kind: 'legacy'; count: number };
19
+
20
+ function matches(model: ModelInfo, query: string): boolean {
21
+ return model.name.toLowerCase().includes(query) || model.slug.toLowerCase().includes(query);
22
+ }
23
+
24
+ interface ModelPickerProps {
25
+ /* Whose models may be picked: every installed chat CLI before the first message, the chat's own after it. */
26
+ providers: ProviderInfo[];
27
+ provider: AgentKind;
28
+ selection: ModelSelection;
29
+ /* Controlled, so `/model` in the composer can open the same popup a click on the trigger does. */
30
+ open: boolean;
31
+ onOpenChange(open: boolean): void;
32
+ onChange(provider: AgentKind, model: string): void;
33
+ /* The composer's `/model`; `null` for a picker outside a chat, where that command does nothing. */
34
+ kbd?: string | null;
35
+ /* Where the popup opens: over the composer by default, below the trigger in a form. */
36
+ side?: 'top' | 'bottom';
37
+ /* `chip` names the provider beside the model, on a ground of its own, for a picker that sits in a card or a form. */
38
+ trigger?: 'plain' | 'chip';
39
+ /* Where the focus goes when the popup closes; by default back to the trigger. */
40
+ finalFocus?: RefObject<HTMLElement | null>;
41
+ /* A note under the list, such as what picking an agent here does. */
42
+ footer?: ReactNode;
43
+ }
44
+
45
+ /*
46
+ * Which model answers: the provider's mark plus the model's short name
47
+ * as the trigger, a search field over the popup, one group per provider and the legacy models
48
+ * behind an expander. Before the first message the list carries every installed CLI, so picking
49
+ * another provider's model is also how a chat picks its provider. A chat bound to one CLI passes
50
+ * only that CLI, which drops the group headers and leaves its own catalog to choose from.
51
+ */
52
+ export function ModelPicker({
53
+ providers,
54
+ provider,
55
+ selection,
56
+ open,
57
+ onOpenChange,
58
+ onChange,
59
+ kbd = '/model',
60
+ side = 'top',
61
+ trigger = 'plain',
62
+ finalFocus,
63
+ footer
64
+ }: ModelPickerProps) {
65
+ const { t } = useTranslation('agent-chat');
66
+ const [query, setQuery] = useState('');
67
+ const [legacyOpen, setLegacyOpen] = useState(false);
68
+ const [index, setIndex] = useState(0);
69
+ const inputRef = useRef<HTMLInputElement>(null);
70
+ const listRef = useRef<HTMLDivElement>(null);
71
+
72
+ const owner = providers.find((entry) => entry.kind === provider);
73
+ const current = modelName(selection.model, owner?.models);
74
+ const chip = agentChipOf(providers, provider, selection.model);
75
+ const grouped = providers.length > 1;
76
+ const trimmed = query.trim().toLowerCase();
77
+
78
+ const entries = useMemo((): PickerEntry[] => {
79
+ const rows = providers.flatMap((entry) => entry.models.map((model) => ({ kind: 'model' as const, provider: entry, model })));
80
+ if (trimmed !== '') {
81
+ return rows.filter((row) => matches(row.model, trimmed));
82
+ }
83
+ const live = rows.filter((row) => !row.model.legacy);
84
+ const legacy = rows.filter((row) => row.model.legacy);
85
+ if (legacy.length === 0) {
86
+ return live;
87
+ }
88
+ return legacyOpen ? [...live, { kind: 'legacy', count: legacy.length }, ...legacy] : [...live, { kind: 'legacy', count: legacy.length }];
89
+ }, [providers, trimmed, legacyOpen]);
90
+
91
+ /* Closing forgets the search, so the next open (a click or `/model`) starts on the whole list. */
92
+ const setOpen = (next: boolean): void => {
93
+ if (!next) {
94
+ setQuery('');
95
+ setLegacyOpen(false);
96
+ }
97
+ setIndex(0);
98
+ onOpenChange(next);
99
+ };
100
+
101
+ // The list scrolls, so the row the arrows moved to has to come along.
102
+ useEffect(() => {
103
+ listRef.current?.querySelector<HTMLElement>('[data-active="true"]')?.scrollIntoView({ block: 'nearest' });
104
+ }, [index]);
105
+
106
+ const choose = (entry: PickerEntry | undefined): void => {
107
+ if (!entry) {
108
+ return;
109
+ }
110
+ if (entry.kind === 'legacy') {
111
+ setLegacyOpen(true);
112
+ inputRef.current?.focus();
113
+ return;
114
+ }
115
+ onChange(entry.provider.kind, entry.model.slug);
116
+ setOpen(false);
117
+ };
118
+
119
+ const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
120
+ if (e.key === 'ArrowDown') {
121
+ e.preventDefault();
122
+ setIndex((i) => (entries.length === 0 ? 0 : (i + 1) % entries.length));
123
+ } else if (e.key === 'ArrowUp') {
124
+ e.preventDefault();
125
+ setIndex((i) => (entries.length === 0 ? 0 : (i - 1 + entries.length) % entries.length));
126
+ } else if (e.key === 'Enter') {
127
+ e.preventDefault();
128
+ choose(entries[index]);
129
+ }
130
+ };
131
+
132
+ return (
133
+ <Popover.Root open={open} onOpenChange={setOpen}>
134
+ <Tooltip label={owner ? `${owner.name} · ${current}` : t('pickers.model.choose')} kbd={kbd ?? undefined}>
135
+ {trigger === 'chip' ? (
136
+ <Popover.Trigger className={chipClass}>
137
+ {chip.mark === 'logo' ? <ProviderLogo provider={provider as UsageProvider} size={12} /> : <AgentIcon kind={provider} size={12} />}
138
+ <span className="truncate">{chip.name}</span>
139
+ <span className="max-w-32 truncate text-text-muted">{chip.model}</span>
140
+ <Icon icon={ChevronDown} size={12} className="shrink-0 text-text-faint" />
141
+ </Popover.Trigger>
142
+ ) : (
143
+ <Popover.Trigger className={triggerClass}>
144
+ <AgentIcon kind={provider} size={12} />
145
+ <span className="max-w-40 truncate">{current}</span>
146
+ <Icon icon={ChevronDown} size={12} className="text-text-faint" />
147
+ </Popover.Trigger>
148
+ )}
149
+ </Tooltip>
150
+ <Popover.Popup variant="picker" side={side} initialFocus={inputRef} {...(finalFocus === undefined ? {} : { finalFocus })}>
151
+ <div className="flex items-center gap-2 border-b border-border px-2.5">
152
+ <Icon icon={Search} size={14} className="shrink-0 text-text-faint" />
153
+ <input
154
+ ref={inputRef}
155
+ className="h-8 w-full bg-transparent text-sm text-text outline-none placeholder:text-text-faint"
156
+ placeholder={t('pickers.model.search')}
157
+ spellCheck={false}
158
+ value={query}
159
+ onChange={(e) => {
160
+ setQuery(e.target.value);
161
+ setIndex(0);
162
+ }}
163
+ onKeyDown={onKeyDown}
164
+ />
165
+ </div>
166
+ <div ref={listRef} className="max-h-72 overflow-auto p-1" role="listbox">
167
+ {entries.length === 0 && (
168
+ <div className="px-3 py-6 text-center text-xs text-text-faint">
169
+ {providers.length === 0 ? t('pickers.model.noProvider') : t('pickers.model.noMatch')}
170
+ </div>
171
+ )}
172
+ {entries.map((entry, i) => {
173
+ if (entry.kind === 'legacy') {
174
+ return (
175
+ <button
176
+ key="legacy"
177
+ data-active={i === index}
178
+ className="cursor-row flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-xs text-text-faint"
179
+ onMouseEnter={() => setIndex(i)}
180
+ onClick={() => choose(entry)}
181
+ >
182
+ <Icon icon={legacyOpen ? ChevronDown : ChevronRight} size={12} />
183
+ {t('pickers.model.legacyGroup', { count: entry.count })}
184
+ </button>
185
+ );
186
+ }
187
+ const previous = entries[i - 1];
188
+ const first = grouped && (previous === undefined || previous.kind !== 'model' || previous.provider.kind !== entry.provider.kind);
189
+ const chosen = entry.provider.kind === provider && entry.model.slug === selection.model;
190
+ return (
191
+ <div key={`${entry.provider.kind}/${entry.model.slug}`}>
192
+ {first && (
193
+ <Menu.Label className="flex items-center gap-1.5">
194
+ <AgentIcon kind={entry.provider.kind} size={12} />
195
+ {entry.provider.name}
196
+ </Menu.Label>
197
+ )}
198
+ <button
199
+ role="option"
200
+ aria-selected={chosen}
201
+ data-active={i === index}
202
+ className="cursor-row flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm text-text-muted"
203
+ onMouseEnter={() => setIndex(i)}
204
+ onClick={() => choose(entry)}
205
+ >
206
+ <Menu.Check kind="radio" checked={chosen} />
207
+ <span className="min-w-0 truncate">{entry.model.name}</span>
208
+ {entry.model.badge && (
209
+ <span className="rounded bg-accent-soft px-1 text-[11px] leading-5 font-medium uppercase text-accent">
210
+ {entry.model.badge}
211
+ </span>
212
+ )}
213
+ <span className="grow" />
214
+ {entry.model.legacy && <span className="text-xs text-text-faint">{t('pickers.model.legacy')}</span>}
215
+ </button>
216
+ </div>
217
+ );
218
+ })}
219
+ </div>
220
+ {footer !== undefined && <div className="border-t border-border-soft px-3 py-2 text-xs text-text-faint">{footer}</div>}
221
+ </Popover.Popup>
222
+ </Popover.Root>
223
+ );
224
+ }
225
+
226
+ /*
227
+ * Prompts put aside with Cmd+S. The shelf is the whole app's, not this chat's. On a canvas a
228
+ * stashed prompt usually moves to another node, which is the reason to put it away in the first
229
+ * place. Restoring drops the text, the mentions and the skills into this composer; the files a
230
+ * draft held are named on the row but not kept, because their bytes never go to storage.
231
+ */
232
+ export function StashPicker({ onRestore }: { onRestore(prompt: StashedPrompt): void }) {
233
+ const { t } = useTranslation('agent-chat');
234
+ const prompts = useStash((s) => s.prompts);
235
+ if (prompts.length === 0) {
236
+ return null;
237
+ }
238
+ return (
239
+ <Popover.Root>
240
+ <Tooltip label={t('pickers.stash.title')} kbd={STASH_SHORTCUT}>
241
+ <Popover.Trigger className={triggerClass}>
242
+ <Icon icon={Bookmark} size={12} />
243
+ <span className="tabular-nums">{prompts.length}</span>
244
+ </Popover.Trigger>
245
+ </Tooltip>
246
+ <Popover.Popup variant="picker" side="top" className="w-80">
247
+ <div className="max-h-72 overflow-auto p-1">
248
+ {prompts.map((prompt) => (
249
+ <div key={prompt.id} className="group/stash flex items-start gap-1">
250
+ <Popover.Close
251
+ className="flex min-w-0 grow flex-col gap-0.5 rounded-md px-2 py-1.5 text-left text-xs text-text-muted hover:bg-surface-hover hover:text-text"
252
+ onClick={() => onRestore(prompt)}
253
+ >
254
+ <span className="line-clamp-2 whitespace-pre-wrap">{prompt.text || t('pickers.stash.noText')}</span>
255
+ {prompt.attachments.length > 0 && (
256
+ <span className="text-text-faint">{t('pickers.stash.files', { count: prompt.attachments.length })}</span>
257
+ )}
258
+ </Popover.Close>
259
+ <IconButton
260
+ icon={Trash2}
261
+ size="xs"
262
+ label={t('common.action.delete')}
263
+ className="mt-1 opacity-0 group-hover/stash:opacity-100 focus-visible:opacity-100"
264
+ onClick={() => forgetStashed(prompt.id)}
265
+ />
266
+ </div>
267
+ ))}
268
+ </div>
269
+ </Popover.Popup>
270
+ </Popover.Root>
271
+ );
272
+ }