@adecore/agents-react 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/README.md +14 -11
  2. package/dist/agents/AccountDot.js +3 -1
  3. package/dist/agents/accounts.d.ts +3 -2
  4. package/dist/agents/accounts.js +5 -6
  5. package/dist/chat/actions.d.ts +2 -2
  6. package/dist/chat/actions.js +8 -5
  7. package/dist/chat/bookmarks.d.ts +5 -3
  8. package/dist/chat/bookmarks.js +13 -14
  9. package/dist/chat/chat-client.d.ts +1 -0
  10. package/dist/chat/chat-client.js +7 -0
  11. package/dist/chat/composer-send.d.ts +3 -0
  12. package/dist/chat/composer-send.js +22 -0
  13. package/dist/chat/forks.js +22 -5
  14. package/dist/chat/index.d.ts +31 -0
  15. package/dist/chat/index.js +28 -0
  16. package/dist/chat/logic/embedded-code.d.ts +8 -0
  17. package/dist/chat/logic/embedded-code.js +371 -0
  18. package/dist/chat/logic/fork.js +1 -0
  19. package/dist/chat/logic/goal.d.ts +6 -0
  20. package/dist/chat/logic/goal.js +26 -0
  21. package/dist/chat/logic/scrubber.d.ts +1 -1
  22. package/dist/chat/logic/scrubber.js +6 -5
  23. package/dist/chat/logic/thought.d.ts +3 -0
  24. package/dist/chat/logic/thought.js +35 -0
  25. package/dist/chat/logic/thread-cards.js +2 -0
  26. package/dist/chat/logic/timeline.d.ts +5 -0
  27. package/dist/chat/logic/timeline.js +22 -1
  28. package/dist/chat/logic/tool-catalog.d.ts +1 -0
  29. package/dist/chat/logic/tool-catalog.js +3 -1
  30. package/dist/chat/logic/tools.d.ts +14 -0
  31. package/dist/chat/logic/tools.js +57 -0
  32. package/dist/chat/preferences.d.ts +6 -0
  33. package/dist/chat/preferences.js +19 -3
  34. package/dist/chat/reading-width.d.ts +9 -0
  35. package/dist/chat/reading-width.js +27 -0
  36. package/dist/chat/send-key.d.ts +10 -0
  37. package/dist/chat/send-key.js +24 -0
  38. package/dist/chat/ui/BookmarkMarker.js +4 -2
  39. package/dist/chat/ui/BookmarkSubmenu.js +4 -3
  40. package/dist/chat/ui/ChatActivity.js +14 -10
  41. package/dist/chat/ui/ChatReferenceChip.js +3 -4
  42. package/dist/chat/ui/CodeBlock.d.ts +2 -1
  43. package/dist/chat/ui/CodeBlock.js +14 -96
  44. package/dist/chat/ui/Composer.d.ts +1 -1
  45. package/dist/chat/ui/Composer.js +117 -906
  46. package/dist/chat/ui/ComposerInput.js +5 -4
  47. package/dist/chat/ui/EditDiff.js +3 -2
  48. package/dist/chat/ui/GoalDock.d.ts +9 -0
  49. package/dist/chat/ui/GoalDock.js +63 -0
  50. package/dist/chat/ui/LimitState.js +5 -3
  51. package/dist/chat/ui/Markdown.d.ts +2 -1
  52. package/dist/chat/ui/Markdown.js +70 -31
  53. package/dist/chat/ui/MermaidBlock.d.ts +3 -0
  54. package/dist/chat/ui/MermaidBlock.js +73 -0
  55. package/dist/chat/ui/MessageActions.js +12 -7
  56. package/dist/chat/ui/Pickers.js +6 -5
  57. package/dist/chat/ui/PromptComposer.js +4 -3
  58. package/dist/chat/ui/RowTime.d.ts +3 -0
  59. package/dist/chat/ui/RowTime.js +12 -0
  60. package/dist/chat/ui/RunSettings.js +5 -4
  61. package/dist/chat/ui/ShellCommand.d.ts +3 -0
  62. package/dist/chat/ui/ShellCommand.js +110 -0
  63. package/dist/chat/ui/SubagentInfo.js +4 -3
  64. package/dist/chat/ui/SubagentStopButton.js +4 -3
  65. package/dist/chat/ui/SubagentTimeline.js +4 -1
  66. package/dist/chat/ui/Timeline.js +70 -40
  67. package/dist/chat/ui/TimelineMenu.d.ts +2 -1
  68. package/dist/chat/ui/TimelineMenu.js +18 -8
  69. package/dist/chat/ui/UnifiedDiff.js +3 -2
  70. package/dist/chat/ui/VisualFrame.js +6 -7
  71. package/dist/chat/ui/code-theme.d.ts +2 -1
  72. package/dist/chat/ui/code-theme.js +4 -4
  73. package/dist/chat/ui/composer/ComposerControls.d.ts +27 -0
  74. package/dist/chat/ui/composer/ComposerControls.js +34 -0
  75. package/dist/chat/ui/composer/ComposerMenu.d.ts +4 -0
  76. package/dist/chat/ui/composer/ComposerMenu.js +46 -0
  77. package/dist/chat/ui/composer/DraftParts.d.ts +18 -0
  78. package/dist/chat/ui/composer/DraftParts.js +25 -0
  79. package/dist/chat/ui/composer/QueuedMessages.d.ts +10 -0
  80. package/dist/chat/ui/composer/QueuedMessages.js +20 -0
  81. package/dist/chat/ui/composer/key-handler.d.ts +20 -0
  82. package/dist/chat/ui/composer/key-handler.js +150 -0
  83. package/dist/chat/ui/composer/keys.d.ts +7 -2
  84. package/dist/chat/ui/composer/keys.js +19 -9
  85. package/dist/chat/ui/composer/placeholder.d.ts +9 -0
  86. package/dist/chat/ui/composer/placeholder.js +38 -0
  87. package/dist/chat/ui/composer/use-chat-controls.d.ts +28 -0
  88. package/dist/chat/ui/composer/use-chat-controls.js +112 -0
  89. package/dist/chat/ui/composer/use-compaction-offer.d.ts +2 -0
  90. package/dist/chat/ui/composer/use-compaction-offer.js +23 -0
  91. package/dist/chat/ui/composer/use-composer-draft.d.ts +14 -0
  92. package/dist/chat/ui/composer/use-composer-draft.js +39 -0
  93. package/dist/chat/ui/composer/use-composer-menus.d.ts +52 -0
  94. package/dist/chat/ui/composer/use-composer-menus.js +232 -0
  95. package/dist/chat/ui/composer/use-composer-submit.d.ts +32 -0
  96. package/dist/chat/ui/composer/use-composer-submit.js +113 -0
  97. package/dist/chat/ui/composer/use-draft-files.d.ts +27 -0
  98. package/dist/chat/ui/composer/use-draft-files.js +111 -0
  99. package/dist/chat/ui/composer/use-notice.d.ts +6 -0
  100. package/dist/chat/ui/composer/use-notice.js +18 -0
  101. package/dist/chat/ui/composer/use-pending-requests.d.ts +2 -0
  102. package/dist/chat/ui/composer/use-pending-requests.js +22 -0
  103. package/dist/chat/ui/composer/use-prompt-history.d.ts +8 -0
  104. package/dist/chat/ui/composer/use-prompt-history.js +57 -0
  105. package/dist/chat/ui/composer/use-prompt-hold.d.ts +6 -0
  106. package/dist/chat/ui/composer/use-prompt-hold.js +48 -0
  107. package/dist/chat/ui/composer/use-take-back.d.ts +13 -0
  108. package/dist/chat/ui/composer/use-take-back.js +82 -0
  109. package/dist/chat/ui/diff/EditorEditDiff.js +4 -3
  110. package/dist/chat/ui/diff/LegacyDiffPool.js +3 -2
  111. package/dist/chat/ui/diff/LegacyEditDiff.js +3 -2
  112. package/dist/chat/ui/diff/LegacyUnifiedDiff.js +3 -2
  113. package/dist/chat/ui/diff/syntax-runtime.d.ts +2 -1
  114. package/dist/chat/ui/diff/syntax-runtime.js +9 -5
  115. package/dist/chat/ui/diff-syntax.d.ts +2 -2
  116. package/dist/chat/ui/diff-syntax.js +1 -2
  117. package/dist/chat/ui/diff-theme.js +10 -9
  118. package/dist/chat/ui/file-links.d.ts +1 -1
  119. package/dist/chat/ui/file-links.js +7 -5
  120. package/dist/chat/ui/highlighter.d.ts +27 -0
  121. package/dist/chat/ui/highlighter.js +151 -0
  122. package/dist/chat/ui/index.d.ts +67 -0
  123. package/dist/chat/ui/index.js +64 -0
  124. package/dist/chat/ui/intelligent-ui/UiReply.js +3 -2
  125. package/dist/chat/ui/intelligent-ui/renderers/content.js +3 -2
  126. package/dist/chat/ui/intelligent-ui/use-ui-links.js +2 -2
  127. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +4 -3
  128. package/dist/chat/ui/markdown-blocks.d.ts +8 -0
  129. package/dist/chat/ui/markdown-blocks.js +260 -12
  130. package/dist/chat/ui/mermaid.d.ts +23 -0
  131. package/dist/chat/ui/mermaid.js +93 -0
  132. package/dist/chat/ui/rows/ForksRow.js +5 -3
  133. package/dist/chat/ui/rows/GeneratedImageRow.js +6 -4
  134. package/dist/chat/ui/rows/MessageRows.d.ts +4 -0
  135. package/dist/chat/ui/rows/MessageRows.js +36 -13
  136. package/dist/chat/ui/rows/Rows.js +3 -1
  137. package/dist/chat/ui/rows/SubagentRow.js +3 -2
  138. package/dist/chat/ui/rows/VisualRow.js +3 -2
  139. package/dist/chat/ui/rows/WorkRows.d.ts +6 -3
  140. package/dist/chat/ui/rows/WorkRows.js +57 -20
  141. package/dist/chat/ui/rows/WorkflowRow.js +3 -2
  142. package/dist/chat/ui/timestamp-format.d.ts +1 -0
  143. package/dist/chat/ui/timestamp-format.js +2 -0
  144. package/dist/chat/ui/use-diff-source-tokens.js +6 -3
  145. package/dist/chat/ui/visual-theme.js +3 -2
  146. package/dist/host.d.ts +9 -6
  147. package/dist/host.js +19 -8
  148. package/dist/index.d.ts +8 -0
  149. package/dist/index.js +9 -0
  150. package/dist/locales/en/agent-chat.d.ts +658 -0
  151. package/dist/locales/en/agent-chat.js +657 -0
  152. package/dist/locales/en/agent-prompts.d.ts +44 -0
  153. package/dist/locales/en/agent-prompts.js +43 -0
  154. package/dist/locales/en/agent-providers.d.ts +132 -0
  155. package/dist/locales/en/agent-providers.js +131 -0
  156. package/dist/locales/en/agent-usage.d.ts +132 -0
  157. package/dist/locales/en/agent-usage.js +131 -0
  158. package/dist/locales/nl/agent-chat.d.ts +658 -0
  159. package/dist/locales/nl/agent-chat.js +657 -0
  160. package/dist/locales/nl/agent-prompts.d.ts +44 -0
  161. package/dist/locales/nl/agent-prompts.js +43 -0
  162. package/dist/locales/nl/agent-providers.d.ts +132 -0
  163. package/dist/locales/nl/agent-providers.js +131 -0
  164. package/dist/locales/nl/agent-usage.d.ts +132 -0
  165. package/dist/locales/nl/agent-usage.js +131 -0
  166. package/dist/locales.js +9 -8
  167. package/dist/prompts/index.d.ts +11 -0
  168. package/dist/prompts/index.js +12 -0
  169. package/dist/prompts/logic/subjects.d.ts +2 -1
  170. package/dist/prompts/logic/subjects.js +6 -4
  171. package/dist/prompts/ui/ApprovalBody.js +6 -4
  172. package/dist/prompts/ui/PromptCard.js +5 -4
  173. package/dist/prompts/ui/PromptView.js +3 -2
  174. package/dist/prompts/ui/QuestionBody.js +3 -2
  175. package/dist/providers/AccountColors.js +2 -2
  176. package/dist/providers/AccountDetail.js +7 -6
  177. package/dist/providers/AccountVariables.js +3 -1
  178. package/dist/providers/AddAccountForm.js +3 -2
  179. package/dist/providers/ProvidersPane.js +4 -3
  180. package/dist/providers/account-actions.d.ts +4 -3
  181. package/dist/providers/account-actions.js +6 -7
  182. package/dist/providers/index.d.ts +16 -0
  183. package/dist/providers/index.js +17 -0
  184. package/dist/providers/provider-abilities.d.ts +2 -1
  185. package/dist/providers/provider-abilities.js +4 -0
  186. package/dist/state/chats.js +4 -2
  187. package/dist/state/index.d.ts +4 -0
  188. package/dist/state/index.js +5 -0
  189. package/dist/testing/index.d.ts +1 -0
  190. package/dist/testing/index.js +2 -0
  191. package/dist/{prompts/logic/prompts.fixtures.d.ts → testing/prompts.d.ts} +1 -1
  192. package/dist/theme.css +13 -3
  193. package/dist/usage/UsageBreakdown.js +3 -2
  194. package/dist/usage/UsageLimits.js +4 -3
  195. package/dist/usage/UsagePage.js +3 -2
  196. package/dist/usage/index.d.ts +15 -0
  197. package/dist/usage/index.js +15 -0
  198. package/package.json +53 -1031
  199. package/src/agents/AccountDot.tsx +3 -1
  200. package/src/agents/accounts.ts +6 -6
  201. package/src/chat/actions.ts +8 -5
  202. package/src/chat/bookmarks.ts +23 -14
  203. package/src/chat/chat-client.ts +8 -0
  204. package/src/chat/composer-send.ts +27 -0
  205. package/src/chat/forks.ts +27 -6
  206. package/src/chat/index.ts +98 -0
  207. package/src/chat/logic/embedded-code.ts +409 -0
  208. package/src/chat/logic/fork.ts +1 -0
  209. package/src/chat/logic/goal.ts +33 -0
  210. package/src/chat/logic/scrubber.ts +6 -5
  211. package/src/chat/logic/thought.ts +40 -0
  212. package/src/chat/logic/thread-cards.ts +2 -0
  213. package/src/chat/logic/timeline.ts +25 -1
  214. package/src/chat/logic/tool-catalog.ts +5 -1
  215. package/src/chat/logic/tools.ts +76 -0
  216. package/src/chat/preferences.ts +26 -3
  217. package/src/chat/reading-width.ts +39 -0
  218. package/src/chat/send-key.ts +38 -0
  219. package/src/chat/ui/BookmarkMarker.tsx +4 -2
  220. package/src/chat/ui/BookmarkSubmenu.tsx +4 -3
  221. package/src/chat/ui/ChatActivity.tsx +14 -10
  222. package/src/chat/ui/ChatReferenceChip.tsx +3 -4
  223. package/src/chat/ui/CodeBlock.tsx +33 -124
  224. package/src/chat/ui/Composer.tsx +162 -1324
  225. package/src/chat/ui/ComposerInput.tsx +5 -4
  226. package/src/chat/ui/EditDiff.tsx +3 -2
  227. package/src/chat/ui/GoalDock.tsx +104 -0
  228. package/src/chat/ui/LimitState.tsx +5 -3
  229. package/src/chat/ui/Markdown.tsx +113 -39
  230. package/src/chat/ui/MermaidBlock.tsx +119 -0
  231. package/src/chat/ui/MessageActions.tsx +13 -8
  232. package/src/chat/ui/Pickers.tsx +8 -7
  233. package/src/chat/ui/PromptComposer.tsx +4 -3
  234. package/src/chat/ui/RowTime.tsx +21 -0
  235. package/src/chat/ui/RunSettings.tsx +9 -4
  236. package/src/chat/ui/ShellCommand.tsx +134 -0
  237. package/src/chat/ui/SubagentInfo.tsx +4 -3
  238. package/src/chat/ui/SubagentStopButton.tsx +4 -3
  239. package/src/chat/ui/SubagentTimeline.tsx +5 -1
  240. package/src/chat/ui/Timeline.tsx +167 -105
  241. package/src/chat/ui/TimelineMenu.tsx +42 -7
  242. package/src/chat/ui/UnifiedDiff.tsx +3 -2
  243. package/src/chat/ui/VisualFrame.tsx +6 -7
  244. package/src/chat/ui/code-theme.ts +4 -4
  245. package/src/chat/ui/composer/ComposerControls.tsx +147 -0
  246. package/src/chat/ui/composer/ComposerMenu.tsx +138 -0
  247. package/src/chat/ui/composer/DraftParts.tsx +86 -0
  248. package/src/chat/ui/composer/QueuedMessages.tsx +64 -0
  249. package/src/chat/ui/composer/key-handler.ts +174 -0
  250. package/src/chat/ui/composer/keys.ts +32 -10
  251. package/src/chat/ui/composer/placeholder.ts +48 -0
  252. package/src/chat/ui/composer/use-chat-controls.ts +134 -0
  253. package/src/chat/ui/composer/use-compaction-offer.ts +25 -0
  254. package/src/chat/ui/composer/use-composer-draft.ts +71 -0
  255. package/src/chat/ui/composer/use-composer-menus.ts +289 -0
  256. package/src/chat/ui/composer/use-composer-submit.ts +163 -0
  257. package/src/chat/ui/composer/use-draft-files.ts +135 -0
  258. package/src/chat/ui/composer/use-notice.ts +28 -0
  259. package/src/chat/ui/composer/use-pending-requests.ts +23 -0
  260. package/src/chat/ui/composer/use-prompt-history.ts +76 -0
  261. package/src/chat/ui/composer/use-prompt-hold.ts +58 -0
  262. package/src/chat/ui/composer/use-take-back.ts +91 -0
  263. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +2 -2
  264. package/src/chat/ui/diff/EditorEditDiff.tsx +4 -3
  265. package/src/chat/ui/diff/LegacyDiffPool.tsx +3 -2
  266. package/src/chat/ui/diff/LegacyEditDiff.tsx +3 -2
  267. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +3 -2
  268. package/src/chat/ui/diff/syntax-runtime.ts +10 -5
  269. package/src/chat/ui/diff-syntax.ts +2 -2
  270. package/src/chat/ui/diff-theme.ts +10 -9
  271. package/src/chat/ui/file-links.ts +7 -5
  272. package/src/chat/ui/highlighter.ts +185 -0
  273. package/src/chat/ui/index.ts +109 -0
  274. package/src/chat/ui/intelligent-ui/UiReply.tsx +5 -4
  275. package/src/chat/ui/intelligent-ui/renderers/content.tsx +3 -2
  276. package/src/chat/ui/intelligent-ui/use-ui-links.ts +2 -2
  277. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +4 -3
  278. package/src/chat/ui/markdown-blocks.ts +300 -12
  279. package/src/chat/ui/mermaid.ts +118 -0
  280. package/src/chat/ui/rows/ForksRow.tsx +5 -3
  281. package/src/chat/ui/rows/GeneratedImageRow.tsx +6 -4
  282. package/src/chat/ui/rows/MessageRows.tsx +73 -18
  283. package/src/chat/ui/rows/Rows.tsx +14 -1
  284. package/src/chat/ui/rows/SubagentRow.tsx +3 -2
  285. package/src/chat/ui/rows/VisualRow.tsx +3 -2
  286. package/src/chat/ui/rows/WorkRows.tsx +148 -47
  287. package/src/chat/ui/rows/WorkflowRow.tsx +3 -2
  288. package/src/chat/ui/timestamp-format.ts +2 -0
  289. package/src/chat/ui/use-diff-source-tokens.ts +10 -4
  290. package/src/chat/ui/visual-theme.ts +3 -2
  291. package/src/host.ts +31 -15
  292. package/src/index.ts +49 -0
  293. package/src/locales/en/agent-chat.ts +657 -0
  294. package/src/locales/en/agent-prompts.ts +43 -0
  295. package/src/locales/en/agent-providers.ts +131 -0
  296. package/src/locales/en/agent-usage.ts +131 -0
  297. package/src/locales/nl/agent-chat.ts +657 -0
  298. package/src/locales/nl/agent-prompts.ts +43 -0
  299. package/src/locales/nl/agent-providers.ts +131 -0
  300. package/src/locales/nl/agent-usage.ts +132 -0
  301. package/src/locales.ts +9 -8
  302. package/src/prompts/index.ts +35 -0
  303. package/src/prompts/logic/subjects.ts +12 -4
  304. package/src/prompts/ui/ApprovalBody.tsx +6 -4
  305. package/src/prompts/ui/PromptCard.tsx +11 -4
  306. package/src/prompts/ui/PromptView.tsx +3 -2
  307. package/src/prompts/ui/QuestionBody.tsx +3 -2
  308. package/src/providers/AccountColors.tsx +2 -2
  309. package/src/providers/AccountDetail.tsx +13 -7
  310. package/src/providers/AccountVariables.tsx +3 -1
  311. package/src/providers/AddAccountForm.tsx +3 -2
  312. package/src/providers/ProvidersPane.tsx +4 -3
  313. package/src/providers/account-actions.ts +7 -7
  314. package/src/providers/index.ts +17 -0
  315. package/src/providers/provider-abilities.ts +6 -1
  316. package/src/state/chats.ts +4 -2
  317. package/src/state/index.ts +39 -0
  318. package/src/testing/index.ts +2 -0
  319. package/src/{prompts/logic/prompts.fixtures.ts → testing/prompts.ts} +1 -1
  320. package/src/theme.css +13 -3
  321. package/src/usage/UsageBreakdown.tsx +3 -2
  322. package/src/usage/UsageLimits.tsx +4 -3
  323. package/src/usage/UsagePage.tsx +3 -2
  324. package/src/usage/index.ts +16 -0
  325. package/dist/locales/en/agent-chat.json +0 -597
  326. package/dist/locales/en/agent-prompts.json +0 -43
  327. package/dist/locales/en/agent-providers.json +0 -131
  328. package/dist/locales/en/agent-usage.json +0 -131
  329. package/dist/locales/nl/agent-chat.json +0 -597
  330. package/dist/locales/nl/agent-prompts.json +0 -43
  331. package/dist/locales/nl/agent-providers.json +0 -131
  332. package/dist/locales/nl/agent-usage.json +0 -131
  333. package/src/locales/en/agent-chat.json +0 -597
  334. package/src/locales/en/agent-prompts.json +0 -43
  335. package/src/locales/en/agent-providers.json +0 -131
  336. package/src/locales/en/agent-usage.json +0 -131
  337. package/src/locales/nl/agent-chat.json +0 -597
  338. package/src/locales/nl/agent-prompts.json +0 -43
  339. package/src/locales/nl/agent-providers.json +0 -131
  340. package/src/locales/nl/agent-usage.json +0 -131
  341. /package/dist/{prompts/logic/prompts.fixtures.js → testing/prompts.js} +0 -0
