@adecore/agents-react 0.38.0 → 0.40.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 (351) hide show
  1. package/dist/agents/AgentModelLabel.d.ts +10 -0
  2. package/dist/agents/AgentModelLabel.js +98 -0
  3. package/dist/agents/model-name.js +2 -1
  4. package/dist/agents/provider-paths.d.ts +2 -1
  5. package/dist/agents/provider-paths.js +7 -3
  6. package/dist/chat/account-choice.js +57 -28
  7. package/dist/chat/bookmarks.js +2 -1
  8. package/dist/chat/drafts.d.ts +0 -1
  9. package/dist/chat/drafts.js +9 -28
  10. package/dist/chat/logic/picker-entries.js +2 -8
  11. package/dist/chat/logic/resume-compaction.js +2 -5
  12. package/dist/chat/logic/timeline-copy.js +2 -8
  13. package/dist/chat/logic/timeline.js +9 -10
  14. package/dist/chat/logic/workflow.js +2 -4
  15. package/dist/chat/subagent-conversation.js +2 -1
  16. package/dist/chat/ui/Composer.js +385 -360
  17. package/dist/chat/ui/GoalDock.js +2 -1
  18. package/dist/chat/ui/LimitState.js +4 -6
  19. package/dist/chat/ui/Pickers.d.ts +2 -1
  20. package/dist/chat/ui/Pickers.js +126 -123
  21. package/dist/chat/ui/PromptEditor.d.ts +19 -0
  22. package/dist/chat/ui/PromptEditor.js +115 -0
  23. package/dist/chat/ui/Timeline.d.ts +2 -1
  24. package/dist/chat/ui/Timeline.js +13 -1
  25. package/dist/chat/ui/composer/ComposerControls.d.ts +3 -0
  26. package/dist/chat/ui/composer/ComposerControls.js +235 -38
  27. package/dist/chat/ui/composer/key-handler.d.ts +1 -1
  28. package/dist/chat/ui/composer/key-handler.js +3 -11
  29. package/dist/chat/ui/composer/prompt-picks.d.ts +4 -0
  30. package/dist/chat/ui/composer/prompt-picks.js +14 -0
  31. package/dist/chat/ui/composer/use-composer-menus.d.ts +4 -49
  32. package/dist/chat/ui/composer/use-composer-menus.js +65 -519
  33. package/dist/chat/ui/composer/use-continuable-turn.js +0 -1
  34. package/dist/chat/ui/composer/use-notice.js +2 -1
  35. package/dist/chat/ui/composer/use-prompt-history.js +1 -1
  36. package/dist/chat/ui/composer/use-prompt-menus.d.ts +73 -0
  37. package/dist/chat/ui/composer/use-prompt-menus.js +499 -0
  38. package/dist/chat/ui/composer/use-take-back.js +2 -2
  39. package/dist/chat/ui/diff/ApprovalReview.js +1 -1
  40. package/dist/chat/ui/highlighter.d.ts +0 -6
  41. package/dist/chat/ui/highlighter.js +0 -12
  42. package/dist/chat/ui/icons.d.ts +0 -1
  43. package/dist/chat/ui/icons.js +0 -3
  44. package/dist/chat/ui/index.d.ts +3 -0
  45. package/dist/chat/ui/index.js +2 -0
  46. package/dist/chat/ui/rows/MessageRows.js +2 -5
  47. package/dist/chat/ui/rows/row-rhythm.js +2 -5
  48. package/dist/chat/ui/theme-watch.js +3 -7
  49. package/dist/host-hooks.d.ts +2 -1
  50. package/dist/host-hooks.js +6 -0
  51. package/dist/host.d.ts +17 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/locales/en/agent-chat.d.ts +1 -0
  54. package/dist/locales/en/agent-chat.js +1 -0
  55. package/dist/locales/nl/agent-chat.d.ts +1 -0
  56. package/dist/locales/nl/agent-chat.js +1 -0
  57. package/dist/prompts/logic/usePromptSession.js +2 -1
  58. package/dist/providers/AddAccountForm.js +2 -1
  59. package/dist/providers/CliDetail.js +104 -130
  60. package/dist/providers/account-actions.js +6 -9
  61. package/dist/providers/index.d.ts +1 -0
  62. package/dist/providers/index.js +1 -0
  63. package/dist/state/chats.d.ts +1 -0
  64. package/dist/state/chats.js +6 -11
  65. package/dist/state/provider-accounts.d.ts +1 -1
  66. package/dist/state/provider-accounts.js +2 -8
  67. package/dist/theme.css +3 -0
  68. package/dist/transport.d.ts +2 -0
  69. package/dist/transport.js +14 -0
  70. package/dist/usage/ChartLogo.d.ts +5 -0
  71. package/dist/usage/ChartLogo.js +48 -0
  72. package/dist/usage/LimitsList.js +6 -16
  73. package/dist/usage/UsageBreakdown.js +17 -40
  74. package/dist/usage/UsageLimits.js +183 -211
  75. package/dist/usage/UsagePage.js +3 -17
  76. package/dist/usage/UsageSummary.js +4 -9
  77. package/dist/usage/limits.js +16 -31
  78. package/dist/usage/summary.d.ts +1 -1
  79. package/dist/usage/summary.js +3 -5
  80. package/package.json +26 -26
  81. package/src/agents/AccountDot.tsx +1 -0
  82. package/src/agents/AgentIcon.tsx +2 -0
  83. package/src/agents/AgentModelLabel.tsx +38 -0
  84. package/src/agents/ModelOptionControl.tsx +8 -0
  85. package/src/agents/account-limits.ts +3 -0
  86. package/src/agents/accounts.ts +12 -0
  87. package/src/agents/model-name.ts +13 -1
  88. package/src/agents/model-options.ts +2 -0
  89. package/src/agents/model-requirement.ts +4 -0
  90. package/src/agents/option-words.ts +2 -0
  91. package/src/agents/provider-paths.ts +8 -3
  92. package/src/agents/status-look.ts +1 -0
  93. package/src/chat/account-choice.ts +25 -19
  94. package/src/chat/actions.ts +5 -0
  95. package/src/chat/attachments.ts +8 -0
  96. package/src/chat/bookmarks.ts +13 -1
  97. package/src/chat/chat-client.ts +63 -0
  98. package/src/chat/chat-references.ts +3 -0
  99. package/src/chat/composer-effort.ts +5 -0
  100. package/src/chat/composer-send.ts +4 -0
  101. package/src/chat/drafts.ts +21 -17
  102. package/src/chat/forks.ts +4 -0
  103. package/src/chat/guards.ts +2 -0
  104. package/src/chat/index.ts +7 -0
  105. package/src/chat/logic/ansi.ts +47 -0
  106. package/src/chat/logic/bookmarks.ts +7 -0
  107. package/src/chat/logic/command-looks.ts +29 -0
  108. package/src/chat/logic/context-usage.ts +3 -0
  109. package/src/chat/logic/continue-after-stop.ts +5 -0
  110. package/src/chat/logic/effort.ts +4 -0
  111. package/src/chat/logic/embedded-code.ts +63 -0
  112. package/src/chat/logic/fork.ts +22 -0
  113. package/src/chat/logic/generated-image.ts +7 -0
  114. package/src/chat/logic/goal.ts +7 -0
  115. package/src/chat/logic/handback.ts +2 -0
  116. package/src/chat/logic/limit.ts +8 -0
  117. package/src/chat/logic/picker-entries.ts +16 -4
  118. package/src/chat/logic/resume-compaction.ts +22 -5
  119. package/src/chat/logic/scrubber.ts +38 -5
  120. package/src/chat/logic/thought.ts +5 -0
  121. package/src/chat/logic/thread-cards.ts +6 -0
  122. package/src/chat/logic/timeline-copy.ts +4 -9
  123. package/src/chat/logic/timeline-target.ts +15 -0
  124. package/src/chat/logic/timeline.ts +113 -10
  125. package/src/chat/logic/tools.ts +30 -0
  126. package/src/chat/logic/visual-height.ts +5 -0
  127. package/src/chat/logic/visual-viewport.ts +1 -0
  128. package/src/chat/logic/welcome.ts +3 -0
  129. package/src/chat/logic/workflow.ts +26 -7
  130. package/src/chat/mentions.ts +56 -0
  131. package/src/chat/pending-prompts.ts +16 -0
  132. package/src/chat/persisted-json.ts +15 -0
  133. package/src/chat/preferences.ts +16 -0
  134. package/src/chat/prompt-hold.ts +2 -0
  135. package/src/chat/quote.ts +7 -0
  136. package/src/chat/reading-width.ts +1 -0
  137. package/src/chat/recent-messages.ts +5 -0
  138. package/src/chat/resume-compaction-dismissals.ts +1 -0
  139. package/src/chat/stash.ts +12 -0
  140. package/src/chat/subagent-conversation.ts +24 -4
  141. package/src/chat/subagent-list.ts +14 -0
  142. package/src/chat/subagent-support.ts +1 -0
  143. package/src/chat/subagent-view.ts +9 -0
  144. package/src/chat/thumbnails.ts +14 -0
  145. package/src/chat/timeline-flash.ts +2 -0
  146. package/src/chat/timeline-scroll.ts +23 -0
  147. package/src/chat/ui/AccountPill.tsx +4 -0
  148. package/src/chat/ui/AnsiOutput.tsx +1 -0
  149. package/src/chat/ui/BookmarkMarker.tsx +5 -0
  150. package/src/chat/ui/BookmarkSubmenu.tsx +1 -0
  151. package/src/chat/ui/ChatActivity.tsx +13 -0
  152. package/src/chat/ui/CodeBlock.tsx +8 -0
  153. package/src/chat/ui/Composer.tsx +27 -3
  154. package/src/chat/ui/ComposerInput.tsx +22 -0
  155. package/src/chat/ui/DiffPool.tsx +3 -0
  156. package/src/chat/ui/EditDiff.tsx +5 -0
  157. package/src/chat/ui/GoalDock.tsx +13 -1
  158. package/src/chat/ui/ImageView.tsx +11 -0
  159. package/src/chat/ui/LimitState.tsx +23 -6
  160. package/src/chat/ui/Markdown.tsx +41 -0
  161. package/src/chat/ui/MermaidBlock.tsx +16 -0
  162. package/src/chat/ui/MessageActions.tsx +8 -0
  163. package/src/chat/ui/ModelMarks.tsx +1 -0
  164. package/src/chat/ui/Pickers.tsx +30 -1
  165. package/src/chat/ui/PromptComposer.tsx +2 -0
  166. package/src/chat/ui/PromptEditor.tsx +136 -0
  167. package/src/chat/ui/QuoteButton.tsx +12 -0
  168. package/src/chat/ui/ReferenceChip.tsx +3 -0
  169. package/src/chat/ui/ResumeCompactionDock.tsx +1 -0
  170. package/src/chat/ui/RowTime.tsx +1 -0
  171. package/src/chat/ui/RunSettings.tsx +38 -0
  172. package/src/chat/ui/Scrubber.tsx +14 -0
  173. package/src/chat/ui/SessionLine.tsx +1 -0
  174. package/src/chat/ui/ShellCommand.tsx +32 -0
  175. package/src/chat/ui/SubagentControls.tsx +7 -0
  176. package/src/chat/ui/SubagentInfo.tsx +1 -0
  177. package/src/chat/ui/SubagentStopButton.tsx +6 -0
  178. package/src/chat/ui/SubagentTimeline.tsx +10 -0
  179. package/src/chat/ui/Timeline.tsx +141 -1
  180. package/src/chat/ui/TimelineMenu.tsx +5 -0
  181. package/src/chat/ui/UnifiedDiff.tsx +6 -0
  182. package/src/chat/ui/UploadThumb.tsx +2 -0
  183. package/src/chat/ui/VisualDialogs.tsx +3 -0
  184. package/src/chat/ui/VisualFrame.tsx +15 -0
  185. package/src/chat/ui/Welcome.tsx +1 -0
  186. package/src/chat/ui/chip-clipboard.ts +20 -0
  187. package/src/chat/ui/code-lines.ts +14 -0
  188. package/src/chat/ui/code-theme.ts +1 -0
  189. package/src/chat/ui/composer/ComposerControls.tsx +70 -15
  190. package/src/chat/ui/composer/ComposerMenu.tsx +12 -0
  191. package/src/chat/ui/composer/DraftParts.tsx +2 -0
  192. package/src/chat/ui/composer/QueuedMessages.tsx +4 -0
  193. package/src/chat/ui/composer/chip-paste.ts +16 -0
  194. package/src/chat/ui/composer/chips.ts +6 -0
  195. package/src/chat/ui/composer/editor.ts +9 -0
  196. package/src/chat/ui/composer/insert.ts +2 -0
  197. package/src/chat/ui/composer/key-handler.ts +42 -9
  198. package/src/chat/ui/composer/keys.ts +23 -0
  199. package/src/chat/ui/composer/placeholder.ts +6 -0
  200. package/src/chat/ui/composer/prompt-picks.ts +18 -0
  201. package/src/chat/ui/composer/surface.ts +5 -0
  202. package/src/chat/ui/composer/use-chat-controls.ts +13 -0
  203. package/src/chat/ui/composer/use-compaction-offer.ts +3 -0
  204. package/src/chat/ui/composer/use-composer-menus.ts +20 -275
  205. package/src/chat/ui/composer/use-composer-submit.ts +27 -0
  206. package/src/chat/ui/composer/use-continuable-turn.ts +3 -5
  207. package/src/chat/ui/composer/use-draft-files.ts +24 -0
  208. package/src/chat/ui/composer/use-effort-stepper.ts +8 -0
  209. package/src/chat/ui/composer/use-notice.ts +4 -1
  210. package/src/chat/ui/composer/use-pending-requests.ts +3 -0
  211. package/src/chat/ui/composer/use-prompt-history.ts +20 -11
  212. package/src/chat/ui/composer/use-prompt-hold.ts +3 -0
  213. package/src/chat/ui/composer/use-prompt-menus.ts +361 -0
  214. package/src/chat/ui/composer/use-take-back.ts +20 -2
  215. package/src/chat/ui/diff/ApprovalReview.tsx +18 -15
  216. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +1 -0
  217. package/src/chat/ui/diff/DiffOmitted.tsx +1 -0
  218. package/src/chat/ui/diff/DiffStack.tsx +2 -0
  219. package/src/chat/ui/diff/DiffUnavailable.tsx +1 -0
  220. package/src/chat/ui/diff/EditorEditDiff.tsx +13 -0
  221. package/src/chat/ui/diff/EditorPatchDiff.tsx +4 -0
  222. package/src/chat/ui/diff/LegacyDiffPool.tsx +2 -0
  223. package/src/chat/ui/diff/LegacyEditDiff.tsx +2 -0
  224. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +8 -0
  225. package/src/chat/ui/diff/RawDiff.tsx +1 -0
  226. package/src/chat/ui/diff/ReviewDiff.tsx +1 -0
  227. package/src/chat/ui/diff/diff-source-budget.ts +8 -0
  228. package/src/chat/ui/diff/editor-edit-model.ts +4 -0
  229. package/src/chat/ui/diff/review-plan.ts +9 -0
  230. package/src/chat/ui/diff/review-settle.ts +1 -0
  231. package/src/chat/ui/diff/syntax-runtime.ts +52 -0
  232. package/src/chat/ui/diff/use-review-stack.ts +9 -0
  233. package/src/chat/ui/diff-syntax.ts +12 -0
  234. package/src/chat/ui/diff-theme.ts +2 -0
  235. package/src/chat/ui/file-links.ts +2 -0
  236. package/src/chat/ui/find-reveal.ts +1 -0
  237. package/src/chat/ui/full-diff.ts +1 -0
  238. package/src/chat/ui/highlighter.ts +29 -15
  239. package/src/chat/ui/icons.tsx +2 -4
  240. package/src/chat/ui/index.ts +7 -0
  241. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +25 -0
  242. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +1 -0
  243. package/src/chat/ui/intelligent-ui/UiReply.tsx +63 -0
  244. package/src/chat/ui/intelligent-ui/block-local.ts +5 -0
  245. package/src/chat/ui/intelligent-ui/chart-data.ts +15 -0
  246. package/src/chat/ui/intelligent-ui/node-text.ts +17 -0
  247. package/src/chat/ui/intelligent-ui/renderers/binding.ts +1 -0
  248. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +31 -0
  249. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +4 -0
  250. package/src/chat/ui/intelligent-ui/renderers/content.tsx +18 -0
  251. package/src/chat/ui/intelligent-ui/renderers/data.tsx +11 -0
  252. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +13 -0
  253. package/src/chat/ui/intelligent-ui/renderers/links.tsx +9 -0
  254. package/src/chat/ui/intelligent-ui/renderers/map-slots.ts +15 -0
  255. package/src/chat/ui/intelligent-ui/renderers/map-style.ts +15 -0
  256. package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +48 -0
  257. package/src/chat/ui/intelligent-ui/renderers/map.tsx +20 -0
  258. package/src/chat/ui/intelligent-ui/renderers/place-point.ts +3 -0
  259. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +3 -0
  260. package/src/chat/ui/intelligent-ui/renderers/text.tsx +8 -0
  261. package/src/chat/ui/intelligent-ui/reply-parts.ts +5 -0
  262. package/src/chat/ui/intelligent-ui/table-data.ts +17 -0
  263. package/src/chat/ui/intelligent-ui/use-in-view.ts +4 -0
  264. package/src/chat/ui/intelligent-ui/use-later.ts +4 -0
  265. package/src/chat/ui/intelligent-ui/use-truncated.ts +5 -0
  266. package/src/chat/ui/intelligent-ui/use-ui-links.ts +29 -0
  267. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +60 -0
  268. package/src/chat/ui/markdown-blocks.ts +64 -0
  269. package/src/chat/ui/mermaid.ts +4 -0
  270. package/src/chat/ui/quote-selection.ts +10 -0
  271. package/src/chat/ui/rehype-chips.ts +5 -0
  272. package/src/chat/ui/rehype-fade.ts +2 -0
  273. package/src/chat/ui/remark-html-as-text.ts +5 -0
  274. package/src/chat/ui/reveal.ts +18 -0
  275. package/src/chat/ui/rows/ForksRow.tsx +5 -0
  276. package/src/chat/ui/rows/GeneratedImageRow.tsx +10 -0
  277. package/src/chat/ui/rows/MessageRows.tsx +29 -5
  278. package/src/chat/ui/rows/RowContainer.tsx +1 -0
  279. package/src/chat/ui/rows/Rows.tsx +2 -0
  280. package/src/chat/ui/rows/SubagentRow.tsx +13 -0
  281. package/src/chat/ui/rows/TaskRow.tsx +7 -0
  282. package/src/chat/ui/rows/VisualRow.tsx +4 -0
  283. package/src/chat/ui/rows/WorkRows.tsx +46 -0
  284. package/src/chat/ui/rows/WorkflowRow.tsx +10 -0
  285. package/src/chat/ui/rows/row-rhythm.ts +7 -5
  286. package/src/chat/ui/theme-watch.ts +8 -7
  287. package/src/chat/ui/thread-references.ts +7 -0
  288. package/src/chat/ui/use-diff-source-tokens.ts +8 -0
  289. package/src/chat/ui/useToggleSet.ts +3 -0
  290. package/src/chat/ui/visual-theme.ts +30 -0
  291. package/src/chat/visual-bridge.ts +23 -0
  292. package/src/chat/visual-viewport.ts +12 -0
  293. package/src/host-hooks.ts +15 -5
  294. package/src/host.ts +29 -1
  295. package/src/index.ts +5 -0
  296. package/src/lazy.tsx +9 -0
  297. package/src/locales/en/agent-chat.ts +1 -0
  298. package/src/locales/nl/agent-chat.ts +1 -0
  299. package/src/mounted-registry.ts +4 -0
  300. package/src/port-transport.ts +32 -0
  301. package/src/prompts/index.ts +2 -0
  302. package/src/prompts/logic/focus.ts +5 -0
  303. package/src/prompts/logic/keys.ts +12 -0
  304. package/src/prompts/logic/prompts.ts +8 -0
  305. package/src/prompts/logic/subjects.ts +8 -0
  306. package/src/prompts/logic/useFocusAfterAnswer.ts +2 -0
  307. package/src/prompts/logic/usePromptSession.ts +13 -1
  308. package/src/prompts/ui/ApprovalBody.tsx +2 -0
  309. package/src/prompts/ui/PromptActions.tsx +2 -0
  310. package/src/prompts/ui/PromptCard.tsx +6 -0
  311. package/src/prompts/ui/PromptView.tsx +6 -0
  312. package/src/prompts/ui/QuestionBody.tsx +11 -0
  313. package/src/providers/AccountColors.tsx +1 -0
  314. package/src/providers/AccountDetail.tsx +7 -0
  315. package/src/providers/AccountVariables.tsx +2 -0
  316. package/src/providers/AddAccountForm.tsx +5 -1
  317. package/src/providers/CliDetail.tsx +6 -5
  318. package/src/providers/InstallPage.tsx +11 -0
  319. package/src/providers/ProviderList.tsx +3 -0
  320. package/src/providers/ProvidersPane.tsx +18 -0
  321. package/src/providers/account-actions.ts +10 -8
  322. package/src/providers/index.ts +1 -0
  323. package/src/providers/install-steps.ts +3 -0
  324. package/src/providers/pane-target.ts +5 -0
  325. package/src/providers/provider-abilities.ts +1 -0
  326. package/src/providers/provider-groups.ts +6 -0
  327. package/src/providers/variables.ts +12 -0
  328. package/src/scope.ts +2 -0
  329. package/src/state/chats.ts +61 -14
  330. package/src/state/index.ts +2 -0
  331. package/src/state/provider-accounts.ts +7 -9
  332. package/src/state/providers.ts +1 -0
  333. package/src/state/usage.ts +6 -0
  334. package/src/storage.ts +18 -0
  335. package/src/testing/prompts.ts +5 -0
  336. package/src/theme.css +3 -0
  337. package/src/transport.ts +18 -0
  338. package/src/usage/ChartLogo.tsx +12 -0
  339. package/src/usage/LimitsList.tsx +7 -8
  340. package/src/usage/UsageBreakdown.tsx +11 -4
  341. package/src/usage/UsageChart.tsx +5 -0
  342. package/src/usage/UsageLimits.tsx +10 -7
  343. package/src/usage/UsageLimitsCard.tsx +2 -0
  344. package/src/usage/UsagePage.tsx +16 -19
  345. package/src/usage/UsageSummary.tsx +4 -5
  346. package/src/usage/UsageTiles.tsx +2 -0
  347. package/src/usage/format.ts +7 -0
  348. package/src/usage/limit-groups.ts +14 -0
  349. package/src/usage/limits.ts +11 -13
  350. package/src/usage/money.ts +1 -0
  351. package/src/usage/summary.ts +15 -7
