@adecore/agents-react 0.23.1 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) 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 +130 -907
  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/surface.d.ts +1 -0
  88. package/dist/chat/ui/composer/surface.js +44 -0
  89. package/dist/chat/ui/composer/use-chat-controls.d.ts +28 -0
  90. package/dist/chat/ui/composer/use-chat-controls.js +112 -0
  91. package/dist/chat/ui/composer/use-compaction-offer.d.ts +2 -0
  92. package/dist/chat/ui/composer/use-compaction-offer.js +23 -0
  93. package/dist/chat/ui/composer/use-composer-draft.d.ts +14 -0
  94. package/dist/chat/ui/composer/use-composer-draft.js +39 -0
  95. package/dist/chat/ui/composer/use-composer-menus.d.ts +52 -0
  96. package/dist/chat/ui/composer/use-composer-menus.js +232 -0
  97. package/dist/chat/ui/composer/use-composer-submit.d.ts +32 -0
  98. package/dist/chat/ui/composer/use-composer-submit.js +113 -0
  99. package/dist/chat/ui/composer/use-draft-files.d.ts +27 -0
  100. package/dist/chat/ui/composer/use-draft-files.js +111 -0
  101. package/dist/chat/ui/composer/use-notice.d.ts +6 -0
  102. package/dist/chat/ui/composer/use-notice.js +18 -0
  103. package/dist/chat/ui/composer/use-pending-requests.d.ts +2 -0
  104. package/dist/chat/ui/composer/use-pending-requests.js +22 -0
  105. package/dist/chat/ui/composer/use-prompt-history.d.ts +8 -0
  106. package/dist/chat/ui/composer/use-prompt-history.js +57 -0
  107. package/dist/chat/ui/composer/use-prompt-hold.d.ts +6 -0
  108. package/dist/chat/ui/composer/use-prompt-hold.js +48 -0
  109. package/dist/chat/ui/composer/use-take-back.d.ts +13 -0
  110. package/dist/chat/ui/composer/use-take-back.js +82 -0
  111. package/dist/chat/ui/diff/EditorEditDiff.js +4 -3
  112. package/dist/chat/ui/diff/LegacyDiffPool.js +3 -2
  113. package/dist/chat/ui/diff/LegacyEditDiff.js +3 -2
  114. package/dist/chat/ui/diff/LegacyUnifiedDiff.js +3 -2
  115. package/dist/chat/ui/diff/syntax-runtime.d.ts +2 -1
  116. package/dist/chat/ui/diff/syntax-runtime.js +9 -5
  117. package/dist/chat/ui/diff-syntax.d.ts +2 -2
  118. package/dist/chat/ui/diff-syntax.js +1 -2
  119. package/dist/chat/ui/diff-theme.js +10 -9
  120. package/dist/chat/ui/file-links.d.ts +1 -1
  121. package/dist/chat/ui/file-links.js +7 -5
  122. package/dist/chat/ui/highlighter.d.ts +27 -0
  123. package/dist/chat/ui/highlighter.js +151 -0
  124. package/dist/chat/ui/index.d.ts +67 -0
  125. package/dist/chat/ui/index.js +64 -0
  126. package/dist/chat/ui/intelligent-ui/UiReply.js +3 -2
  127. package/dist/chat/ui/intelligent-ui/renderers/content.js +3 -2
  128. package/dist/chat/ui/intelligent-ui/use-ui-links.js +2 -2
  129. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +4 -3
  130. package/dist/chat/ui/markdown-blocks.d.ts +8 -0
  131. package/dist/chat/ui/markdown-blocks.js +260 -12
  132. package/dist/chat/ui/mermaid.d.ts +23 -0
  133. package/dist/chat/ui/mermaid.js +93 -0
  134. package/dist/chat/ui/rows/ForksRow.js +5 -3
  135. package/dist/chat/ui/rows/GeneratedImageRow.js +6 -4
  136. package/dist/chat/ui/rows/MessageRows.d.ts +4 -0
  137. package/dist/chat/ui/rows/MessageRows.js +36 -13
  138. package/dist/chat/ui/rows/Rows.js +3 -1
  139. package/dist/chat/ui/rows/SubagentRow.js +3 -2
  140. package/dist/chat/ui/rows/VisualRow.js +3 -2
  141. package/dist/chat/ui/rows/WorkRows.d.ts +6 -3
  142. package/dist/chat/ui/rows/WorkRows.js +57 -20
  143. package/dist/chat/ui/rows/WorkflowRow.js +3 -2
  144. package/dist/chat/ui/timestamp-format.d.ts +1 -0
  145. package/dist/chat/ui/timestamp-format.js +2 -0
  146. package/dist/chat/ui/use-diff-source-tokens.js +6 -3
  147. package/dist/chat/ui/visual-theme.js +3 -2
  148. package/dist/host.d.ts +9 -6
  149. package/dist/host.js +19 -8
  150. package/dist/index.d.ts +8 -0
  151. package/dist/index.js +9 -0
  152. package/dist/locales/en/agent-chat.d.ts +658 -0
  153. package/dist/locales/en/agent-chat.js +657 -0
  154. package/dist/locales/en/agent-prompts.d.ts +44 -0
  155. package/dist/locales/en/agent-prompts.js +43 -0
  156. package/dist/locales/en/agent-providers.d.ts +132 -0
  157. package/dist/locales/en/agent-providers.js +131 -0
  158. package/dist/locales/en/agent-usage.d.ts +132 -0
  159. package/dist/locales/en/agent-usage.js +131 -0
  160. package/dist/locales/nl/agent-chat.d.ts +658 -0
  161. package/dist/locales/nl/agent-chat.js +657 -0
  162. package/dist/locales/nl/agent-prompts.d.ts +44 -0
  163. package/dist/locales/nl/agent-prompts.js +43 -0
  164. package/dist/locales/nl/agent-providers.d.ts +132 -0
  165. package/dist/locales/nl/agent-providers.js +131 -0
  166. package/dist/locales/nl/agent-usage.d.ts +132 -0
  167. package/dist/locales/nl/agent-usage.js +131 -0
  168. package/dist/locales.js +9 -8
  169. package/dist/prompts/index.d.ts +11 -0
  170. package/dist/prompts/index.js +12 -0
  171. package/dist/prompts/logic/subjects.d.ts +2 -1
  172. package/dist/prompts/logic/subjects.js +6 -4
  173. package/dist/prompts/ui/ApprovalBody.js +6 -4
  174. package/dist/prompts/ui/PromptCard.js +5 -4
  175. package/dist/prompts/ui/PromptView.js +3 -2
  176. package/dist/prompts/ui/QuestionBody.js +3 -2
  177. package/dist/providers/AccountColors.js +2 -2
  178. package/dist/providers/AccountDetail.js +7 -6
  179. package/dist/providers/AccountVariables.js +3 -1
  180. package/dist/providers/AddAccountForm.js +3 -2
  181. package/dist/providers/ProvidersPane.js +4 -3
  182. package/dist/providers/account-actions.d.ts +4 -3
  183. package/dist/providers/account-actions.js +6 -7
  184. package/dist/providers/index.d.ts +16 -0
  185. package/dist/providers/index.js +17 -0
  186. package/dist/providers/provider-abilities.d.ts +2 -1
  187. package/dist/providers/provider-abilities.js +4 -0
  188. package/dist/state/chats.js +4 -2
  189. package/dist/state/index.d.ts +4 -0
  190. package/dist/state/index.js +5 -0
  191. package/dist/testing/index.d.ts +1 -0
  192. package/dist/testing/index.js +2 -0
  193. package/dist/{prompts/logic/prompts.fixtures.d.ts → testing/prompts.d.ts} +1 -1
  194. package/dist/theme.css +13 -3
  195. package/dist/usage/UsageBreakdown.js +3 -2
  196. package/dist/usage/UsageLimits.js +4 -3
  197. package/dist/usage/UsagePage.js +3 -2
  198. package/dist/usage/index.d.ts +15 -0
  199. package/dist/usage/index.js +15 -0
  200. package/package.json +53 -1031
  201. package/src/agents/AccountDot.tsx +3 -1
  202. package/src/agents/accounts.ts +6 -6
  203. package/src/chat/actions.ts +8 -5
  204. package/src/chat/bookmarks.ts +23 -14
  205. package/src/chat/chat-client.ts +8 -0
  206. package/src/chat/composer-send.ts +27 -0
  207. package/src/chat/forks.ts +27 -6
  208. package/src/chat/index.ts +98 -0
  209. package/src/chat/logic/embedded-code.ts +409 -0
  210. package/src/chat/logic/fork.ts +1 -0
  211. package/src/chat/logic/goal.ts +33 -0
  212. package/src/chat/logic/scrubber.ts +6 -5
  213. package/src/chat/logic/thought.ts +40 -0
  214. package/src/chat/logic/thread-cards.ts +2 -0
  215. package/src/chat/logic/timeline.ts +25 -1
  216. package/src/chat/logic/tool-catalog.ts +5 -1
  217. package/src/chat/logic/tools.ts +76 -0
  218. package/src/chat/preferences.ts +26 -3
  219. package/src/chat/reading-width.ts +39 -0
  220. package/src/chat/send-key.ts +38 -0
  221. package/src/chat/ui/BookmarkMarker.tsx +4 -2
  222. package/src/chat/ui/BookmarkSubmenu.tsx +4 -3
  223. package/src/chat/ui/ChatActivity.tsx +14 -10
  224. package/src/chat/ui/ChatReferenceChip.tsx +3 -4
  225. package/src/chat/ui/CodeBlock.tsx +33 -124
  226. package/src/chat/ui/Composer.tsx +212 -1358
  227. package/src/chat/ui/ComposerInput.tsx +5 -4
  228. package/src/chat/ui/EditDiff.tsx +3 -2
  229. package/src/chat/ui/GoalDock.tsx +104 -0
  230. package/src/chat/ui/LimitState.tsx +5 -3
  231. package/src/chat/ui/Markdown.tsx +113 -39
  232. package/src/chat/ui/MermaidBlock.tsx +119 -0
  233. package/src/chat/ui/MessageActions.tsx +13 -8
  234. package/src/chat/ui/Pickers.tsx +8 -7
  235. package/src/chat/ui/PromptComposer.tsx +4 -3
  236. package/src/chat/ui/RowTime.tsx +21 -0
  237. package/src/chat/ui/RunSettings.tsx +9 -4
  238. package/src/chat/ui/ShellCommand.tsx +134 -0
  239. package/src/chat/ui/SubagentInfo.tsx +4 -3
  240. package/src/chat/ui/SubagentStopButton.tsx +4 -3
  241. package/src/chat/ui/SubagentTimeline.tsx +5 -1
  242. package/src/chat/ui/Timeline.tsx +167 -105
  243. package/src/chat/ui/TimelineMenu.tsx +42 -7
  244. package/src/chat/ui/UnifiedDiff.tsx +3 -2
  245. package/src/chat/ui/VisualFrame.tsx +6 -7
  246. package/src/chat/ui/code-theme.ts +4 -4
  247. package/src/chat/ui/composer/ComposerControls.tsx +147 -0
  248. package/src/chat/ui/composer/ComposerMenu.tsx +138 -0
  249. package/src/chat/ui/composer/DraftParts.tsx +86 -0
  250. package/src/chat/ui/composer/QueuedMessages.tsx +64 -0
  251. package/src/chat/ui/composer/key-handler.ts +174 -0
  252. package/src/chat/ui/composer/keys.ts +32 -10
  253. package/src/chat/ui/composer/placeholder.ts +48 -0
  254. package/src/chat/ui/composer/surface.ts +45 -0
  255. package/src/chat/ui/composer/use-chat-controls.ts +134 -0
  256. package/src/chat/ui/composer/use-compaction-offer.ts +25 -0
  257. package/src/chat/ui/composer/use-composer-draft.ts +71 -0
  258. package/src/chat/ui/composer/use-composer-menus.ts +289 -0
  259. package/src/chat/ui/composer/use-composer-submit.ts +163 -0
  260. package/src/chat/ui/composer/use-draft-files.ts +135 -0
  261. package/src/chat/ui/composer/use-notice.ts +28 -0
  262. package/src/chat/ui/composer/use-pending-requests.ts +23 -0
  263. package/src/chat/ui/composer/use-prompt-history.ts +76 -0
  264. package/src/chat/ui/composer/use-prompt-hold.ts +58 -0
  265. package/src/chat/ui/composer/use-take-back.ts +91 -0
  266. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +2 -2
  267. package/src/chat/ui/diff/EditorEditDiff.tsx +4 -3
  268. package/src/chat/ui/diff/LegacyDiffPool.tsx +3 -2
  269. package/src/chat/ui/diff/LegacyEditDiff.tsx +3 -2
  270. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +3 -2
  271. package/src/chat/ui/diff/syntax-runtime.ts +10 -5
  272. package/src/chat/ui/diff-syntax.ts +2 -2
  273. package/src/chat/ui/diff-theme.ts +10 -9
  274. package/src/chat/ui/file-links.ts +7 -5
  275. package/src/chat/ui/highlighter.ts +185 -0
  276. package/src/chat/ui/index.ts +109 -0
  277. package/src/chat/ui/intelligent-ui/UiReply.tsx +5 -4
  278. package/src/chat/ui/intelligent-ui/renderers/content.tsx +3 -2
  279. package/src/chat/ui/intelligent-ui/use-ui-links.ts +2 -2
  280. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +4 -3
  281. package/src/chat/ui/markdown-blocks.ts +300 -12
  282. package/src/chat/ui/mermaid.ts +118 -0
  283. package/src/chat/ui/rows/ForksRow.tsx +5 -3
  284. package/src/chat/ui/rows/GeneratedImageRow.tsx +6 -4
  285. package/src/chat/ui/rows/MessageRows.tsx +73 -18
  286. package/src/chat/ui/rows/Rows.tsx +14 -1
  287. package/src/chat/ui/rows/SubagentRow.tsx +3 -2
  288. package/src/chat/ui/rows/VisualRow.tsx +3 -2
  289. package/src/chat/ui/rows/WorkRows.tsx +148 -47
  290. package/src/chat/ui/rows/WorkflowRow.tsx +3 -2
  291. package/src/chat/ui/timestamp-format.ts +2 -0
  292. package/src/chat/ui/use-diff-source-tokens.ts +10 -4
  293. package/src/chat/ui/visual-theme.ts +3 -2
  294. package/src/host.ts +31 -15
  295. package/src/index.ts +49 -0
  296. package/src/locales/en/agent-chat.ts +657 -0
  297. package/src/locales/en/agent-prompts.ts +43 -0
  298. package/src/locales/en/agent-providers.ts +131 -0
  299. package/src/locales/en/agent-usage.ts +131 -0
  300. package/src/locales/nl/agent-chat.ts +657 -0
  301. package/src/locales/nl/agent-prompts.ts +43 -0
  302. package/src/locales/nl/agent-providers.ts +131 -0
  303. package/src/locales/nl/agent-usage.ts +132 -0
  304. package/src/locales.ts +9 -8
  305. package/src/prompts/index.ts +35 -0
  306. package/src/prompts/logic/subjects.ts +12 -4
  307. package/src/prompts/ui/ApprovalBody.tsx +6 -4
  308. package/src/prompts/ui/PromptCard.tsx +11 -4
  309. package/src/prompts/ui/PromptView.tsx +3 -2
  310. package/src/prompts/ui/QuestionBody.tsx +3 -2
  311. package/src/providers/AccountColors.tsx +2 -2
  312. package/src/providers/AccountDetail.tsx +13 -7
  313. package/src/providers/AccountVariables.tsx +3 -1
  314. package/src/providers/AddAccountForm.tsx +3 -2
  315. package/src/providers/ProvidersPane.tsx +4 -3
  316. package/src/providers/account-actions.ts +7 -7
  317. package/src/providers/index.ts +17 -0
  318. package/src/providers/provider-abilities.ts +6 -1
  319. package/src/state/chats.ts +4 -2
  320. package/src/state/index.ts +39 -0
  321. package/src/testing/index.ts +2 -0
  322. package/src/{prompts/logic/prompts.fixtures.ts → testing/prompts.ts} +1 -1
  323. package/src/theme.css +13 -3
  324. package/src/usage/UsageBreakdown.tsx +3 -2
  325. package/src/usage/UsageLimits.tsx +4 -3
  326. package/src/usage/UsagePage.tsx +3 -2
  327. package/src/usage/index.ts +16 -0
  328. package/dist/locales/en/agent-chat.json +0 -597
  329. package/dist/locales/en/agent-prompts.json +0 -43
  330. package/dist/locales/en/agent-providers.json +0 -131
  331. package/dist/locales/en/agent-usage.json +0 -131
  332. package/dist/locales/nl/agent-chat.json +0 -597
  333. package/dist/locales/nl/agent-prompts.json +0 -43
  334. package/dist/locales/nl/agent-providers.json +0 -131
  335. package/dist/locales/nl/agent-usage.json +0 -131
  336. package/src/locales/en/agent-chat.json +0 -597
  337. package/src/locales/en/agent-prompts.json +0 -43
  338. package/src/locales/en/agent-providers.json +0 -131
  339. package/src/locales/en/agent-usage.json +0 -131
  340. package/src/locales/nl/agent-chat.json +0 -597
  341. package/src/locales/nl/agent-prompts.json +0 -43
  342. package/src/locales/nl/agent-providers.json +0 -131
  343. package/src/locales/nl/agent-usage.json +0 -131
  344. /package/dist/{prompts/logic/prompts.fixtures.js → testing/prompts.js} +0 -0
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { Compartment, EditorState, Prec, Transaction } from '@codemirror/state';
4
4
  import { EditorView, placeholder as placeholderText } from '@codemirror/view';
