@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
@@ -1,4 +1,16 @@
1
- import { type CSSProperties, type RefObject, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react';
1
+ import {
2
+ type CSSProperties,
3
+ memo,
4
+ type RefObject,
5
+ useCallback,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useReducer,
10
+ useRef,
11
+ useState,
12
+ type ReactNode
13
+ } from 'react';
2
14
  import { useTranslation } from 'react-i18next';
3
15
  import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';
4
16
  import clsx from 'clsx';
@@ -21,6 +33,8 @@ import {
21
33
  type TurnTarget
22
34
  } from '../timeline-scroll';
23
35
  import { useTimelineFlash } from '../timeline-flash';
36
+ import { useChatPreferences } from '../preferences';
37
+ import { readingMeasure, readingWidthStyle } from '../reading-width';
24
38
  import {
25
39
  SCRUBBER_MIN_TICKS,
26
40
  STRIP_INSET_PX,
@@ -55,11 +69,11 @@ import { QuoteButton } from './QuoteButton';
55
69
  import { QuoteTakerContext, type QuoteTaker } from './quote-selection';
56
70
  import { useToggleSet } from './useToggleSet';
57
71
  import { WelcomeGreeting } from './Welcome';
58
- import { Row } from './rows/Rows';
72
+ import { Row, type RowProps } from './rows/Rows';
59
73
  import { RowContainer } from './rows/RowContainer';
60
74
  import { ReplyHeader } from './rows/MessageRows';
61
75
  import { useChatRow, useChats } from '../../state/chats';
62
- import { chatHost } from '../../host';
76
+ import { useChatHost, type TimelineFindOptions } from '../../host';
63
77
  import { useChatScope } from '../../scope';
64
78
  import { FileLinkContext } from './file-links';
65
79
  import { AgentIcon } from '../../agents/AgentIcon';
@@ -81,11 +95,12 @@ function showRowAtTop(virtualizer: Virtualizer<HTMLDivElement, Element>, followR
81
95
  * and the folder it works in, so a first question can lean on all three.
82
96
  */
83
97
  function EmptyThread({ chatId }: { chatId: string }) {
98
+ const host = useChatHost();
84
99
  const { t } = useTranslation('agent-chat');
85
100
  const info = useChatRow(chatId, (row) => row?.info ?? null);
86
101
  const model = useModelName(info?.provider, info?.selection.model ?? '');
87
- const sources = chatHost().useContextSources(chatId);
88
- const hidesFolder = chatHost().useHidesFolder(chatId);
102
+ const sources = host.useContextSources(chatId);
103
+ const hidesFolder = host.useHidesFolder(chatId);
89
104
  return (
90
105
  <div className="flex grow items-center justify-center">
91
106
  <div className="flex max-w-sm flex-col items-center gap-3 px-6 py-8 text-center">
@@ -133,11 +148,42 @@ function MarkableRow({ row, chatId, bookmark, children }: { row: TimelineRow; ch
133
148
  );
134
149
  }
135
150
 
151
+ interface ThreadRowProps extends Omit<RowProps, 'openConversation'> {
152
+ previous: TimelineRow | null;
153
+ index: number;
154
+ top: number;
155
+ measure: (element: HTMLDivElement | null) => void;
156
+ /* The nonce of the flash on this row, or null when it has none. */
157
+ flashNonce: number | null;
158
+ bookmark: ChatBookmark | null;
159
+ openConversation(step: SubagentStep): void;
160
+ }
161
+
162
+ /* Memoized with stable props, so a streamed word or a scroll draws only the rows it changed. */
163
+ const ThreadRow = memo(function ThreadRow({ row, previous, index, top, measure, chatId, flashNonce, bookmark, ...actions }: ThreadRowProps) {
164
+ const header = replyHeader(row, previous);
165
+ return (
166
+ <RowContainer row={row} previous={previous} index={index} top={top} ref={measure}>
167
+ {flashNonce !== null && <span key={flashNonce} className="chat-flash pointer-events-none absolute inset-0 rounded-lg" aria-hidden />}
168
+ {header !== null && (
169
+ <div className="chat-column-content w-full">
170
+ <ReplyHeader chatId={chatId} at={header.at} />
171
+ </div>
172
+ )}
173
+ <MarkableRow row={row} chatId={chatId} bookmark={bookmark}>
174
+ <Row row={row} chatId={chatId} {...actions} />
175
+ </MarkableRow>
176
+ </RowContainer>
177
+ );
178
+ });
179
+
136
180
  /*
137
181
  * `overlay` stands in the thread's place and ends where the composer starts, so the thread keeps
138
182
  * where it was underneath and the composer stays in reach for the prompts it answers.
139
183
  */
140
184
  export function Timeline({ chatId, composer, overlay }: { chatId: string; composer?: ReactNode; overlay?: ReactNode }) {
185
+ const host = useChatHost();
186
+ const { fork } = host;
141
187
  const { t } = useTranslation('agent-chat');
142
188
  const order = useChatRow(chatId, (row) => row?.order);
143
189
  // The structure, not the items. A delta growing a reply must not derive every row again.
@@ -209,15 +255,15 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
209
255
  { expandedGroups: groups.ids, expandedTurns: turns.ids, expandedSubagents: subagents.ids, activeTurnId, forkedTurns }
210
256
  );
211
257
  }, [order, items, groups.ids, turns.ids, subagents.ids, activeTurnId, forkedTurns]);
212
- const cards = chatHost().useThreadCards(scopeId, chatId);
213
- const drawsVisuals = chatHost().visuals !== null;
258
+ const cards = host.useThreadCards(scopeId, chatId);
259
+ const drawsVisuals = host.visuals !== null;
214
260
  // Where the part of the thread this client holds begins, while there is a page before it.
215
261
  const heldFrom = cursor === null ? null : (items?.[order?.[0] ?? '']?.createdAt ?? null);
216
262
  const rows = useMemo(
217
263
  () => withTimedRows(threadRows, timedRowsOf(cards, drawsVisuals ? (visuals ?? []) : null, heldFrom)),
218
264
  [threadRows, cards, drawsVisuals, visuals, heldFrom]
219
265
  );
220
- const welcomes = chatHost().useWelcome(chatId);
266
+ const welcomes = host.useWelcome(chatId);
221
267
 
222
268
  const empty = rows.length === 0;
223
269
  // A chat that took a turn before is one whose thread has not come in yet, not one to greet.
@@ -308,17 +354,11 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
308
354
  scrollPaddingEnd: coveredHeight
309
355
  });
310
356
 
311
- // A change in what the last row says (a streaming delta) must also pull the view down.
312
- const lastRow = rows[rows.length - 1];
313
- const lastReplyLength = useChatRow(chatId, (row) => {
314
- const item = lastRow?.kind === 'assistant' ? row?.items[lastRow.id] : undefined;
315
- return item?.kind === 'assistant' ? item.text.length : null;
316
- });
317
- const tail = lastReplyLength ?? rows.length;
318
357
  /*
319
- * Rows are measured as they render, so the end moves while it is drawn (a diff that highlights, an
320
- * image that loads). `scrollToIndex` aims at where a row was when asked and fell short; the
321
- * scroller's own bottom is a fact, and a new total size says a row was measured again.
358
+ * Rows are measured as they render, so the end moves while it is drawn (a streamed line, a diff that
359
+ * highlights, an image that loads). `scrollToIndex` aims at where a row was when asked and fell
360
+ * short; the scroller's own bottom is a fact, and a new total size says a row was measured again.
361
+ * A word that does not add a line moves nothing, so the thread does not listen to the words.
322
362
  */
323
363
  const totalSize = virtualizer.getTotalSize();
324
364
  useLayoutEffect(() => {
@@ -329,7 +369,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
329
369
  // The composer is in the scroll flow, so its measured height is part of this end position.
330
370
  element.scrollTop = element.scrollHeight;
331
371
  scrollGeometry.current = { top: element.scrollTop, height: element.scrollHeight, viewport: element.clientHeight };
332
- }, [rows.length, tail, totalSize, composerHeight, frame.height]);
372
+ }, [rows.length, totalSize, composerHeight, frame.height]);
333
373
 