@@ -10,8 +10,8 @@ import { modelName, shortModelName } from '../../agents/model-name.js';
10
10
  import { CONTEXT_OPTION, contextFraction, contextSegments, orderOptions } from '../logic/context-usage.js';
11
11
  import { RUNTIME_MODES } from '../runtime-modes.js';
12
12
  import { formatClock, formatWeekdayClock, isSameDay, formatPercent, formatTokens } from '@adecore/ui/format';
13
- import { Icon, Menu, Tooltip, useNow } from '@adecore/ui';
14
- import { chatHost } from '../../host.js';
13
+ import { Icon, Menu, Pill, Tooltip, useNow } from '@adecore/ui';
14
+ import { useChatHost } from '../../host.js';
15
15
  import { useUsageLimits } from '../../usage/limits.js';
16
16
  const MINUTE_MS = 60_000;
17
17
  const MODE_ICONS = {
@@ -87,7 +87,7 @@ export function RunSettings({ disabled = false, providers, provider, selection,
87
87
  const slash = value.indexOf('/');
88
88
  onModel(value.slice(0, slash), value.slice(slash + 1));
89
89
  };
90
- const modelGroups = (groups) => groups.map(({ entry, models }) => (_jsxs(Menu.Group, { children: [grouped && (_jsxs(Menu.GroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.kind, size: 12 }), entry.name] })), models.map((row) => (_jsxs(Menu.RadioItem, { value: modelValue(entry.kind, row.slug), closeOnClick: true, children: [_jsx(AgentIcon, { kind: entry.kind, size: 14, className: "shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: row.name }), row.badge && _jsx("span", { className: "rounded bg-accent-soft px-1 text-[11px] leading-5 font-medium uppercase text-accent", children: row.badge })] }, row.slug)))] }, entry.kind)));
90
+ const modelGroups = (groups) => groups.map(({ entry, models }) => (_jsxs(Menu.Group, { children: [grouped && (_jsxs(Menu.GroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.kind, size: 12 }), entry.name] })), models.map((row) => (_jsxs(Menu.RadioItem, { value: modelValue(entry.kind, row.slug), closeOnClick: true, children: [_jsx(AgentIcon, { kind: entry.kind, size: 14, className: "shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: row.name }), row.badge && (_jsx(Pill, { tone: "accent", shape: "tag", size: "sm", children: row.badge.charAt(0).toLocaleUpperCase() + row.badge.slice(1) }))] }, row.slug)))] }, entry.kind)));
91
91
  return (_jsxs(Menu.Root, { open: open, onOpenChange: onOpenChange, children: [_jsx(Tooltip, { label: permissionHint ?? t('pickers.settings.title'), kbd: "/model", children: _jsxs(Menu.Trigger, { disabled: disabled, className: clsx('flex h-8 min-w-0 items-center gap-2 rounded-full border pr-3 pl-2.5 text-xs whitespace-nowrap text-text-muted @max-xl/composer:gap-1.5 @max-sm/composer:pr-2.5', open ? 'border-accent bg-accent-soft' : 'border-border hover:bg-surface-hover'), children: [_jsx(AgentIcon, { kind: provider, size: 14, className: "shrink-0" }), _jsxs("span", { className: "flex min-w-0 items-center gap-2 overflow-hidden @max-xl/composer:gap-1.5", children: [_jsx("span", { className: "truncate font-medium text-text @max-xl/composer:hidden", children: current }), _jsx("span", { className: "hidden truncate font-medium text-text @max-xl/composer:inline", children: short }), account !== null && (_jsxs("span", { className: "flex min-w-0 items-center gap-2 @max-xl/composer:gap-1.5", children: [_jsx("span", { className: "shrink-0 text-text-faint", children: "\u00B7" }), _jsx(AccountDot, { color: account.choice.current?.account.color }), _jsx("span", { className: "truncate @max-xl/composer:hidden", children: account.choice.current === null ? account.choice.currentId : account.choice.nameOf(account.choice.current) })] })), summary.map((entry) => (_jsxs("span", { className: clsx('flex shrink-0 items-center gap-2 @max-xl/composer:gap-1.5', !entry.ring && '@max-xl/composer:hidden'), children: [_jsx("span", { className: "text-text-faint", children: "\u00B7" }), entry.ring && _jsx(ContextRing, { usage: usage }), _jsx("span", { className: clsx(entry.ring && '@max-sm/composer:hidden'), children: entry.label })] }, entry.id))), _jsx("span", { className: "shrink-0 text-text-faint", children: "\u00B7" }), _jsxs("span", { className: clsx('flex shrink-0 items-center gap-1', fullAccess && 'text-status-needs-you'), children: [_jsx(Icon, { icon: MODE_ICONS[shownMode], size: 12, className: "shrink-0" }), _jsx("span", { children: effectiveRuntimeMode === undefined && permissionMode !== undefined ? permissionMode : t(`modes.${shownMode}.short`) }), permissionFallback && _jsx(Icon, { icon: TriangleAlert, size: 12, className: "shrink-0 text-status-needs-you" })] })] }), _jsx(Icon, { icon: open ? ChevronUp : ChevronDown, size: 12, className: "shrink-0 @max-sm/composer:hidden" })] }) }), _jsxs(Menu.Popup, { side: "top", sideOffset: 8, align: "start", className: "w-80", children: [providers.length === 0 && _jsx("div", { className: "px-2.5 py-3 text-xs text-text-faint", children: t('pickers.model.noProvider') }), _jsx(Menu.RadioGroup, { value: chosenModel, onValueChange: chooseModel, children: modelGroups(live) }), legacy.length > 0 && (_jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: "text-text-muted", children: [_jsx("span", { className: "min-w-0 grow truncate", children: t('pickers.model.legacyGroup', { count: legacy.reduce((count, group) => count + group.models.length, 0) }) }), chosenLegacy && _jsx(Menu.Hint, { className: "truncate", children: chosenLegacy.name })] }), _jsx(Menu.Popup, { className: "min-w-56", children: _jsx(Menu.RadioGroup, { value: chosenModel, onValueChange: chooseModel, children: modelGroups(legacy) }) })] })), _jsx(Menu.Separator, {}), account !== null && _jsx(AccountSubmenu, { provider: provider, account: account }), _jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: "text-text-muted", children: [_jsx(Icon, { icon: MODE_ICONS[runtimeMode], size: 14, className: clsx('shrink-0', fullAccess ? 'text-status-needs-you' : 'text-text-faint') }), _jsx("span", { className: "min-w-0 grow truncate", children: t('pickers.mode.label') }), _jsx(Menu.Hint, { className: "truncate", children: t(`modes.${runtimeMode}.label`) })] }), _jsxs(Menu.Popup, { className: "w-72", children: [permissionHint !== null && _jsx("p", { className: "px-2.5 py-2 text-xs text-text-muted", children: permissionHint }), _jsx(Menu.RadioGroup, { value: runtimeMode, onValueChange: (mode) => onMode(mode), children: RUNTIME_MODES.map((mode) => (_jsxs(Menu.RadioItem, { value: mode, className: "items-start", children: [_jsx("span", { className: "grid h-lh w-4 shrink-0 place-items-center", children: _jsx(Icon, { icon: MODE_ICONS[mode], size: 14, className: mode === 'full-access' ? 'text-status-needs-you' : 'text-text-faint' }) }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [t(`modes.${mode}.label`), _jsx("span", { className: "text-xs text-text-faint", children: t(`modes.${mode}.hint`) })] })] }, mode))) })] })] }), options.map((option) => (_jsx(OptionRow, { option: option, selection: selection, onChange: (value) => onOption(option.id, value) }, option.id))), (hasContextOption || standaloneContext) && (_jsxs(_Fragment, { children: [_jsx(Menu.Separator, {}), _jsx(ContextUsage, { usage: usage, disabled: compactDisabled, onCompact: onCompact })] }))] })] }));
92
92
  }
93
93
  /* Red where a window is nearly spent, amber where it is worth knowing, as the usage bars have it. */
@@ -104,9 +104,10 @@ function sessionTone(used) {
104
104
  * the conversation goes along to those. A switch applies from the next turn.
105
105
  */
106
106
  function AccountSubmenu({ provider, account }) {
107
+ const host = useChatHost();
107
108
  const { t } = useTranslation('agent-chat');
108
109
  const { choice, started, onPick } = account;
109
- return (_jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: "text-text-muted", children: [_jsx("span", { className: "grid w-3.5 shrink-0 place-items-center", children: _jsx(AccountDot, { color: choice.current?.account.color }) }), _jsx("span", { className: "min-w-0 grow truncate", children: t('pickers.account.label') }), _jsx(Menu.Hint, { className: "truncate", children: choice.current === null ? choice.currentId : choice.nameOf(choice.current) })] }), _jsxs(Menu.Popup, { className: "min-w-64", children: [_jsxs(Menu.Group, { children: [_jsxs(Menu.GroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: provider, size: 12 }), choice.providerName] }), _jsx(Menu.RadioGroup, { value: choice.currentId, onValueChange: (id) => onPick(id), children: _jsx(AccountRows, { choice: choice, locked: (id) => started && !canContinueOn(choice.accounts, provider, choice.currentId, id) }) })] }), _jsx(Menu.Separator, {}), _jsxs(Menu.Item, { className: "text-text-muted", onClick: () => chatHost().openSettings('providers'), children: [_jsx(Icon, { icon: Settings, size: 14, className: "shrink-0 text-text-faint" }), t('pickers.account.manage')] })] })] }));
110
+ return (_jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: "text-text-muted", children: [_jsx("span", { className: "grid w-3.5 shrink-0 place-items-center", children: _jsx(AccountDot, { color: choice.current?.account.color }) }), _jsx("span", { className: "min-w-0 grow truncate", children: t('pickers.account.label') }), _jsx(Menu.Hint, { className: "truncate", children: choice.current === null ? choice.currentId : choice.nameOf(choice.current) })] }), _jsxs(Menu.Popup, { className: "min-w-64", children: [_jsxs(Menu.Group, { children: [_jsxs(Menu.GroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: provider, size: 12 }), choice.providerName] }), _jsx(Menu.RadioGroup, { value: choice.currentId, onValueChange: (id) => onPick(id), children: _jsx(AccountRows, { choice: choice, locked: (id) => started && !canContinueOn(choice.accounts, provider, choice.currentId, id) }) })] }), _jsx(Menu.Separator, {}), _jsxs(Menu.Item, { className: "text-text-muted", onClick: () => host.openSettings('providers'), children: [_jsx(Icon, { icon: Settings, size: 14, className: "shrink-0 text-text-faint" }), t('pickers.account.manage')] })] })] }));
110
111
  }