5
- import { chatHost } from '../../host.js';
5
+ import { useChatHost } from '../../host.js';
6
6
  import { composerEditorExtensions, externalChange } from './composer/editor.js';
7
7
  import { insertAtSelection } from './composer/insert.js';
8
8
  const NO_EXTENSIONS = [];
@@ -13,6 +13,7 @@ const NO_ARIA = {};
13
13
  * character, which a textarea with a painted layer behind it could not.
14
14
  */
15
15
  export function ComposerInput({ ref, value, placeholder, disabled, tabbable, className, dictationToolbar, extensions = NO_EXTENSIONS, aria = NO_ARIA, ...handlers }) {
16
+ const { dictation } = useChatHost();
16
17
  const hostRef = useRef(null);
17
18
  const targetRef = useRef(null);
18
19
  const viewRef = useRef(null);
@@ -41,7 +42,7 @@ export function ComposerInput({ ref, value, placeholder, disabled, tabbable, cla
41
42
  selection: { anchor: initial.length },
42
43
  extensions: [
43
44
  composerEditorExtensions(),
44
- chatHost().dictation.composer?.extensions ?? [],
45
+ dictation.composer?.extensions ?? [],
45
46
  compartments.attributes.of([]),
46
47
  compartments.editable.of([]),
47
48
  compartments.placeholder.of([]),
@@ -84,7 +85,7 @@ export function ComposerInput({ ref, value, placeholder, disabled, tabbable, cla
84
85
  view.destroy();
85
86
  viewRef.current = null;
86
87
  };
87
- }, [compartments]);
88
+ }, [compartments, dictation.composer?.extensions]);
88
89
  useLayoutEffect(() => {
89
90
  viewRef.current?.dispatch({
90
91
  effects: [
@@ -131,6 +132,6 @@ export function ComposerInput({ ref, value, placeholder, disabled, tabbable, cla
131
132
  view.focus();
132
133
  }
133
134
  }), []);
134
- const Dictation = chatHost().dictation.composer?.Control;
135
+ const Dictation = dictation.composer?.Control;
135
136
  return (_jsxs("div", { ref: targetRef, className: "min-w-0", children: [_jsx("div", { ref: hostRef, className: className }), Dictation && _jsx(Dictation, { buttonContainer: dictationToolbar, targetRef: targetRef, disabled: disabled, editor: () => viewRef.current })] }));
136
137
  }
@@ -6,12 +6,13 @@ import { diffSourceBudget, legacyDiffBudget } from './diff/diff-source-budget.js
6
6
  import { DiffOmitted } from './diff/DiffOmitted.js';
7
7
  import { DiffUnavailable } from './diff/DiffUnavailable.js';
8
8
  import { DiffFallbackBoundary } from './diff/DiffFallbackBoundary.js';
9
- import { chatHost } from '../../host.js';
9
+ import { useChatHost } from '../../host.js';
10
10
  const LegacyEditDiff = lazyNamed(() => import('./diff/LegacyEditDiff.js'), 'default', { prefetch: false });
11
11
  const EditorEditDiff = lazyNamed(() => import('./diff/EditorEditDiff.js'), 'default');
12
12
  export default function EditDiff({ change, allowEditor = true, maxHeight }) {
13
+ const host = useChatHost();
13
14
  const { t } = useTranslation('agent-chat');
14
- const loadEngine = chatHost().code.editor;
15
+ const loadEngine = host.code.editor;
15
16
  const sourceAllowed = useMemo(() => diffSourceBudget(change.before, change.after), [change.before, change.after]);
16
17
  const legacyMode = !allowEditor || loadEngine === undefined;
17
18
  const legacyAllowed = useMemo(() => legacyMode && legacyDiffBudget(change.before, change.after), [legacyMode, change.before, change.after]);
@@ -0,0 +1,9 @@
1
+ import type { ChatInfo, ProviderCapabilities } from '@adecore/agent-contracts';
2
+ export declare function GoalDock({ chatId, info, goals }: {
3
+ chatId: string;
4
+ info: ChatInfo;
5
+ goals: ProviderCapabilities['goals'];
6
+ }): import("react").JSX.Element | null;
7
+ export declare function GoalPill({ chatId }: {
8
+ chatId: string;
9
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,63 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import clsx from 'clsx';
5
+ import { Target } from 'lucide-react';
6
+ import { Button, Icon, Pill, Tooltip } from '@adecore/ui';
7
+ import { formatTokens, useFormatLocale } from '@adecore/ui/format';
8
+ import { useChatHost } from '../../host.js';
9
+ import { useChatScope } from '../../scope.js';
10
+ import { useChats } from '../../state/chats.js';
11
+ import { useChatActions } from '../actions.js';
12
+ import { goalView } from '../logic/goal.js';
13
+ /* How far the goal got, in what its CLI counts: the checks that sent the agent on, or the tokens it spent. */
14
+ function progressOf(goal, t) {
15
+ const parts = [];
16
+ if (goal.checks !== undefined && goal.checks > 0) {
17
+ parts.push(t('goal.checks', { count: goal.checks }));
18
+ }
19
+ if (goal.tokensUsed !== undefined && (goal.tokensUsed > 0 || goal.tokenBudget !== undefined)) {
20
+ parts.push(goal.tokenBudget === undefined
21
+ ? t('goal.tokens', { tokens: formatTokens(goal.tokensUsed) })
22
+ : t('goal.budget', { tokens: formatTokens(goal.tokensUsed), budget: formatTokens(goal.tokenBudget) }));
23
+ }
24
+ return parts.length === 0 ? null : parts.join(' · ');
25
+ }
26
+ /*
27
+ * At the top of the composer while the chat has a goal: where it stands, the objective, how far it got,
28
+ * and what a person may do to it. The whole objective and what the last check said are in the tooltip.
29
+ */
30
+ export function GoalDock({ chatId, info, goals }) {
31
+ const host = useChatHost();
32
+ const { t } = useTranslation('agent-chat');
33
+ useFormatLocale();
34
+ const actions = useChatActions();
35
+ const [changing, setChanging] = useState(false);
36
+ const view = goalView(info, goals, actions.goal !== undefined);
37
+ if (view === null) {
38
+ return null;
39
+ }
40
+ const { goal } = view;
41
+ const progress = progressOf(goal, t);
42
+ const act = (action) => {
43
+ if (!actions.goal) {
44
+ return;
45
+ }
46
+ setChanging(true);
47
+ actions
48
+ .goal(chatId, action)
49
+ .catch((e) => host.notify({ kind: 'error', title: t('goal.failed'), description: e instanceof Error ? e.message : String(e) }))
50
+ .finally(() => setChanging(false));
51
+ };
52
+ return (_jsxs("div", { className: "flex items-center gap-2 border-b border-border px-3.5 py-2.5", children: [_jsx(Icon, { icon: Target, size: 16, className: clsx('shrink-0', goal.status === 'active' ? 'text-accent' : 'text-text-faint') }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "truncate text-sm text-text", children: t(`goal.status.${goal.status}`) }), _jsx(Tooltip, { label: goal.lastCheck === undefined ? (goal.objective) : (_jsxs(_Fragment, { children: [goal.objective, _jsx("br", {}), t('goal.lastCheck', { check: goal.lastCheck })] })), children: _jsx("span", { className: "truncate text-xs tabular-nums text-text-faint", children: progress === null ? goal.objective : `${goal.objective} · ${progress}` }) })] }), view.actions.map((action) => (_jsx(Button, { size: "sm", disabled: changing, onClick: () => act(action), children: t(`goal.${action}`) }, action)))] }));
53
+ }
54
+ /* 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. */
55
+ export function GoalPill({ chatId }) {
56
+ const { t } = useTranslation('agent-chat');
57
+ const { keyOf } = useChatScope();
58
+ const goal = useChats((s) => s.statusByKey[keyOf(chatId)]?.info.goal);
59
+ if (goal?.status !== 'active') {
60
+ return null;
61
+ }
62
+ return (_jsx(Tooltip, { label: `${t('goal.status.active')}: ${goal.objective}`, children: _jsx(Pill, { tone: "accent", icon: _jsx(Icon, { icon: Target, size: 12 }), children: t('goal.pill') }) }));
63
+ }
@@ -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.js';
12
12
  import { useChats } from '../../state/chats.js';
13
- import { chatHost } from '../../host.js';
13
+ import { useChatHost } from '../../host.js';
14
14
  import { useChatScope } from '../../scope.js';
15
15
  import { useChatActions } from '../actions.js';
16
16
  import { useUsageStore } from '../../state/usage.js';
@@ -39,10 +39,11 @@ function readUnreadAccounts(scope) {
39
39
  * switch off it stands off and says where that switch is.
40
40
  */
41
41
  function ResumeAtResetToggle({ chatId, info }) {
42
+ const host = useChatHost();
42
43
  const { t } = useTranslation('agent-chat');
43
44
  const { id } = useChatScope();
44
45
  const actions = useChatActions();
45
- const machineAllows = chatHost().useResumeAtReset(id);
46
+ const machineAllows = host.useResumeAtReset(id);
46
47
  const set = (on) => {
47
48
  void actions.configure(chatId, { resumeAtReset: on }).catch(() => undefined);
48
49
  };
@@ -54,6 +55,7 @@ function ResumeAtResetToggle({ chatId, info }) {
54
55
  * conversation takes it over; any other goes on in a fork, which then opens the way a fork does.
55
56
  */
56
57
  function ContinueOn({ chatId, info, choice }) {
58
+ const host = useChatHost();
57
59
  const { t } = useTranslation('agent-chat');
58
60
  const scope = useChatScope();
59
61
  const actions = useChatActions();
@@ -74,7 +76,7 @@ function ContinueOn({ chatId, info, choice }) {
74
76
  setBusy(true);
75
77
  actions
76
78
  .continueOn(chatId, target.id)
77
- .catch((e) => chatHost().notify({
79
+ .catch((e) => host.notify({
78
80
  kind: 'error',
79
81
  title: t('limit.continueFailed', { account: name }),
80
82
  description: e instanceof Error ? e.message : String(e)
@@ -14,10 +14,11 @@ export declare const MessageMarkdown: import("react").MemoExoticComponent<({ tex
14
14
  mentions?: string[];
15
15
  skills?: string[];
16
16
  }) => import("react").JSX.Element>;
17
- export declare const Markdown: import("react").MemoExoticComponent<({ text, breaks, fileLinks, rehypePlugins, componentOverrides }: {
17
+ export declare const Markdown: import("react").MemoExoticComponent<({ text, breaks, fileLinks, muted, rehypePlugins, componentOverrides }: {
18
18
  text: string;
19
19
  breaks?: boolean;
20
20
  fileLinks?: boolean;
21
+ muted?: boolean;
21
22
  rehypePlugins?: Options['rehypePlugins'];
22
23
  componentOverrides?: Components;
23
24
  }) => import("react").JSX.Element>;
@@ -7,16 +7,29 @@ import remarkBreaks from 'remark-breaks';
7
7
  import remarkGfm from 'remark-gfm';
8
8
  import { CHIP_IN_MESSAGE, MENTION_TONE, SKILL_TONE } from './chips.js';
9
9
  import { CodeBlock } from './CodeBlock.js';
10
+ import { MermaidBlock } from './MermaidBlock.js';
10
11
  import { CodeStreamingContext } from './code-streaming.js';
11
12
  import { splitMarkdownBlocks } from './markdown-blocks.js';
12
13
  import { rehypeChips } from './rehype-chips.js';
13
14
  import { rehypeFadeWords } from './rehype-fade.js';
14
15
  import { remarkHtmlAsText } from './remark-html-as-text.js';
15
- import { chatHost } from '../../host.js';
16
- import { openFileLink, useFileLinkCwd, useFileLinkScopeId, useFileLinkTarget } from './file-links.js';
17
- import { FileIcon, Icon } from '@adecore/ui';
16
+ import { useChatHost } from '../../host.js';
17
+ import { useFileLinkCwd, useFileLinkScopeId, useFileLinkTarget, useOpenFileLink } from './file-links.js';
18
+ import { ErrorBoundary, FileIcon, Icon } from '@adecore/ui';
19
+ import { useTranslation } from 'react-i18next';
18
20
  const ShellCodeBlockReplyContext = createContext(null);
19
21
  const BlockCodeContext = createContext(false);
22
+ /* The command a shell fence holds, read from the fence as written, so it keeps the indentation and line endings it has. */
23
+ function shellFence(source) {
24
+ const opening = /^ {0,3}(`{3,}|~{3,})[ \t]*(sh|bash|zsh)[ \t]*\r?\n/.exec(source);
25
+ if (opening === null) {
26
+ return null;
27
+ }
28
+ const body = source.slice(opening[0].length);
29
+ const closing = /(?:^|\r?\n) {0,3}(`{3,}|~{3,})[ \t]*$/.exec(body);
30
+ const closed = closing !== null && closing[1][0] === opening[1][0] && closing[1].length >= opening[1].length;
31
+ return { language: opening[2], code: closed ? body.slice(0, closing.index) : body, closed };
32
+ }
20
33
  /* Only root children qualify; Markdown's parser decides whether a fence belongs to a list or quote. */
21
34
  function remarkShellCodeBlocks() {
22
35
  return (tree, file) => {
@@ -29,26 +42,30 @@ function remarkShellCodeBlocks() {
29
42
  if (start === undefined || end === undefined) {
30
43
  continue;
31
44
  }
32
- const raw = String(file.value).slice(start, end);
33
- const opening = /^ {0,3}(`{3,}|~{3,})[ \t]*(sh|bash|zsh)[ \t]*\r?\n/.exec(raw);
34
- if (opening === null) {
45
+ const shell = shellFence(String(file.value).slice(start, end));
46
+ if (shell === null) {
35
47
  continue;
36
48
  }
37
- const body = raw.slice(opening[0].length);
38
- const closing = /(?:^|\r?\n) {0,3}(`{3,}|~{3,})[ \t]*$/.exec(body);
39
- const closed = closing !== null && closing[1][0] === opening[1][0] && closing[1].length >= opening[1].length;
40
49
  node.data = {
41
50
  ...node.data,
42
51
  hProperties: {
43
52
  ...node.data?.hProperties,
44
- 'data-shell-language': opening[2],
45
- 'data-shell-code': closed ? body.slice(0, closing.index) : body,
46
- 'data-shell-closed': closed
53
+ 'data-shell-language': shell.language,
54
+ 'data-shell-code': shell.code,
55
+ 'data-shell-closed': shell.closed
47
56
  }
48
57
  };
49
58
  }
50
59
  };
51
60
  }
61
+ /* What the host adds to a shell fence of a main reply, which is complete once the fence closed and the reply stopped. */
62
+ function shellCodeBlockActions(reply, shell, render) {
63
+ if (reply === null || shell === null) {
64
+ return undefined;
65
+ }
66
+ const { scopeId, chatId, itemId } = reply;
67
+ return render?.({ scopeId, chatId, itemId, language: shell.language, code: shell.code, complete: !reply.streaming && shell.closed });
68
+ }
52
69
  function languageOf(className) {
53
70
  return /language-([\w-]+)/.exec(className ?? '')?.[1] ?? 'text';
54
71
  }
@@ -60,6 +77,7 @@ const INLINE_CODE = 'rounded-sm bg-surface-sunken px-1 py-px font-mono text-code
60
77
  function FileLink({ target, className, children }) {
61
78
  const cwd = useFileLinkCwd();
62
79
  const scopeId = useFileLinkScopeId();
80
+ const openFileLink = useOpenFileLink();
63
81
  return (_jsx("button", { type: "button", className: clsx('cursor-pointer select-text', className), "data-file-path": target.path, "data-file-line": target.line, "data-file-column": target.column, "data-file-end-line": target.endLine, "data-file-directory": target.directory, "data-file-cwd": cwd ?? '', "data-file-scope-id": scopeId ?? '', onClick: () => openFileLink(cwd, target, scopeId), children: children }));
64
82
  }
65
83
  /* Inline code, which is a file reference often enough that it is worth asking every time. */
@@ -94,11 +112,18 @@ function MarkdownLink({ href, children }) {
94
112
  function ExternalLink({ href, children }) {
95
113
  return (_jsx("a", { href: href, target: "_blank", rel: "noreferrer", children: children }));
96
114
  }
115
+ /* A ```mermaid fence, which draws what it says. */
116
+ function Diagram({ code }) {
117
+ const { t } = useTranslation('agent-chat');
118
+ return (_jsx(ErrorBoundary, { label: t('code.mermaid.crashed'), resetKeys: [code], compact: true, className: "rounded-lg border border-border", children: _jsx(MermaidBlock, { code: code }) }));
119
+ }
97
120
  function MarkdownCode({ className, children, plain = false }) {
98
121
  const block = useContext(BlockCodeContext);
99
122
  const text = String(children ?? '');
100
123
  if (block) {
101
- return _jsx(CodeBlock, { code: text.replace(/\n$/, ''), lang: languageOf(className) });
124
+ const lang = languageOf(className);
125
+ const code = text.replace(/\n$/, '');
126
+ return lang === 'mermaid' ? _jsx(Diagram, { code: code }) : _jsx(CodeBlock, { code: code, lang: lang });
102
127
  }
103
128
  return plain ? _jsx("code", { className: INLINE_CODE, children: text }) : _jsx(InlineCode, { text: text });
104
129
  }
@@ -145,22 +170,19 @@ const replyComponents = {
145
170
  function ReplyCode({ className, children, node }) {
146
171
  const reply = useContext(ShellCodeBlockReplyContext);
147
172
  const block = useContext(BlockCodeContext);
173
+ const { renderShellCodeBlock } = useChatHost();
148
174
  const properties = node?.properties;
149
175
  const language = properties?.['data-shell-language'];
150
176
  const code = properties?.['data-shell-code'];
151
- const context = reply !== null && (language === 'sh' || language === 'bash' || language === 'zsh') && typeof code === 'string'
152
- ? {
153
- scopeId: reply.scopeId,
154
- chatId: reply.chatId,
155
- itemId: reply.itemId,
156
- language,
157
- code,
158
- complete: !reply.streaming && properties?.['data-shell-closed'] === true
159
- }
177
+ const shell = (language === 'sh' || language === 'bash' || language === 'zsh') && typeof code === 'string'
178
+ ? { language, code, closed: properties?.['data-shell-closed'] === true }
160
179
  : null;
161
180
  const text = String(children ?? '');
181
+ if (block && languageOf(className) === 'mermaid') {
182
+ return _jsx(Diagram, { code: text.replace(/\n$/, '') });
183
+ }
162
184
  if (block) {
163
- return (_jsx(CodeBlock, { code: text.replace(/\n$/, ''), lang: languageOf(className), actions: context !== null ? chatHost().renderShellCodeBlock?.(context) : undefined }));
185
+ return _jsx(CodeBlock, { code: text.replace(/\n$/, ''), lang: languageOf(className), actions: shellCodeBlockActions(reply, shell, renderShellCodeBlock) });
164
186
  }
165
187
  return _jsx(InlineCode, { text: text });
166
188
  }
@@ -192,14 +214,28 @@ const ReplyBlock = memo(function ReplyBlock({ text, fade, open, reply }) {
192
214
  return (_jsx(CodeStreamingContext.Provider, { value: open, children: _jsx(ShellCodeBlockReplyContext.Provider, { value: reply ?? null, children: _jsx(ReactMarkdown, { remarkPlugins: REPLY_PLUGINS, rehypePlugins: fade ? FADE_PLUGINS : NO_PLUGINS, components: replyComponents, children: text }) }) }));
193
215
  });
194
216
  /*
195
- * A reply in a chat thread, parsed a block at a time so a delta only parses the block that grows.
196
- * While it streams every new word fades in, and a fence that has not closed yet is highlighted a
197
- * line at a time as it grows. `arriving` fades in each block as it is added instead, for a reply
198
- * that is shown a block at a time.
217
+ * A code fence at the top level of a reply, drawn as markdown draws it but without parsing, so a word
218
+ * added to a long fence costs no more than one added to a short one. It is the same tree whether the
219
+ * fence is open or closed, so closing it keeps the lines its code block already has.
220
+ */
221
+ const ReplyFence = memo(function ReplyFence({ info, body, source, open, reply }) {
222
+ const { renderShellCodeBlock } = useChatHost();
223
+ // The class markdown gives the code, read back the way a parsed fence's is.
224
+ const lang = languageOf(`language-${info.split(/[ \t]/, 1)[0]}`);
225
+ const shell = reply === undefined ? null : shellFence(source);
226
+ return (_jsx(CodeStreamingContext.Provider, { value: open, children: lang === 'mermaid' ? (_jsx(Diagram, { code: body })) : (_jsx(CodeBlock, { code: body, lang: lang, actions: shellCodeBlockActions(reply ?? null, shell, renderShellCodeBlock) })) }));
227
+ });
228
+ /*
229
+ * A reply in a chat thread, parsed a block at a time so a delta only parses the block that grows,
230
+ * and a code fence at the top level not at all. While it streams every new word fades in, and a fence
231
+ * that has not closed yet is highlighted a line at a time as it grows. `arriving` fades in each block
232
+ * as it is added instead, for a reply that is shown a block at a time.
199
233
  */
200
234
  export const ReplyMarkdown = memo(function ReplyMarkdown({ text, streaming, arriving = false, reply }) {
201
235
  const blocks = useMemo(() => splitMarkdownBlocks(text), [text]);
202
- return (_jsx("div", { className: "chat-markdown prose prose-sm max-w-none text-sm", "data-arriving": arriving || undefined, children: blocks.map((block, index) => (_jsx(ReplyBlock, { text: block.text, fade: streaming, open: streaming && block.openFence, reply: reply }, index))) }));
236
+ return (_jsx("div", { className: "chat-markdown prose prose-sm max-w-none text-sm", "data-arriving": arriving || undefined, children: blocks.map((block, index) =>
237
+ // Blocks only ever grow at the end, so the place of a block is a stable key.
238
+ block.fence !== undefined ? (_jsx(ReplyFence, { info: block.fence.info, body: block.fence.body, source: block.fence.source, open: streaming && !block.fence.closed, reply: reply }, index)) : (_jsx(ReplyBlock, { text: block.text, fade: streaming, open: streaming && block.openFence, reply: reply }, index))) }));
203
239
  });
204
240
  /*
205
241
  * A message a person sent. Enter meant a line break, a tag typed without backticks stays the text
@@ -209,8 +245,11 @@ export const MessageMarkdown = memo(function MessageMarkdown({ text, mentions, s
209
245
  const rehypePlugins = useMemo(() => [[rehypeChips, { mentions, skills }]], [mentions, skills]);
210
246
  return (_jsx("div", { className: "chat-markdown prose prose-sm max-w-none text-sm", children: _jsx(ReactMarkdown, { remarkPlugins: MESSAGE_PLUGINS, rehypePlugins: rehypePlugins, components: messageComponents, children: text }) }));
211
247
  });
212
- /* `breaks` is for person-authored notes; applying it to CLI markdown would change its layout. */
213
- export const Markdown = memo(function Markdown({ text, breaks = false, fileLinks = true, rehypePlugins, componentOverrides }) {
248
+ /*
249
+ * `breaks` is for person-authored notes; applying it to CLI markdown would change its layout. `muted`
250
+ * sets the text a step back, for what stands beside an answer rather than being one.
251
+ */
252
+ export const Markdown = memo(function Markdown({ text, breaks = false, fileLinks = true, muted = false, rehypePlugins, componentOverrides }) {
214
253
  const renderComponents = useMemo(() => ({ ...(fileLinks ? components : plainComponents), ...componentOverrides }), [fileLinks, componentOverrides]);
215
- return (_jsx("div", { className: "chat-markdown prose prose-sm max-w-none text-sm", children: _jsx(ReactMarkdown, { remarkPlugins: breaks ? PLUGINS_WITH_BREAKS : PLUGINS, rehypePlugins: rehypePlugins, components: renderComponents, children: text }) }));
254
+ return (_jsx("div", { className: "chat-markdown prose prose-sm max-w-none text-sm", "data-muted": muted || undefined, children: _jsx(ReactMarkdown, { remarkPlugins: breaks ? PLUGINS_WITH_BREAKS : PLUGINS, rehypePlugins: rehypePlugins, components: renderComponents, children: text }) }));
216
255
  });
@@ -0,0 +1,3 @@
1
+ export declare function MermaidBlock({ code }: {
2
+ code: string;
3
+ }): import("react").JSX.Element;
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useContext, useEffect, useId, useRef, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Code, Workflow } from 'lucide-react';
5
+ import { IconButton } from '@adecore/ui';
6
+ import { useChatHost } from '../../host.js';
7
+ import { CodeBlock } from './CodeBlock.js';
8
+ import { CodeStreamingContext } from './code-streaming.js';
9
+ import { MERMAID_TOKENS, mermaidConfig, sizedSvg, svgDataUrl } from './mermaid.js';
10
+ // Mermaid keeps one configuration for the page, so one diagram is drawn at a time with its own.
11
+ let queue = Promise.resolve();
12
+ let probeCanvas = null;
13
+ /* A token as an opaque `#rrggbb` over `ground`, which is what Mermaid's color math reads. */
14
+ function resolveColor(element, token, ground) {
15
+ probeCanvas ??= document.createElement('canvas');
16
+ probeCanvas.width = 1;
17
+ probeCanvas.height = 1;
18
+ const context = probeCanvas.getContext('2d', { willReadFrequently: true });
19
+ const value = getComputedStyle(element).getPropertyValue(token).trim();
20
+ if (context === null || value === '') {
21
+ return ground;
22
+ }
23
+ context.fillStyle = ground;
24
+ context.fillRect(0, 0, 1, 1);
25
+ context.fillStyle = value;
26
+ context.fillRect(0, 0, 1, 1);
27
+ const [red, green, blue] = context.getImageData(0, 0, 1, 1).data;
28
+ return `#${[red, green, blue].map((channel) => channel.toString(16).padStart(2, '0')).join('')}`;
29
+ }
30
+ function themeColors(element) {
31
+ const ground = resolveColor(element, MERMAID_TOKENS.ground, '#808080');
32
+ const entries = Object.entries(MERMAID_TOKENS).map(([name, token]) => [name, name === 'ground' ? ground : resolveColor(element, token, ground)]);
33
+ return Object.fromEntries(entries);
34
+ }
35
+ async function drawDiagram(id, source, element, dark) {
36
+ const { default: mermaid } = await import('mermaid');
37
+ const style = getComputedStyle(element);
38
+ mermaid.initialize(mermaidConfig(themeColors(element), dark, { family: style.fontFamily, size: style.fontSize }));
39
+ const { svg } = await mermaid.render(id, source);
40
+ return sizedSvg(svg);
41
+ }
42
+ /*
43
+ * A ```mermaid fence, drawn as the diagram once the fence is closed. Mermaid is large, so it loads on
44
+ * the first diagram of the page. While the fence grows, and when the text is no diagram Mermaid can
45
+ * draw, it is the code block it would otherwise be.
46
+ */
47
+ export function MermaidBlock({ code }) {
48
+ const host = useChatHost();
49
+ const { t } = useTranslation('agent-chat');
50
+ const streaming = useContext(CodeStreamingContext);
51
+ const dark = host.code.useMode() === 'dark';
52
+ const id = `mermaid-${useId().replace(/[^\w-]/g, '')}`;
53
+ const frame = useRef(null);
54
+ const [source, setSource] = useState(false);
55
+ const [drawing, setDrawing] = useState({ state: 'drawing' });
56
+ useEffect(() => {
57
+ const element = frame.current;
58
+ if (streaming || element === null) {
59
+ return;
60
+ }
61
+ let cancelled = false;
62
+ setDrawing({ state: 'drawing' });
63
+ const drawn = queue.then(() => (cancelled ? null : drawDiagram(id, code, element, dark)));
64
+ queue = drawn.catch(() => undefined);
65
+ drawn.then((image) => !cancelled && setDrawing(image === null ? { state: 'failed' } : { state: 'drawn', image }), () => !cancelled && setDrawing({ state: 'failed' }));
66
+ return () => {
67
+ cancelled = true;
68
+ };
69
+ }, [code, dark, id, streaming]);
70
+ const showsDiagram = !streaming && !source && drawing.state !== 'failed';
71
+ const toggle = streaming || drawing.state === 'failed' ? undefined : (_jsx(IconButton, { icon: source ? Workflow : Code, size: "sm", label: source ? t('code.mermaid.showDiagram') : t('code.mermaid.showSource'), onClick: () => setSource((shown) => !shown) }));
72
+ return (_jsxs("div", { ref: frame, children: [_jsx(CodeBlock, { code: code, lang: "mermaid", actions: toggle, view: showsDiagram ? (_jsx("div", { className: "overflow-x-auto p-3", "aria-busy": drawing.state === 'drawing' || undefined, children: drawing.state === 'drawn' && (_jsx("img", { src: svgDataUrl(drawing.image.svg), width: drawing.image.width, height: drawing.image.height, alt: t('code.mermaid.diagram'), className: "m-0 max-w-none", draggable: false })) })) : undefined }), drawing.state === 'failed' && _jsx("p", { className: "m-0 mt-1 text-xs text-text-faint", children: t('code.mermaid.failed') })] }));
73
+ }
@@ -6,30 +6,35 @@ import { Bookmark, Check, Copy, GitFork } from 'lucide-react';
6
6
  import { placeBookmark } from '../bookmarks.js';
7
7
  import { forkRefusal } from '../logic/fork.js';
8
8
  import { markdownOf, messageTextOf } from '../logic/timeline-copy.js';
9
- import { chatHost } from '../../host.js';
9
+ import { useChatHost } from '../../host.js';
10
10
  import { useChatScope } from '../../scope.js';
11
11
  import { useChatRow, useChats } from '../../state/chats.js';
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.js';
14
15
  const COPIED_MS = 1500;
15
- const TIMESTAMP_FORMAT = { dateStyle: 'full', timeStyle: 'medium' };
16
16
  /* Hidden actions keep their height, so hovering never forces the virtualizer to remeasure. */
17
17
  export function MessageActions({ chatId, row }) {
18
18
  const { t } = useTranslation('agent-chat');
19
19
  useFormatLocale();
20
20
  const scope = useChatScope();
21
- const { fork } = chatHost();
21
+ const host = useChatHost();
22
+ const { fork } = host;
22
23
  const turnId = row.item.turnId;
23
- const bookmark = useChatRow(chatId, (chat) => chat?.bookmarks?.find((candidate) => candidate.itemId === row.id) ?? null);
24
- const canFork = useChatRow(chatId, (chat) => turnId !== null && forkRefusal(chat?.info ?? null, chat?.structure[turnId]) === null);
24
+ // Every selector runs on every streamed word of the thread, so each only looks a field up; the work happens in render.
25
+ const bookmarks = useChatRow(chatId, (chat) => chat?.bookmarks);
26
+ const info = useChatRow(chatId, (chat) => chat?.info ?? null);
27
+ const turn = useChatRow(chatId, (chat) => (turnId === null ? undefined : chat?.structure[turnId]));
25
28
  const streaming = useChatRow(chatId, (chat) => {
26
29
  const item = chat?.items[row.id];
27
30
  return item?.kind === 'assistant' && item.streaming;
28
31
  });
29
32
  const hasText = useChatRow(chatId, (chat) => {
30
33
  const item = chat?.items[row.id];
31
- return (item?.kind === 'user' || item?.kind === 'assistant') && item.text.trim() !== '';
34
+ return (item?.kind === 'user' || item?.kind === 'assistant') && /\S/.test(item.text);
32
35
  });
36
+ const bookmarked = bookmarks?.some((candidate) => candidate.itemId === row.id) === true;
37
+ const canFork = turnId !== null && forkRefusal(info, turn) === null;
33
38
  const [copied, setCopied] = useState(false);
34
39
  useEffect(() => {
35
40
  if (!copied) {
@@ -48,5 +53,5 @@ export function MessageActions({ chatId, row }) {
48
53
  setCopied(true);
49
54
  }
50
55
  };
51
- return (_jsxs("div", { role: "toolbar", "aria-label": t('timeline.actions.label'), className: clsx('flex h-7 items-center gap-2 opacity-0 transition-opacity group-has-focus-visible/message:opacity-100 group-hover/message:opacity-100', row.kind === 'user' && 'mt-1.5 justify-end'), children: [!streaming && (_jsxs(ButtonGroup, { render: _jsx("span", {}), children: [bookmark === null && (_jsx(IconButton, { icon: Bookmark, size: "sm", label: t('bookmarks.add'), onClick: () => void placeBookmark(scope, chatId, row.id) })), canFork && turnId !== null && fork !== null && (_jsx(IconButton, { icon: GitFork, size: "sm", label: t('timeline.menu.forkFromHere'), onClick: () => fork(chatId, turnId) })), hasText && (_jsx(IconButton, { icon: copied ? Check : Copy, size: "sm", label: copied ? t('timeline.actions.copied') : t('timeline.menu.copyMessage'), onClick: (e) => copy(e.shiftKey) }))] })), _jsx(Tooltip, { label: formatDateTime(row.item.createdAt, TIMESTAMP_FORMAT), children: _jsx("time", { dateTime: new Date(row.item.createdAt).toISOString(), className: clsx('shrink-0 text-xs text-text-faint tabular-nums select-none', row.kind === 'assistant' && 'order-first'), children: formatClock(row.item.createdAt) }) })] }));
56
+ return (_jsxs("div", { role: "toolbar", "aria-label": t('timeline.actions.label'), className: clsx('flex h-7 items-center gap-2 opacity-0 transition-opacity group-has-focus-visible/message:opacity-100 group-hover/message:opacity-100', row.kind === 'user' && 'mt-1.5 justify-end'), children: [!streaming && (_jsxs(ButtonGroup, { render: _jsx("span", {}), children: [!bookmarked && (_jsx(IconButton, { icon: Bookmark, size: "sm", label: t('bookmarks.add'), onClick: () => void placeBookmark(scope, host, chatId, row.id) })), canFork && turnId !== null && fork !== null && (_jsx(IconButton, { icon: GitFork, size: "sm", label: t('timeline.menu.forkFromHere'), onClick: () => fork(chatId, turnId) })), hasText && (_jsx(IconButton, { icon: copied ? Check : Copy, size: "sm", label: copied ? t('timeline.actions.copied') : t('timeline.menu.copyMessage'), onClick: (e) => copy(e.shiftKey) }))] })), _jsx(Tooltip, { label: formatDateTime(row.item.createdAt, TIMESTAMP_FORMAT), children: _jsx("time", { dateTime: new Date(row.item.createdAt).toISOString(), className: clsx('shrink-0 text-xs text-text-faint tabular-nums select-none', row.kind === 'assistant' && 'order-first'), children: formatClock(row.item.createdAt) }) })] }));
52
57
  }
@@ -6,12 +6,12 @@ import { AgentIcon } from '../../agents/AgentIcon.js';
6
6
  import { ProviderLogo } from '../../agents/ProviderLogo.js';
7
7
  import { agentChipOf, modelName } from '../../agents/model-name.js';
8
8
  import { modelRequirementOf } from '../../agents/model-requirement.js';
9
- import { chatHost } from '../../host.js';
9
+ import { useChatHost } from '../../host.js';
10
10
  import { ChatScopeContext } from '../../scope.js';
11
11
  import { useReportedModels } from '../../state/chats.js';
12
12
  import { pickerEntriesOf } from '../logic/picker-entries.js';
13
13
  import { forgetStashed, STASH_SHORTCUT, useStash } from '../stash.js';
14
- import { Icon, IconButton, Menu, Popover, Tooltip } from '@adecore/ui';
14
+ import { Icon, IconButton, Menu, Pill, Popover, Tooltip } from '@adecore/ui';
15
15
  const triggerClass = 'flex h-7 shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-2 text-xs text-text-muted hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text';
16
16
  const chipClass = 'flex h-6 min-w-0 shrink items-center gap-1.5 whitespace-nowrap rounded-md bg-surface-hover px-2 text-xs text-text hover:bg-surface-active data-[popup-open]:bg-surface-active';
17
17
  /*
@@ -20,6 +20,7 @@ const chipClass = 'flex h-6 min-w-0 shrink items-center gap-1.5 whitespace-nowra
20
20
  * provider's model is also how a chat picks its provider; with one CLI the group headers drop.
21
21
  */
22
22
  export function ModelPicker({ providers, provider, selection, open, onOpenChange, onChange, kbd = '/model', side = 'top', trigger = 'plain', finalFocus, footer }) {
23
+ const host = useChatHost();
23
24
  const { t } = useTranslation('agent-chat');
24
25
  const [query, setQuery] = useState('');
25
26
  const [legacyOpen, setLegacyOpen] = useState(false);
@@ -33,7 +34,7 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
33
34
  const trimmed = query.trim().toLowerCase();
34
35
  const entries = useMemo(() => pickerEntriesOf(providers, trimmed, legacyOpen), [providers, trimmed, legacyOpen]);
35
36
  const scope = useContext(ChatScopeContext);
36
- const machine = chatHost().useMachineLabel(scope?.id ?? '');
37
+ const machine = host.useMachineLabel(scope?.id ?? '');
37
38
  const reported = useReportedModels(provider);
38
39
  const requirementOf = (entry) => modelRequirementOf(entry.model, entry.provider, reported);
39
40
  // A CLI that is too old blocks all its models; one way to its settings under the group says it once.
@@ -93,11 +94,11 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
93
94
  const next = entries[i + 1];
94
95
  const lastOfGroup = next === undefined || next.kind !== 'model' || next.provider.kind !== entry.provider.kind;
95
96
  const chosen = entry.provider.kind === provider && entry.model.slug === selection.model;
96
- return (_jsxs("div", { children: [first && (_jsxs(Menu.Label, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.provider.kind, size: 12 }), entry.provider.name] })), _jsxs("button", { role: "option", "aria-selected": chosen, "aria-disabled": requirement !== null || undefined, "data-active": i === index, className: `cursor-row flex w-full flex-col items-stretch gap-0.5 rounded-md px-2.5 py-1.5 text-left text-sm ${requirement === null ? 'text-text-muted' : 'text-text-faint'}`, onMouseEnter: () => setIndex(i), onClick: () => choose(entry), children: [_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx(Menu.Check, { kind: "radio", checked: chosen }), _jsx("span", { className: "min-w-0 truncate", children: entry.model.name }), entry.model.badge && (_jsx("span", { className: "rounded bg-accent-soft px-1 text-[11px] leading-5 font-medium uppercase text-accent", children: entry.model.badge })), _jsx("span", { className: "grow" }), entry.model.legacy && _jsx("span", { className: "text-xs text-text-faint", children: t('pickers.model.legacy') })] }), requirement !== null && (_jsx("span", { className: "pl-6 text-xs text-text-faint", children: machine === null
97
+ return (_jsxs("div", { children: [first && (_jsxs(Menu.Label, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.provider.kind, size: 12 }), entry.provider.name] })), _jsxs("button", { role: "option", "aria-selected": chosen, "aria-disabled": requirement !== null || undefined, "data-active": i === index, className: `cursor-row flex w-full flex-col items-stretch gap-0.5 rounded-md px-2.5 py-1.5 text-left text-sm ${requirement === null ? 'text-text-muted' : 'text-text-faint'}`, onMouseEnter: () => setIndex(i), onClick: () => choose(entry), children: [_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx(Menu.Check, { kind: "radio", checked: chosen }), _jsx("span", { className: "min-w-0 truncate", children: entry.model.name }), entry.model.badge && (_jsx(Pill, { tone: "accent", shape: "tag", size: "sm", children: entry.model.badge.charAt(0).toLocaleUpperCase() + entry.model.badge.slice(1) })), _jsx("span", { className: "grow" }), entry.model.legacy && _jsx("span", { className: "text-xs text-text-faint", children: t('pickers.model.legacy') })] }), requirement !== null && (_jsx("span", { className: "pl-6 text-xs text-text-faint", children: machine === null
97
98
  ? t('pickers.model.requiresHere', { cli: requirement.cli, version: requirement.minVersion })
98
99
  : t('pickers.model.requires', { cli: requirement.cli, version: requirement.minVersion, machine }) }))] }), lastOfGroup && blockedKinds.has(entry.provider.kind) && (_jsx("button", { type: "button", className: "cursor-row flex w-full items-center rounded-md px-2.5 py-1.5 text-left text-xs text-text-muted", onClick: () => {
99
100
  setOpen(false);
100
- chatHost().openSettings('providers');
101
+ host.openSettings('providers');
101
102
  }, children: t('pickers.model.providerSettings') }))] }, `${entry.provider.kind}/${entry.model.slug}`));
102
103
  })] }), footer !== undefined && _jsx("div", { className: "border-t border-border-soft px-3 py-2 text-xs text-text-faint", children: footer })] })] }));
103
104
  }
@@ -8,7 +8,7 @@ import { answerPrompt, isBlockingSubject, promptCreatedAt, promptIdOf } from '..
8
8
  import { useFocusAfterAnswer } from '../../prompts/logic/useFocusAfterAnswer.js';
9
9
  import { usePromptSession } from '../../prompts/logic/usePromptSession.js';
10
10
  import { PromptView } from '../../prompts/ui/PromptView.js';
11
- import { chatHost } from '../../host.js';
11
+ import { useChatHost } from '../../host.js';
12
12
  import { useChatScope } from '../../scope.js';
13
13
  import { useChatActions } from '../actions.js';
14
14
  import { Button, Icon } from '@adecore/ui';
@@ -19,10 +19,11 @@ function pickSubject(waiting, activeId) {
19
19
  export function PromptComposer({ chatId, pending, focused, onShowElsewhere, disabled, hasDraft, denyReason, onAllAnswered, hold, children }) {
20
20
  const { t } = useTranslation('agent-chat');
21
21
  const { id } = useChatScope();
22
+ const host = useChatHost();
22
23
  const actions = useChatActions();
23
24
  const elsewhere = onShowElsewhere !== undefined;
24
25
  // A card the app raises itself waits in the chat beside the CLI's own requests.
25
- const extra = chatHost().prompts.useExtra(id, chatId);
26
+ const extra = host.prompts.useExtra(id, chatId);
26
27
  const subjects = useMemo(() => [
27
28
  ...pending.map((item) => ({ kind: 'chat', nodeId: chatId, item })),
28
29
  ...extra.map((prompt) => ({ kind: 'host', nodeId: chatId, prompt }))
@@ -58,7 +59,7 @@ export function PromptComposer({ chatId, pending, focused, onShowElsewhere, disa
58
59
  return (_jsxs("div", { ref: ref, onPointerDownCapture: session.onPointerDownCapture, onClickCapture: session.onClickCapture, children: [active && elsewhere && (_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 px-3.5 py-3 text-left text-sm text-text-muted hover:text-text", onClick: onShowElsewhere, children: [_jsx(Icon, { icon: asksApproval ? Hand : MessageCircleQuestionMark, size: 16, className: "shrink-0 text-status-needs-you" }), _jsx("span", { className: "grow", children: t('composer.waitingBelow') }), _jsx(Icon, { icon: ArrowDown, size: 16, className: "shrink-0" })] })), active && !elsewhere && (_jsx("div", { hidden: !expanded, children: _jsx(PromptView, { subject: active, draft: session.draftOf(promptIdOf(active)), onDraft: (draft) => session.setDraft(promptIdOf(active), draft), onAction: (action) => {
59
60
  refocus.hold();
60
61
  void session
61
- .act(active, () => answerPrompt(active, action, actions))
62
+ .act(active, () => answerPrompt(active, action, actions, host))
62
63
  .then((result) => {
63
64
  if (result === 'failed') {
64
65
  refocus.release();
@@ -0,0 +1,3 @@
1
+ export declare function RowTime({ at }: {
2
+ at: number;
3
+ }): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Tooltip } from '@adecore/ui';
3
+ import { formatClock, formatDateTime, useFormatLocale } from '@adecore/ui/format';
4
+ import { TIMESTAMP_FORMAT } from './timestamp-format.js';
5
+ /*
6
+ * The wall-clock time of a row at its right edge, shown only while an element marked `group/line`
7
+ * around it is hovered or has the keyboard focus. Hidden, it takes no room and is no tab stop.
8
+ */
9
+ export function RowTime({ at }) {
10
+ useFormatLocale();
11
+ return (_jsx(Tooltip, { label: formatDateTime(at, TIMESTAMP_FORMAT), children: _jsx("time", { dateTime: new Date(at).toISOString(), className: "hidden shrink-0 text-xs text-text-faint tabular-nums select-none group-hover/line:inline group-focus-visible/line:inline group-has-focus-visible/line:inline", children: formatClock(at) }) }));
12
+ }