@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
@@ -32,6 +32,82 @@ export function toolSummary(name: string, input: unknown): string {
32
32
  return '';
33
33
  }
34
34
 
35
+ // The input keys whose value is a file a call is about.
36
+ const PATH_KEYS = new Set(['file_path', 'notebook_path']);
37
+
38
+ /* Whether the one line of a call is the path of a file, which a row cuts in the middle so the name stays. */
39
+ export function toolSummaryIsPath(name: string, input: unknown): boolean {
40
+ const known = toolEntry(name);
41
+ if (known === undefined || !isRecord(input)) {
42
+ return false;
43
+ }
44
+ const key = known.summary.find((candidate) => str(input[candidate]) !== null);
45
+ return key !== undefined && PATH_KEYS.has(key);
46
+ }
47
+
48
+ /* The command of a shell call, as it was run; null for any other call or one without a command. */
49
+ export function shellCommand(name: string, input: unknown): string | null {
50
+ if (toolEntry(name)?.shell !== true || !isRecord(input)) {
51
+ return null;
52
+ }
53
+ const command = str(input.command);
54
+ return command === null || command.trim() === '' ? null : command;
55
+ }
56
+
57
+ /* What a call was asked to do, as the lines its opened row shows. */
58
+ export interface ToolCallLines {
59
+ /* The command of a shell call, whole. */
60
+ readonly command: string | null;
61
+ /* The rest of the input as key and value: a string as it is, anything else as compact JSON. */
62
+ readonly args: ReadonlyArray<readonly [key: string, value: string]>;
63
+ /* An input that is not an object, as text. */
64
+ readonly text: string | null;
65
+ }
66
+
67
+ // A shell call's description is already the detail of its row.
68
+ const SHELL_SHOWN = new Set(['command', 'description']);
69
+
70
+ function valueText(value: unknown): string {
71
+ // An empty string can be the point of a call (clearing a field), so it shows as one.
72
+ return typeof value === 'string' && value !== '' ? value : (JSON.stringify(value) ?? String(value));
73
+ }
74
+
75
+ /* The input of a call as lines: a shell command as itself, every other key as `key value`, null included. */
76
+ export function toolCallLines(name: string, input: unknown): ToolCallLines {
77
+ if (!isRecord(input)) {
78
+ const text = input === null || input === undefined || input === '' ? null : valueText(input);
79
+ return { command: null, args: [], text };
80
+ }
81
+ const command = shellCommand(name, input);
82
+ const args = Object.entries(input)
83
+ .filter(([key, value]) => value !== undefined && !(command !== null && SHELL_SHOWN.has(key)))
84
+ .map(([key, value]) => [key, valueText(value)] as const);
85
+ return { command, args, text: null };
86
+ }
87
+
88
+ /* A failed call's output with the exit code its tool put on the first line ("Exit code 1"), as Claude Code's shell does. */
89
+ export function exitCodeOf(output: string): { code: number | null; rest: string } {
90
+ const match = /^Exit code (-?\d+)[ \t]*(?:\r?\n|$)/.exec(output);
91
+ if (match === null || Number(match[1]) === 0) {
92
+ return { code: null, rest: output };
93
+ }
94
+ return { code: Number(match[1]), rest: output.slice(match[0].length) };
95
+ }
96
+
97
+ /* Whether an output says anything; whitespace alone does not. */
98
+ export function hasOutput(output: string | null): output is string {
99
+ return output !== null && output.trim() !== '';
100
+ }
101
+
102
+ /* Whether opening a call shows anything: a diff, the input it was given or what it answered. */
103
+ export function hasToolBody(tool: ChatToolItem): boolean {
104
+ if (hasFileChanges(tool) || hasOutput(tool.output)) {
105
+ return true;
106
+ }
107
+ const lines = toolCallLines(tool.name, tool.input);
108
+ return lines.command !== null || lines.args.length > 0 || lines.text !== null;
109
+ }
110
+
35
111
  // What `GET /fs/file` will serve; a path with another suffix is not worth asking the host about.
36
112
  const IMAGE_SUFFIXES = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'];
37
113
 
@@ -1,6 +1,8 @@
1
1
  import { chatStorageKey, chatStorageLegacyKeys, createChatStore, onChatStorageConfiguration } from '../storage';
2
2
  import type { AgentKind, ChatPreferencesPayload, ModelSelection, ProviderInfo, ProviderAccounts, RuntimeMode } from '@adecore/agent-contracts';