111
112
  /* Mounted only while the submenu is open, since asking for the plan windows starts a CLI per account. */
112
113
  function AccountRows({ choice, locked }) {
@@ -0,0 +1,3 @@
1
+ export declare function ShellCommand({ command }: {
2
+ command: string;
3
+ }): import("react").JSX.Element;
@@ -0,0 +1,110 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { EMBEDDED_CODE_LIMIT, SHELL_LANGUAGE, embeddedCode } from '../logic/embedded-code.js';
4
+ import { useChatHost } from '../../host.js';
5
+ import { useCodeTheme } from './code-theme.js';
6
+ import { tokenStyle, useCodeHighlighter } from './highlighter.js';
7
+ // Rows redraw often and a thread holds many commands, so each is tokenized once per theme.
8
+ const CACHE_LIMIT = 300;
9
+ const cache = new Map();
10
+ /* The runs of `code` in one language, placed at `offset`; a run in the theme's own foreground takes the color around it. */
11
+ function runsOf(highlighter, code, language, theme, offset) {
12
+ const fg = highlighter.getTheme(theme).fg.toLowerCase();
13
+ const { tokens } = highlighter.codeToTokens(code, { lang: language, theme });
14
+ return tokens.flat().map((token) => ({
15
+ start: offset + token.offset,
16
+ end: offset + token.offset + token.content.length,
17
+ content: token.content,
18
+ color: token.color?.toLowerCase() === fg ? undefined : token.color,
19
+ fontStyle: token.fontStyle
20
+ }));
21
+ }
22
+ /* `base` with `[start, end)` cut out of it and `inner` in its place. */
23
+ function overlay(base, start, end, inner) {
24
+ const kept = [];
25
+ for (const run of base) {
26
+ if (run.end <= start || run.start >= end) {
27
+ kept.push(run);
28
+ continue;
29
+ }
30
+ if (run.start < start) {
31
+ kept.push({ ...run, end: start, content: run.content.slice(0, start - run.start) });
32
+ }
33
+ if (run.end > end) {
34
+ kept.push({ ...run, start: end, content: run.content.slice(end - run.start) });
35
+ }
36
+ }
37
+ return [...kept, ...inner].sort((left, right) => left.start - right.start);
38
+ }
39
+ function languagesOf(command) {
40
+ return new Set([SHELL_LANGUAGE, ...embeddedCode(command).map(({ language }) => language)]);
41
+ }
42
+ /* The colored runs of a command, or null while a grammar or the theme is still on its way. */
43
+ function highlight(command, theme, code) {
44
+ const key = `${theme}\n${command}`;
45
+ const known = cache.get(key);
46
+ if (known !== undefined) {
47
+ // Touched entries move to the end, so the oldest falls out first.
48
+ cache.delete(key);
49
+ cache.set(key, known);
50
+ return known;
51
+ }
52
+ const embedded = embeddedCode(command);
53
+ const highlighter = code.ready([SHELL_LANGUAGE, ...embedded.map(({ language }) => language)], theme);
54
+ if (highlighter === null) {
55
+ return null;
56
+ }
57
+ let runs = runsOf(highlighter, command, code.language(SHELL_LANGUAGE), theme, 0);
58
+ for (const part of embedded) {
59
+ runs = overlay(runs, part.start, part.end, runsOf(highlighter, command.slice(part.start, part.end), code.language(part.language), theme, part.start));
60
+ }
61
+ cache.set(key, runs);
62
+ if (cache.size > CACHE_LIMIT) {
63
+ cache.delete(cache.keys().next().value);
64
+ }
65
+ return runs;
66
+ }
67
+ function draw(command, runs) {
68
+ const parts = [];
69
+ let at = 0;
70
+ for (const run of runs) {
71
+ // Shiki leaves the line breaks out of its tokens; they come from the command itself.
72
+ if (run.start > at) {
73
+ parts.push(command.slice(at, run.start));
74
+ }
75
+ const style = tokenStyle(run);
76
+ parts.push(style === undefined ? (run.content) : (_jsx("span", { style: style, children: run.content }, run.start)));
77
+ at = run.end;
78
+ }
79
+ if (at < command.length) {
80
+ parts.push(command.slice(at));
81
+ }
82
+ return parts;
83
+ }
84
+ /*
85
+ * A shell command in the colors of the code theme, with the code inside it (a `bash -lc` script, a
86
+ * `python3 -c` argument, a heredoc) in its own language. Plain text until the grammars are in, which
87
+ * changes no size; a command past the limit stays plain.
88
+ */
89
+ export function ShellCommand({ command }) {
90
+ const theme = useCodeTheme();
91
+ const { custom } = useChatHost().code;
92
+ const highlighter = useCodeHighlighter();
93
+ // Bumped when a load settles, so the cache is asked again.
94
+ const [, setLoads] = useState(0);
95
+ const tooLong = command.length > EMBEDDED_CODE_LIMIT;
96
+ const runs = tooLong ? null : highlight(command, theme, highlighter);
97
+ useEffect(() => {
98
+ if (runs !== null || tooLong) {
99
+ return;
100
+ }
101
+ let cancelled = false;
102
+ Promise.all([highlighter.loadTheme(theme, custom), ...[...languagesOf(command)].map((lang) => highlighter.loadLanguage(lang))])
103
+ .then(() => !cancelled && setLoads((count) => count + 1))
104
+ .catch(() => undefined);
105
+ return () => {
106
+ cancelled = true;
107
+ };
108
+ }, [highlighter, command, theme, custom, runs, tooLong]);
109
+ return _jsx(_Fragment, { children: runs === null ? command : draw(command, runs) });
110
+ }
@@ -8,21 +8,22 @@ import { SubagentActions, SubagentBreadcrumb } from './SubagentControls.js';
8
8
  import { useNow } from '@adecore/ui';
9
9
  import { useModelName } from '../../agents/model-name.js';
10
10
  import { useChatRow } from '../../state/chats.js';
11
- import { chatHost } from '../../host.js';
11
+ import { useChatHost } from '../../host.js';
12
12
  import { useChatScope } from '../../scope.js';
13
13
  /*
14
14
  * What a sub-agent is, over its conversation: the way down to it, its state and time, the model it runs on
15
15
  * and what it spent, and the way out. Only the way down and out for a conversation whose row the thread lost.
16
16
  */
17
17
  export function SubagentInfo({ chatId, toolUseId }) {
18
+ const host = useChatHost();
18
19
  const { t } = useTranslation('agent-chat');
19
20
  const { id } = useChatScope();
20
21
  const item = useSubagentItem(chatId, toolUseId);
21
22
  const provider = useChatRow(chatId, (row) => row?.info.provider);
22
- const task = chatHost().tasks.useTask(id, item === null ? null : taskIdOf(item));
23
+ const task = host.tasks.useTask(id, item === null ? null : taskIdOf(item));
23
24
  const modelName = useModelName(provider, item?.model ?? '');
24
25
  const now = useNow(1000, item?.status === 'running');
25
26
  const word = item === null ? null : statusWordOf(item, task);
26
27
  const facts = item === null ? [] : subagentFacts(item, item.model === undefined ? null : modelName);
27
- return (_jsx("section", { "aria-label": t('subagents.info.label'), className: "shrink-0 border-b border-border px-4 py-1.5", children: _jsxs("div", { className: "flex min-w-0 items-center gap-2 text-xs text-text-faint", children: [_jsx(SubagentBreadcrumb, { chatId: chatId, className: "max-w-1/3 shrink-0 font-medium" }), item !== null && word !== null && (_jsxs(_Fragment, { children: [_jsx(StatusIcon, { word: word }), _jsx("span", { className: "sr-only", children: t(`common.status.${word}`) }), _jsx("span", { className: "shrink-0 text-text-muted tabular-nums", children: entryTimeOf(item, task, now) })] })), facts.map((fact) => (_jsxs(Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "min-w-0 truncate", children: fact })] }, fact))), _jsx("span", { className: "grow" }), _jsx(SubagentActions, { chatId: chatId })] }) }));
28
+ return (_jsx("section", { "aria-label": t('subagents.info.label'), className: "shrink-0 border-b border-border py-1.5 pr-1.5 pl-4", children: _jsxs("div", { className: "flex min-w-0 items-center gap-2 text-xs text-text-faint", children: [_jsx(SubagentBreadcrumb, { chatId: chatId, className: "max-w-1/3 shrink-0 font-medium" }), item !== null && word !== null && (_jsxs(_Fragment, { children: [_jsx(StatusIcon, { word: word }), _jsx("span", { className: "sr-only", children: t(`common.status.${word}`) }), _jsx("span", { className: "shrink-0 text-text-muted tabular-nums", children: entryTimeOf(item, task, now) })] })), facts.map((fact) => (_jsxs(Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "min-w-0 truncate", children: fact })] }, fact))), _jsx("span", { className: "grow" }), _jsx(SubagentActions, { chatId: chatId })] }) }));
28
29
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { Square } from 'lucide-react';
4
- import { chatHost } from '../../host.js';
4
+ import { useChatHost } from '../../host.js';
5
5
  import { useChatScope } from '../../scope.js';
