@adecore/agents-react 0.23.1 → 0.24.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 (341) hide show
  1. package/README.md +14 -11
  2. package/dist/agents/AccountDot.js +3 -1
  3. package/dist/agents/accounts.d.ts +3 -2
  4. package/dist/agents/accounts.js +5 -6
  5. package/dist/chat/actions.d.ts +2 -2
  6. package/dist/chat/actions.js +8 -5
  7. package/dist/chat/bookmarks.d.ts +5 -3
  8. package/dist/chat/bookmarks.js +13 -14
  9. package/dist/chat/chat-client.d.ts +1 -0
  10. package/dist/chat/chat-client.js +7 -0
  11. package/dist/chat/composer-send.d.ts +3 -0
  12. package/dist/chat/composer-send.js +22 -0
  13. package/dist/chat/forks.js +22 -5
  14. package/dist/chat/index.d.ts +31 -0
  15. package/dist/chat/index.js +28 -0
  16. package/dist/chat/logic/embedded-code.d.ts +8 -0
  17. package/dist/chat/logic/embedded-code.js +371 -0
  18. package/dist/chat/logic/fork.js +1 -0
  19. package/dist/chat/logic/goal.d.ts +6 -0
  20. package/dist/chat/logic/goal.js +26 -0
  21. package/dist/chat/logic/scrubber.d.ts +1 -1
  22. package/dist/chat/logic/scrubber.js +6 -5
  23. package/dist/chat/logic/thought.d.ts +3 -0
  24. package/dist/chat/logic/thought.js +35 -0
  25. package/dist/chat/logic/thread-cards.js +2 -0
  26. package/dist/chat/logic/timeline.d.ts +5 -0
  27. package/dist/chat/logic/timeline.js +22 -1
  28. package/dist/chat/logic/tool-catalog.d.ts +1 -0
  29. package/dist/chat/logic/tool-catalog.js +3 -1
  30. package/dist/chat/logic/tools.d.ts +14 -0
  31. package/dist/chat/logic/tools.js +57 -0
  32. package/dist/chat/preferences.d.ts +6 -0
  33. package/dist/chat/preferences.js +19 -3
  34. package/dist/chat/reading-width.d.ts +9 -0
  35. package/dist/chat/reading-width.js +27 -0
  36. package/dist/chat/send-key.d.ts +10 -0
  37. package/dist/chat/send-key.js +24 -0
  38. package/dist/chat/ui/BookmarkMarker.js +4 -2
  39. package/dist/chat/ui/BookmarkSubmenu.js +4 -3
  40. package/dist/chat/ui/ChatActivity.js +14 -10
  41. package/dist/chat/ui/ChatReferenceChip.js +3 -4
  42. package/dist/chat/ui/CodeBlock.d.ts +2 -1
  43. package/dist/chat/ui/CodeBlock.js +14 -96
  44. package/dist/chat/ui/Composer.d.ts +1 -1
  45. package/dist/chat/ui/Composer.js +117 -906
  46. package/dist/chat/ui/ComposerInput.js +5 -4
  47. package/dist/chat/ui/EditDiff.js +3 -2
  48. package/dist/chat/ui/GoalDock.d.ts +9 -0
  49. package/dist/chat/ui/GoalDock.js +63 -0
  50. package/dist/chat/ui/LimitState.js +5 -3
  51. package/dist/chat/ui/Markdown.d.ts +2 -1
  52. package/dist/chat/ui/Markdown.js +70 -31
  53. package/dist/chat/ui/MermaidBlock.d.ts +3 -0
  54. package/dist/chat/ui/MermaidBlock.js +73 -0
  55. package/dist/chat/ui/MessageActions.js +12 -7
  56. package/dist/chat/ui/Pickers.js +6 -5
  57. package/dist/chat/ui/PromptComposer.js +4 -3
  58. package/dist/chat/ui/RowTime.d.ts +3 -0
  59. package/dist/chat/ui/RowTime.js +12 -0
  60. package/dist/chat/ui/RunSettings.js +5 -4
  61. package/dist/chat/ui/ShellCommand.d.ts +3 -0
  62. package/dist/chat/ui/ShellCommand.js +110 -0
  63. package/dist/chat/ui/SubagentInfo.js +4 -3
  64. package/dist/chat/ui/SubagentStopButton.js +4 -3
  65. package/dist/chat/ui/SubagentTimeline.js +4 -1
  66. package/dist/chat/ui/Timeline.js +70 -40
  67. package/dist/chat/ui/TimelineMenu.d.ts +2 -1
  68. package/dist/chat/ui/TimelineMenu.js +18 -8
  69. package/dist/chat/ui/UnifiedDiff.js +3 -2
  70. package/dist/chat/ui/VisualFrame.js +6 -7
  71. package/dist/chat/ui/code-theme.d.ts +2 -1
  72. package/dist/chat/ui/code-theme.js +4 -4
  73. package/dist/chat/ui/composer/ComposerControls.d.ts +27 -0
  74. package/dist/chat/ui/composer/ComposerControls.js +34 -0
  75. package/dist/chat/ui/composer/ComposerMenu.d.ts +4 -0
  76. package/dist/chat/ui/composer/ComposerMenu.js +46 -0
  77. package/dist/chat/ui/composer/DraftParts.d.ts +18 -0
  78. package/dist/chat/ui/composer/DraftParts.js +25 -0
  79. package/dist/chat/ui/composer/QueuedMessages.d.ts +10 -0
  80. package/dist/chat/ui/composer/QueuedMessages.js +20 -0
  81. package/dist/chat/ui/composer/key-handler.d.ts +20 -0
  82. package/dist/chat/ui/composer/key-handler.js +150 -0
  83. package/dist/chat/ui/composer/keys.d.ts +7 -2
  84. package/dist/chat/ui/composer/keys.js +19 -9
  85. package/dist/chat/ui/composer/placeholder.d.ts +9 -0
  86. package/dist/chat/ui/composer/placeholder.js +38 -0
  87. package/dist/chat/ui/composer/use-chat-controls.d.ts +28 -0
  88. package/dist/chat/ui/composer/use-chat-controls.js +112 -0
  89. package/dist/chat/ui/composer/use-compaction-offer.d.ts +2 -0
  90. package/dist/chat/ui/composer/use-compaction-offer.js +23 -0
  91. package/dist/chat/ui/composer/use-composer-draft.d.ts +14 -0
  92. package/dist/chat/ui/composer/use-composer-draft.js +39 -0
  93. package/dist/chat/ui/composer/use-composer-menus.d.ts +52 -0
  94. package/dist/chat/ui/composer/use-composer-menus.js +232 -0
  95. package/dist/chat/ui/composer/use-composer-submit.d.ts +32 -0
  96. package/dist/chat/ui/composer/use-composer-submit.js +113 -0
  97. package/dist/chat/ui/composer/use-draft-files.d.ts +27 -0
  98. package/dist/chat/ui/composer/use-draft-files.js +111 -0
  99. package/dist/chat/ui/composer/use-notice.d.ts +6 -0
  100. package/dist/chat/ui/composer/use-notice.js +18 -0
  101. package/dist/chat/ui/composer/use-pending-requests.d.ts +2 -0
  102. package/dist/chat/ui/composer/use-pending-requests.js +22 -0
  103. package/dist/chat/ui/composer/use-prompt-history.d.ts +8 -0
  104. package/dist/chat/ui/composer/use-prompt-history.js +57 -0
  105. package/dist/chat/ui/composer/use-prompt-hold.d.ts +6 -0
  106. package/dist/chat/ui/composer/use-prompt-hold.js +48 -0
  107. package/dist/chat/ui/composer/use-take-back.d.ts +13 -0
  108. package/dist/chat/ui/composer/use-take-back.js +82 -0
  109. package/dist/chat/ui/diff/EditorEditDiff.js +4 -3
  110. package/dist/chat/ui/diff/LegacyDiffPool.js +3 -2
  111. package/dist/chat/ui/diff/LegacyEditDiff.js +3 -2
  112. package/dist/chat/ui/diff/LegacyUnifiedDiff.js +3 -2
  113. package/dist/chat/ui/diff/syntax-runtime.d.ts +2 -1
  114. package/dist/chat/ui/diff/syntax-runtime.js +9 -5
  115. package/dist/chat/ui/diff-syntax.d.ts +2 -2
  116. package/dist/chat/ui/diff-syntax.js +1 -2
  117. package/dist/chat/ui/diff-theme.js +10 -9
  118. package/dist/chat/ui/file-links.d.ts +1 -1
  119. package/dist/chat/ui/file-links.js +7 -5
  120. package/dist/chat/ui/highlighter.d.ts +27 -0
  121. package/dist/chat/ui/highlighter.js +151 -0
  122. package/dist/chat/ui/index.d.ts +67 -0
  123. package/dist/chat/ui/index.js +64 -0
  124. package/dist/chat/ui/intelligent-ui/UiReply.js +3 -2
  125. package/dist/chat/ui/intelligent-ui/renderers/content.js +3 -2
  126. package/dist/chat/ui/intelligent-ui/use-ui-links.js +2 -2
  127. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +4 -3
  128. package/dist/chat/ui/markdown-blocks.d.ts +8 -0
  129. package/dist/chat/ui/markdown-blocks.js +260 -12
  130. package/dist/chat/ui/mermaid.d.ts +23 -0
  131. package/dist/chat/ui/mermaid.js +93 -0
  132. package/dist/chat/ui/rows/ForksRow.js +5 -3
  133. package/dist/chat/ui/rows/GeneratedImageRow.js +6 -4
  134. package/dist/chat/ui/rows/MessageRows.d.ts +4 -0
  135. package/dist/chat/ui/rows/MessageRows.js +36 -13
  136. package/dist/chat/ui/rows/Rows.js +3 -1
  137. package/dist/chat/ui/rows/SubagentRow.js +3 -2
  138. package/dist/chat/ui/rows/VisualRow.js +3 -2
  139. package/dist/chat/ui/rows/WorkRows.d.ts +6 -3
  140. package/dist/chat/ui/rows/WorkRows.js +57 -20
  141. package/dist/chat/ui/rows/WorkflowRow.js +3 -2
  142. package/dist/chat/ui/timestamp-format.d.ts +1 -0
  143. package/dist/chat/ui/timestamp-format.js +2 -0
  144. package/dist/chat/ui/use-diff-source-tokens.js +6 -3
  145. package/dist/chat/ui/visual-theme.js +3 -2
  146. package/dist/host.d.ts +9 -6
  147. package/dist/host.js +19 -8
  148. package/dist/index.d.ts +8 -0
  149. package/dist/index.js +9 -0
  150. package/dist/locales/en/agent-chat.d.ts +658 -0
  151. package/dist/locales/en/agent-chat.js +657 -0
  152. package/dist/locales/en/agent-prompts.d.ts +44 -0
  153. package/dist/locales/en/agent-prompts.js +43 -0
  154. package/dist/locales/en/agent-providers.d.ts +132 -0
  155. package/dist/locales/en/agent-providers.js +131 -0
  156. package/dist/locales/en/agent-usage.d.ts +132 -0
  157. package/dist/locales/en/agent-usage.js +131 -0
  158. package/dist/locales/nl/agent-chat.d.ts +658 -0
  159. package/dist/locales/nl/agent-chat.js +657 -0
  160. package/dist/locales/nl/agent-prompts.d.ts +44 -0
  161. package/dist/locales/nl/agent-prompts.js +43 -0
  162. package/dist/locales/nl/agent-providers.d.ts +132 -0
  163. package/dist/locales/nl/agent-providers.js +131 -0
  164. package/dist/locales/nl/agent-usage.d.ts +132 -0
  165. package/dist/locales/nl/agent-usage.js +131 -0
  166. package/dist/locales.js +9 -8
  167. package/dist/prompts/index.d.ts +11 -0
  168. package/dist/prompts/index.js +12 -0
  169. package/dist/prompts/logic/subjects.d.ts +2 -1
  170. package/dist/prompts/logic/subjects.js +6 -4
  171. package/dist/prompts/ui/ApprovalBody.js +6 -4
  172. package/dist/prompts/ui/PromptCard.js +5 -4
  173. package/dist/prompts/ui/PromptView.js +3 -2
  174. package/dist/prompts/ui/QuestionBody.js +3 -2
  175. package/dist/providers/AccountColors.js +2 -2
  176. package/dist/providers/AccountDetail.js +7 -6
  177. package/dist/providers/AccountVariables.js +3 -1
  178. package/dist/providers/AddAccountForm.js +3 -2
  179. package/dist/providers/ProvidersPane.js +4 -3
  180. package/dist/providers/account-actions.d.ts +4 -3
  181. package/dist/providers/account-actions.js +6 -7
  182. package/dist/providers/index.d.ts +16 -0
  183. package/dist/providers/index.js +17 -0
  184. package/dist/providers/provider-abilities.d.ts +2 -1
  185. package/dist/providers/provider-abilities.js +4 -0
  186. package/dist/state/chats.js +4 -2
  187. package/dist/state/index.d.ts +4 -0
  188. package/dist/state/index.js +5 -0
  189. package/dist/testing/index.d.ts +1 -0
  190. package/dist/testing/index.js +2 -0
  191. package/dist/{prompts/logic/prompts.fixtures.d.ts → testing/prompts.d.ts} +1 -1
  192. package/dist/theme.css +13 -3
  193. package/dist/usage/UsageBreakdown.js +3 -2
  194. package/dist/usage/UsageLimits.js +4 -3
  195. package/dist/usage/UsagePage.js +3 -2
  196. package/dist/usage/index.d.ts +15 -0
  197. package/dist/usage/index.js +15 -0
  198. package/package.json +53 -1031
  199. package/src/agents/AccountDot.tsx +3 -1
  200. package/src/agents/accounts.ts +6 -6
  201. package/src/chat/actions.ts +8 -5
  202. package/src/chat/bookmarks.ts +23 -14
  203. package/src/chat/chat-client.ts +8 -0
  204. package/src/chat/composer-send.ts +27 -0
  205. package/src/chat/forks.ts +27 -6
  206. package/src/chat/index.ts +98 -0
  207. package/src/chat/logic/embedded-code.ts +409 -0
  208. package/src/chat/logic/fork.ts +1 -0
  209. package/src/chat/logic/goal.ts +33 -0
  210. package/src/chat/logic/scrubber.ts +6 -5
  211. package/src/chat/logic/thought.ts +40 -0
  212. package/src/chat/logic/thread-cards.ts +2 -0
  213. package/src/chat/logic/timeline.ts +25 -1
  214. package/src/chat/logic/tool-catalog.ts +5 -1
  215. package/src/chat/logic/tools.ts +76 -0
  216. package/src/chat/preferences.ts +26 -3
  217. package/src/chat/reading-width.ts +39 -0
  218. package/src/chat/send-key.ts +38 -0
  219. package/src/chat/ui/BookmarkMarker.tsx +4 -2
  220. package/src/chat/ui/BookmarkSubmenu.tsx +4 -3
  221. package/src/chat/ui/ChatActivity.tsx +14 -10
  222. package/src/chat/ui/ChatReferenceChip.tsx +3 -4
  223. package/src/chat/ui/CodeBlock.tsx +33 -124
  224. package/src/chat/ui/Composer.tsx +162 -1324
  225. package/src/chat/ui/ComposerInput.tsx +5 -4
  226. package/src/chat/ui/EditDiff.tsx +3 -2
  227. package/src/chat/ui/GoalDock.tsx +104 -0
  228. package/src/chat/ui/LimitState.tsx +5 -3
  229. package/src/chat/ui/Markdown.tsx +113 -39
  230. package/src/chat/ui/MermaidBlock.tsx +119 -0
  231. package/src/chat/ui/MessageActions.tsx +13 -8
  232. package/src/chat/ui/Pickers.tsx +8 -7
  233. package/src/chat/ui/PromptComposer.tsx +4 -3
  234. package/src/chat/ui/RowTime.tsx +21 -0
  235. package/src/chat/ui/RunSettings.tsx +9 -4
  236. package/src/chat/ui/ShellCommand.tsx +134 -0
  237. package/src/chat/ui/SubagentInfo.tsx +4 -3
  238. package/src/chat/ui/SubagentStopButton.tsx +4 -3
  239. package/src/chat/ui/SubagentTimeline.tsx +5 -1
  240. package/src/chat/ui/Timeline.tsx +167 -105
  241. package/src/chat/ui/TimelineMenu.tsx +42 -7
  242. package/src/chat/ui/UnifiedDiff.tsx +3 -2
  243. package/src/chat/ui/VisualFrame.tsx +6 -7
  244. package/src/chat/ui/code-theme.ts +4 -4
  245. package/src/chat/ui/composer/ComposerControls.tsx +147 -0
  246. package/src/chat/ui/composer/ComposerMenu.tsx +138 -0
  247. package/src/chat/ui/composer/DraftParts.tsx +86 -0
  248. package/src/chat/ui/composer/QueuedMessages.tsx +64 -0
  249. package/src/chat/ui/composer/key-handler.ts +174 -0
  250. package/src/chat/ui/composer/keys.ts +32 -10
  251. package/src/chat/ui/composer/placeholder.ts +48 -0
  252. package/src/chat/ui/composer/use-chat-controls.ts +134 -0
  253. package/src/chat/ui/composer/use-compaction-offer.ts +25 -0
  254. package/src/chat/ui/composer/use-composer-draft.ts +71 -0
  255. package/src/chat/ui/composer/use-composer-menus.ts +289 -0
  256. package/src/chat/ui/composer/use-composer-submit.ts +163 -0
  257. package/src/chat/ui/composer/use-draft-files.ts +135 -0
  258. package/src/chat/ui/composer/use-notice.ts +28 -0
  259. package/src/chat/ui/composer/use-pending-requests.ts +23 -0
  260. package/src/chat/ui/composer/use-prompt-history.ts +76 -0
  261. package/src/chat/ui/composer/use-prompt-hold.ts +58 -0
  262. package/src/chat/ui/composer/use-take-back.ts +91 -0
  263. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +2 -2
  264. package/src/chat/ui/diff/EditorEditDiff.tsx +4 -3
  265. package/src/chat/ui/diff/LegacyDiffPool.tsx +3 -2
  266. package/src/chat/ui/diff/LegacyEditDiff.tsx +3 -2
  267. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +3 -2
  268. package/src/chat/ui/diff/syntax-runtime.ts +10 -5
  269. package/src/chat/ui/diff-syntax.ts +2 -2
  270. package/src/chat/ui/diff-theme.ts +10 -9
  271. package/src/chat/ui/file-links.ts +7 -5
  272. package/src/chat/ui/highlighter.ts +185 -0
  273. package/src/chat/ui/index.ts +109 -0
  274. package/src/chat/ui/intelligent-ui/UiReply.tsx +5 -4
  275. package/src/chat/ui/intelligent-ui/renderers/content.tsx +3 -2
  276. package/src/chat/ui/intelligent-ui/use-ui-links.ts +2 -2
  277. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +4 -3
  278. package/src/chat/ui/markdown-blocks.ts +300 -12
  279. package/src/chat/ui/mermaid.ts +118 -0
  280. package/src/chat/ui/rows/ForksRow.tsx +5 -3
  281. package/src/chat/ui/rows/GeneratedImageRow.tsx +6 -4
  282. package/src/chat/ui/rows/MessageRows.tsx +73 -18
  283. package/src/chat/ui/rows/Rows.tsx +14 -1
  284. package/src/chat/ui/rows/SubagentRow.tsx +3 -2
  285. package/src/chat/ui/rows/VisualRow.tsx +3 -2
  286. package/src/chat/ui/rows/WorkRows.tsx +148 -47
  287. package/src/chat/ui/rows/WorkflowRow.tsx +3 -2
  288. package/src/chat/ui/timestamp-format.ts +2 -0
  289. package/src/chat/ui/use-diff-source-tokens.ts +10 -4
  290. package/src/chat/ui/visual-theme.ts +3 -2
  291. package/src/host.ts +31 -15
  292. package/src/index.ts +49 -0
  293. package/src/locales/en/agent-chat.ts +657 -0
  294. package/src/locales/en/agent-prompts.ts +43 -0
  295. package/src/locales/en/agent-providers.ts +131 -0
  296. package/src/locales/en/agent-usage.ts +131 -0
  297. package/src/locales/nl/agent-chat.ts +657 -0
  298. package/src/locales/nl/agent-prompts.ts +43 -0
  299. package/src/locales/nl/agent-providers.ts +131 -0
  300. package/src/locales/nl/agent-usage.ts +132 -0
  301. package/src/locales.ts +9 -8
  302. package/src/prompts/index.ts +35 -0
  303. package/src/prompts/logic/subjects.ts +12 -4
  304. package/src/prompts/ui/ApprovalBody.tsx +6 -4
  305. package/src/prompts/ui/PromptCard.tsx +11 -4
  306. package/src/prompts/ui/PromptView.tsx +3 -2
  307. package/src/prompts/ui/QuestionBody.tsx +3 -2
  308. package/src/providers/AccountColors.tsx +2 -2
  309. package/src/providers/AccountDetail.tsx +13 -7
  310. package/src/providers/AccountVariables.tsx +3 -1
  311. package/src/providers/AddAccountForm.tsx +3 -2
  312. package/src/providers/ProvidersPane.tsx +4 -3
  313. package/src/providers/account-actions.ts +7 -7
  314. package/src/providers/index.ts +17 -0
  315. package/src/providers/provider-abilities.ts +6 -1
  316. package/src/state/chats.ts +4 -2
  317. package/src/state/index.ts +39 -0
  318. package/src/testing/index.ts +2 -0
  319. package/src/{prompts/logic/prompts.fixtures.ts → testing/prompts.ts} +1 -1
  320. package/src/theme.css +13 -3
  321. package/src/usage/UsageBreakdown.tsx +3 -2
  322. package/src/usage/UsageLimits.tsx +4 -3
  323. package/src/usage/UsagePage.tsx +3 -2
  324. package/src/usage/index.ts +16 -0
  325. package/dist/locales/en/agent-chat.json +0 -597
  326. package/dist/locales/en/agent-prompts.json +0 -43
  327. package/dist/locales/en/agent-providers.json +0 -131
  328. package/dist/locales/en/agent-usage.json +0 -131
  329. package/dist/locales/nl/agent-chat.json +0 -597
  330. package/dist/locales/nl/agent-prompts.json +0 -43
  331. package/dist/locales/nl/agent-providers.json +0 -131
  332. package/dist/locales/nl/agent-usage.json +0 -131
  333. package/src/locales/en/agent-chat.json +0 -597
  334. package/src/locales/en/agent-prompts.json +0 -43
  335. package/src/locales/en/agent-providers.json +0 -131
  336. package/src/locales/en/agent-usage.json +0 -131
  337. package/src/locales/nl/agent-chat.json +0 -597
  338. package/src/locales/nl/agent-prompts.json +0 -43
  339. package/src/locales/nl/agent-providers.json +0 -131
  340. package/src/locales/nl/agent-usage.json +0 -131
  341. /package/dist/{prompts/logic/prompts.fixtures.js → testing/prompts.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { useImperativeHandle, useLayoutEffect, useRef, useState, type Ref } from 'react';
2
2
  import { Compartment, EditorState, Prec, Transaction, type Extension } from '@codemirror/state';
3
3
  import { EditorView, placeholder as placeholderText } from '@codemirror/view';
4
- import { chatHost } from '../../host';
4
+ import { useChatHost } from '../../host';
5
5
  import { composerEditorExtensions, externalChange } from './composer/editor';
6
6
  import { insertAtSelection } from './composer/insert';
7
7
 
@@ -63,6 +63,7 @@ export function ComposerInput({
63
63
  aria = NO_ARIA,
64
64
  ...handlers
65
65
  }: ComposerInputProps) {
66
+ const { dictation } = useChatHost();
66
67
  const hostRef = useRef<HTMLDivElement>(null);
67
68
  const targetRef = useRef<HTMLDivElement>(null);
68
69
  const viewRef = useRef<EditorView | null>(null);
@@ -93,7 +94,7 @@ export function ComposerInput({
93
94
  selection: { anchor: initial.length },
94
95
  extensions: [
95
96
  composerEditorExtensions(),
96
- chatHost().dictation.composer?.extensions ?? [],
97
+ dictation.composer?.extensions ?? [],
97
98
  compartments.attributes.of([]),
98
99
  compartments.editable.of([]),
99
100
  compartments.placeholder.of([]),
@@ -137,7 +138,7 @@ export function ComposerInput({
137
138
  view.destroy();
138
139
  viewRef.current = null;
139
140
  };
140
- }, [compartments]);
141
+ }, [compartments, dictation.composer?.extensions]);
141
142
 
142
143
  useLayoutEffect(() => {
143
144
  viewRef.current?.dispatch({
@@ -192,7 +193,7 @@ export function ComposerInput({
192
193
  []
193
194
  );
194
195
 
195
- const Dictation = chatHost().dictation.composer?.Control;
196
+ const Dictation = dictation.composer?.Control;
196
197
  return (
197
198
  <div ref={targetRef} className="min-w-0">
198
199
  <div ref={hostRef} className={className} />
@@ -5,7 +5,7 @@ import { diffSourceBudget, legacyDiffBudget } from './diff/diff-source-budget';
5
5
  import { DiffOmitted } from './diff/DiffOmitted';
6
6
  import { DiffUnavailable } from './diff/DiffUnavailable';
7
7
  import { DiffFallbackBoundary } from './diff/DiffFallbackBoundary';
8
- import { chatHost } from '../../host';
8
+ import { useChatHost } from '../../host';
9
9
 
10
10
  import type { FileChange } from '../logic/tools';
11
11
 
@@ -13,8 +13,9 @@ const LegacyEditDiff = lazyNamed(() => import('./diff/LegacyEditDiff'), 'default
13
13
  const EditorEditDiff = lazyNamed(() => import('./diff/EditorEditDiff'), 'default');
14
14
 
15
15
  export default function EditDiff({ change, allowEditor = true, maxHeight }: { change: FileChange; allowEditor?: boolean; maxHeight?: number }) {
16
+ const host = useChatHost();
16
17
  const { t } = useTranslation('agent-chat');
17
- const loadEngine = chatHost().code.editor;
18
+ const loadEngine = host.code.editor;
18
19
  const sourceAllowed = useMemo(() => diffSourceBudget(change.before, change.after), [change.before, change.after]);
19
20
  const legacyMode = !allowEditor || loadEngine === undefined;
20
21
  const legacyAllowed = useMemo(() => legacyMode && legacyDiffBudget(change.before, change.after), [legacyMode, change.before, change.after]);
@@ -0,0 +1,104 @@
1
+ import { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import type { TFunction } from 'i18next';
4
+ import clsx from 'clsx';
5
+ import { Target } from 'lucide-react';
6
+ import type { ChatGoal, ChatGoalAction, ChatInfo, ProviderCapabilities } from '@adecore/agent-contracts';
7
+ import { Button, Icon, Pill, Tooltip } from '@adecore/ui';
8
+ import { formatTokens, useFormatLocale } from '@adecore/ui/format';
9
+ import { useChatHost } from '../../host';
10
+ import { useChatScope } from '../../scope';
11
+ import { useChats } from '../../state/chats';
12
+ import { useChatActions } from '../actions';
13
+ import { goalView } from '../logic/goal';
14
+
15
+ /* How far the goal got, in what its CLI counts: the checks that sent the agent on, or the tokens it spent. */
16
+ function progressOf(goal: ChatGoal, t: TFunction<'agent-chat'>): string | null {
17
+ const parts: string[] = [];
18
+ if (goal.checks !== undefined && goal.checks > 0) {
19
+ parts.push(t('goal.checks', { count: goal.checks }));
20
+ }
21
+ if (goal.tokensUsed !== undefined && (goal.tokensUsed > 0 || goal.tokenBudget !== undefined)) {
22
+ parts.push(
23
+ goal.tokenBudget === undefined
24
+ ? t('goal.tokens', { tokens: formatTokens(goal.tokensUsed) })
25
+ : t('goal.budget', { tokens: formatTokens(goal.tokensUsed), budget: formatTokens(goal.tokenBudget) })
26
+ );
27
+ }
28
+ return parts.length === 0 ? null : parts.join(' · ');
29
+ }
30
+
31
+ /*
32
+ * At the top of the composer while the chat has a goal: where it stands, the objective, how far it got,
33
+ * and what a person may do to it. The whole objective and what the last check said are in the tooltip.
34
+ */
35
+ export function GoalDock({ chatId, info, goals }: { chatId: string; info: ChatInfo; goals: ProviderCapabilities['goals'] }) {
36
+ const host = useChatHost();
37
+ const { t } = useTranslation('agent-chat');
38
+ useFormatLocale();
39
+ const actions = useChatActions();
40
+ const [changing, setChanging] = useState(false);
41
+ const view = goalView(info, goals, actions.goal !== undefined);
42
+ if (view === null) {
43
+ return null;
44
+ }
45
+ const { goal } = view;
46
+ const progress = progressOf(goal, t);
47
+ const act = (action: ChatGoalAction): void => {
48
+ if (!actions.goal) {
49
+ return;
50
+ }
51
+ setChanging(true);
52
+ actions
53
+ .goal(chatId, action)
54
+ .catch((e: unknown) => host.notify({ kind: 'error', title: t('goal.failed'), description: e instanceof Error ? e.message : String(e) }))
55
+ .finally(() => setChanging(false));
56
+ };
57
+ return (
58
+ <div className="flex items-center gap-2 border-b border-border px-3.5 py-2.5">
59
+ <Icon icon={Target} size={16} className={clsx('shrink-0', goal.status === 'active' ? 'text-accent' : 'text-text-faint')} />
60
+ <span className="flex min-w-0 grow flex-col">
61
+ <span className="truncate text-sm text-text">{t(`goal.status.${goal.status}`)}</span>
62
+ <Tooltip
63
+ label={
64
+ goal.lastCheck === undefined ? (
65
+ goal.objective
66
+ ) : (
67
+ <>
68
+ {goal.objective}
69
+ <br />
70
+ {t('goal.lastCheck', { check: goal.lastCheck })}
71
+ </>
72
+ )
73
+ }
74
+ >
75
+ <span className="truncate text-xs tabular-nums text-text-faint">
76
+ {progress === null ? goal.objective : `${goal.objective} · ${progress}`}
77
+ </span>
78
+ </Tooltip>
79
+ </span>
80
+ {view.actions.map((action) => (
81
+ <Button key={action} size="sm" disabled={changing} onClick={() => act(action)}>
82
+ {t(`goal.${action}`)}
83
+ </Button>
84
+ ))}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ /* The same in a chat node's header while a goal works, read from the status every client gets, so a node nobody opened says it too. */
90
+ export function GoalPill({ chatId }: { chatId: string }) {
91
+ const { t } = useTranslation('agent-chat');
92
+ const { keyOf } = useChatScope();
93
+ const goal = useChats((s) => s.statusByKey[keyOf(chatId)]?.info.goal);
94
+ if (goal?.status !== 'active') {
95
+ return null;
96
+ }
97
+ return (
98
+ <Tooltip label={`${t('goal.status.active')}: ${goal.objective}`}>
99
+ <Pill tone="accent" icon={<Icon icon={Target} size={12} />}>
100
+ {t('goal.pill')}
101
+ </Pill>
102
+ </Tooltip>
103
+ );
104
+ }
@@ -10,7 +10,7 @@ import { useFormatLocale } from '@adecore/ui/format';
10
10
  import { Switch, Button, Icon, Pill, Tooltip, useNow } from '@adecore/ui';
11
11
  import { useUsageLimits } from '../../usage/limits';
12
12
  import { useChats } from '../../state/chats';
13
- import { chatHost } from '../../host';
13
+ import { useChatHost } from '../../host';
14
14
  import { useChatScope, type ChatScope } from '../../scope';
15
15
  import { useChatActions } from '../actions';
16
16
  import { useUsageStore } from '../../state/usage';
@@ -43,10 +43,11 @@ function readUnreadAccounts(scope: ChatScope): void {
43
43
  * switch off it stands off and says where that switch is.
44
44
  */
45
45
  function ResumeAtResetToggle({ chatId, info }: { chatId: string; info: ChatInfo }) {
46
+ const host = useChatHost();
46
47
  const { t } = useTranslation('agent-chat');
47
48
  const { id } = useChatScope();
48
49
  const actions = useChatActions();
49
- const machineAllows = chatHost().useResumeAtReset(id);
50
+ const machineAllows = host.useResumeAtReset(id);
50
51
  const set = (on: boolean): void => {
51
52
  void actions.configure(chatId, { resumeAtReset: on }).catch(() => undefined);
52
53
  };
@@ -66,6 +67,7 @@ function ResumeAtResetToggle({ chatId, info }: { chatId: string; info: ChatInfo
66
67
  * conversation takes it over; any other goes on in a fork, which then opens the way a fork does.
67
68
  */
68
69
  function ContinueOn({ chatId, info, choice }: { chatId: string; info: ChatInfo; choice: AccountChoice }) {
70
+ const host = useChatHost();
69
71
  const { t } = useTranslation('agent-chat');
70
72
  const scope = useChatScope();
71
73
  const actions = useChatActions();
@@ -89,7 +91,7 @@ function ContinueOn({ chatId, info, choice }: { chatId: string; info: ChatInfo;
89
91
  actions
90
92
  .continueOn(chatId, target.id)
91
93
  .catch((e: unknown) =>
92
- chatHost().notify({
94
+ host.notify({
93
95
  kind: 'error',
94
96
  title: t('limit.continueFailed', { account: name }),
95
97
  description: e instanceof Error ? e.message : String(e)
@@ -6,17 +6,21 @@ import remarkBreaks from 'remark-breaks';
6
6
  import remarkGfm from 'remark-gfm';
7
7
  import { CHIP_IN_MESSAGE, MENTION_TONE, SKILL_TONE } from './chips';
8
8
  import { CodeBlock } from './CodeBlock';
9
+ import { MermaidBlock } from './MermaidBlock';
9
10
  import { CodeStreamingContext } from './code-streaming';
10
11
  import { splitMarkdownBlocks } from './markdown-blocks';
11
12
  import { rehypeChips, type ChipOptions } from './rehype-chips';
12
13
  import { rehypeFadeWords } from './rehype-fade';
13
14
  import { remarkHtmlAsText } from './remark-html-as-text';
14
- import { chatHost, type FileRef, type ShellCodeBlockContext } from '../../host';
15
- import { openFileLink, useFileLinkCwd, useFileLinkScopeId, useFileLinkTarget } from './file-links';
16
- import { FileIcon, Icon } from '@adecore/ui';
15
+ import { useChatHost, type ChatHost, type FileRef, type ShellCodeBlockContext } from '../../host';
16
+ import { useFileLinkCwd, useFileLinkScopeId, useFileLinkTarget, useOpenFileLink } from './file-links';
17
+ import { ErrorBoundary, FileIcon, Icon } from '@adecore/ui';
18
+ import { useTranslation } from 'react-i18next';
17
19
 
18
20
  type ShellCodeBlockReply = Pick<ShellCodeBlockContext, 'scopeId' | 'chatId' | 'itemId'> & { streaming: boolean };
19
21
 
22
+ type ShellFence = Pick<ShellCodeBlockContext, 'language' | 'code'> & { closed: boolean };
23
+
20
24
  const ShellCodeBlockReplyContext = createContext<ShellCodeBlockReply | null>(null);
21
25
  const BlockCodeContext = createContext(false);
22
26
 
@@ -27,6 +31,18 @@ interface MarkdownNode {
27
31
  data?: { hProperties?: Record<string, unknown> };
28
32
  }
29
33
 
34
+ /* The command a shell fence holds, read from the fence as written, so it keeps the indentation and line endings it has. */
35
+ function shellFence(source: string): ShellFence | null {
36
+ const opening = /^ {0,3}(`{3,}|~{3,})[ \t]*(sh|bash|zsh)[ \t]*\r?\n/.exec(source);
37
+ if (opening === null) {
38
+ return null;
39
+ }
40
+ const body = source.slice(opening[0].length);
41
+ const closing = /(?:^|\r?\n) {0,3}(`{3,}|~{3,})[ \t]*$/.exec(body);
42
+ const closed = closing !== null && closing[1]![0] === opening[1]![0] && closing[1]!.length >= opening[1]!.length;
43
+ return { language: opening[2] as ShellFence['language'], code: closed ? body.slice(0, closing.index) : body, closed };
44
+ }
45
+
30
46
  /* Only root children qualify; Markdown's parser decides whether a fence belongs to a list or quote. */
31
47
  function remarkShellCodeBlocks() {
32
48
  return (tree: MarkdownNode, file: { value: unknown }) => {
@@ -39,27 +55,32 @@ function remarkShellCodeBlocks() {
39
55
  if (start === undefined || end === undefined) {
40
56
  continue;
41
57
  }
42
- const raw = String(file.value).slice(start, end);
43
- const opening = /^ {0,3}(`{3,}|~{3,})[ \t]*(sh|bash|zsh)[ \t]*\r?\n/.exec(raw);
44
- if (opening === null) {
58
+ const shell = shellFence(String(file.value).slice(start, end));
59
+ if (shell === null) {
45
60
  continue;
46
61
  }
47
- const body = raw.slice(opening[0].length);
48
- const closing = /(?:^|\r?\n) {0,3}(`{3,}|~{3,})[ \t]*$/.exec(body);
49
- const closed = closing !== null && closing[1]![0] === opening[1]![0] && closing[1]!.length >= opening[1]!.length;
50
62
  node.data = {
51
63
  ...node.data,
52
64
  hProperties: {
53
65
  ...node.data?.hProperties,
54
- 'data-shell-language': opening[2],
55
- 'data-shell-code': closed ? body.slice(0, closing.index) : body,
56
- 'data-shell-closed': closed
66
+ 'data-shell-language': shell.language,
67
+ 'data-shell-code': shell.code,
68
+ 'data-shell-closed': shell.closed
57
69
  }
58
70
  };
59
71
  }
60
72
  };
61
73
  }
62
74
 
75
+ /* What the host adds to a shell fence of a main reply, which is complete once the fence closed and the reply stopped. */
76
+ function shellCodeBlockActions(reply: ShellCodeBlockReply | null, shell: ShellFence | null, render: ChatHost['renderShellCodeBlock']): ReactNode {
77
+ if (reply === null || shell === null) {
78
+ return undefined;
79
+ }
80
+ const { scopeId, chatId, itemId } = reply;
81
+ return render?.({ scopeId, chatId, itemId, language: shell.language, code: shell.code, complete: !reply.streaming && shell.closed });
82
+ }
83
+
63
84
  function languageOf(className: string | undefined): string {
64
85
  return /language-([\w-]+)/.exec(className ?? '')?.[1] ?? 'text';
65
86
  }
@@ -73,6 +94,7 @@ const INLINE_CODE = 'rounded-sm bg-surface-sunken px-1 py-px font-mono text-code
73
94
  function FileLink({ target, className, children }: { target: FileRef; className?: string; children: ReactNode }) {
74
95
  const cwd = useFileLinkCwd();
75
96
  const scopeId = useFileLinkScopeId();
97
+ const openFileLink = useOpenFileLink();
76
98
  return (
77
99
  // A button is not selectable on its own, and a thread is copied as often as it is clicked.
78
100
  <button
@@ -139,11 +161,23 @@ function ExternalLink({ href, children }: { href?: string; children?: ReactNode
139
161
  );
140
162
  }
141
163
 
164
+ /* A ```mermaid fence, which draws what it says. */
165
+ function Diagram({ code }: { code: string }) {
166
+ const { t } = useTranslation('agent-chat');
167
+ return (
168
+ <ErrorBoundary label={t('code.mermaid.crashed')} resetKeys={[code]} compact className="rounded-lg border border-border">
169
+ <MermaidBlock code={code} />
170
+ </ErrorBoundary>
171
+ );
172
+ }
173
+
142
174
  function MarkdownCode({ className, children, plain = false }: { className?: string; children?: ReactNode; plain?: boolean }) {
143
175
  const block = useContext(BlockCodeContext);
144
176
  const text = String(children ?? '');
145
177
  if (block) {
146
- return <CodeBlock code={text.replace(/\n$/, '')} lang={languageOf(className)} />;
178
+ const lang = languageOf(className);
179
+ const code = text.replace(/\n$/, '');
180
+ return lang === 'mermaid' ? <Diagram code={code} /> : <CodeBlock code={code} lang={lang} />;
147
181
  }
148
182
  return plain ? <code className={INLINE_CODE}>{text}</code> : <InlineCode text={text} />;
149
183
  }
@@ -204,29 +238,20 @@ const replyComponents = {
204
238
  function ReplyCode({ className, children, node }: { className?: string; children?: ReactNode } & ExtraProps) {
205
239
  const reply = useContext(ShellCodeBlockReplyContext);
206
240
  const block = useContext(BlockCodeContext);
241
+ const { renderShellCodeBlock } = useChatHost();
207
242
  const properties = node?.properties;
208
243
  const language = properties?.['data-shell-language'];
209
244
  const code = properties?.['data-shell-code'];
210
- const context: ShellCodeBlockContext | null =
211
- reply !== null && (language === 'sh' || language === 'bash' || language === 'zsh') && typeof code === 'string'
212
- ? {
213
- scopeId: reply.scopeId,
214
- chatId: reply.chatId,
215
- itemId: reply.itemId,
216
- language,
217
- code,
218
- complete: !reply.streaming && properties?.['data-shell-closed'] === true
219
- }
245
+ const shell: ShellFence | null =
246
+ (language === 'sh' || language === 'bash' || language === 'zsh') && typeof code === 'string'
247
+ ? { language, code, closed: properties?.['data-shell-closed'] === true }
220
248
  : null;
221
249
  const text = String(children ?? '');
250
+ if (block && languageOf(className) === 'mermaid') {
251
+ return <Diagram code={text.replace(/\n$/, '')} />;
252
+ }
222
253
  if (block) {
223
- return (
224
- <CodeBlock
225
- code={text.replace(/\n$/, '')}
226
- lang={languageOf(className)}
227
- actions={context !== null ? chatHost().renderShellCodeBlock?.(context) : undefined}
228
- />
229
- );
254
+ return <CodeBlock code={text.replace(/\n$/, '')} lang={languageOf(className)} actions={shellCodeBlockActions(reply, shell, renderShellCodeBlock)} />;
230
255
  }
231
256
  return <InlineCode text={text} />;
232
257
  }
@@ -284,10 +309,43 @@ const ReplyBlock = memo(function ReplyBlock({ text, fade, open, reply }: { text:
284
309
  });
285
310
 
286
311
  /*
287
- * A reply in a chat thread, parsed a block at a time so a delta only parses the block that grows.
288
- * While it streams every new word fades in, and a fence that has not closed yet is highlighted a
289
- * line at a time as it grows. `arriving` fades in each block as it is added instead, for a reply
290
- * that is shown a block at a time.
312
+ * A code fence at the top level of a reply, drawn as markdown draws it but without parsing, so a word
313
+ * added to a long fence costs no more than one added to a short one. It is the same tree whether the
314
+ * fence is open or closed, so closing it keeps the lines its code block already has.
315
+ */
316
+ const ReplyFence = memo(function ReplyFence({
317
+ info,
318
+ body,
319
+ source,
320
+ open,
321
+ reply
322
+ }: {
323
+ info: string;
324
+ body: string;
325
+ source: string;
326
+ open: boolean;
327
+ reply?: ShellCodeBlockReply;
328
+ }) {
329
+ const { renderShellCodeBlock } = useChatHost();
330
+ // The class markdown gives the code, read back the way a parsed fence's is.
331
+ const lang = languageOf(`language-${info.split(/[ \t]/, 1)[0]}`);
332
+ const shell = reply === undefined ? null : shellFence(source);
333
+ return (
334
+ <CodeStreamingContext.Provider value={open}>
335
+ {lang === 'mermaid' ? (
336
+ <Diagram code={body} />
337
+ ) : (
338
+ <CodeBlock code={body} lang={lang} actions={shellCodeBlockActions(reply ?? null, shell, renderShellCodeBlock)} />
339
+ )}
340
+ </CodeStreamingContext.Provider>
341
+ );
342
+ });
343
+
344
+ /*
345
+ * A reply in a chat thread, parsed a block at a time so a delta only parses the block that grows,
346
+ * and a code fence at the top level not at all. While it streams every new word fades in, and a fence
347
+ * that has not closed yet is highlighted a line at a time as it grows. `arriving` fades in each block
348
+ * as it is added instead, for a reply that is shown a block at a time.
291
349
  */
292
350
  export const ReplyMarkdown = memo(function ReplyMarkdown({
293
351
  text,
@@ -303,10 +361,21 @@ export const ReplyMarkdown = memo(function ReplyMarkdown({
303
361
  const blocks = useMemo(() => splitMarkdownBlocks(text), [text]);
304
362
  return (
305
363
  <div className="chat-markdown prose prose-sm max-w-none text-sm" data-arriving={arriving || undefined}>
306
- {blocks.map((block, index) => (
364
+ {blocks.map((block, index) =>
307
365
  // Blocks only ever grow at the end, so the place of a block is a stable key.
308
- <ReplyBlock key={index} text={block.text} fade={streaming} open={streaming && block.openFence} reply={reply} />
309
- ))}
366
+ block.fence !== undefined ? (
367
+ <ReplyFence
368
+ key={index}
369
+ info={block.fence.info}
370
+ body={block.fence.body}
371
+ source={block.fence.source}
372
+ open={streaming && !block.fence.closed}
373
+ reply={reply}
374
+ />
375
+ ) : (
376
+ <ReplyBlock key={index} text={block.text} fade={streaming} open={streaming && block.openFence} reply={reply} />
377
+ )
378
+ )}
310
379
  </div>
311
380
  );
312
381
  });
@@ -326,23 +395,28 @@ export const MessageMarkdown = memo(function MessageMarkdown({ text, mentions, s
326
395
  );
327
396
  });
328
397
 
329
- /* `breaks` is for person-authored notes; applying it to CLI markdown would change its layout. */
398
+ /*
399
+ * `breaks` is for person-authored notes; applying it to CLI markdown would change its layout. `muted`
400
+ * sets the text a step back, for what stands beside an answer rather than being one.
401
+ */
330
402
  export const Markdown = memo(function Markdown({
331
403
  text,
332
404
  breaks = false,
333
405
  fileLinks = true,
406
+ muted = false,
334
407
  rehypePlugins,
335
408
  componentOverrides
336
409
  }: {
337
410
  text: string;
338
411
  breaks?: boolean;
339
412
  fileLinks?: boolean;
413
+ muted?: boolean;
340
414
  rehypePlugins?: Options['rehypePlugins'];
341
415
  componentOverrides?: Components;
342
416
  }) {
343
417
  const renderComponents = useMemo(() => ({ ...(fileLinks ? components : plainComponents), ...componentOverrides }), [fileLinks, componentOverrides]);
344
418
  return (
345
- <div className="chat-markdown prose prose-sm max-w-none text-sm">
419
+ <div className="chat-markdown prose prose-sm max-w-none text-sm" data-muted={muted || undefined}>
346
420
  <ReactMarkdown remarkPlugins={breaks ? PLUGINS_WITH_BREAKS : PLUGINS} rehypePlugins={rehypePlugins} components={renderComponents}>
347
421
  {text}
348
422
  </ReactMarkdown>
@@ -0,0 +1,119 @@
1
+ import { useContext, useEffect, useId, useRef, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Code, Workflow } from 'lucide-react';
4
+ import { IconButton } from '@adecore/ui';
5
+ import { useChatHost } from '../../host';
6
+ import { CodeBlock } from './CodeBlock';
7
+ import { CodeStreamingContext } from './code-streaming';
8
+ import { MERMAID_TOKENS, mermaidConfig, sizedSvg, svgDataUrl, type MermaidColors, type SizedSvg } from './mermaid';
9
+
10
+ type Drawing = { state: 'drawing' } | { state: 'drawn'; image: SizedSvg } | { state: 'failed' };
11
+
12
+ // Mermaid keeps one configuration for the page, so one diagram is drawn at a time with its own.
13
+ let queue: Promise<unknown> = Promise.resolve();
14
+
15
+ let probeCanvas: HTMLCanvasElement | null = null;
16
+
17
+ /* A token as an opaque `#rrggbb` over `ground`, which is what Mermaid's color math reads. */
18
+ function resolveColor(element: HTMLElement, token: string, ground: string): string {
19
+ probeCanvas ??= document.createElement('canvas');
20
+ probeCanvas.width = 1;
21
+ probeCanvas.height = 1;
22
+ const context = probeCanvas.getContext('2d', { willReadFrequently: true });
23
+ const value = getComputedStyle(element).getPropertyValue(token).trim();
24
+ if (context === null || value === '') {
25
+ return ground;
26
+ }
27
+ context.fillStyle = ground;
28
+ context.fillRect(0, 0, 1, 1);
29
+ context.fillStyle = value;
30
+ context.fillRect(0, 0, 1, 1);
31
+ const [red, green, blue] = context.getImageData(0, 0, 1, 1).data;
32
+ return `#${[red, green, blue].map((channel) => channel!.toString(16).padStart(2, '0')).join('')}`;
33
+ }
34
+
35
+ function themeColors(element: HTMLElement): MermaidColors {
36
+ const ground = resolveColor(element, MERMAID_TOKENS.ground, '#808080');
37
+ const entries = Object.entries(MERMAID_TOKENS).map(([name, token]) => [name, name === 'ground' ? ground : resolveColor(element, token, ground)]);
38
+ return Object.fromEntries(entries) as unknown as MermaidColors;
39
+ }
40
+
41
+ async function drawDiagram(id: string, source: string, element: HTMLElement, dark: boolean): Promise<SizedSvg | null> {
42
+ const { default: mermaid } = await import('mermaid');
43
+ const style = getComputedStyle(element);
44
+ mermaid.initialize(mermaidConfig(themeColors(element), dark, { family: style.fontFamily, size: style.fontSize }));
45
+ const { svg } = await mermaid.render(id, source);
46
+ return sizedSvg(svg);
47
+ }
48
+
49
+ /*
50
+ * A ```mermaid fence, drawn as the diagram once the fence is closed. Mermaid is large, so it loads on
51
+ * the first diagram of the page. While the fence grows, and when the text is no diagram Mermaid can
52
+ * draw, it is the code block it would otherwise be.
53
+ */
54
+ export function MermaidBlock({ code }: { code: string }) {
55
+ const host = useChatHost();
56
+ const { t } = useTranslation('agent-chat');
57
+ const streaming = useContext(CodeStreamingContext);
58
+ const dark = host.code.useMode() === 'dark';
59
+ const id = `mermaid-${useId().replace(/[^\w-]/g, '')}`;
60
+ const frame = useRef<HTMLDivElement>(null);
61
+ const [source, setSource] = useState(false);
62
+ const [drawing, setDrawing] = useState<Drawing>({ state: 'drawing' });
63
+
64
+ useEffect(() => {
65
+ const element = frame.current;
66
+ if (streaming || element === null) {
67
+ return;
68
+ }
69
+ let cancelled = false;
70
+ setDrawing({ state: 'drawing' });
71
+ const drawn = queue.then(() => (cancelled ? null : drawDiagram(id, code, element, dark)));
72
+ queue = drawn.catch(() => undefined);
73
+ drawn.then(
74
+ (image) => !cancelled && setDrawing(image === null ? { state: 'failed' } : { state: 'drawn', image }),
75
+ () => !cancelled && setDrawing({ state: 'failed' })
76
+ );
77
+ return () => {
78
+ cancelled = true;
79
+ };
80
+ }, [code, dark, id, streaming]);
81
+
82
+ const showsDiagram = !streaming && !source && drawing.state !== 'failed';
83
+ const toggle =
84
+ streaming || drawing.state === 'failed' ? undefined : (
85
+ <IconButton
86
+ icon={source ? Workflow : Code}
87
+ size="sm"
88
+ label={source ? t('code.mermaid.showDiagram') : t('code.mermaid.showSource')}
89
+ onClick={() => setSource((shown) => !shown)}
90
+ />
91
+ );
92
+ return (
93
+ // One tree in every state, so the code block keeps the lines it has when the fence closes.
94
+ <div ref={frame}>
95
+ <CodeBlock
96
+ code={code}
97
+ lang="mermaid"
98
+ actions={toggle}
99
+ view={
100
+ showsDiagram ? (
101
+ <div className="overflow-x-auto p-3" aria-busy={drawing.state === 'drawing' || undefined}>
102
+ {drawing.state === 'drawn' && (
103
+ <img
104
+ src={svgDataUrl(drawing.image.svg)}
105
+ width={drawing.image.width}
106
+ height={drawing.image.height}
107
+ alt={t('code.mermaid.diagram')}
108
+ className="m-0 max-w-none"
109
+ draggable={false}
110
+ />
111
+ )}
112
+ </div>
113
+ ) : undefined
114
+ }
115
+ />
116
+ {drawing.state === 'failed' && <p className="m-0 mt-1 text-xs text-text-faint">{t('code.mermaid.failed')}</p>}
117
+ </div>
118
+ );
119
+ }
@@ -6,14 +6,14 @@ import { placeBookmark } from '../bookmarks';
6
6
  import { forkRefusal } from '../logic/fork';
7
7
  import type { TimelineRow } from '../logic/timeline';
8
8
  import { markdownOf, messageTextOf } from '../logic/timeline-copy';
9
- import { chatHost } from '../../host';
9
+ import { useChatHost } from '../../host';
10
10
  import { useChatScope } from '../../scope';
11
11
  import { useChatRow, useChats } from '../../state/chats';
12
12
  import { ButtonGroup, copyText, IconButton, Tooltip } from '@adecore/ui';
13
13
  import { formatClock, formatDateTime, useFormatLocale } from '@adecore/ui/format';
14
+ import { TIMESTAMP_FORMAT } from './timestamp-format';
14
15
 
15
16
  const COPIED_MS = 1500;
16
- const TIMESTAMP_FORMAT: Intl.DateTimeFormatOptions = { dateStyle: 'full', timeStyle: 'medium' };
17
17
 
18
18
  type MessageRow = Extract<TimelineRow, { kind: 'user' | 'assistant' }>;
19
19
 
@@ -22,18 +22,23 @@ export function MessageActions({ chatId, row }: { chatId: string; row: MessageRo
22
22
  const { t } = useTranslation('agent-chat');
23
23
  useFormatLocale();
24
24
  const scope = useChatScope();
25
- const { fork } = chatHost();
25
+ const host = useChatHost();
26
+ const { fork } = host;
26
27
  const turnId = row.item.turnId;
27
- const bookmark = useChatRow(chatId, (chat) => chat?.bookmarks?.find((candidate) => candidate.itemId === row.id) ?? null);
28
- const canFork = useChatRow(chatId, (chat) => turnId !== null && forkRefusal(chat?.info ?? null, chat?.structure[turnId]) === null);
28
+ // Every selector runs on every streamed word of the thread, so each only looks a field up; the work happens in render.
29
+ const bookmarks = useChatRow(chatId, (chat) => chat?.bookmarks);
30
+ const info = useChatRow(chatId, (chat) => chat?.info ?? null);
31
+ const turn = useChatRow(chatId, (chat) => (turnId === null ? undefined : chat?.structure[turnId]));
29
32
  const streaming = useChatRow(chatId, (chat) => {
30
33
  const item = chat?.items[row.id];
31
34
  return item?.kind === 'assistant' && item.streaming;
32
35
  });
33
36
  const hasText = useChatRow(chatId, (chat) => {
34
37
  const item = chat?.items[row.id];
35
- return (item?.kind === 'user' || item?.kind === 'assistant') && item.text.trim() !== '';
38
+ return (item?.kind === 'user' || item?.kind === 'assistant') && /\S/.test(item.text);
36
39
  });
40
+ const bookmarked = bookmarks?.some((candidate) => candidate.itemId === row.id) === true;
41
+ const canFork = turnId !== null && forkRefusal(info, turn) === null;
37
42
  const [copied, setCopied] = useState(false);
38
43
 
39
44
  useEffect(() => {
@@ -66,8 +71,8 @@ export function MessageActions({ chatId, row }: { chatId: string; row: MessageRo
66
71
  >
67
72
  {!streaming && (
68
73
  <ButtonGroup render={<span />}>
69
- {bookmark === null && (
70
- <IconButton icon={Bookmark} size="sm" label={t('bookmarks.add')} onClick={() => void placeBookmark(scope, chatId, row.id)} />
74
+ {!bookmarked && (
75
+ <IconButton icon={Bookmark} size="sm" label={t('bookmarks.add')} onClick={() => void placeBookmark(scope, host, chatId, row.id)} />
71
76
  )}
72
77
  {canFork && turnId !== null && fork !== null && (
73
78
  <IconButton icon={GitFork} size="sm" label={t('timeline.menu.forkFromHere')} onClick={() => fork(chatId, turnId)} />