3
3
  import { persistedJson } from './persisted-json';
4
+ import { DEFAULT_READING_WIDTH, isChatReadingWidth, type ChatReadingWidth } from './reading-width';
5
+ import { DEFAULT_SEND_KEY, isChatSendKey, type ChatSendKey } from './send-key';
4
6
 
5
7
  const storageKey = (): string => chatStorageKey('preferences');
6
8
 
@@ -14,6 +16,10 @@ export interface ChatPreferences {
14
16
  terminalRuntimeMode: RuntimeMode;
15
17
  /* Per machine, the account a new agent of each CLI starts under; an account id only means something on its own machine. */
16
18
  accountByMachine: Record<string, Partial<Record<AgentKind, string>>>;
19
+ /* How wide a chat of its own reads. It never reaches a machine. */
20
+ readingWidth: ChatReadingWidth;
21
+ /* Which key sends from the composer. It never reaches a machine either. */
22
+ sendKey: ChatSendKey;
17
23
  /* When a person last changed any of it; what settles whose pick a machine uses when several clients told it one. */
18
24
  changedAt: number;
19
25
  }
@@ -24,6 +30,8 @@ export const DEFAULT_CHAT_PREFERENCES: ChatPreferences = {
24
30
  runtimeMode: 'full-access',
25
31
  terminalRuntimeMode: 'full-access',
26
32
  accountByMachine: {},
33
+ readingWidth: DEFAULT_READING_WIDTH,
34
+ sendKey: DEFAULT_SEND_KEY,
27
35
  changedAt: 0
28
36
  };
29
37
 
@@ -44,6 +52,8 @@ export function parseChatPreferences(raw: string | null): ChatPreferences {
44
52
  runtimeMode: stored.runtimeMode ?? DEFAULT_CHAT_PREFERENCES.runtimeMode,
45
53
  terminalRuntimeMode: stored.terminalRuntimeMode ?? DEFAULT_CHAT_PREFERENCES.terminalRuntimeMode,
46
54
  accountByMachine: { ...stored.accountByMachine },
55
+ readingWidth: isChatReadingWidth(stored.readingWidth) ? stored.readingWidth : DEFAULT_READING_WIDTH,
56
+ sendKey: isChatSendKey(stored.sendKey) ? stored.sendKey : DEFAULT_SEND_KEY,
47
57
  changedAt: typeof stored.changedAt === 'number' ? stored.changedAt : 0
48
58
  };