6
6
  import { useChatRow } from '../../state/chats.js';
7
7
  import { useChatActions } from '../actions.js';
@@ -9,6 +9,7 @@ import { stopLabel, stopOf, subagentTitle } from '../subagent-list.js';
9
9
  import { IconButton } from '@adecore/ui';
10
10
  /* Stops one active sub-agent of a chat, or says nothing when stopping it is not on offer. */
11
11
  export function SubagentStopButton({ chatId, item, size = 'xs', className }) {
12
+ const host = useChatHost();
12
13
  const { t } = useTranslation('agent-chat');
13
14
  const { id } = useChatScope();
14
15
  const actions = useChatActions();
@@ -21,12 +22,12 @@ export function SubagentStopButton({ chatId, item, size = 'xs', className }) {
21
22
  void actions.stopSubagent(chatId, item.toolUseId).catch((e) => {
22
23
  const stale = e instanceof Error && 'code' in e && e.code === 'subagent-stale';
23
24
  const description = stale ? t('subagents.stale') : e instanceof Error ? e.message : t('subagents.noAnswer');
24
- chatHost().notify({ kind: 'error', title: t('subagents.stopFailed'), description });
25
+ host.notify({ kind: 'error', title: t('subagents.stopFailed'), description });
25
26
  });
26
27
  };
27
28
  const run = () => {
28
29
  if (stop === 'task' && item.childId !== undefined) {
29
- chatHost().confirm.stopTask(id, item.childId, subagentTitle(item), send);
30
+ host.confirm.stopTask(id, item.childId, subagentTitle(item), send);
30
31
  return;
31
32
  }
32
33
  send();
@@ -18,6 +18,8 @@ import { FOLLOW_THRESHOLD_PX, rowRhythm } from './rows/row-rhythm.js';
18
18
  import { useToggleSet } from './useToggleSet.js';
19
19
  import { FileLinkContext } from './file-links.js';
20
20
  import { useChatRow } from '../../state/chats.js';
21
+ import { useChatPreferences } from '../preferences.js';
22
+ import { readingWidthStyle } from '../reading-width.js';
21
23
  import { useChatScope } from '../../scope.js';
22
24
  import { EmptyState, ContextMenu, Icon } from '@adecore/ui';
23
25
  const NO_TURNS = new Set();
@@ -95,10 +97,11 @@ export function SubagentTimeline({ chatId, toolUseId }) {
95
97
  loadEarlier();
96
98
  }
97
99
  }, [rows, state.status, loadEarlier]);