@@ -18,6 +18,7 @@ import { useChatActions } from '../actions';
18
18
  import { useUsageStore } from '../../state/usage';
19
19
  import { SessionLine } from './SessionLine';
20
20
  import { useHostResumeAtReset } from '../../host-hooks';
21
+ import { errorMessage } from '../../transport';
21
22
 
22
23
  // A time a limit names reads as a clock today and with its day after that, so the words follow the minute.
23
24
  const MINUTE_MS = 60_000;
@@ -32,10 +33,13 @@ const refreshedAt = new Map<string, number>();
32
33
  */
33
34
  function readUnreadAccounts(scope: ChatScope): void {
34
35
  const last = refreshedAt.get(scope.id);
36
+
35
37
  if (last !== undefined && Date.now() - last < UNREAD_REFRESH_MS) {
36
38
  return;
37
39
  }
40
+
38
41
  refreshedAt.set(scope.id, Date.now());
42
+
39
43
  void scope.transport
40
44
  .request('usage.refreshLimits', {})
41
45
  .then((snapshot) => useUsageStore.getState().setLimits(scope.id, snapshot))
@@ -51,9 +55,11 @@ function ResumeAtResetToggle({ chatId, info }: { chatId: string; info: ChatInfo
51
55
  const { id } = useChatScope();
52
56
  const actions = useChatActions();
53
57
  const machineAllows = useHostResumeAtReset(id);
58
+
54
59
  const set = (on: boolean): void => {
55
60
  void actions.configure(chatId, { resumeAtReset: on }).catch(() => undefined);
56
61
  };
62
+
57
63
  return (
58
64
  <Tooltip label={machineAllows ? t('limit.toggle') : t('limit.machineOff')}>
59
65
  {/* A disabled switch does not take the pointer, so the tooltip hangs on a wrapper. */}
@@ -65,11 +71,8 @@ function ResumeAtResetToggle({ chatId, info }: { chatId: string; info: ChatInfo
65
71
  }
66
72
 
67
73
  /*
68
- * Another account of the chat's CLI that has room: the one with the least of its session spent, and
69
- * beside it a menu of every account with room, for a person whose companies each pay for their own
70
- * work. Only ever offered, since going on there spends someone's other plan, and nothing is
71
- * remembered: the next limit suggests the one with the most room again. An account that reads the
72
- * chat's conversation takes it over; any other goes on in a fork, which then opens the way a fork does.
74
+ * Another account of the chat's CLI with room, only ever offered since it spends someone's other plan, and
75
+ * never remembered. One that reads the chat's conversation takes it over; any other goes on in a fork.
73
76
  */
74
77
  function ContinueOn({ chatId, info, choice }: { chatId: string; info: ChatInfo; choice: AccountChoice }) {
75
78
  const host = useChatHost();
@@ -88,24 +91,29 @@ function ContinueOn({ chatId, info, choice }: { chatId: string; info: ChatInfo;
88
91
  }, [scope, unread]);
89
92
 
90
93
  const target = targets[0];
94
+
91
95
  if (target === undefined) {
92
96
  return null;
93
97
  }
98
+
94
99
  const go = (entry: AccountEntry): void => {
95
100
  setBusy(true);
101
+
96
102
  actions
97
103
  .continueOn(chatId, entry.id)
98
104
  .catch((e: unknown) =>
99
105
  host.notify({
100
106
  kind: 'error',
101
107
  title: t('limit.continueFailed', { account: choice.nameOf(entry) }),
102
- description: e instanceof Error ? e.message : String(e)
108
+ description: errorMessage(e)
103
109
  })
104
110
  )
105
111
  .finally(() => setBusy(false));
106
112
  };
113
+
107
114
  const name = choice.nameOf(target);
108
115
  const split = targets.length > 1;
116
+
109
117
  const suggestion = (
110
118
  <Tooltip label={t('limit.continueOnTip', { account: name })}>
111
119
  <Button
@@ -122,9 +130,11 @@ function ContinueOn({ chatId, info, choice }: { chatId: string; info: ChatInfo;
122
130
  </Button>
123
131
  </Tooltip>
124
132
  );
133
+
125
134
  if (!split) {
126
135
  return suggestion;
127
136
  }
137
+
128
138
  return (
129
139
  <ButtonGroup className="shrink-0">
130
140
  {suggestion}
@@ -160,10 +170,12 @@ function ContinueOnRows({
160
170
  const { t } = useTranslation('agent-chat');
161
171
  const limits = useUsageLimits();
162
172
  const now = useNow(MINUTE_MS);
173
+
163
174
  return targets.map((entry) => {
164
175
  const name = choice.nameOf(entry);
165
176
  const session = sessionWindow(limitsOfAccount(limits, entry.id));
166
177
  const fork = !canContinueOn(choice.accounts, info.provider, info.account, entry.id);
178
+
167
179
  return (
168
180
  <Menu.Item
169
181
  key={entry.id}
@@ -191,12 +203,15 @@ export function LimitDock({ chatId, info }: { chatId: string; info: ChatInfo })
191
203
  const now = useNow(MINUTE_MS, info.limit !== undefined);
192
204
  const choice = useAccountChoice(info.provider, info.account);
193
205
  const view = limitView(info, now);
206
+
194
207
  if (view === null) {
195
208
  return null;
196
209
  }
210
+
197
211
  // With a choice of accounts the line names the one that ran into the limit.
198
212
  const account = choice === null ? null : choice.current === null ? choice.currentId : choice.nameOf(choice.current);
199
213
  const detail = account === null ? view.detail : view.detail === null ? account : `${account} · ${view.detail}`;
214
+
200
215
  return (
201
216
  <div className="flex items-center gap-2 border-b border-border px-3.5 py-2.5">
202
217
  <Icon icon={Hourglass} size={16} className="shrink-0 text-text-faint" />
@@ -218,9 +233,11 @@ export function LimitPill({ chatId }: { chatId: string }) {
218
233
  useFormatLocale();
219
234
  const now = useNow(MINUTE_MS, info?.limit !== undefined);
220
235
  const view = info === undefined ? null : limitView(info, now);
236
+
221
237
  if (info === undefined || view === null) {
222
238
  return null;
223
239
  }
240
+
224
241
  return (
225
242
  <>
226
243
  <Tooltip label={view.detail === null ? view.title : `${view.title}. ${view.detail}`}>
@@ -37,12 +37,15 @@ interface MarkdownNode {
37
37
  /* The command a shell fence holds, read from the fence as written, so it keeps the indentation and line endings it has. */
38
38
  function shellFence(source: string): ShellFence | null {
39
39
  const opening = /^ {0,3}(`{3,}|~{3,})[ \t]*(sh|bash|zsh)[ \t]*\r?\n/.exec(source);
40
+
40
41
  if (opening === null) {
41
42
  return null;
42
43
  }
44
+
43
45
  const body = source.slice(opening[0].length);
44
46
  const closing = /(?:^|\r?\n) {0,3}(`{3,}|~{3,})[ \t]*$/.exec(body);
45
47
  const closed = closing !== null && closing[1]![0] === opening[1]![0] && closing[1]!.length >= opening[1]!.length;
48
+
46
49
  return { language: opening[2] as ShellFence['language'], code: closed ? body.slice(0, closing.index) : body, closed };
47
50
  }
48
51
 
@@ -53,15 +56,20 @@ function remarkShellCodeBlocks() {
53
56
  if (node.type !== 'code') {
54
57
  continue;
55
58
  }
59
+
56
60
  const start = node.position?.start.offset;
57
61
  const end = node.position?.end.offset;
62
+
58
63
  if (start === undefined || end === undefined) {
59
64
  continue;
60
65
  }
66
+
61
67
  const shell = shellFence(String(file.value).slice(start, end));
68
+
62
69
  if (shell === null) {
63
70
  continue;
64
71
  }
72
+
65
73
  node.data = {
66
74
  ...node.data,
67
75
  hProperties: {
@@ -80,7 +88,9 @@ function shellCodeBlockActions(reply: ShellCodeBlockReply | null, shell: ShellFe
80
88
  if (reply === null || shell === null) {
81
89
  return undefined;
82
90
  }
91
+
83
92
  const { scopeId, chatId, itemId } = reply;
93
+
84
94
  return render?.({ scopeId, chatId, itemId, language: shell.language, code: shell.code, complete: !reply.streaming && shell.closed });
85
95
  }
86
96
 
@@ -98,6 +108,7 @@ function FileLink({ target, className, children }: { target: FileRef; className?
98
108
  const cwd = useFileLinkCwd();
99
109
  const scopeId = useFileLinkScopeId();
100
110
  const openFileLink = useOpenFileLink();
111
+
101
112
  return (
102
113
  // A button is not selectable on its own, and a thread is copied as often as it is clicked.
103
114
  <button
@@ -120,9 +131,11 @@ function FileLink({ target, className, children }: { target: FileRef; className?
120
131
  /* Inline code, which is a file reference often enough that it is worth asking every time. */
121
132
  function InlineCode({ text }: { text: string }) {
122
133
  const target = useFileLinkTarget(text);
134
+
123
135
  if (target === null) {
124
136
  return <code className={INLINE_CODE}>{text}</code>;
125
137
  }
138
+
126
139
  return (
127
140
  // The chip sits on a code inside the button, since a button is always a box of its own and its
128
141
  // background would fill the whole box once a long path wraps.
@@ -138,6 +151,7 @@ function InlineCode({ text }: { text: string }) {
138
151
  */
139
152
  function hrefPath(href: string): string {
140
153
  const bare = href.replace(/^file:\/\//, '');
154
+
141
155
  try {
142
156
  return decodeURI(bare);
143
157
  } catch {
@@ -148,6 +162,7 @@ function hrefPath(href: string): string {
148
162
  /* A markdown link, which is a file when its target names no scheme of its own. */
149
163
  function MarkdownLink({ href, children }: { href?: string; children?: ReactNode }) {
150
164
  const target = useFileLinkTarget(hrefPath(href ?? ''));
165
+
151
166
  if (target !== null) {
152
167
  return (
153
168
  <FileLink target={target} className="text-accent underline underline-offset-2">
@@ -155,6 +170,7 @@ function MarkdownLink({ href, children }: { href?: string; children?: ReactNode
155
170
  </FileLink>
156
171
  );
157
172
  }
173
+
158
174
  return <ExternalLink href={href}>{children}</ExternalLink>;
159
175
  }
160
176
 
@@ -174,13 +190,17 @@ function localImagePath(src: string, cwd: string | null): string | null {
174
190
  if (src === '' || /^[a-z][a-z0-9+.-]*:/i.test(src)) {
175
191
  return null;
176
192
  }
193
+
177
194
  const target = hrefPath(src);
195
+
178
196
  if (target.startsWith('/')) {
179
197
  return target;
180
198
  }
199
+
181
200
  if (cwd === null) {
182
201
  return null;
183
202
  }
203
+
184
204
  return `${cwd.replace(/\/+$/, '')}/${target.replace(/^\.\//, '')}`;
185
205
  }
186
206
 
@@ -189,9 +209,11 @@ function MarkdownImage({ src, alt }: { src?: string; alt?: string }) {
189
209
  const { MarkdownImage: HostImage } = useChatHost();
190
210
  const cwd = useFileLinkCwd();
191
211
  const path = typeof src === 'string' ? localImagePath(src, cwd) : null;
212
+
192
213
  if (HostImage === undefined || path === null) {
193
214
  return <img src={src} alt={alt} />;
194
215
  }
216
+
195
217
  return <HostImage path={path} alt={alt ?? ''} />;
196
218
  }
197
219
 
@@ -202,6 +224,7 @@ function PlainImage({ src, alt }: { src?: string; alt?: string }) {
202
224
  /* A ```mermaid fence, which draws what it says. */
203
225
  function Diagram({ code }: { code: string }) {
204
226
  const { t } = useTranslation('agent-chat');
227
+
205
228
  return (
206
229
  <ErrorBoundary label={t('code.mermaid.crashed')} resetKeys={[code]} compact className="rounded-lg border border-border">
207
230
  <MermaidBlock code={code} />
@@ -212,11 +235,14 @@ function Diagram({ code }: { code: string }) {
212
235
  function MarkdownCode({ className, children, plain = false }: { className?: string; children?: ReactNode; plain?: boolean }) {
213
236
  const block = useContext(BlockCodeContext);
214
237
  const text = String(children ?? '');
238
+
215
239
  if (block) {
216
240
  const lang = languageOf(className);
217
241
  const code = text.replace(/\n$/, '');
242
+
218
243
  return lang === 'mermaid' ? <Diagram code={code} /> : <CodeBlock code={code} lang={lang} />;
219
244
  }
245
+
220
246
  return plain ? <code className={INLINE_CODE}>{text}</code> : <InlineCode text={text} />;
221
247
  }
222
248
 
@@ -259,6 +285,7 @@ const REPLY_HEADING_OFFSET = 3;
259
285
 
260
286
  function replyHeading(level: 1 | 2 | 3 | 4 | 5 | 6) {
261
287
  const Tag = `h${level}` as const;
288
+
262
289
  return function ReplyHeading({ children }: { children?: ReactNode }) {
263
290
  return <Tag aria-level={Math.min(6, level + REPLY_HEADING_OFFSET)}>{children}</Tag>;
264
291
  };
@@ -283,17 +310,22 @@ function ReplyCode({ className, children, node }: { className?: string; children
283
310
  const properties = node?.properties;
284
311
  const language = properties?.['data-shell-language'];
285
312
  const code = properties?.['data-shell-code'];
313
+
286
314
  const shell: ShellFence | null =
287
315
  (language === 'sh' || language === 'bash' || language === 'zsh') && typeof code === 'string'
288
316
  ? { language, code, closed: properties?.['data-shell-closed'] === true }
289
317
  : null;
318
+
290
319
  const text = String(children ?? '');
320
+
291
321
  if (block && languageOf(className) === 'mermaid') {
292
322
  return <Diagram code={text.replace(/\n$/, '')} />;
293
323
  }
324
+
294
325
  if (block) {
295
326
  return <CodeBlock code={text.replace(/\n$/, '')} lang={languageOf(className)} actions={shellCodeBlockActions(reply, shell, renderShellCodeBlock)} />;
296
327
  }
328
+
297
329
  return <InlineCode text={text} />;
298
330
  }
299
331
 
@@ -302,6 +334,7 @@ function Chip({ kind, value }: { kind: string; value: string }) {
302
334
  if (kind === 'reference') {
303
335
  return <ReferenceChip id={value} />;
304
336
  }
337
+
305
338
  if (kind === 'skill') {
306
339
  return (
307
340
  <TokenChip
@@ -314,6 +347,7 @@ function Chip({ kind, value }: { kind: string; value: string }) {
314
347
  />
315
348
  );
316
349
  }
350
+
317
351
  return (
318
352
  // The path is what the thread's menu opens in the preview from here.
319
353
  <TokenChip
@@ -336,6 +370,7 @@ function ChipSpan({ children, node: _node, 'data-chip': chip, 'data-value': valu
336
370
  if (chip === undefined || value === undefined) {
337
371
  return <span {...rest}>{children}</span>;
338
372
  }
373
+
339
374
  return <Chip kind={chip} value={value} />;
340
375
  }
341
376
 
@@ -352,8 +387,10 @@ const ReplyBlock = memo(function ReplyBlock({ text, fade, open, reply }: { text:
352
387
  const { items, referenced } = useContext(ReferenceLookupContext);
353
388
  // Chips first, so the fade wraps the words around a chip and never cuts a name in two.
354
389
  const named: NamedReferenceOptions = { items, referenced };
390
+
355
391
  const rehypePlugins: Options['rehypePlugins'] =
356
392
  items.length === 0 ? (fade ? FADE_PLUGINS : NO_PLUGINS) : [[rehypeNamedReferences, named], ...(fade ? FADE_PLUGINS : [])];
393
+
357
394
  return (
358
395
  // A context rather than a second set of components. A component that changed would mount the
359
396
  // code block again the moment its fence closes, and it would lose the lines it already has.
@@ -389,6 +426,7 @@ const ReplyFence = memo(function ReplyFence({
389
426
  // The class markdown gives the code, read back the way a parsed fence's is.
390
427
  const lang = languageOf(`language-${info.split(/[ \t]/, 1)[0]}`);
391
428
  const shell = reply === undefined ? null : shellFence(source);
429
+
392
430
  return (
393
431
  <CodeStreamingContext.Provider value={open}>
394
432
  {lang === 'mermaid' ? (
@@ -418,6 +456,7 @@ export const ReplyMarkdown = memo(function ReplyMarkdown({
418
456
  reply?: ShellCodeBlockReply;
419
457
  }) {
420
458
  const blocks = splitMarkdownBlocks(text);
459
+
421
460
  return (
422
461
  <div className="chat-markdown prose prose-sm max-w-none text-sm" data-arriving={arriving || undefined}>
423
462
  {blocks.map((block, index) =>
@@ -455,6 +494,7 @@ export const MessageMarkdown = memo(function MessageMarkdown({
455
494
  references?: readonly ReferenceToken[];
456
495
  }) {
457
496
  const rehypePlugins: [typeof rehypeChips, ChipOptions][] = [[rehypeChips, { mentions, skills, references }]];
497
+
458
498
  return (
459
499
  <div className="chat-markdown prose prose-sm max-w-none text-sm">
460
500
  <ReactMarkdown remarkPlugins={MESSAGE_PLUGINS} rehypePlugins={rehypePlugins} components={replyComponents}>
@@ -484,6 +524,7 @@ export const Markdown = memo(function Markdown({
484
524
  componentOverrides?: Components;
485
525
  }) {
486
526
  const renderComponents = { ...(fileLinks ? components : plainComponents), ...componentOverrides };
527
+
487
528
  return (
488
529
  <div className="chat-markdown prose prose-sm max-w-none text-sm" data-muted={muted || undefined}>
489
530
  <ReactMarkdown remarkPlugins={breaks ? PLUGINS_WITH_BREAKS : PLUGINS} rehypePlugins={rehypePlugins} components={renderComponents}>
@@ -25,14 +25,17 @@ function resolveColor(element: HTMLElement, token: string, ground: string): stri
25
25
  probeCanvas.height = 1;
26
26
  const context = probeCanvas.getContext('2d', { willReadFrequently: true });
27
27
  const value = getComputedStyle(element).getPropertyValue(token).trim();
28
+
28
29
  if (context === null || value === '') {
29
30
  return ground;
30
31
  }
32
+
31
33
  context.fillStyle = ground;
32
34
  context.fillRect(0, 0, 1, 1);
33
35
  context.fillStyle = value;
34
36
  context.fillRect(0, 0, 1, 1);
35
37
  const [red, green, blue] = context.getImageData(0, 0, 1, 1).data;
38
+
36
39
  return `#${[red, green, blue].map((channel) => channel!.toString(16).padStart(2, '0')).join('')}`;
37
40
  }
38
41
 
@@ -45,14 +48,17 @@ interface DiagramLook {
45
48
  function diagramLook(element: HTMLElement): DiagramLook {
46
49
  const style = getComputedStyle(element);
47
50
  const ground = resolveColor(element, GROUND_TOKEN, '#808080');
51
+
48
52
  const colors = Object.fromEntries(
49
53
  Object.entries(DIAGRAM_TOKENS).map(([name, token]) => [name, token === GROUND_TOKEN ? ground : resolveColor(element, token, ground)])
50
54
  );
55
+
51
56
  const font = {
52
57
  family: style.fontFamily,
53
58
  mono: style.getPropertyValue('--font-mono').trim() || 'monospace',
54
59
  size: Number.parseFloat(style.fontSize) || 14
55
60
  };
61
+
56
62
  return { colors, font };
57
63
  }
58
64
 
@@ -64,6 +70,7 @@ function keepingSame(next: DiagramLook) {
64
70
  async function layOut(id: string, source: string, font: DiagramFont): Promise<LaidOut> {
65
71
  const { renderAsync } = await import('pele/lazy');
66
72
  const { svg, width, height } = await renderAsync(source, renderOptions(font, id));
73
+
67
74
  return { svg, width: Math.ceil(width), height: Math.ceil(height) };
68
75
  }
69
76
 
@@ -84,6 +91,7 @@ export function MermaidBlock({ code }: { code: string }) {
84
91
 
85
92
  useEffect(() => {
86
93
  const element = frame.current;
94
+
87
95
  if (element !== null) {
88
96
  setLook(keepingSame(diagramLook(element)));
89
97
  }
@@ -92,25 +100,31 @@ export function MermaidBlock({ code }: { code: string }) {
92
100
  // An app may change its colors without turning light or dark, which only the root of the page tells.
93
101
  useEffect(() => {
94
102
  const element = frame.current;
103
+
95
104
  if (element === null) {
96
105
  return;
97
106
  }
107
+
98
108
  return watchTheme(element.ownerDocument, () => setLook(keepingSame(diagramLook(element))));
99
109
  }, []);
100
110
 
101
111
  const fontFamily = look?.font.family;
102
112
  const fontMono = look?.font.mono;
103
113
  const fontSize = look?.font.size;
114
+
104
115
  useEffect(() => {
105
116
  if (streaming || fontFamily === undefined || fontMono === undefined || fontSize === undefined) {
106
117
  return;
107
118
  }
119
+
108
120
  let cancelled = false;
121
+
109
122
  // A diagram already drawn stays until the next one is there, so it does not blink.
110
123
  layOut(id, code, { family: fontFamily, mono: fontMono, size: fontSize }).then(
111
124
  (image) => !cancelled && setDrawing({ state: 'drawn', image }),
112
125
  () => !cancelled && setDrawing({ state: 'failed' })
113
126
  );
127
+
114
128
  return () => {
115
129
  cancelled = true;
116
130
  };
@@ -118,6 +132,7 @@ export function MermaidBlock({ code }: { code: string }) {
118
132
 
119
133
  const image = drawing.state === 'drawn' && look !== null ? { ...drawing.image, svg: themedSvg(drawing.image.svg, look.colors) } : null;
120
134
  const showsDiagram = !streaming && !source && drawing.state !== 'failed';
135
+
121
136
  const toggle =
122
137
  streaming || drawing.state === 'failed' ? undefined : (
123
138
  <IconButton
@@ -127,6 +142,7 @@ export function MermaidBlock({ code }: { code: string }) {
127
142
  onClick={() => setSource((shown) => !shown)}
128
143
  />
129
144
  );
145
+
130
146
  return (
131
147
  // One tree in every state, so the code block keeps the lines it has when the fence closes.
132
148
  <div ref={frame}>
@@ -31,14 +31,19 @@ export function MessageActions({ chatId, row }: { chatId: string; row: MessageRo
31
31
  const bookmarks = useChatRow(chatId, (chat) => chat?.bookmarks);
32
32
  const info = useChatRow(chatId, (chat) => chat?.info ?? null);
33
33
  const turn = useChatRow(chatId, (chat) => (turnId === null ? undefined : chat?.structure[turnId]));
34
+
34
35
  const streaming = useChatRow(chatId, (chat) => {
35
36
  const item = chat?.items[row.id];
37
+
36
38
  return item?.kind === 'assistant' && item.streaming;
37
39
  });
40
+
38
41
  const hasText = useChatRow(chatId, (chat) => {
39
42
  const item = chat?.items[row.id];
43
+
40
44
  return (item?.kind === 'user' || item?.kind === 'assistant') && /\S/.test(item.text);
41
45
  });
46
+
42
47
  const bookmarked = bookmarks?.some((candidate) => candidate.itemId === row.id) === true;
43
48
  const canFork = turnId !== null && forkRefusal(info, turn) === null;
44
49
  const [copied, setCopied] = useState(false);
@@ -48,7 +53,9 @@ export function MessageActions({ chatId, row }: { chatId: string; row: MessageRo
48
53
  if (!copied) {
49
54
  return;
50
55
  }
56
+
51
57
  const timer = setTimeout(() => setCopied(false), COPIED_MS);
58
+
52
59
  return () => clearTimeout(timer);
53
60
  }, [copied]);
54
61
 
@@ -57,6 +64,7 @@ export function MessageActions({ chatId, row }: { chatId: string; row: MessageRo
57
64
  const item = useChats.getState().byKey[scope.keyOf(chatId)]?.items[row.id];
58
65
  const current = item?.kind === row.kind ? ({ ...row, item } as MessageRow) : row;
59
66
  const text = (plain ? null : markdownOf(current)) ?? messageTextOf(current);
67
+
60
68
  if (text !== null) {
61
69
  copyWithChips(messageClipboardChips(current.item, text, lookup));
62
70
  setCopied(true);
@@ -6,6 +6,7 @@ import { Icon, Pill, Tooltip } from '@adecore/ui';
6
6
  /* What a model row says beside the name: its badge, and a mark when the provider says the model can call tools. */
7
7
  export function ModelMarks({ model }: { model: ModelInfo }) {
8
8
  const { t } = useTranslation('agent-chat');
9
+
9
10
  return (
10
11
  <>
11
12
  {model.badge && (
@@ -34,6 +34,8 @@ interface ModelPickerProps {
34
34
  side?: 'top' | 'bottom';
35
35
  /* `chip` names the provider beside the model on a secondary button, for a picker that sits in a card or a form. */
36
36
  trigger?: 'plain' | 'chip';
37
+ /* The chip's height: `sm` in a card, `md` beside the fields of a form. */
38
+ chipSize?: 'sm' | 'md';
37
39
  /* Where the focus goes when the popup closes; by default back to the trigger. */
38
40
  finalFocus?: RefObject<HTMLElement | null>;
39
41
  /* A note under the list, such as what picking an agent here does. */
@@ -55,6 +57,7 @@ export function ModelPicker({
55
57
  kbd = '/model',
56
58
  side = 'top',
57
59
  trigger = 'plain',
60
+ chipSize = 'sm',
58
61
  finalFocus,
59
62
  footer
60
63
  }: ModelPickerProps) {
@@ -74,19 +77,26 @@ export function ModelPicker({
74
77
  const trimmed = query.trim().toLowerCase();
75
78
 
76
79
  const favorites = useChatPreferences((preferences) => preferences.favoriteModels);
80
+
77
81
  const starredHere = (row: PickerEntry | undefined): boolean => row?.kind === 'model' && favorites[row.provider.kind]?.includes(row.model.slug) === true;
82
+
78
83
  const entries = pickerEntriesOf(inPickerOrder(providers), trimmed, legacyOpen, expanded, { provider, model: selection.model }, (kind) =>
79
84
  favoriteModelsOf(favorites, kind)
80
85
  );
86
+
81
87
  const scope = useContext(ChatScopeContext);
82
88
  const machine = useHostMachineLabel(scope?.id ?? '');
83
89
  const reported = useReportedModels(provider);
90
+
84
91
  const requirementOf = (entry: PickerEntry & { kind: 'model' }) => modelRequirementOf(entry.model, entry.provider, reported);
92
+
85
93
  // A CLI that is too old blocks all its models; one way to its settings under the group says it once.
86
94
  const kindBefore = (i: number): AgentKind | undefined => {
87
95
  const before = entries[i - 1];
96
+
88
97
  return before === undefined || before.kind === 'legacy' ? undefined : before.provider.kind;
89
98
  };
99
+
90
100
  const blockedKinds = new Set(entries.flatMap((entry) => (entry.kind === 'model' && requirementOf(entry) !== null ? [entry.provider.kind] : [])));
91
101
 
92
102
  /* Closing forgets the search, so the next open (a click or `/model`) starts on the whole list. */
@@ -96,6 +106,7 @@ export function ModelPicker({
96
106
  setLegacyOpen(false);
97
107
  setExpanded(new Set());
98
108
  }
109
+
99
110
  setIndex(0);
100
111
  onOpenChange(next);
101
112
  };
@@ -109,26 +120,36 @@ export function ModelPicker({
109
120
  if (!entry) {
110
121
  return;
111
122
  }
123
+
112
124
  if (entry.kind === 'legacy') {
113
125
  setLegacyOpen(true);
114
126
  inputRef.current?.focus();
127
+
115
128
  return;
116
129
  }
130
+
117
131
  if (entry.kind === 'all') {
118
132
  const kind = entry.provider.kind;
133
+
119
134
  setExpanded((now) => {
120
135
  const next = new Set(now);
136
+
121
137
  if (!next.delete(kind)) {
122
138
  next.add(kind);
123
139
  }
140
+
124
141
  return next;
125
142
  });
143
+
126
144
  inputRef.current?.focus();
145
+
127
146
  return;
128
147
  }
148
+
129
149
  if (requirementOf(entry) !== null) {
130
150
  return;
131
151
  }
152
+
132
153
  onChange(entry.provider.kind, entry.model.slug);
133
154
  setOpen(false);
134
155
  };
@@ -150,7 +171,7 @@ export function ModelPicker({
150
171
  <Popover.Root open={open} onOpenChange={setOpen}>
151
172
  <Tooltip label={owner ? `${owner.name} · ${current}` : t('pickers.model.choose')} kbd={kbd ?? undefined}>
152
173
  {trigger === 'chip' ? (
153
- <Popover.Trigger render={<Button variant="secondary" size="sm" className="min-w-0 shrink gap-1.5" />}>
174
+ <Popover.Trigger render={<Button variant="secondary" size={chipSize} className="min-w-0 shrink gap-1.5" />}>
154
175
  {chip.mark === 'logo' ? <ProviderLogo provider={provider as UsageProvider} size={12} /> : <AgentIcon kind={provider} size={12} />}
155
176
  <span className="truncate">{chip.name}</span>
156
177
  <span className="max-w-32 truncate text-text-muted">{chip.model}</span>
@@ -190,6 +211,7 @@ export function ModelPicker({
190
211
  if (entry.kind === 'all') {
191
212
  const before = entries[i - 1];
192
213
  const header = grouped && (before === undefined || before.kind === 'legacy' || before.provider.kind !== entry.provider.kind);
214
+
193
215
  return (
194
216
  <div key={`all-${entry.provider.kind}`}>
195
217
  {separatesGroups(kindBefore(i), entry.provider.kind) && <Menu.Separator />}
@@ -212,6 +234,7 @@ export function ModelPicker({
212
234
  </div>
213
235
  );
214
236
  }
237
+
215
238
  if (entry.kind === 'legacy') {
216
239
  return (
217
240
  <button
@@ -226,11 +249,13 @@ export function ModelPicker({
226
249
  </button>
227
250
  );
228
251
  }
252
+
229
253
  const previous = entries[i - 1];
230
254
  // A provider's rows start where the one before it ended; its row that opens the rest belongs to it.
231
255
  const first = grouped && (previous === undefined || previous.kind === 'legacy' || previous.provider.kind !== entry.provider.kind);
232
256
  // The makers read as groups only where the whole list is shown, so a short list is not cut by labels.
233
257
  const vendorOpen = trimmed !== '' || expanded.has(entry.provider.kind);
258
+
234
259
  // The starred rows lead their CLI without a maker's label; the makers start after them.
235
260
  const vendorFirst =
236
261
  vendorOpen &&
@@ -241,11 +266,13 @@ export function ModelPicker({
241
266
  previous.provider.kind !== entry.provider.kind ||
242
267
  starredHere(previous) ||
243
268
  previous.model.vendor !== entry.model.vendor);
269
+
244
270
  const requirement = requirementOf(entry);
245
271
  const next = entries[i + 1];
246
272
  const lastOfGroup = next === undefined || next.kind !== 'model' || next.provider.kind !== entry.provider.kind;
247
273
  const chosen = entry.provider.kind === provider && entry.model.slug === selection.model;
248
274
  const starred = starredHere(entry);
275
+
249
276
  return (
250
277
  <div key={`${entry.provider.kind}/${entry.model.slug}`}>
251
278
  {separatesGroups(kindBefore(i), entry.provider.kind) && <Menu.Separator />}
@@ -322,9 +349,11 @@ export function ModelPicker({
322
349
  export function StashPicker({ onRestore }: { onRestore(prompt: StashedPrompt): void }) {
323
350
  const { t } = useTranslation('agent-chat');
324
351
  const prompts = useStash((s) => s.prompts);
352
+
325
353
  if (prompts.length === 0) {
326
354
  return null;
327
355
  }
356
+
328
357
  return (
329
358
  <Popover.Root>
330
359
  <Tooltip label={t('pickers.stash.title')} kbd={STASH_SHORTCUT}>
@@ -82,6 +82,7 @@ export function PromptComposer({
82
82
  if (hold?.releasedBy === 'blur' && heldKey.current === activeKey) {
83
83
  return;
84
84
  }
85
+
85
86
  if (focused && expanded && !elsewhere && !ref.current?.querySelector('.prompt-card')?.contains(document.activeElement)) {
86
87
  // Keys meant for the draft may still be coming, and on a button the next Space would answer the card.
87
88
  if (hasDraft) {
@@ -114,6 +115,7 @@ export function PromptComposer({
114
115
  onDraft={(draft) => session.setDraft(promptIdOf(active), draft)}
115
116
  onAction={(action) => {
116
117
  refocus.hold();
118
+
117
119
  void session
118
120
  .act(active, () => answerPrompt(active, action, actions, host))
119
121
  .then((result) => {