334
374
  // A page that went in above moves every row down by what it measures; the reader stays on the row they were on.
335
375
  useLayoutEffect(() => {
@@ -364,7 +404,8 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
364
404
  const onMainAgent = trail.length === 0;
365
405
  // A node on a canvas has no strip. It is too narrow to give up a column of its width.
366
406
  const showsScrubber = frame.column && onMainAgent && ticks.length >= SCRUBBER_MIN_TICKS;
367
- const paddingLeft = threadPaddingLeft(frame.width, showsScrubber);
407
+ const readingWidth = useChatPreferences((preferences) => preferences.readingWidth);
408
+ const paddingLeft = threadPaddingLeft(frame.width, showsScrubber, readingMeasure(readingWidth));
368
409
 
369
410
  /*
370
411
  * Where each message starts, from the virtualizer's measurements (an estimate for a row it never
@@ -375,35 +416,59 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
375
416
  const scrollTop = virtualizer.scrollOffset ?? 0;
376
417
  const viewport = virtualizer.scrollRect?.height ?? 0;
377
418
  const visibleHeight = Math.max(0, viewport - coveredHeight);
419
+ const visibleHeightRef = useRef(visibleHeight);
420
+ visibleHeightRef.current = visibleHeight;
378
421
  const inView = showsScrubber ? messagesInView({ starts, scrollTop, visibleHeight }) : null;
422
+ /*
423
+ * The virtualizer draws again only when its range of rows changes, which a scroll inside one long
424
+ * row never does; the scroll itself redraws the strip once the messages in view are others.
425
+ */
426
+ const [, redrawStrip] = useReducer((count: number) => count + 1, 0);
427
+ const stripRef = useRef({ starts, inView });
428
+ stripRef.current = { starts, inView };
429
+ const followStrip = (element: HTMLElement): void => {
430
+ const { starts: current, inView: shown } = stripRef.current;
431
+ if (shown === null) {
432
+ return;
433
+ }
434
+ const next = messagesInView({ starts: current, scrollTop: element.scrollTop, visibleHeight: visibleHeightRef.current });
435
+ if (next?.first !== shown.first || next?.last !== shown.last) {
436
+ redrawStrip();
437
+ }
438
+ };
379
439
 
380
- const chatFind = chatHost().useTimelineFind({
381
- chatId,
382
- frame: frameRef,
383
- thread: threadRef,
384
- scroller: scrollRef,
385
- enabled: onMainAgent,
386
- rows,
387
- structure: items,
388
- order,
389
- firstRowInView: () => {
390
- const top = scrollRef.current?.scrollTop ?? 0;
391
- return virtualizer.getVirtualItems().find((virtualRow) => virtualRow.end > top)?.index ?? 0;
392
- },
393
- openTurn: turns.add,
394
- openGroup: groups.add,
395
- openSubagent: subagents.add,
396
- // A row already on screen stays put; the find scrolls to the match itself once it is drawn.
397
- scrollToRow: (index) => {
398
- const top = scrollRef.current?.scrollTop ?? 0;
399
- const bottom = top + visibleHeight;
400
- if (!virtualizer.getVirtualItems().some((virtualRow) => virtualRow.index === index && virtualRow.end > top && virtualRow.start < bottom)) {
401
- virtualizer.scrollToIndex(index, { align: 'center' });
402
- }
403
- },
404
- coveredHeight: () => coveredHeightRef.current,
405
- stopFollowing
406
- });
440
+ // One object while what it holds stays, so a find that keys an effect on it does not run it for every scroll.
441
+ const findOptions = useMemo<TimelineFindOptions>(
442
+ () => ({
443
+ chatId,
444
+ frame: frameRef,
445
+ thread: threadRef,
446
+ scroller: scrollRef,
447
+ enabled: onMainAgent,
448
+ rows,
449
+ structure: items,
450
+ order,
451
+ firstRowInView: () => {
452
+ const top = scrollRef.current?.scrollTop ?? 0;
453
+ return virtualizer.getVirtualItems().find((virtualRow) => virtualRow.end > top)?.index ?? 0;
454
+ },
455
+ openTurn: turns.add,
456
+ openGroup: groups.add,
457
+ openSubagent: subagents.add,
458
+ // A row already on screen stays put; the find scrolls to the match itself once it is drawn.
459
+ scrollToRow: (index) => {
460
+ const top = scrollRef.current?.scrollTop ?? 0;
461
+ const bottom = top + visibleHeightRef.current;
462
+ if (!virtualizer.getVirtualItems().some((virtualRow) => virtualRow.index === index && virtualRow.end > top && virtualRow.start < bottom)) {
463
+ virtualizer.scrollToIndex(index, { align: 'center' });
464
+ }
465
+ },
466
+ coveredHeight: () => coveredHeightRef.current,
467
+ stopFollowing
468
+ }),
469
+ [chatId, onMainAgent, rows, items, order, virtualizer, turns.add, groups.add, subagents.add, stopFollowing]
470
+ );
471
+ const chatFind = host.useTimelineFind(findOptions);
407
472
  const searching = chatFind.open;
408
473
  // A find covers the whole thread, so every page before this one comes in while it is open.
409
474
  useEffect(() => {
@@ -548,15 +613,15 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
548
613
  () => ({
549
614
  chatId,
550
615
  canFork: (turnId) => {
551
- if (chatHost().fork === null) {
616
+ if (fork === null) {
552
617
  return false;
553
618
  }
554
619
  const row = useChats.getState().byKey[keyOf(chatId)];
555
620
  return forkRefusal(row?.info ?? null, row?.structure[turnId]) === null;
556
621
  },
557
- fork: (turnId) => chatHost().fork?.(chatId, turnId)
622
+ fork: (turnId) => fork?.(chatId, turnId)
558
623
  }),
559
- [keyOf, chatId]
624
+ [keyOf, chatId, fork]
560
625
  );
561
626
 
562
627
  const stepRef = useRef<(direction: -1 | 1) => boolean>(() => false);
@@ -573,26 +638,40 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
573
638
  };
574
639
  useEffect(() => registerMessageStepper(keyOf(chatId), (direction) => stepRef.current(direction)), [keyOf, chatId]);
575
640
 
641
+ // Read through a ref, so the rows keep one callback for their life and their memo holds.
642
+ const opensRef = useRef({ rows, show });
643
+ opensRef.current = { rows, show };
576
644
  /* The whole of what a sub-agent did, in the thread's place. Its row only keeps the beginning. */
577
- const openConversation = (step: SubagentStep): void => {
578
- show(openFromMain(step));
579
- };
645
+ const openConversation = useCallback((step: SubagentStep): void => {
646
+ opensRef.current.show(openFromMain(step));
647
+ }, []);
580
648
 
649
+ const addSubagent = subagents.add;
581
650
  /* The header of a turn a sub-agent woke. It points at the row that agent worked in, or the one it hangs under. */
582
- const openSubagent = (toolUseId: string): void => {
583
- const found = findSubagentBranch(rows, toolUseId);
584
- if (!found) {
585
- return;
586
- }
587
- subagents.add(found.branch.id);
588
- showRowAtTop(virtualizer, followRef, found.index);
589
- };
651
+ const openSubagent = useCallback(
652
+ (toolUseId: string): void => {
653
+ const found = findSubagentBranch(opensRef.current.rows, toolUseId);
654
+ if (!found) {
655
+ return;
656
+ }
657
+ addSubagent(found.branch.id);
658
+ showRowAtTop(virtualizer, followRef, found.index);
659
+ },
660
+ [addSubagent, virtualizer]
661
+ );
662
+ const flashedRow = flash !== null && flash.key === keyOf(chatId) && !flash.block ? flash.rowId : null;
590
663
 
591
664
  return (
592
665
  // A file an answer names is relative to the folder this chat runs in, which is a worktree as
593
666
  // often as it is the project itself.
594
667
  <FileLinkContext.Provider value={info?.cwd ?? null}>
595
- <div ref={frameRef} className="relative flex min-h-0 grow flex-col" data-file-cwd={info?.cwd ?? ''} data-file-scope-id={scope.id}>
668
+ <div
669
+ ref={frameRef}
670
+ className="relative flex min-h-0 grow flex-col"
671
+ style={readingWidthStyle(readingWidth)}
672
+ data-file-cwd={info?.cwd ?? ''}
673
+ data-file-scope-id={scope.id}
674
+ >
596
675
  {searching && chatFind.bar}
597
676
  <ContextMenu.Root>
598
677
  <div
@@ -618,6 +697,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
618
697
  if (wantsEarlier(el)) {
619
698
  loadEarlier();
620
699
  }
700
+ followStrip(el);
621
701
  }}
622
702
  >
623
703
  {/* A welcome centers the greeting and the composer as one group. The composer keeps its
@@ -643,49 +723,31 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
643
723
  <EmptyThread chatId={chatId} />
644
724
  ) : (
645
725
  <div className="relative w-full shrink-0" style={{ height: virtualizer.getTotalSize() }}>
646
- {virtualizer.getVirtualItems().map((virtualRow) => {
647
- const row = rows[virtualRow.index]!;
648
- const previous = virtualRow.index > 0 ? rows[virtualRow.index - 1]! : null;
649
- const header = replyHeader(row, previous);
650
- return (
651
- <RowContainer
652
- key={row.id}
653
- row={row}
654
- previous={previous}
655
- index={virtualRow.index}
656
- top={virtualRow.start}
657
- ref={virtualizer.measureElement}
658
- >
659
- {flash !== null && flash.key === keyOf(chatId) && flash.rowId === row.id && !flash.block && (
660
- <span
661
- key={flash.nonce}
662
- className="chat-flash pointer-events-none absolute inset-0 rounded-lg"
663
- aria-hidden
726
+ <UiReplyNavigationContext.Provider value={uiNavigation}>
727
+ <FindRevealContext.Provider value={chatFind.reveal}>
728
+ {virtualizer.getVirtualItems().map((virtualRow) => {
729
+ const row = rows[virtualRow.index]!;
730
+ return (
731
+ <ThreadRow
732
+ key={row.id}
733
+ row={row}
734
+ previous={virtualRow.index > 0 ? rows[virtualRow.index - 1]! : null}
735
+ index={virtualRow.index}
736
+ top={virtualRow.start}
737
+ measure={virtualizer.measureElement}
738
+ chatId={chatId}
739
+ flashNonce={flash !== null && flashedRow === row.id ? flash.nonce : null}
740
+ bookmark={marks.get(row.id) ?? null}
741
+ toggleGroup={groups.toggle}
742
+ toggleTurn={turns.toggle}
743
+ toggleSubagent={subagents.toggle}
744
+ openSubagent={openSubagent}
745
+ openConversation={openConversation}
664
746
  />
665
- )}
666
- {header !== null && (
667
- <div className="chat-column-content w-full">
668
- <ReplyHeader chatId={chatId} at={header.at} />
669
- </div>
670
- )}
671
- <UiReplyNavigationContext.Provider value={uiNavigation}>
672
- <FindRevealContext.Provider value={chatFind.reveal}>
673
- <MarkableRow row={row} chatId={chatId} bookmark={marks.get(row.id) ?? null}>
674
- <Row
675
- row={row}
676
- chatId={chatId}
677
- toggleGroup={groups.toggle}
678
- toggleTurn={turns.toggle}
679
- toggleSubagent={subagents.toggle}
680
- openSubagent={openSubagent}
681
- openConversation={openConversation}
682
- />
683
- </MarkableRow>
684
- </FindRevealContext.Provider>
685
- </UiReplyNavigationContext.Provider>
686
- </RowContainer>
687
- );
688
- })}
747
+ );
748
+ })}
749
+ </FindRevealContext.Provider>
750
+ </UiReplyNavigationContext.Provider>
689
751
  </div>
690
752
  )}
691
753
  </ContextMenu.Trigger>
@@ -703,7 +765,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
703
765
  </div>
704
766
  </div>
705
767
  </div>
706
- <TimelineMenuPopup target={target} thread={threadRef} chatId={onMainAgent ? chatId : null} />
768
+ <TimelineMenuPopup target={target} thread={threadRef} chatId={onMainAgent ? chatId : null} column={frame.column} />
707
769
  </ContextMenu.Root>
708
770
  {drawsVisuals && <VisualDialogs chatId={chatId} />}
709
771
  {loadingEarlier && !overlay && (
@@ -1,26 +1,31 @@
1
1
  import type { RefObject } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { BookmarkPlus, BookmarkX, Braces, Copy, Eye, FileText, GitFork, MessageSquare, Pencil, Scan } from 'lucide-react';
3
+ import { BookmarkPlus, BookmarkX, Braces, Copy, Eye, FileText, GitFork, MessageSquare, MoveHorizontal, Pencil, Scan } from 'lucide-react';
4
4
  import { placeBookmark, removeBookmark, useBookmarkNaming } from '../bookmarks';
5
+ import { rememberReadingWidth, useChatPreferences } from '../preferences';
6
+ import { CHAT_READING_WIDTHS, readingWidthLabel, type ChatReadingWidth } from '../reading-width';
5
7
  import { forkRefusal, turnIdOfRow } from '../logic/fork';
6
8
  import { markdownOf, messageTextOf } from '../logic/timeline-copy';
7
9
  import type { TimelineTarget } from '../logic/timeline-target';
8
- import { chatHost } from '../../host';
10
+ import { useChatHost } from '../../host';
9
11
  import { useChatScope } from '../../scope';
10
12
  import { useChatRow } from '../../state/chats';
11
- import { openFileLink, useFileLinkCwd, useFileLinkScopeId } from './file-links';
13
+ import { useFileLinkCwd, useFileLinkScopeId, useOpenFileLink } from './file-links';
12
14
  import { ContextMenu, copyText, DisabledReason, EDIT_SHORTCUTS, Icon, Kbd, selectAllWithin } from '@adecore/ui';
13
15
 
14
16
  /* The menu behind a right-click in a thread, mostly for copying: a whole message, a code block or an answer's markdown. */
15
17
  export function TimelineMenuPopup({
16
18
  target,
17
19
  thread,
18
- chatId = null
20
+ chatId = null,
21
+ column = false
19
22
  }: {
20
23
  target: TimelineTarget;
21
24
  thread: RefObject<HTMLDivElement | null>;
22
25
  /* The chat whose own thread this is; a thread in its place (a sub-agent's) has nothing to fork. */
23
26
  chatId?: string | null;
27
+ /* The thread is a view of its own (`.chat-column`), the one place its reading width shows. */
28
+ column?: boolean;
24
29
  }) {
25
30
  const { t } = useTranslation('agent-chat');
26
31
  const message = target.row === null ? null : messageTextOf(target.row);
@@ -31,7 +36,8 @@ export function TimelineMenuPopup({
31
36
  // A thread outside a chat (a sub-agent's transcript) has no cwd of its own; the app answers for it then.
32
37
  const cwd = useFileLinkCwd();
33
38
  const scopeId = useFileLinkScopeId();
34
- const { fork, fileLinks } = chatHost();
39
+ const { fork, fileLinks } = useChatHost();
40
+ const openFileLink = useOpenFileLink();
35
41
  const openInPreview = (): void => {
36
42
  if (target.file != null) {
37
43
  openFileLink(target.file.cwd, target.file.ref, target.file.scopeId);
@@ -79,6 +85,12 @@ export function TimelineMenuPopup({
79
85
  </ContextMenu.Item>
80
86
  </>
81
87
  )}
88
+ {column && (
89
+ <>
90
+ <ContextMenu.Separator />
91
+ <ReadingWidthSubmenu />
92
+ </>
93
+ )}
82
94
  </ContextMenu.Popup>
83
95
  );
84
96
  }
@@ -90,12 +102,13 @@ export function TimelineMenuPopup({
90
102
  export function BookmarkMenuItems({ chatId, itemId, onName }: { chatId: string; itemId: string; onName?: () => void }) {
91
103
  const { t } = useTranslation('agent-chat');
92
104
  const scope = useChatScope();
105
+ const host = useChatHost();
93
106
  const bookmark = useChatRow(chatId, (row) => row?.bookmarks?.find((candidate) => candidate.itemId === itemId) ?? null);
94
107
  if (bookmark === null) {
95
108
  return (
96
109
  <ContextMenu.Item
97
110
  onClick={() => {
98
- void placeBookmark(scope, chatId, itemId);
111
+ void placeBookmark(scope, host, chatId, itemId);
99
112
  onName?.();
100
113
  }}
101
114
  >
@@ -113,9 +126,31 @@ export function BookmarkMenuItems({ chatId, itemId, onName }: { chatId: string;
113
126
  >
114
127
  <Icon icon={Pencil} size={14} /> {t('bookmarks.rename')}
115
128
  </ContextMenu.Item>
116
- <ContextMenu.Item onClick={() => void removeBookmark(scope, chatId, bookmark)}>
129
+ <ContextMenu.Item onClick={() => void removeBookmark(scope, host, chatId, bookmark)}>
117
130
  <Icon icon={BookmarkX} size={14} /> {t('bookmarks.remove')}
118
131
  </ContextMenu.Item>
119
132
  </>
120
133
  );
121
134
  }
135
+
136
+ /* The person's reading width for every chat of its own on this client. */
137
+ function ReadingWidthSubmenu() {
138
+ const { t } = useTranslation('agent-chat');
139
+ const readingWidth = useChatPreferences((preferences) => preferences.readingWidth);
140
+ return (
141
+ <ContextMenu.SubmenuRoot>
142
+ <ContextMenu.SubmenuTrigger>
143
+ <Icon icon={MoveHorizontal} size={14} /> {t('readingWidth.label')}
144
+ </ContextMenu.SubmenuTrigger>
145
+ <ContextMenu.Popup className="min-w-40">
146
+ <ContextMenu.RadioGroup value={readingWidth} onValueChange={(width: ChatReadingWidth) => rememberReadingWidth(width)}>
147
+ {CHAT_READING_WIDTHS.map((width) => (
148
+ <ContextMenu.RadioItem key={width} value={width}>
149
+ {readingWidthLabel(width)}
150
+ </ContextMenu.RadioItem>
151
+ ))}
152
+ </ContextMenu.RadioGroup>
153
+ </ContextMenu.Popup>
154
+ </ContextMenu.SubmenuRoot>
155
+ );
156
+ }
@@ -6,16 +6,17 @@ import { DiffOmitted } from './diff/DiffOmitted';
6
6
  import { DiffUnavailable } from './diff/DiffUnavailable';
7
7
  import { RawDiff } from './diff/RawDiff';
8
8
  import { DiffFallbackBoundary } from './diff/DiffFallbackBoundary';
9
- import { chatHost } from '../../host';
9
+ import { useChatHost } from '../../host';
10
10
  import type { UnifiedDiffProps } from './diff/diff-props';
11
11
 
12
12
  const LegacyUnifiedDiff = lazyNamed(() => import('./diff/LegacyUnifiedDiff'), 'default', { prefetch: false });
13
13
  const EditorPatchDiff = lazyNamed(() => import('./diff/EditorPatchDiff'), 'default');
14
14
 
15
15
  export default function UnifiedDiff(props: UnifiedDiffProps) {
16
+ const host = useChatHost();
16
17
  const { t } = useTranslation('agent-chat');
17
18
  const { change, contents, allowEditor = true, ignoreWhitespace = false, overflow = 'wrap', diffStyle = 'unified', fill = false, maxHeight } = props;
18
- const loadEngine = chatHost().code.editor;
19
+ const loadEngine = host.code.editor;
19
20
  const oldText = contents?.old ?? '';
20
21
  const newText = contents?.new ?? '';
21
22
  const sourceAllowed = useMemo(() => diffSourceBudget(oldText, newText) && patchSourceBudget(change.diff), [oldText, newText, change.diff]);
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
3
3
  import clsx from 'clsx';
4
4
  import { visualThemeFragment, type ChatVisual, type VisualAppearance, type VisualTheme } from '@adecore/agent-contracts/visual';
5
5
  import { Spinner, useMeasuredWidth } from '@adecore/ui';
6
- import { chatHost } from '../../host';
6
+ import { useChatHost } from '../../host';
7
7
  import { useChatScope } from '../../scope';
8
8
  import { initialVisualContentHeight, rememberVisualHeight } from '../logic/visual-height';
9
9
  import { VisualBridge } from '../visual-bridge';
@@ -33,9 +33,10 @@ function personActsIn(frame: HTMLIFrameElement): boolean {
33
33
  * when the host page is served on the app's own. Its box keeps one height while it loads or fails.
34
34
  */
35
35
  export function VisualFrame({ chatId, visual, fill = false, className }: VisualFrameProps) {
36
+ const { visuals, attachments } = useChatHost();
36
37
  const { t } = useTranslation('agent-chat');
37
38
  const scope = useChatScope();
38
- const frameUrl = chatHost().visuals?.frameUrl ?? null;
39
+ const frameUrl = visuals?.frameUrl ?? null;
39
40
  const boxRef = useRef<HTMLDivElement | null>(null);
40
41
  const frameRef = useRef<HTMLIFrameElement>(null);
41
42
  const bridgeRef = useRef<VisualBridge | null>(null);
@@ -75,12 +76,10 @@ export function VisualFrame({ chatId, visual, fill = false, className }: VisualF
75
76
  const bridge = new VisualBridge({
76
77
  frame: () => frame.contentWindow,
77
78
  visual: { maxHeight: visual.maxHeight },
78
- page: chatHost()
79
- .attachments.read(scope.id, chatId, visual.id)
80
- .then((blob) => blob.text()),
79
+ page: attachments.read(scope.id, chatId, visual.id).then((blob) => blob.text()),
81
80
  theme: opening.theme,
82
81
  mayOpenLink: () => personActsIn(frame),
83
- openLink: (url) => chatHost().visuals?.openLink(url),
82
+ openLink: (url) => visuals?.openLink(url),
84
83
  onHeight: (next) => {
85
84
  if (fill) {
86
85
  return;
@@ -106,7 +105,7 @@ export function VisualFrame({ chatId, visual, fill = false, className }: VisualF
106
105
  bridge.dispose();
107
106
  bridgeRef.current = null;
108
107
  };
109
- }, [opening, scope.id, chatId, visual.id, visual.maxHeight, fill]);
108
+ }, [opening, scope.id, chatId, visual.id, visual.maxHeight, fill, visuals, attachments]);
110
109
 
111
110
  useLayoutEffect(() => {
112
111
  const box = boxRef.current;
@@ -1,15 +1,15 @@
1
1
  import type { ThemeRegistration } from 'shiki';
2
- import { chatHost } from '../../host';
2
+ import { useChatHost, type ChatHost } from '../../host';
3
3
 
4
4
  /* The Shiki theme code is drawn in under the app's current light or dark. */
5
5
  export function useCodeTheme(): string {
6
- const { code } = chatHost();
6
+ const { code } = useChatHost();
7
7
  const mode = code.useMode();
8
8
  const themes = code.useThemes();
9
9
  return mode === 'dark' ? themes.dark : themes.light;
10
10
  }
11
11
 
12
12
  /* What to hand Shiki for a theme id: a theme the app ships itself, or the id of a bundled one for Shiki to load. */
13
- export function shikiThemeOf(id: string): string | ThemeRegistration {
14
- return chatHost().code.custom.find((theme) => theme.name === id) ?? id;
13
+ export function shikiThemeOf(id: string, custom: ChatHost['code']['custom']): string | ThemeRegistration {
14
+ return custom.find((theme) => theme.name === id) ?? id;
15
15
  }