100
+ const readingWidth = useChatPreferences((preferences) => preferences.readingWidth);
98
101
  const openChild = (step) => {
99
102
  show(openBelow(trail, step));
100
103
  };
101
- return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx(SubagentInfo, { chatId: chatId, toolUseId: toolUseId }), state.status === 'loading' && _jsx("div", { className: "chat-column-content px-4 pt-4 text-xs text-text-faint", children: t('subagents.loading') }), state.status === 'failed' && (_jsx(EmptyState, { icon: Bot, children: state.unsupported ? t('subagents.unsupported') : (state.error ?? t('subagents.unreadable')) })), state.status === 'ready' && (_jsx(FileLinkContext.Provider, { value: state.context?.cwd ?? cwd, children: _jsx(ReplyContext.Provider, { value: state.context ?? { provider: state.source === 'codex-thread' ? 'codex' : 'claude' }, children: _jsx("div", { ref: scrollRef, "data-file-cwd": state.context?.cwd ?? cwd ?? '', "data-file-scope-id": scope.id, className: "chat-thread relative min-h-0 grow overflow-auto px-4 pt-4 pb-3",
104
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", style: readingWidthStyle(readingWidth), children: [_jsx(SubagentInfo, { chatId: chatId, toolUseId: toolUseId }), state.status === 'loading' && _jsx("div", { className: "chat-column-content px-4 pt-4 text-xs text-text-faint", children: t('subagents.loading') }), state.status === 'failed' && (_jsx(EmptyState, { icon: Bot, children: state.unsupported ? t('subagents.unsupported') : (state.error ?? t('subagents.unreadable')) })), state.status === 'ready' && (_jsx(FileLinkContext.Provider, { value: state.context?.cwd ?? cwd, children: _jsx(ReplyContext.Provider, { value: state.context ?? { provider: state.source === 'codex-thread' ? 'codex' : 'claude' }, children: _jsx("div", { ref: scrollRef, "data-file-cwd": state.context?.cwd ?? cwd ?? '', "data-file-scope-id": scope.id, className: "chat-thread relative min-h-0 grow overflow-auto px-4 pt-4 pb-3",
102
105
  // The thread keeps its reader in place itself when a page goes in above.
103
106
  style: { overflowAnchor: 'none' }, onScroll: (event) => {
104
107
  const element = event.currentTarget;
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { useVirtualizer } from '@tanstack/react-virtual';
5
5
  import clsx from 'clsx';
@@ -11,6 +11,8 @@ import { timedRowsOf, withTimedRows } from '../logic/thread-cards.js';
11
11
  import { openFromMain, useSubagentTrail } from '../subagent-view.js';
12
12
  import { registerItemJumper, registerMessageStepper, registerTimeline, registerTurnJumper, setTimelineAtEnd, wantsEarlier } from '../timeline-scroll.js';
13
13
  import { useTimelineFlash } from '../timeline-flash.js';
14
+ import { useChatPreferences } from '../preferences.js';
15
+ import { readingMeasure, readingWidthStyle } from '../reading-width.js';
14
16
  import { SCRUBBER_MIN_TICKS, STRIP_INSET_PX, STRIP_WIDTH_PX, messagesInView, stepMessage, threadPaddingLeft, STRIP_CLEARANCE_PX, tickOfRow, ticksOf, ticksWithHits } from '../logic/scrubber.js';
15
17
  import { EMPTY_TARGET, readTimelineTarget, revealUiReplyBlock, uiReplyJump, withCurrentText } from '../logic/timeline-target.js';
16
18
  import { forkRefusal } from '../logic/fork.js';
@@ -30,7 +32,7 @@ import { Row } from './rows/Rows.js';
30
32
  import { RowContainer } from './rows/RowContainer.js';
31
33
  import { ReplyHeader } from './rows/MessageRows.js';
32
34
  import { useChatRow, useChats } from '../../state/chats.js';
33
- import { chatHost } from '../../host.js';
35
+ import { useChatHost } from '../../host.js';
34
36
  import { useChatScope } from '../../scope.js';
35
37
  import { FileLinkContext } from './file-links.js';
36
38
  import { AgentIcon } from '../../agents/AgentIcon.js';
@@ -49,11 +51,12 @@ function showRowAtTop(virtualizer, followRef, index) {
49
51
  * and the folder it works in, so a first question can lean on all three.
50
52
  */
51
53
  function EmptyThread({ chatId }) {
54
+ const host = useChatHost();
52
55
  const { t } = useTranslation('agent-chat');
53
56
  const info = useChatRow(chatId, (row) => row?.info ?? null);
54
57
  const model = useModelName(info?.provider, info?.selection.model ?? '');
55
- const sources = chatHost().useContextSources(chatId);
56
- const hidesFolder = chatHost().useHidesFolder(chatId);
58
+ const sources = host.useContextSources(chatId);
59
+ const hidesFolder = host.useHidesFolder(chatId);
57
60
  return (_jsx("div", { className: "flex grow items-center justify-center", children: _jsxs("div", { className: "flex max-w-sm flex-col items-center gap-3 px-6 py-8 text-center", children: [_jsx(EmptyState, { icon: info ? _jsx(AgentIcon, { kind: info.provider }) : undefined, className: "p-0", children: info ? t('timeline.empty.ready', { model }) : t('timeline.empty.ask') }), sources.length > 0 && (_jsxs("div", { className: "flex max-w-full flex-col items-center gap-1", children: [_jsx(SectionLabel, { children: t('timeline.empty.canRead') }), _jsx("span", { className: "max-w-full text-xs text-text-muted", children: sources.map((source) => source.title).join(', ') })] })), info?.cwd && !hidesFolder && (_jsxs("div", { className: "flex max-w-full flex-col items-center gap-1", children: [_jsx(SectionLabel, { children: t('timeline.empty.worksIn') }), _jsx("span", { className: "max-w-full font-mono text-xs break-all text-text-muted", children: info.cwd })] }))] }) }));
58
61
  }
59
62
  /*
@@ -66,11 +69,18 @@ function MarkableRow({ row, chatId, bookmark, children }) {
66
69
  }
67
70
  return (_jsxs(_Fragment, { children: [_jsx(BookmarkMarker, { chatId: chatId, itemId: row.id, bookmark: bookmark }), _jsxs("div", { className: "group/message", children: [children, _jsx(MessageActions, { chatId: chatId, row: row })] })] }));
68
71
  }
72
+ /* Memoized with stable props, so a streamed word or a scroll draws only the rows it changed. */
73
+ const ThreadRow = memo(function ThreadRow({ row, previous, index, top, measure, chatId, flashNonce, bookmark, ...actions }) {
74
+ const header = replyHeader(row, previous);
75
+ return (_jsxs(RowContainer, { row: row, previous: previous, index: index, top: top, ref: measure, children: [flashNonce !== null && _jsx("span", { className: "chat-flash pointer-events-none absolute inset-0 rounded-lg", "aria-hidden": true }, flashNonce), header !== null && (_jsx("div", { className: "chat-column-content w-full", children: _jsx(ReplyHeader, { chatId: chatId, at: header.at }) })), _jsx(MarkableRow, { row: row, chatId: chatId, bookmark: bookmark, children: _jsx(Row, { row: row, chatId: chatId, ...actions }) })] }));
76
+ });
69
77
  /*
70
78
  * `overlay` stands in the thread's place and ends where the composer starts, so the thread keeps
71
79
  * where it was underneath and the composer stays in reach for the prompts it answers.
72
80
  */
73
81
  export function Timeline({ chatId, composer, overlay }) {
82
+ const host = useChatHost();
83
+ const { fork } = host;
74
84
  const { t } = useTranslation('agent-chat');
75
85
  const order = useChatRow(chatId, (row) => row?.order);
76
86
  // The structure, not the items. A delta growing a reply must not derive every row again.
@@ -137,12 +147,12 @@ export function Timeline({ chatId, composer, overlay }) {
137
147
  }
138
148
  return deriveTimelineRows(order.map((id) => items[id]), { expandedGroups: groups.ids, expandedTurns: turns.ids, expandedSubagents: subagents.ids, activeTurnId, forkedTurns });
139
149
  }, [order, items, groups.ids, turns.ids, subagents.ids, activeTurnId, forkedTurns]);
140
- const cards = chatHost().useThreadCards(scopeId, chatId);
141
- const drawsVisuals = chatHost().visuals !== null;
150
+ const cards = host.useThreadCards(scopeId, chatId);
151
+ const drawsVisuals = host.visuals !== null;
142
152
  // Where the part of the thread this client holds begins, while there is a page before it.
143
153
  const heldFrom = cursor === null ? null : (items?.[order?.[0] ?? '']?.createdAt ?? null);
144
154
  const rows = useMemo(() => withTimedRows(threadRows, timedRowsOf(cards, drawsVisuals ? (visuals ?? []) : null, heldFrom)), [threadRows, cards, drawsVisuals, visuals, heldFrom]);
145
- const welcomes = chatHost().useWelcome(chatId);
155
+ const welcomes = host.useWelcome(chatId);
146
156
  const empty = rows.length === 0;
147
157
  // A chat that took a turn before is one whose thread has not come in yet, not one to greet.
148
158
  const welcome = welcomes && empty && (info === null || (info.usage.turns === 0 && info.activeTurnId === null));
@@ -208,17 +218,11 @@ export function Timeline({ chatId, composer, overlay }) {
208
218
  overscan: 8,
209
219
  scrollPaddingEnd: coveredHeight
210
220
  });
211
- // A change in what the last row says (a streaming delta) must also pull the view down.
212
- const lastRow = rows[rows.length - 1];
213
- const lastReplyLength = useChatRow(chatId, (row) => {
214
- const item = lastRow?.kind === 'assistant' ? row?.items[lastRow.id] : undefined;
215
- return item?.kind === 'assistant' ? item.text.length : null;
216
- });
217
- const tail = lastReplyLength ?? rows.length;
218
221
  /*
219
- * Rows are measured as they render, so the end moves while it is drawn (a diff that highlights, an
220
- * image that loads). `scrollToIndex` aims at where a row was when asked and fell short; the
221
- * scroller's own bottom is a fact, and a new total size says a row was measured again.
222
+ * Rows are measured as they render, so the end moves while it is drawn (a streamed line, a diff that
223
+ * highlights, an image that loads). `scrollToIndex` aims at where a row was when asked and fell
224
+ * short; the scroller's own bottom is a fact, and a new total size says a row was measured again.
225
+ * A word that does not add a line moves nothing, so the thread does not listen to the words.
222
226
  */
223
227
  const totalSize = virtualizer.getTotalSize();
224
228
  useLayoutEffect(() => {
@@ -229,7 +233,7 @@ export function Timeline({ chatId, composer, overlay }) {
229
233
  // The composer is in the scroll flow, so its measured height is part of this end position.
230
234
  element.scrollTop = element.scrollHeight;
231
235
  scrollGeometry.current = { top: element.scrollTop, height: element.scrollHeight, viewport: element.clientHeight };
232
- }, [rows.length, tail, totalSize, composerHeight, frame.height]);
236
+ }, [rows.length, totalSize, composerHeight, frame.height]);
233
237
  // A page that went in above moves every row down by what it measures; the reader stays on the row they were on.
234
238
  useLayoutEffect(() => {
235
239
  const element = scrollRef.current;
@@ -261,7 +265,8 @@ export function Timeline({ chatId, composer, overlay }) {
261
265
  const onMainAgent = trail.length === 0;
262
266
  // A node on a canvas has no strip. It is too narrow to give up a column of its width.
263
267
  const showsScrubber = frame.column && onMainAgent && ticks.length >= SCRUBBER_MIN_TICKS;
264
- const paddingLeft = threadPaddingLeft(frame.width, showsScrubber);
268
+ const readingWidth = useChatPreferences((preferences) => preferences.readingWidth);
269
+ const paddingLeft = threadPaddingLeft(frame.width, showsScrubber, readingMeasure(readingWidth));
265
270
  /*
266
271
  * Where each message starts, from the virtualizer's measurements (an estimate for a row it never
267
272
  * drew), so reading the scroll position costs no layout.
@@ -271,8 +276,28 @@ export function Timeline({ chatId, composer, overlay }) {
271
276
  const scrollTop = virtualizer.scrollOffset ?? 0;
272
277
  const viewport = virtualizer.scrollRect?.height ?? 0;
273
278
  const visibleHeight = Math.max(0, viewport - coveredHeight);
279
+ const visibleHeightRef = useRef(visibleHeight);
280
+ visibleHeightRef.current = visibleHeight;
274
281
  const inView = showsScrubber ? messagesInView({ starts, scrollTop, visibleHeight }) : null;
275
- const chatFind = chatHost().useTimelineFind({
282
+ /*
283
+ * The virtualizer draws again only when its range of rows changes, which a scroll inside one long
284
+ * row never does; the scroll itself redraws the strip once the messages in view are others.
285
+ */
286
+ const [, redrawStrip] = useReducer((count) => count + 1, 0);
287
+ const stripRef = useRef({ starts, inView });
288
+ stripRef.current = { starts, inView };
289
+ const followStrip = (element) => {
290
+ const { starts: current, inView: shown } = stripRef.current;
291
+ if (shown === null) {
292
+ return;
293
+ }
294
+ const next = messagesInView({ starts: current, scrollTop: element.scrollTop, visibleHeight: visibleHeightRef.current });
295
+ if (next?.first !== shown.first || next?.last !== shown.last) {
296
+ redrawStrip();
297
+ }
298
+ };
299
+ // One object while what it holds stays, so a find that keys an effect on it does not run it for every scroll.
300
+ const findOptions = useMemo(() => ({
276
301
  chatId,
277
302
  frame: frameRef,
278
303
  thread: threadRef,
@@ -291,14 +316,15 @@ export function Timeline({ chatId, composer, overlay }) {
291
316
  // A row already on screen stays put; the find scrolls to the match itself once it is drawn.
292
317
  scrollToRow: (index) => {
293
318
  const top = scrollRef.current?.scrollTop ?? 0;
294
- const bottom = top + visibleHeight;
319
+ const bottom = top + visibleHeightRef.current;
295
320
  if (!virtualizer.getVirtualItems().some((virtualRow) => virtualRow.index === index && virtualRow.end > top && virtualRow.start < bottom)) {
296
321
  virtualizer.scrollToIndex(index, { align: 'center' });
297
322
  }
298
323
  },
299
324
  coveredHeight: () => coveredHeightRef.current,
300
325
  stopFollowing
301
- });
326
+ }), [chatId, onMainAgent, rows, items, order, virtualizer, turns.add, groups.add, subagents.add, stopFollowing]);
327
+ const chatFind = host.useTimelineFind(findOptions);
302
328
  const searching = chatFind.open;
303
329
  // A find covers the whole thread, so every page before this one comes in while it is open.
304
330
  useEffect(() => {
@@ -439,14 +465,14 @@ export function Timeline({ chatId, composer, overlay }) {
439
465
  const cardChat = useMemo(() => ({
440
466
  chatId,
441
467
  canFork: (turnId) => {
442
- if (chatHost().fork === null) {
468
+ if (fork === null) {
443
469
  return false;
444
470
  }
445
471
  const row = useChats.getState().byKey[keyOf(chatId)];
446
472
  return forkRefusal(row?.info ?? null, row?.structure[turnId]) === null;
447
473
  },
448
- fork: (turnId) => chatHost().fork?.(chatId, turnId)
449
- }), [keyOf, chatId]);
474
+ fork: (turnId) => fork?.(chatId, turnId)
475
+ }), [keyOf, chatId, fork]);
450
476
  const stepRef = useRef(() => false);
451
477
  stepRef.current = (direction) => {
452
478
  if (!onMainAgent) {
@@ -460,20 +486,25 @@ export function Timeline({ chatId, composer, overlay }) {
460
486
  return true;
461
487
  };
462
488
  useEffect(() => registerMessageStepper(keyOf(chatId), (direction) => stepRef.current(direction)), [keyOf, chatId]);
489
+ // Read through a ref, so the rows keep one callback for their life and their memo holds.
490
+ const opensRef = useRef({ rows, show });
491
+ opensRef.current = { rows, show };
463
492
  /* The whole of what a sub-agent did, in the thread's place. Its row only keeps the beginning. */
464
- const openConversation = (step) => {
465
- show(openFromMain(step));
466
- };
493
+ const openConversation = useCallback((step) => {
494
+ opensRef.current.show(openFromMain(step));
495
+ }, []);
496
+ const addSubagent = subagents.add;
467
497
  /* The header of a turn a sub-agent woke. It points at the row that agent worked in, or the one it hangs under. */
468
- const openSubagent = (toolUseId) => {
469
- const found = findSubagentBranch(rows, toolUseId);
498
+ const openSubagent = useCallback((toolUseId) => {
499
+ const found = findSubagentBranch(opensRef.current.rows, toolUseId);
470
500
  if (!found) {
471
501
  return;
472
502
  }
473
- subagents.add(found.branch.id);
503
+ addSubagent(found.branch.id);
474
504
  showRowAtTop(virtualizer, followRef, found.index);
475
- };
476
- return (_jsx(FileLinkContext.Provider, { value: info?.cwd ?? null, children: _jsxs("div", { ref: frameRef, className: "relative flex min-h-0 grow flex-col", "data-file-cwd": info?.cwd ?? '', "data-file-scope-id": scope.id, children: [searching && chatFind.bar, _jsxs(ContextMenu.Root, { children: [_jsx("div", { ref: scrollRef,
505
+ }, [addSubagent, virtualizer]);
506
+ const flashedRow = flash !== null && flash.key === keyOf(chatId) && !flash.block ? flash.rowId : null;
507
+ return (_jsx(FileLinkContext.Provider, { value: info?.cwd ?? null, children: _jsxs("div", { ref: frameRef, className: "relative flex min-h-0 grow flex-col", style: readingWidthStyle(readingWidth), "data-file-cwd": info?.cwd ?? '', "data-file-scope-id": scope.id, children: [searching && chatFind.bar, _jsxs(ContextMenu.Root, { children: [_jsx("div", { ref: scrollRef,
477
508
  // No CSS scroll-padding here: Chromium counts the sticky composer's caret as hidden behind
478
509
  // it and scrolls the thread toward the end on every keystroke.
479
510
  className: "chat-scroll min-h-0 grow overflow-auto",
@@ -495,15 +526,14 @@ export function Timeline({ chatId, composer, overlay }) {
495
526
  if (wantsEarlier(el)) {
496
527
  loadEarlier();
497
528
  }
529
+ followStrip(el);
498
530
  }, children: _jsxs("div", { ref: contentRef, className: clsx('relative flex min-h-full flex-col', welcome && 'justify-center'), children: [_jsx(ContextMenu.Trigger, { ref: threadRef, className: clsx('chat-thread flex flex-col px-4 pt-4', !welcome && 'grow'), style: {
499
531
  paddingLeft,
500
532
  '--chat-wide-inset': `${2 * (showsScrubber ? STRIP_CLEARANCE_PX : paddingLeft) - paddingLeft}px`
501
- }, inert: overlay ? true : undefined, onContextMenu: (e) => setTarget(readTimelineTarget(e.target, threadRef.current, withCurrentText(rows, fullItems()))), children: welcome ? (_jsx(WelcomeGreeting, { chatId: chatId })) : empty ? (_jsx(EmptyThread, { chatId: chatId })) : (_jsx("div", { className: "relative w-full shrink-0", style: { height: virtualizer.getTotalSize() }, children: virtualizer.getVirtualItems().map((virtualRow) => {
502
- const row = rows[virtualRow.index];
503
- const previous = virtualRow.index > 0 ? rows[virtualRow.index - 1] : null;
504
- const header = replyHeader(row, previous);
505
- return (_jsxs(RowContainer, { row: row, previous: previous, index: virtualRow.index, top: virtualRow.start, ref: virtualizer.measureElement, children: [flash !== null && flash.key === keyOf(chatId) && flash.rowId === row.id && !flash.block && (_jsx("span", { className: "chat-flash pointer-events-none absolute inset-0 rounded-lg", "aria-hidden": true }, flash.nonce)), header !== null && (_jsx("div", { className: "chat-column-content w-full", children: _jsx(ReplyHeader, { chatId: chatId, at: header.at }) })), _jsx(UiReplyNavigationContext.Provider, { value: uiNavigation, children: _jsx(FindRevealContext.Provider, { value: chatFind.reveal, children: _jsx(MarkableRow, { row: row, chatId: chatId, bookmark: marks.get(row.id) ?? null, children: _jsx(Row, { row: row, chatId: chatId, toggleGroup: groups.toggle, toggleTurn: turns.toggle, toggleSubagent: subagents.toggle, openSubagent: openSubagent, openConversation: openConversation }) }) }) })] }, row.id));
506
- }) })) }), _jsx(QuoteButton, { thread: threadRef, frame: contentRef, taker: quoteTakerRef }), _jsx("div", { ref: composerRef,
533
+ }, inert: overlay ? true : undefined, onContextMenu: (e) => setTarget(readTimelineTarget(e.target, threadRef.current, withCurrentText(rows, fullItems()))), children: welcome ? (_jsx(WelcomeGreeting, { chatId: chatId })) : empty ? (_jsx(EmptyThread, { chatId: chatId })) : (_jsx("div", { className: "relative w-full shrink-0", style: { height: virtualizer.getTotalSize() }, children: _jsx(UiReplyNavigationContext.Provider, { value: uiNavigation, children: _jsx(FindRevealContext.Provider, { value: chatFind.reveal, children: virtualizer.getVirtualItems().map((virtualRow) => {
534
+ const row = rows[virtualRow.index];
535
+ return (_jsx(ThreadRow, { row: row, previous: virtualRow.index > 0 ? rows[virtualRow.index - 1] : null, index: virtualRow.index, top: virtualRow.start, measure: virtualizer.measureElement, chatId: chatId, flashNonce: flash !== null && flashedRow === row.id ? flash.nonce : null, bookmark: marks.get(row.id) ?? null, toggleGroup: groups.toggle, toggleTurn: turns.toggle, toggleSubagent: subagents.toggle, openSubagent: openSubagent, openConversation: openConversation }, row.id));
536
+ }) }) }) })) }), _jsx(QuoteButton, { thread: threadRef, frame: contentRef, taker: quoteTakerRef }), _jsx("div", { ref: composerRef,
507
537
  // The card is translucent so the thread shows through it, but the pb-3 strip under it must stay opaque.
508
- className: clsx('relative z-10 shrink-0', composer && 'px-3 pb-3 pt-3 bg-[linear-gradient(to_bottom,transparent_calc(100%-12px),var(--surface)_calc(100%-12px))]'), style: { position: stickyComposer ? 'sticky' : 'relative', bottom: 0 }, children: _jsx(QuoteTakerContext.Provider, { value: registerQuoteTaker, children: composer }) })] }) }), _jsx(TimelineMenuPopup, { target: target, thread: threadRef, chatId: onMainAgent ? chatId : null })] }), drawsVisuals && _jsx(VisualDialogs, { chatId: chatId }), loadingEarlier && !overlay && (_jsxs("div", { className: "pointer-events-none absolute inset-x-0 top-3 z-10 flex justify-center", role: "status", children: [_jsx(Icon, { icon: LoaderCircle, size: 16, className: "animate-spin text-text-faint" }), _jsx("span", { className: "sr-only", children: t('timeline.loadingEarlier') })] })), showsScrubber && !overlay && (_jsx("div", { className: "absolute top-4", style: { left: STRIP_INSET_PX, width: STRIP_WIDTH_PX, bottom: coveredHeight }, children: _jsx(ErrorBoundary, { label: t('timeline.stripFailed'), resetKeys: [ticks.length], children: _jsx(Scrubber, { ticks: ticks, firstInView: inView?.first ?? null, lastInView: inView?.last ?? null, onPick: pick, chat: cardChat, found: found, foundCurrent: foundCurrent }) }) })), overlay && (_jsx("div", { className: "absolute inset-0 z-5 flex flex-col bg-surface", style: { paddingBottom: coveredHeight }, children: overlay }))] }) }));
538
+ className: clsx('relative z-10 shrink-0', composer && 'px-3 pb-3 pt-3 bg-[linear-gradient(to_bottom,transparent_calc(100%-12px),var(--surface)_calc(100%-12px))]'), style: { position: stickyComposer ? 'sticky' : 'relative', bottom: 0 }, children: _jsx(QuoteTakerContext.Provider, { value: registerQuoteTaker, children: composer }) })] }) }), _jsx(TimelineMenuPopup, { target: target, thread: threadRef, chatId: onMainAgent ? chatId : null, column: frame.column })] }), drawsVisuals && _jsx(VisualDialogs, { chatId: chatId }), loadingEarlier && !overlay && (_jsxs("div", { className: "pointer-events-none absolute inset-x-0 top-3 z-10 flex justify-center", role: "status", children: [_jsx(Icon, { icon: LoaderCircle, size: 16, className: "animate-spin text-text-faint" }), _jsx("span", { className: "sr-only", children: t('timeline.loadingEarlier') })] })), showsScrubber && !overlay && (_jsx("div", { className: "absolute top-4", style: { left: STRIP_INSET_PX, width: STRIP_WIDTH_PX, bottom: coveredHeight }, children: _jsx(ErrorBoundary, { label: t('timeline.stripFailed'), resetKeys: [ticks.length], children: _jsx(Scrubber, { ticks: ticks, firstInView: inView?.first ?? null, lastInView: inView?.last ?? null, onPick: pick, chat: cardChat, found: found, foundCurrent: foundCurrent }) }) })), overlay && (_jsx("div", { className: "absolute inset-0 z-5 flex flex-col bg-surface", style: { paddingBottom: coveredHeight }, children: overlay }))] }) }));
509
539
  }
@@ -1,9 +1,10 @@
1
1
  import type { RefObject } from 'react';
2
2
  import type { TimelineTarget } from '../logic/timeline-target.js';
3
- export declare function TimelineMenuPopup({ target, thread, chatId }: {
3
+ export declare function TimelineMenuPopup({ target, thread, chatId, column }: {
4
4
  target: TimelineTarget;
5
5
  thread: RefObject<HTMLDivElement | null>;
6
6
  chatId?: string | null;
7
+ column?: boolean;
7
8
  }): import("react").JSX.Element;
8
9
  export declare function BookmarkMenuItems({ chatId, itemId, onName }: {
9
10
  chatId: string;
@@ -1,16 +1,18 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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.js';
5
+ import { rememberReadingWidth, useChatPreferences } from '../preferences.js';
6
+ import { CHAT_READING_WIDTHS, readingWidthLabel } from '../reading-width.js';
5
7
  import { forkRefusal, turnIdOfRow } from '../logic/fork.js';
6
8
  import { markdownOf, messageTextOf } from '../logic/timeline-copy.js';
7
- import { chatHost } from '../../host.js';
9
+ import { useChatHost } from '../../host.js';
8
10
  import { useChatScope } from '../../scope.js';
9
11
  import { useChatRow } from '../../state/chats.js';
10
- import { openFileLink, useFileLinkCwd, useFileLinkScopeId } from './file-links.js';
12
+ import { useFileLinkCwd, useFileLinkScopeId, useOpenFileLink } from './file-links.js';
11
13
  import { ContextMenu, copyText, DisabledReason, EDIT_SHORTCUTS, Icon, Kbd, selectAllWithin } from '@adecore/ui';
12
14
  /* The menu behind a right-click in a thread, mostly for copying: a whole message, a code block or an answer's markdown. */
13
- export function TimelineMenuPopup({ target, thread, chatId = null }) {
15
+ export function TimelineMenuPopup({ target, thread, chatId = null, column = false }) {
14
16
  const { t } = useTranslation('agent-chat');
15
17
  const message = target.row === null ? null : messageTextOf(target.row);
16
18
  const turnId = chatId === null || target.row === null ? null : turnIdOfRow(target.row);
@@ -20,7 +22,8 @@ export function TimelineMenuPopup({ target, thread, chatId = null }) {
20
22
  // A thread outside a chat (a sub-agent's transcript) has no cwd of its own; the app answers for it then.
21
23
  const cwd = useFileLinkCwd();
22
24
  const scopeId = useFileLinkScopeId();
23
- const { fork, fileLinks } = chatHost();
25
+ const { fork, fileLinks } = useChatHost();
26
+ const openFileLink = useOpenFileLink();
24
27
  const openInPreview = () => {
25
28
  if (target.file != null) {
26
29
  openFileLink(target.file.cwd, target.file.ref, target.file.scopeId);
@@ -29,7 +32,7 @@ export function TimelineMenuPopup({ target, thread, chatId = null }) {
29
32
  openFileLink(cwd, { path: target.path, ...(target.line === null ? {} : { line: target.line }), directory: false }, scopeId);
30
33
  }
31
34
  };
32
- return (_jsxs(ContextMenu.Popup, { children: [_jsxs(ContextMenu.Item, { disabled: target.selection === '', onClick: () => copyText(target.selection), children: [_jsx(Icon, { icon: Copy, size: 14 }), " ", t('common.action.copy'), " ", _jsx(Kbd, { shortcut: EDIT_SHORTCUTS.copy })] }), message !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(message), children: [_jsx(Icon, { icon: MessageSquare, size: 14 }), " ", t('timeline.menu.copyMessage')] })), target.code !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(target.code ?? ''), children: [_jsx(Icon, { icon: Braces, size: 14 }), " ", t('timeline.menu.copyCode')] })), markdown !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(markdown), children: [_jsx(Icon, { icon: FileText, size: 14 }), " ", t('timeline.menu.copyMarkdown')] })), chatId !== null && turnId !== null && fork !== null && (_jsx(DisabledReason, { reason: forkBlocked, children: _jsxs(ContextMenu.Item, { disabled: forkBlocked !== null, onClick: () => fork(chatId, turnId), children: [_jsx(Icon, { icon: GitFork, size: 14 }), " ", t('timeline.menu.forkFromHere')] }) })), chatId !== null && markable !== null && _jsx(BookmarkMenuItems, { chatId: chatId, itemId: markable }), _jsx(ContextMenu.Separator, {}), _jsxs(ContextMenu.Item, { onClick: () => selectAllWithin(thread.current), children: [_jsx(Icon, { icon: Scan, size: 14 }), " ", t('common.action.selectAll')] }), target.path !== null && fileLinks !== null && (_jsxs(_Fragment, { children: [_jsx(ContextMenu.Separator, {}), _jsxs(ContextMenu.Item, { onClick: openInPreview, children: [_jsx(Icon, { icon: Eye, size: 14 }), " ", t('timeline.menu.openInPreview')] })] }))] }));
35
+ return (_jsxs(ContextMenu.Popup, { children: [_jsxs(ContextMenu.Item, { disabled: target.selection === '', onClick: () => copyText(target.selection), children: [_jsx(Icon, { icon: Copy, size: 14 }), " ", t('common.action.copy'), " ", _jsx(Kbd, { shortcut: EDIT_SHORTCUTS.copy })] }), message !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(message), children: [_jsx(Icon, { icon: MessageSquare, size: 14 }), " ", t('timeline.menu.copyMessage')] })), target.code !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(target.code ?? ''), children: [_jsx(Icon, { icon: Braces, size: 14 }), " ", t('timeline.menu.copyCode')] })), markdown !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(markdown), children: [_jsx(Icon, { icon: FileText, size: 14 }), " ", t('timeline.menu.copyMarkdown')] })), chatId !== null && turnId !== null && fork !== null && (_jsx(DisabledReason, { reason: forkBlocked, children: _jsxs(ContextMenu.Item, { disabled: forkBlocked !== null, onClick: () => fork(chatId, turnId), children: [_jsx(Icon, { icon: GitFork, size: 14 }), " ", t('timeline.menu.forkFromHere')] }) })), chatId !== null && markable !== null && _jsx(BookmarkMenuItems, { chatId: chatId, itemId: markable }), _jsx(ContextMenu.Separator, {}), _jsxs(ContextMenu.Item, { onClick: () => selectAllWithin(thread.current), children: [_jsx(Icon, { icon: Scan, size: 14 }), " ", t('common.action.selectAll')] }), target.path !== null && fileLinks !== null && (_jsxs(_Fragment, { children: [_jsx(ContextMenu.Separator, {}), _jsxs(ContextMenu.Item, { onClick: openInPreview, children: [_jsx(Icon, { icon: Eye, size: 14 }), " ", t('timeline.menu.openInPreview')] })] })), column && (_jsxs(_Fragment, { children: [_jsx(ContextMenu.Separator, {}), _jsx(ReadingWidthSubmenu, {})] }))] }));
33
36
  }