49
59
  } catch {
@@ -142,10 +152,13 @@ export function reloadChatPreferences(): void {
142
152
  useChatPreferences.setState(storage.read(), true);
143
153
  }
144
154
 
155
+ function store(preferences: ChatPreferences): void {
156
+ useChatPreferences.setState(preferences);
157
+ storage.write(preferences);
158
+ }
159
+
145
160
  function write(preferences: ChatPreferences): void {
146
- const next = { ...preferences, changedAt: Date.now() };
147
- useChatPreferences.setState(next);
148
- storage.write(next);
161
+ store({ ...preferences, changedAt: Date.now() });
149
162
  }
150
163
 
151
164
  /* The composer writes here on every change and the settings dialog too; both edit the same remembered default. */
@@ -153,6 +166,16 @@ export function rememberChatPreferences(patch: Partial<ChatPreferences>): void {
153
166
  write({ ...useChatPreferences.getState(), ...patch });
154
167
  }
155
168
 
169
+ /* The reading width leaves `changedAt` alone: it is no pick a machine weighs against another client's. */
170
+ export function rememberReadingWidth(readingWidth: ChatReadingWidth): void {
171
+ store({ ...useChatPreferences.getState(), readingWidth });
172
+ }
173
+
174
+ /* Leaves `changedAt` alone, like the reading width. */
175
+ export function rememberSendKey(sendKey: ChatSendKey): void {
176
+ store({ ...useChatPreferences.getState(), sendKey });
177
+ }
178
+
156
179
  export function rememberChatSelection(provider: AgentKind, selection: ModelSelection): void {
157
180
  const current = useChatPreferences.getState();
158
181
  const previous = current.selectionByProvider[provider];
@@ -0,0 +1,39 @@
1
+ import type { CSSProperties } from 'react';
2
+ import i18next from 'i18next';
3
+
4
+ /* How wide a chat that is a view of its own lets its thread and composer run. A node on a canvas is narrower than any of them. */
5
+ export type ChatReadingWidth = 'narrow' | 'comfortable' | 'full';
6
+
7
+ export const CHAT_READING_WIDTHS: readonly ChatReadingWidth[] = ['narrow', 'comfortable', 'full'];
8
+
9
+ export const DEFAULT_READING_WIDTH: ChatReadingWidth = 'comfortable';
10
+
11
+ // About 75 and 90 characters of the thread's text a line.
12
+ const MEASURES: Readonly<Record<ChatReadingWidth, number>> = {
13
+ narrow: 640,
14
+ comfortable: 768,
15
+ full: Number.POSITIVE_INFINITY
16
+ };
17
+
18
+ export function isChatReadingWidth(value: unknown): value is ChatReadingWidth {
19
+ return typeof value === 'string' && (CHAT_READING_WIDTHS as readonly string[]).includes(value);
20
+ }
21
+
22
+ /* The widest the content column runs, in pixels; `full` is unbounded. */
23
+ export function readingMeasure(width: ChatReadingWidth): number {
24
+ return MEASURES[width];
25
+ }
26
+
27
+ /* What `.chat-column-content` and `.chat-composer-content` read their width from. */
28
+ export function readingWidthStyle(width: ChatReadingWidth): CSSProperties {
29
+ const measure = MEASURES[width];
30
+ return { '--chat-measure': Number.isFinite(measure) ? `${measure}px` : '100%' } as CSSProperties;
31
+ }
32
+
33
+ export function readingWidthLabel(width: ChatReadingWidth): string {
34
+ return i18next.t(`agent-chat:readingWidth.${width}.label`);
35
+ }
36
+
37
+ export function readingWidthHint(width: ChatReadingWidth): string {
38
+ return i18next.t(`agent-chat:readingWidth.${width}.hint`);
39
+ }
@@ -0,0 +1,38 @@
1
+ import i18next from 'i18next';
2
+ import { KEY_SHORTCUTS, shortcut, type Shortcut } from '@adecore/ui';
3
+
4
+ /*
5
+ * Which key sends a message: Enter, Enter until the message runs over more than one line and Mod+Enter
6
+ * after that, or Mod+Enter always. Where Enter does not send, it adds a line.
7
+ */
8
+ export type ChatSendKey = 'enter' | 'mod-enter-multiline' | 'mod-enter';
9
+
10
+ export const CHAT_SEND_KEYS: readonly ChatSendKey[] = ['enter', 'mod-enter-multiline', 'mod-enter'];
11
+
12
+ export const DEFAULT_SEND_KEY: ChatSendKey = 'enter';
13
+
14
+ /* Sends, then opens a new chat through the host's `openNewChat`; a key of the composer, for a menu or a list of keys to print. */
15
+ export const SEND_AND_NEW_SHORTCUT = /* @__PURE__ */ shortcut('Mod+Alt+Enter');
16
+
17
+ export function isChatSendKey(value: unknown): value is ChatSendKey {
18
+ return typeof value === 'string' && (CHAT_SEND_KEYS as readonly string[]).includes(value);
19
+ }
20
+
21
+ /* Whether Enter alone adds a line to a message, so only Mod+Enter sends it. */
22
+ export function modSends(sendKey: ChatSendKey, multiline: boolean): boolean {
23
+ return sendKey === 'mod-enter' || (sendKey === 'mod-enter-multiline' && multiline);
24
+ }
25
+
26
+ /* The key that sends a message, as a hint prints it. */
27
+ export function sendShortcut(sendKey: ChatSendKey, multiline: boolean): Shortcut {
28
+ return modSends(sendKey, multiline) ? KEY_SHORTCUTS.modEnter : KEY_SHORTCUTS.enter;
29
+ }
30
+
31
+ /* `apple` writes Mod as the Command sign, and as Ctrl elsewhere. */
32
+ export function sendKeyLabel(sendKey: ChatSendKey, apple: boolean): string {
33
+ return i18next.t(`agent-chat:sendKey.${sendKey}.label`, { mod: apple ? '⌘' : 'Ctrl' });
34
+ }
35
+
36
+ export function sendKeyHint(sendKey: ChatSendKey, apple: boolean): string {
37
+ return i18next.t(`agent-chat:sendKey.${sendKey}.hint`, { mod: apple ? '⌘' : 'Ctrl' });
38
+ }
@@ -5,6 +5,7 @@ import { Bookmark, Pencil, X } from 'lucide-react';
5
5
  import { CHAT_BOOKMARK_LIMITS, type ChatBookmark } from '@adecore/agent-contracts';
6
6
  import { nameBookmark, removeBookmark, useBookmarkNaming } from '../bookmarks';
7
7
  import { bookmarkLabel } from '../logic/bookmarks';
8
+ import { useChatHost } from '../../host';
8
9
  import { useChatScope } from '../../scope';
9
10
  import { ButtonGroup, Icon, IconButton, Input } from '@adecore/ui';
10
11
 
@@ -22,6 +23,7 @@ interface BookmarkMarkerProps {
22
23
  export function BookmarkMarker({ chatId, itemId, bookmark }: BookmarkMarkerProps) {
23
24
  const { t } = useTranslation('agent-chat');
24
25
  const scope = useChatScope();
26
+ const host = useChatHost();
25
27
  const chatKey = scope.keyOf(chatId);
26
28
  const naming = useBookmarkNaming((s) => s.chatKey === chatKey && s.itemId === itemId);
27
29
 
@@ -38,7 +40,7 @@ export function BookmarkMarker({ chatId, itemId, bookmark }: BookmarkMarkerProps
38
40
  onDone={(name) => {
39
41
  useBookmarkNaming.getState().close();
40
42
  if (name !== null) {
41
- void nameBookmark(scope, chatId, itemId, name, bookmark?.name);
43
+ void nameBookmark(scope, host, chatId, itemId, name, bookmark?.name);
42
44
  }
43
45
  }}
44
46
  />
@@ -54,7 +56,7 @@ export function BookmarkMarker({ chatId, itemId, bookmark }: BookmarkMarkerProps
54
56
  className="-ml-2 max-w-0 shrink-0 overflow-hidden opacity-0 group-hover/bookmark:ml-0 group-hover/bookmark:max-w-none group-hover/bookmark:opacity-100 group-has-focus-visible/bookmark:ml-0 group-has-focus-visible/bookmark:max-w-none group-has-focus-visible/bookmark:opacity-100"
55
57
  >
56
58
  <IconButton icon={Pencil} size="xs" label={t('bookmarks.rename')} onClick={() => useBookmarkNaming.getState().open(chatKey, itemId)} />
57
- <IconButton icon={X} size="xs" label={t('bookmarks.remove')} onClick={() => void removeBookmark(scope, chatId, bookmark)} />
59
+ <IconButton icon={X} size="xs" label={t('bookmarks.remove')} onClick={() => void removeBookmark(scope, host, chatId, bookmark)} />
58
60
  </ButtonGroup>
59
61
  )}
60
62
  </div>
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
4
4
  import { Bookmark, Pencil, X } from 'lucide-react';
5
5
  import { goToBookmark, removeBookmark } from '../bookmarks';
6
6
  import { bookmarkLabel, bookmarksInThreadOrder } from '../logic/bookmarks';
7
- import { chatHost } from '../../host';
7
+ import { useChatHost } from '../../host';
8
8
  import { useChatScope } from '../../scope';
9
9
  import { useChatRow } from '../../state/chats';
10
10
  import { Icon, Menu } from '@adecore/ui';
@@ -15,7 +15,8 @@ export function BookmarkSubmenu({ chatId }: { chatId: string }) {
15
15
  const scope = useChatScope();
16
16
  const bookmarks = useChatRow(chatId, (row) => row?.bookmarks);
17
17
  const order = useChatRow(chatId, (row) => row?.order);
18
- const place = chatHost().useChatPlace(chatId);
18
+ const host = useChatHost();
19
+ const place = host.useChatPlace(chatId);
19
20
  const sorted = useMemo(() => bookmarksInThreadOrder(bookmarks ?? [], order ?? []), [bookmarks, order]);
20
21
  if (sorted.length === 0) {
21
22
  return null;
@@ -40,7 +41,7 @@ export function BookmarkSubmenu({ chatId }: { chatId: string }) {
40
41
  <span className={clsx('min-w-0 grow truncate', bookmark.name === undefined && 'text-text-muted')}>{bookmarkLabel(bookmark)}</span>
41
42
  </Menu.Item>
42
43
  <Menu.RowAction icon={Pencil} label={t('bookmarks.rename')} onClick={() => go(bookmark.itemId, true)} />
43
- <Menu.RowAction icon={X} label={t('bookmarks.remove')} onClick={() => void removeBookmark(scope, chatId, bookmark)} />
44
+ <Menu.RowAction icon={X} label={t('bookmarks.remove')} onClick={() => void removeBookmark(scope, host, chatId, bookmark)} />
44
45
  </Menu.Row>
45
46
  ))}
46
47
  </Menu.Popup>
@@ -1,5 +1,6 @@
1
1
  import { useMemo, type ReactNode } from 'react';
2
2
  import clsx from 'clsx';
3
+ import { useShallow } from 'zustand/shallow';
3
4
  import { useTranslation } from 'react-i18next';
4
5
  import { Activity, MessageCircleQuestionMark, Square, SquareTerminal } from 'lucide-react';
5
6
  import type { ChatBackgroundTask, ChatSubagentItem } from '@adecore/agent-contracts';
@@ -11,7 +12,7 @@ import { SubagentStopButton } from './SubagentStopButton';
11
12
  import { formatElapsedShort } from '@adecore/ui/format';
12
13
  import { Icon, IconButton, Popover, Surface, useNow } from '@adecore/ui';
13
14
  import { useChatRow, useChats } from '../../state/chats';
14
- import { chatHost } from '../../host';
15
+ import { useChatHost } from '../../host';
15
16
  import { useChatScope } from '../../scope';
16
17
  import { useChatActions } from '../actions';
17
18
  import { statusLookOf, type StatusWord } from '../../agents/status-look';
@@ -24,23 +25,24 @@ const ROW_BODY = 'flex min-h-9 min-w-0 grow items-center gap-2.5 rounded-md px-2
24
25
 
25
26
  /* What the chat keeps working on beside the thread, in one small line over the composer: its sub-agents and what runs in the background. */
26
27
  export function ChatActivity({ chatId }: { chatId: string }) {
28
+ const host = useChatHost();
27
29
  const { t } = useTranslation('agent-chat');
28
30
  const order = useChatRow(chatId, (row) => row?.order) ?? NO_ITEMS;
29
31
  const structure = useChatRow(chatId, (row) => row?.structure);
30
32
  const subagents = useMemo(() => (structure === undefined ? [] : flyoutSubagents(order, structure)), [order, structure]);
31
33
  const { id, keyOf } = useChatScope();
32
- const tasks = chatHost().tasks.useTasks(id);
33
- const statuses = useChats((s) => s.statusByKey);
34
+ const tasks = host.tasks.useTasks(id);
35
+ // The infos of these sub-agents' own chats, so the info of any other chat changing does not draw this again.
36
+ const children = useChats(useShallow((s) => subagents.map((item) => (item.childId === undefined ? undefined : s.statusByKey[keyOf(item.childId)]?.info))));
34
37
  const progress = useMemo(
35
38
  () =>
36
39
  progressOf(
37
- subagents.map((item) => {
40
+ subagents.map((item, index) => {
38
41
  const taskId = taskIdOf(item);
39
- const child = item.childId === undefined ? undefined : statuses[keyOf(item.childId)]?.info;
40
- return taskRowStateOf(item, taskId === null ? null : (tasks?.[taskId] ?? null), child);
42
+ return taskRowStateOf(item, taskId === null ? null : (tasks?.[taskId] ?? null), children[index]);
41
43
  })
42
44
  ),
43
- [subagents, tasks, statuses, keyOf]
45
+ [subagents, tasks, children]
44
46
  );
45
47
  const words = useMemo(
46
48
  () =>
@@ -55,7 +57,7 @@ export function ChatActivity({ chatId }: { chatId: string }) {
55
57
  return null;
56
58
  }
57
59
  const { shells, monitors } = backgroundCounts(background);
58
- const Slot = chatHost().SubagentSlot;
60
+ const Slot = host.SubagentSlot;
59
61
  const counting = progress.active > 0 && progress.total > 1;
60
62
  const label = counting
61
63
  ? [t('activity.progress', { done: progress.done, total: progress.total }), progress.waiting > 0 && t('activity.needsYou', { count: progress.waiting })]
@@ -171,10 +173,11 @@ function SubagentRows({ chatId, items }: { chatId: string; items: readonly ChatS
171
173
  }
172
174
 
173
175
  function SubagentEntry({ chatId, item, now }: { chatId: string; item: ChatSubagentItem; now: number }) {
176
+ const host = useChatHost();
174
177
  const { t } = useTranslation('agent-chat');
175
178
  const { id, keyOf } = useChatScope();
176
179
  const refused = useSubagentSupport((s) => s.unsupported[id] === true);
177
- const task = chatHost().tasks.useTask(id, taskIdOf(item));
180
+ const task = host.tasks.useTask(id, taskIdOf(item));
178
181
  const { show } = useSubagentTrail(chatId);
179
182
  const child = useChats((s) => (item.childId === undefined ? undefined : s.statusByKey[keyOf(item.childId)]?.info));
180
183
  const word = statusWordOf(item, task);
@@ -199,6 +202,7 @@ function SubagentEntry({ chatId, item, now }: { chatId: string; item: ChatSubage
199
202
 
200
203
  /* Only drawn while the flyout is open, so the clock ticks for nobody otherwise. */
201
204
  function BackgroundTaskRows({ chatId, tasks }: { chatId: string; tasks: readonly ChatBackgroundTask[] }) {
205
+ const host = useChatHost();
202
206
  const { t } = useTranslation('agent-chat');
203
207
  const now = useNow(1000);
204
208
  const actions = useChatActions();
@@ -206,7 +210,7 @@ function BackgroundTaskRows({ chatId, tasks }: { chatId: string; tasks: readonly
206
210
  function stop(taskId: string): void {
207
211
  void actions.stopTask(chatId, taskId).catch((e: unknown) => {
208
212
  const description = e instanceof Error ? e.message : t('subagents.noAnswer');
209
- chatHost().notify({ kind: 'error', title: t('activity.stopFailed'), description });
213
+ host.notify({ kind: 'error', title: t('activity.stopFailed'), description });
210
214
  });
211
215
  }
212
216
 
@@ -1,16 +1,15 @@
1
1
  import clsx from 'clsx';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { MessageSquare, X } from 'lucide-react';
4
- import { chatHost } from '../../host';
4
+ import { useChatHost } from '../../host';
5
5
  import { CHIP_IN_MESSAGE, MENTION_TONE } from './chips';
6
6
  import { Icon, Tooltip } from '@adecore/ui';
7
7
 
8
8
  /* A chat attached to a message, under the title it has now rather than the one it had when it was attached. */
9
9
  export function ChatReferenceChip({ chatId, onRemove }: { chatId: string; onRemove?: () => void }) {
10
+ const host = useChatHost();
10
11
  const { t } = useTranslation('agent-chat');
11
- const chat = chatHost()
12
- .useReferableChats()
13
- .find((candidate) => candidate.id === chatId);
12
+ const chat = host.useReferableChats().find((candidate) => candidate.id === chatId);
14
13
  const title = chat === undefined ? t('chatReference.gone') : chat.title || t('chatReference.untitled');
15
14
  return (
16
15
  <span className={clsx(MENTION_TONE, CHIP_IN_MESSAGE)}>
@@ -1,119 +1,23 @@
1
- import { Fragment, memo, useContext, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
1
+ import { Fragment, memo, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { Check, Copy } from 'lucide-react';
5
5
  import { ButtonGroup, IconButton } from '@adecore/ui';
6
- import type { BundledLanguage, BundledTheme, createHighlighter, ThemeRegistration } from 'shiki/bundle/web';
7
- import { IncrementalLines, type CodeToken, type Tokenize } from './code-lines';
6
+ import { IncrementalLines, type CodeToken } from './code-lines';
8
7
  import { CodeStreamingContext } from './code-streaming';
9
8
  import { WHOLE_FADE_CLASS } from './rehype-fade';
10
- import { shikiThemeOf, useCodeTheme } from './code-theme';
9
+ import { useChatHost } from '../../host';
10
+ import { useCodeTheme } from './code-theme';
11
+ import { tokenStyle, useCodeHighlighter } from './highlighter';
11
12
 
12
- type Highlighter = Awaited<ReturnType<typeof createHighlighter>>;
13
- type GrammarState = ReturnType<Highlighter['getLastGrammarState']>;
14
- // A plain language needs no grammar; shiki draws it in the theme's own foreground.
15
- const PLAIN = 'text';
16
13
  const COPIED_MS = 1500;
17
14
 
18
- let highlighter: Highlighter | null = null;
19
- let highlighterLoad: Promise<Highlighter> | null = null;
20
- const loadedLanguages = new Set<string>([PLAIN]);
21
- const languageLoads = new Map<string, Promise<void>>();
22
- const loadedThemes = new Set<string>();
23
- const themeLoads = new Map<string, Promise<void>>();
24
- const tokenizers = new Map<string, { tokenize: Tokenize<GrammarState>; fg: string | undefined }>();
25
-
26
- // Shiki loads on the first code block, not with the app; the web bundle covers the languages a coding agent writes.
27
- function loadHighlighter(): Promise<Highlighter> {
28
- highlighterLoad ??= import('shiki/bundle/web').then(async ({ createHighlighter }) => {
29
- highlighter = await createHighlighter({ themes: [], langs: [] });
30
- return highlighter;
31
- });
32
- return highlighterLoad;
33
- }
34
-
35
- function resolveLanguage(lang: string, known: Record<string, unknown> | null): string {
36
- return known !== null && lang in known ? lang : PLAIN;
37
- }
38
-
39
- let bundled: Record<string, unknown> | null = null;
40
-
41
- /* Loads the highlighter and the grammar of one language, once each. */
42
- function loadLanguage(lang: string): Promise<void> {
43
- let load = languageLoads.get(lang);
44
- if (!load) {
45
- load = Promise.all([loadHighlighter(), import('shiki/bundle/web')]).then(async ([loaded, { bundledLanguages }]) => {
46
- bundled = bundledLanguages;
47
- const language = resolveLanguage(lang, bundledLanguages);
48
- if (!loadedLanguages.has(language)) {
49
- await loaded.loadLanguage(language as BundledLanguage);
50
- loadedLanguages.add(language);
51
- }
52
- });
53
- languageLoads.set(lang, load);
54
- }
55
- return load;
56
- }
57
-
58
- /* Loads a theme into the highlighter, once. */
59
- function loadTheme(theme: string): Promise<void> {
60
- let load = themeLoads.get(theme);
61
- if (!load) {
62
- load = loadHighlighter().then(async (loaded) => {
63
- await loaded.loadTheme(shikiThemeOf(theme) as BundledTheme | ThemeRegistration);
64
- loadedThemes.add(theme);
65
- });
66
- themeLoads.set(theme, load);
67
- }
68
- return load;
69
- }
70
-
71
- /* The tokenizer for a language in a theme, or null while shiki, the grammar or the theme is still on its way. */
72
- function tokenizerFor(lang: string, theme: string): { tokenize: Tokenize<GrammarState>; fg: string | undefined } | null {
73
- if (highlighter === null || bundled === null || !languageLoads.has(lang) || !loadedThemes.has(theme)) {
74
- return null;
75
- }
76
- const language = resolveLanguage(lang, bundled);
77
- if (!loadedLanguages.has(language)) {
78
- return null;
79
- }
80
- const key = `${language}:${theme}`;
81
- let entry = tokenizers.get(key);
82
- if (!entry) {
83
- const loaded = highlighter;
84
- const options = { lang: language as BundledLanguage, theme };
85
- entry = {
86
- tokenize: (code, state) => {
87
- const result = loaded.codeToTokens(code, state ? { ...options, grammarState: state } : options);
88
- return { lines: result.tokens, state: result.grammarState };
89
- },
90
- fg: loaded.getTheme(theme).fg
91
- };
92
- tokenizers.set(key, entry);
93
- }
94
- return entry;
95
- }
96
-
97
- function styleOf(token: CodeToken): CSSProperties | undefined {
98
- const style = token.fontStyle ?? 0;
99
- if (token.color === undefined && style <= 0) {
100
- return undefined;
101
- }
102
- const decorations = [style & 4 ? 'underline' : '', style & 8 ? 'line-through' : ''].filter(Boolean).join(' ');
103
- return {
104
- color: token.color,
105
- ...(style & 1 ? { fontStyle: 'italic' } : {}),
106
- ...(style & 2 ? { fontWeight: 'bold' } : {}),
107
- ...(decorations ? { textDecoration: decorations } : {})
108
- };
109
- }
110
-
111
15
  /* A line whose tokens did not change keeps its elements, so a selection in it survives the next delta. */
112
16
  const CodeLine = memo(function CodeLine({ tokens }: { tokens: CodeToken[] }) {
113
17
  return (
114
18
  <span className="line">
115
19
  {tokens.map((token, index) => (
116
- <span key={index} style={styleOf(token)}>
20
+ <span key={index} style={tokenStyle(token)}>
117
21
  {token.content}
118
22
  </span>
119
23
  ))}
@@ -124,17 +28,20 @@ const CodeLine = memo(function CodeLine({ tokens }: { tokens: CodeToken[] }) {
124
28
  /*
125
29
  * A fenced block, highlighted a line at a time. Until shiki and the grammar are there it holds its
126
30
  * place invisibly, so bare code never turns colored. An open fence is tokenized as it grows, and
127
- * closing it does not draw it again.
31
+ * closing it does not draw it again. `view` takes the place of the code, such as what the code draws,
32
+ * and Copy still copies the code.
128
33
  */
129
- export function CodeBlock({ code, lang, actions }: { code: string; lang: string; actions?: ReactNode }) {
34
+ export function CodeBlock({ code, lang, actions, view }: { code: string; lang: string; actions?: ReactNode; view?: ReactNode }) {
130
35
  const { t } = useTranslation('agent-chat');
131
36
  const theme = useCodeTheme();
37
+ const { custom } = useChatHost().code;
38
+ const highlighter = useCodeHighlighter();
132
39
  const streaming = useContext(CodeStreamingContext);
133
40
  const [copyState, setCopyState] = useState<'idle' | 'copying' | 'copied' | 'failed'>('idle');
134
41
  const copying = useRef(false);
135
42
  // What loaded last, so a block whose theme changes draws again once the new one is in.
136
43
  const [loaded, setLoaded] = useState<string | null>(null);
137
- const tokenizer = tokenizerFor(lang, theme);
44
+ const tokenizer = highlighter.tokenizer(lang, theme);
138
45
  // Only a block that had to wait fades in; one drawn highlighted from its first frame just stands there.
139
46
  const [waited] = useState(tokenizer === null);
140
47
  const lines = useMemo(() => (tokenizer ? new IncrementalLines(tokenizer.tokenize) : null), [tokenizer]);
@@ -170,34 +77,36 @@ export function CodeBlock({ code, lang, actions }: { code: string; lang: string;
170
77
  return;
171
78
  }
172
79
  let cancelled = false;
173
- Promise.all([loadLanguage(lang), loadTheme(theme)])
80
+ Promise.all([highlighter.loadLanguage(lang), highlighter.loadTheme(theme, custom)])
174
81
  .then(() => !cancelled && setLoaded(`${lang}:${theme}`))
175
82
  .catch(() => !cancelled && setLoaded('failed'));
176
83
  return () => {
177
84
  cancelled = true;
178
85
  };
179
- }, [lang, theme, tokenizer]);
86
+ }, [highlighter, lang, theme, custom, tokenizer]);
180
87
 
181
88
  const pending = (tokenizer === null || tokens === null) && loaded !== 'failed';
182
89
  return (
183
90
  <div className="chat-code" data-copy-state={copyState}>
184
- <pre
185
- className={clsx(pending && 'invisible', !pending && tokens !== null && waited && WHOLE_FADE_CLASS)}
186
- aria-hidden={pending || undefined}
187
- style={{ color: tokenizer?.fg }}
188
- >
189
- <code>
190
- {tokens === null
191
- ? code
192
- : tokens.map((line, index) => (
193
- // Lines only ever grow at the end, so the place of a line is a stable key.
194
- <Fragment key={index}>
195
- <CodeLine tokens={line} />
196
- {index < tokens.length - 1 && '\n'}
197
- </Fragment>
198
- ))}
199
- </code>
200
- </pre>
91
+ {view ?? (
92
+ <pre
93
+ className={clsx(pending && 'invisible', !pending && tokens !== null && waited && WHOLE_FADE_CLASS)}
94
+ aria-hidden={pending || undefined}
95
+ style={{ color: tokenizer?.fg }}
96
+ >
97
+ <code>
98
+ {tokens === null
99
+ ? code
100
+ : tokens.map((line, index) => (
101
+ // Lines only ever grow at the end, so the place of a line is a stable key.
102
+ <Fragment key={index}>
103
+ <CodeLine tokens={line} />
104
+ {index < tokens.length - 1 && '\n'}
105
+ </Fragment>
106
+ ))}
107
+ </code>
108
+ </pre>
109
+ )}
201
110
  <ButtonGroup className="chat-code-actions" role="group" aria-label={t('code.actions')}>
202
111
  <IconButton
203
112
  icon={copyState === 'copied' ? Check : Copy}