34
37
  /*
35
38
  * Place a bookmark on a message, or rename and remove the one it has, from the thread and from the strip.
@@ -38,15 +41,22 @@ export function TimelineMenuPopup({ target, thread, chatId = null }) {
38
41
  export function BookmarkMenuItems({ chatId, itemId, onName }) {
39
42
  const { t } = useTranslation('agent-chat');
40
43
  const scope = useChatScope();
44
+ const host = useChatHost();
41
45
  const bookmark = useChatRow(chatId, (row) => row?.bookmarks?.find((candidate) => candidate.itemId === itemId) ?? null);
42
46
  if (bookmark === null) {
43
47
  return (_jsxs(ContextMenu.Item, { onClick: () => {
44
- void placeBookmark(scope, chatId, itemId);
48
+ void placeBookmark(scope, host, chatId, itemId);
45
49
  onName?.();
46
50
  }, children: [_jsx(Icon, { icon: BookmarkPlus, size: 14 }), " ", t('bookmarks.add')] }));
47
51
  }
48
52
  return (_jsxs(_Fragment, { children: [_jsxs(ContextMenu.Item, { onClick: () => {
49
53
  useBookmarkNaming.getState().open(scope.keyOf(chatId), bookmark.itemId);
50
54
  onName?.();
51
- }, children: [_jsx(Icon, { icon: Pencil, size: 14 }), " ", t('bookmarks.rename')] }), _jsxs(ContextMenu.Item, { onClick: () => void removeBookmark(scope, chatId, bookmark), children: [_jsx(Icon, { icon: BookmarkX, size: 14 }), " ", t('bookmarks.remove')] })] }));
55
+ }, children: [_jsx(Icon, { icon: Pencil, size: 14 }), " ", t('bookmarks.rename')] }), _jsxs(ContextMenu.Item, { onClick: () => void removeBookmark(scope, host, chatId, bookmark), children: [_jsx(Icon, { icon: BookmarkX, size: 14 }), " ", t('bookmarks.remove')] })] }));
56
+ }
57
+ /* The person's reading width for every chat of its own on this client. */
58
+ function ReadingWidthSubmenu() {
59
+ const { t } = useTranslation('agent-chat');
60
+ const readingWidth = useChatPreferences((preferences) => preferences.readingWidth);
61
+ return (_jsxs(ContextMenu.SubmenuRoot, { children: [_jsxs(ContextMenu.SubmenuTrigger, { children: [_jsx(Icon, { icon: MoveHorizontal, size: 14 }), " ", t('readingWidth.label')] }), _jsx(ContextMenu.Popup, { className: "min-w-40", children: _jsx(ContextMenu.RadioGroup, { value: readingWidth, onValueChange: (width) => rememberReadingWidth(width), children: CHAT_READING_WIDTHS.map((width) => (_jsx(ContextMenu.RadioItem, { value: width, children: readingWidthLabel(width) }, width))) }) })] }));
52
62
  }
@@ -7,13 +7,14 @@ import { DiffOmitted } from './diff/DiffOmitted.js';
7
7
  import { DiffUnavailable } from './diff/DiffUnavailable.js';
8
8
  import { RawDiff } from './diff/RawDiff.js';
9
9
  import { DiffFallbackBoundary } from './diff/DiffFallbackBoundary.js';
10
- import { chatHost } from '../../host.js';
10
+ import { useChatHost } from '../../host.js';
11
11
  const LegacyUnifiedDiff = lazyNamed(() => import('./diff/LegacyUnifiedDiff.js'), 'default', { prefetch: false });
12
12
  const EditorPatchDiff = lazyNamed(() => import('./diff/EditorPatchDiff.js'), 'default');
13
13
  export default function UnifiedDiff(props) {
14
+ const host = useChatHost();
14
15
  const { t } = useTranslation('agent-chat');
15
16
  const { change, contents, allowEditor = true, ignoreWhitespace = false, overflow = 'wrap', diffStyle = 'unified', fill = false, maxHeight } = props;
16
- const loadEngine = chatHost().code.editor;
17
+ const loadEngine = host.code.editor;
17
18
  const oldText = contents?.old ?? '';
18
19
  const newText = contents?.new ?? '';
19
20
  const sourceAllowed = useMemo(() => diffSourceBudget(oldText, newText) && patchSourceBudget(change.diff), [oldText, newText, change.diff]);