@adecore/agents-react 0.38.0 → 0.40.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 (351) hide show
  1. package/dist/agents/AgentModelLabel.d.ts +10 -0
  2. package/dist/agents/AgentModelLabel.js +98 -0
  3. package/dist/agents/model-name.js +2 -1
  4. package/dist/agents/provider-paths.d.ts +2 -1
  5. package/dist/agents/provider-paths.js +7 -3
  6. package/dist/chat/account-choice.js +57 -28
  7. package/dist/chat/bookmarks.js +2 -1
  8. package/dist/chat/drafts.d.ts +0 -1
  9. package/dist/chat/drafts.js +9 -28
  10. package/dist/chat/logic/picker-entries.js +2 -8
  11. package/dist/chat/logic/resume-compaction.js +2 -5
  12. package/dist/chat/logic/timeline-copy.js +2 -8
  13. package/dist/chat/logic/timeline.js +9 -10
  14. package/dist/chat/logic/workflow.js +2 -4
  15. package/dist/chat/subagent-conversation.js +2 -1
  16. package/dist/chat/ui/Composer.js +385 -360
  17. package/dist/chat/ui/GoalDock.js +2 -1
  18. package/dist/chat/ui/LimitState.js +4 -6
  19. package/dist/chat/ui/Pickers.d.ts +2 -1
  20. package/dist/chat/ui/Pickers.js +126 -123
  21. package/dist/chat/ui/PromptEditor.d.ts +19 -0
  22. package/dist/chat/ui/PromptEditor.js +115 -0
  23. package/dist/chat/ui/Timeline.d.ts +2 -1
  24. package/dist/chat/ui/Timeline.js +13 -1
  25. package/dist/chat/ui/composer/ComposerControls.d.ts +3 -0
  26. package/dist/chat/ui/composer/ComposerControls.js +235 -38
  27. package/dist/chat/ui/composer/key-handler.d.ts +1 -1
  28. package/dist/chat/ui/composer/key-handler.js +3 -11
  29. package/dist/chat/ui/composer/prompt-picks.d.ts +4 -0
  30. package/dist/chat/ui/composer/prompt-picks.js +14 -0
  31. package/dist/chat/ui/composer/use-composer-menus.d.ts +4 -49
  32. package/dist/chat/ui/composer/use-composer-menus.js +65 -519
  33. package/dist/chat/ui/composer/use-continuable-turn.js +0 -1
  34. package/dist/chat/ui/composer/use-notice.js +2 -1
  35. package/dist/chat/ui/composer/use-prompt-history.js +1 -1
  36. package/dist/chat/ui/composer/use-prompt-menus.d.ts +73 -0
  37. package/dist/chat/ui/composer/use-prompt-menus.js +499 -0
  38. package/dist/chat/ui/composer/use-take-back.js +2 -2
  39. package/dist/chat/ui/diff/ApprovalReview.js +1 -1
  40. package/dist/chat/ui/highlighter.d.ts +0 -6
  41. package/dist/chat/ui/highlighter.js +0 -12
  42. package/dist/chat/ui/icons.d.ts +0 -1
  43. package/dist/chat/ui/icons.js +0 -3
  44. package/dist/chat/ui/index.d.ts +3 -0
  45. package/dist/chat/ui/index.js +2 -0
  46. package/dist/chat/ui/rows/MessageRows.js +2 -5
  47. package/dist/chat/ui/rows/row-rhythm.js +2 -5
  48. package/dist/chat/ui/theme-watch.js +3 -7
  49. package/dist/host-hooks.d.ts +2 -1
  50. package/dist/host-hooks.js +6 -0
  51. package/dist/host.d.ts +17 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/locales/en/agent-chat.d.ts +1 -0
  54. package/dist/locales/en/agent-chat.js +1 -0
  55. package/dist/locales/nl/agent-chat.d.ts +1 -0
  56. package/dist/locales/nl/agent-chat.js +1 -0
  57. package/dist/prompts/logic/usePromptSession.js +2 -1
  58. package/dist/providers/AddAccountForm.js +2 -1
  59. package/dist/providers/CliDetail.js +104 -130
  60. package/dist/providers/account-actions.js +6 -9
  61. package/dist/providers/index.d.ts +1 -0
  62. package/dist/providers/index.js +1 -0
  63. package/dist/state/chats.d.ts +1 -0
  64. package/dist/state/chats.js +6 -11
  65. package/dist/state/provider-accounts.d.ts +1 -1
  66. package/dist/state/provider-accounts.js +2 -8
  67. package/dist/theme.css +3 -0
  68. package/dist/transport.d.ts +2 -0
  69. package/dist/transport.js +14 -0
  70. package/dist/usage/ChartLogo.d.ts +5 -0
  71. package/dist/usage/ChartLogo.js +48 -0
  72. package/dist/usage/LimitsList.js +6 -16
  73. package/dist/usage/UsageBreakdown.js +17 -40
  74. package/dist/usage/UsageLimits.js +183 -211
  75. package/dist/usage/UsagePage.js +3 -17
  76. package/dist/usage/UsageSummary.js +4 -9
  77. package/dist/usage/limits.js +16 -31
  78. package/dist/usage/summary.d.ts +1 -1
  79. package/dist/usage/summary.js +3 -5
  80. package/package.json +26 -26
  81. package/src/agents/AccountDot.tsx +1 -0
  82. package/src/agents/AgentIcon.tsx +2 -0
  83. package/src/agents/AgentModelLabel.tsx +38 -0
  84. package/src/agents/ModelOptionControl.tsx +8 -0
  85. package/src/agents/account-limits.ts +3 -0
  86. package/src/agents/accounts.ts +12 -0
  87. package/src/agents/model-name.ts +13 -1
  88. package/src/agents/model-options.ts +2 -0
  89. package/src/agents/model-requirement.ts +4 -0
  90. package/src/agents/option-words.ts +2 -0
  91. package/src/agents/provider-paths.ts +8 -3
  92. package/src/agents/status-look.ts +1 -0
  93. package/src/chat/account-choice.ts +25 -19
  94. package/src/chat/actions.ts +5 -0
  95. package/src/chat/attachments.ts +8 -0
  96. package/src/chat/bookmarks.ts +13 -1
  97. package/src/chat/chat-client.ts +63 -0
  98. package/src/chat/chat-references.ts +3 -0
  99. package/src/chat/composer-effort.ts +5 -0
  100. package/src/chat/composer-send.ts +4 -0
  101. package/src/chat/drafts.ts +21 -17
  102. package/src/chat/forks.ts +4 -0
  103. package/src/chat/guards.ts +2 -0
  104. package/src/chat/index.ts +7 -0
  105. package/src/chat/logic/ansi.ts +47 -0
  106. package/src/chat/logic/bookmarks.ts +7 -0
  107. package/src/chat/logic/command-looks.ts +29 -0
  108. package/src/chat/logic/context-usage.ts +3 -0
  109. package/src/chat/logic/continue-after-stop.ts +5 -0
  110. package/src/chat/logic/effort.ts +4 -0
  111. package/src/chat/logic/embedded-code.ts +63 -0
  112. package/src/chat/logic/fork.ts +22 -0
  113. package/src/chat/logic/generated-image.ts +7 -0
  114. package/src/chat/logic/goal.ts +7 -0
  115. package/src/chat/logic/handback.ts +2 -0
  116. package/src/chat/logic/limit.ts +8 -0
  117. package/src/chat/logic/picker-entries.ts +16 -4
  118. package/src/chat/logic/resume-compaction.ts +22 -5
  119. package/src/chat/logic/scrubber.ts +38 -5
  120. package/src/chat/logic/thought.ts +5 -0
  121. package/src/chat/logic/thread-cards.ts +6 -0
  122. package/src/chat/logic/timeline-copy.ts +4 -9
  123. package/src/chat/logic/timeline-target.ts +15 -0
  124. package/src/chat/logic/timeline.ts +113 -10
  125. package/src/chat/logic/tools.ts +30 -0
  126. package/src/chat/logic/visual-height.ts +5 -0
  127. package/src/chat/logic/visual-viewport.ts +1 -0
  128. package/src/chat/logic/welcome.ts +3 -0
  129. package/src/chat/logic/workflow.ts +26 -7
  130. package/src/chat/mentions.ts +56 -0
  131. package/src/chat/pending-prompts.ts +16 -0
  132. package/src/chat/persisted-json.ts +15 -0
  133. package/src/chat/preferences.ts +16 -0
  134. package/src/chat/prompt-hold.ts +2 -0
  135. package/src/chat/quote.ts +7 -0
  136. package/src/chat/reading-width.ts +1 -0
  137. package/src/chat/recent-messages.ts +5 -0
  138. package/src/chat/resume-compaction-dismissals.ts +1 -0
  139. package/src/chat/stash.ts +12 -0
  140. package/src/chat/subagent-conversation.ts +24 -4
  141. package/src/chat/subagent-list.ts +14 -0
  142. package/src/chat/subagent-support.ts +1 -0
  143. package/src/chat/subagent-view.ts +9 -0
  144. package/src/chat/thumbnails.ts +14 -0
  145. package/src/chat/timeline-flash.ts +2 -0
  146. package/src/chat/timeline-scroll.ts +23 -0
  147. package/src/chat/ui/AccountPill.tsx +4 -0
  148. package/src/chat/ui/AnsiOutput.tsx +1 -0
  149. package/src/chat/ui/BookmarkMarker.tsx +5 -0
  150. package/src/chat/ui/BookmarkSubmenu.tsx +1 -0
  151. package/src/chat/ui/ChatActivity.tsx +13 -0
  152. package/src/chat/ui/CodeBlock.tsx +8 -0
  153. package/src/chat/ui/Composer.tsx +27 -3
  154. package/src/chat/ui/ComposerInput.tsx +22 -0
  155. package/src/chat/ui/DiffPool.tsx +3 -0
  156. package/src/chat/ui/EditDiff.tsx +5 -0
  157. package/src/chat/ui/GoalDock.tsx +13 -1
  158. package/src/chat/ui/ImageView.tsx +11 -0
  159. package/src/chat/ui/LimitState.tsx +23 -6
  160. package/src/chat/ui/Markdown.tsx +41 -0
  161. package/src/chat/ui/MermaidBlock.tsx +16 -0
  162. package/src/chat/ui/MessageActions.tsx +8 -0
  163. package/src/chat/ui/ModelMarks.tsx +1 -0
  164. package/src/chat/ui/Pickers.tsx +30 -1
  165. package/src/chat/ui/PromptComposer.tsx +2 -0
  166. package/src/chat/ui/PromptEditor.tsx +136 -0
  167. package/src/chat/ui/QuoteButton.tsx +12 -0
  168. package/src/chat/ui/ReferenceChip.tsx +3 -0
  169. package/src/chat/ui/ResumeCompactionDock.tsx +1 -0
  170. package/src/chat/ui/RowTime.tsx +1 -0
  171. package/src/chat/ui/RunSettings.tsx +38 -0
  172. package/src/chat/ui/Scrubber.tsx +14 -0
  173. package/src/chat/ui/SessionLine.tsx +1 -0
  174. package/src/chat/ui/ShellCommand.tsx +32 -0
  175. package/src/chat/ui/SubagentControls.tsx +7 -0
  176. package/src/chat/ui/SubagentInfo.tsx +1 -0
  177. package/src/chat/ui/SubagentStopButton.tsx +6 -0
  178. package/src/chat/ui/SubagentTimeline.tsx +10 -0
  179. package/src/chat/ui/Timeline.tsx +141 -1
  180. package/src/chat/ui/TimelineMenu.tsx +5 -0
  181. package/src/chat/ui/UnifiedDiff.tsx +6 -0
  182. package/src/chat/ui/UploadThumb.tsx +2 -0
  183. package/src/chat/ui/VisualDialogs.tsx +3 -0
  184. package/src/chat/ui/VisualFrame.tsx +15 -0
  185. package/src/chat/ui/Welcome.tsx +1 -0
  186. package/src/chat/ui/chip-clipboard.ts +20 -0
  187. package/src/chat/ui/code-lines.ts +14 -0
  188. package/src/chat/ui/code-theme.ts +1 -0
  189. package/src/chat/ui/composer/ComposerControls.tsx +70 -15
  190. package/src/chat/ui/composer/ComposerMenu.tsx +12 -0
  191. package/src/chat/ui/composer/DraftParts.tsx +2 -0
  192. package/src/chat/ui/composer/QueuedMessages.tsx +4 -0
  193. package/src/chat/ui/composer/chip-paste.ts +16 -0
  194. package/src/chat/ui/composer/chips.ts +6 -0
  195. package/src/chat/ui/composer/editor.ts +9 -0
  196. package/src/chat/ui/composer/insert.ts +2 -0
  197. package/src/chat/ui/composer/key-handler.ts +42 -9
  198. package/src/chat/ui/composer/keys.ts +23 -0
  199. package/src/chat/ui/composer/placeholder.ts +6 -0
  200. package/src/chat/ui/composer/prompt-picks.ts +18 -0
  201. package/src/chat/ui/composer/surface.ts +5 -0
  202. package/src/chat/ui/composer/use-chat-controls.ts +13 -0
  203. package/src/chat/ui/composer/use-compaction-offer.ts +3 -0
  204. package/src/chat/ui/composer/use-composer-menus.ts +20 -275
  205. package/src/chat/ui/composer/use-composer-submit.ts +27 -0
  206. package/src/chat/ui/composer/use-continuable-turn.ts +3 -5
  207. package/src/chat/ui/composer/use-draft-files.ts +24 -0
  208. package/src/chat/ui/composer/use-effort-stepper.ts +8 -0
  209. package/src/chat/ui/composer/use-notice.ts +4 -1
  210. package/src/chat/ui/composer/use-pending-requests.ts +3 -0
  211. package/src/chat/ui/composer/use-prompt-history.ts +20 -11
  212. package/src/chat/ui/composer/use-prompt-hold.ts +3 -0
  213. package/src/chat/ui/composer/use-prompt-menus.ts +361 -0
  214. package/src/chat/ui/composer/use-take-back.ts +20 -2
  215. package/src/chat/ui/diff/ApprovalReview.tsx +18 -15
  216. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +1 -0
  217. package/src/chat/ui/diff/DiffOmitted.tsx +1 -0
  218. package/src/chat/ui/diff/DiffStack.tsx +2 -0
  219. package/src/chat/ui/diff/DiffUnavailable.tsx +1 -0
  220. package/src/chat/ui/diff/EditorEditDiff.tsx +13 -0
  221. package/src/chat/ui/diff/EditorPatchDiff.tsx +4 -0
  222. package/src/chat/ui/diff/LegacyDiffPool.tsx +2 -0
  223. package/src/chat/ui/diff/LegacyEditDiff.tsx +2 -0
  224. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +8 -0
  225. package/src/chat/ui/diff/RawDiff.tsx +1 -0
  226. package/src/chat/ui/diff/ReviewDiff.tsx +1 -0
  227. package/src/chat/ui/diff/diff-source-budget.ts +8 -0
  228. package/src/chat/ui/diff/editor-edit-model.ts +4 -0
  229. package/src/chat/ui/diff/review-plan.ts +9 -0
  230. package/src/chat/ui/diff/review-settle.ts +1 -0
  231. package/src/chat/ui/diff/syntax-runtime.ts +52 -0
  232. package/src/chat/ui/diff/use-review-stack.ts +9 -0
  233. package/src/chat/ui/diff-syntax.ts +12 -0
  234. package/src/chat/ui/diff-theme.ts +2 -0
  235. package/src/chat/ui/file-links.ts +2 -0
  236. package/src/chat/ui/find-reveal.ts +1 -0
  237. package/src/chat/ui/full-diff.ts +1 -0
  238. package/src/chat/ui/highlighter.ts +29 -15
  239. package/src/chat/ui/icons.tsx +2 -4
  240. package/src/chat/ui/index.ts +7 -0
  241. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +25 -0
  242. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +1 -0
  243. package/src/chat/ui/intelligent-ui/UiReply.tsx +63 -0
  244. package/src/chat/ui/intelligent-ui/block-local.ts +5 -0
  245. package/src/chat/ui/intelligent-ui/chart-data.ts +15 -0
  246. package/src/chat/ui/intelligent-ui/node-text.ts +17 -0
  247. package/src/chat/ui/intelligent-ui/renderers/binding.ts +1 -0
  248. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +31 -0
  249. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +4 -0
  250. package/src/chat/ui/intelligent-ui/renderers/content.tsx +18 -0
  251. package/src/chat/ui/intelligent-ui/renderers/data.tsx +11 -0
  252. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +13 -0
  253. package/src/chat/ui/intelligent-ui/renderers/links.tsx +9 -0
  254. package/src/chat/ui/intelligent-ui/renderers/map-slots.ts +15 -0
  255. package/src/chat/ui/intelligent-ui/renderers/map-style.ts +15 -0
  256. package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +48 -0
  257. package/src/chat/ui/intelligent-ui/renderers/map.tsx +20 -0
  258. package/src/chat/ui/intelligent-ui/renderers/place-point.ts +3 -0
  259. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +3 -0
  260. package/src/chat/ui/intelligent-ui/renderers/text.tsx +8 -0
  261. package/src/chat/ui/intelligent-ui/reply-parts.ts +5 -0
  262. package/src/chat/ui/intelligent-ui/table-data.ts +17 -0
  263. package/src/chat/ui/intelligent-ui/use-in-view.ts +4 -0
  264. package/src/chat/ui/intelligent-ui/use-later.ts +4 -0
  265. package/src/chat/ui/intelligent-ui/use-truncated.ts +5 -0
  266. package/src/chat/ui/intelligent-ui/use-ui-links.ts +29 -0
  267. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +60 -0
  268. package/src/chat/ui/markdown-blocks.ts +64 -0
  269. package/src/chat/ui/mermaid.ts +4 -0
  270. package/src/chat/ui/quote-selection.ts +10 -0
  271. package/src/chat/ui/rehype-chips.ts +5 -0
  272. package/src/chat/ui/rehype-fade.ts +2 -0
  273. package/src/chat/ui/remark-html-as-text.ts +5 -0
  274. package/src/chat/ui/reveal.ts +18 -0
  275. package/src/chat/ui/rows/ForksRow.tsx +5 -0
  276. package/src/chat/ui/rows/GeneratedImageRow.tsx +10 -0
  277. package/src/chat/ui/rows/MessageRows.tsx +29 -5
  278. package/src/chat/ui/rows/RowContainer.tsx +1 -0
  279. package/src/chat/ui/rows/Rows.tsx +2 -0
  280. package/src/chat/ui/rows/SubagentRow.tsx +13 -0
  281. package/src/chat/ui/rows/TaskRow.tsx +7 -0
  282. package/src/chat/ui/rows/VisualRow.tsx +4 -0
  283. package/src/chat/ui/rows/WorkRows.tsx +46 -0
  284. package/src/chat/ui/rows/WorkflowRow.tsx +10 -0
  285. package/src/chat/ui/rows/row-rhythm.ts +7 -5
  286. package/src/chat/ui/theme-watch.ts +8 -7
  287. package/src/chat/ui/thread-references.ts +7 -0
  288. package/src/chat/ui/use-diff-source-tokens.ts +8 -0
  289. package/src/chat/ui/useToggleSet.ts +3 -0
  290. package/src/chat/ui/visual-theme.ts +30 -0
  291. package/src/chat/visual-bridge.ts +23 -0
  292. package/src/chat/visual-viewport.ts +12 -0
  293. package/src/host-hooks.ts +15 -5
  294. package/src/host.ts +29 -1
  295. package/src/index.ts +5 -0
  296. package/src/lazy.tsx +9 -0
  297. package/src/locales/en/agent-chat.ts +1 -0
  298. package/src/locales/nl/agent-chat.ts +1 -0
  299. package/src/mounted-registry.ts +4 -0
  300. package/src/port-transport.ts +32 -0
  301. package/src/prompts/index.ts +2 -0
  302. package/src/prompts/logic/focus.ts +5 -0
  303. package/src/prompts/logic/keys.ts +12 -0
  304. package/src/prompts/logic/prompts.ts +8 -0
  305. package/src/prompts/logic/subjects.ts +8 -0
  306. package/src/prompts/logic/useFocusAfterAnswer.ts +2 -0
  307. package/src/prompts/logic/usePromptSession.ts +13 -1
  308. package/src/prompts/ui/ApprovalBody.tsx +2 -0
  309. package/src/prompts/ui/PromptActions.tsx +2 -0
  310. package/src/prompts/ui/PromptCard.tsx +6 -0
  311. package/src/prompts/ui/PromptView.tsx +6 -0
  312. package/src/prompts/ui/QuestionBody.tsx +11 -0
  313. package/src/providers/AccountColors.tsx +1 -0
  314. package/src/providers/AccountDetail.tsx +7 -0
  315. package/src/providers/AccountVariables.tsx +2 -0
  316. package/src/providers/AddAccountForm.tsx +5 -1
  317. package/src/providers/CliDetail.tsx +6 -5
  318. package/src/providers/InstallPage.tsx +11 -0
  319. package/src/providers/ProviderList.tsx +3 -0
  320. package/src/providers/ProvidersPane.tsx +18 -0
  321. package/src/providers/account-actions.ts +10 -8
  322. package/src/providers/index.ts +1 -0
  323. package/src/providers/install-steps.ts +3 -0
  324. package/src/providers/pane-target.ts +5 -0
  325. package/src/providers/provider-abilities.ts +1 -0
  326. package/src/providers/provider-groups.ts +6 -0
  327. package/src/providers/variables.ts +12 -0
  328. package/src/scope.ts +2 -0
  329. package/src/state/chats.ts +61 -14
  330. package/src/state/index.ts +2 -0
  331. package/src/state/provider-accounts.ts +7 -9
  332. package/src/state/providers.ts +1 -0
  333. package/src/state/usage.ts +6 -0
  334. package/src/storage.ts +18 -0
  335. package/src/testing/prompts.ts +5 -0
  336. package/src/theme.css +3 -0
  337. package/src/transport.ts +18 -0
  338. package/src/usage/ChartLogo.tsx +12 -0
  339. package/src/usage/LimitsList.tsx +7 -8
  340. package/src/usage/UsageBreakdown.tsx +11 -4
  341. package/src/usage/UsageChart.tsx +5 -0
  342. package/src/usage/UsageLimits.tsx +10 -7
  343. package/src/usage/UsageLimitsCard.tsx +2 -0
  344. package/src/usage/UsagePage.tsx +16 -19
  345. package/src/usage/UsageSummary.tsx +4 -5
  346. package/src/usage/UsageTiles.tsx +2 -0
  347. package/src/usage/format.ts +7 -0
  348. package/src/usage/limit-groups.ts +14 -0
  349. package/src/usage/limits.ts +11 -13
  350. package/src/usage/money.ts +1 -0
  351. package/src/usage/summary.ts +15 -7
@@ -101,6 +101,7 @@ function EmptyThread({ chatId }: { chatId: string }) {
101
101
  const model = useModelName(info?.provider, info?.selection.model ?? '');
102
102
  const sources = useHostContextSources(chatId);
103
103
  const hidesFolder = useHostHidesFolder(chatId);
104
+
104
105
  return (
105
106
  <div className="flex grow items-center justify-center">
106
107
  <div className="flex max-w-sm flex-col items-center gap-3 px-6 py-8 text-center">
@@ -137,6 +138,7 @@ function MarkableRow({ row, chatId, bookmark, children }: { row: TimelineRow; ch
137
138
  </>
138
139
  );
139
140
  }
141
+
140
142
  return (
141
143
  <>
142
144
  <BookmarkMarker chatId={chatId} itemId={row.id} bookmark={bookmark} />
@@ -162,6 +164,7 @@ interface ThreadRowProps extends Omit<RowProps, 'openConversation'> {
162
164
  /* Memoized with stable props, so a streamed word or a scroll draws only the rows it changed. */
163
165
  const ThreadRow = memo(function ThreadRow({ row, previous, index, top, measure, chatId, flashNonce, bookmark, ...actions }: ThreadRowProps) {
164
166
  const header = replyHeader(row, previous);
167
+
165
168
  return (
166
169
  <RowContainer row={row} previous={previous} index={index} top={top} ref={measure}>
167
170
  {flashNonce !== null && <span key={flashNonce} className="chat-flash pointer-events-none absolute inset-0 rounded-lg" aria-hidden />}
@@ -180,8 +183,21 @@ const ThreadRow = memo(function ThreadRow({ row, previous, index, top, measure,
180
183
  /*
181
184
  * `overlay` stands in the thread's place and ends where the composer starts, so the thread keeps
182
185
  * where it was underneath and the composer stays in reach for the prompts it answers.
186
+ * `onContentHeight` hears what the thread's rows and its padding take, without the composer, once
187
+ * the rows in view are laid out at their real heights, for a host that sizes its frame to the thread.
188
+ * Rows out of view still count at an estimate, so a frame up to the viewport's height gets it exact.
183
189
  */
184
- export function Timeline({ chatId, composer, overlay }: { chatId: string; composer?: ReactNode; overlay?: ReactNode }) {
190
+ export function Timeline({
191
+ chatId,
192
+ composer,
193
+ overlay,
194
+ onContentHeight
195
+ }: {
196
+ chatId: string;
197
+ composer?: ReactNode;
198
+ overlay?: ReactNode;
199
+ onContentHeight?: (height: number) => void;
200
+ }) {
185
201
  const host = useChatHost();
186
202
  const { fork } = host;
187
203
  const { t } = useTranslation('agent-chat');
@@ -197,24 +213,31 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
197
213
  const cursor = useChatRow(chatId, (row) => row?.history?.cursor ?? null);
198
214
  const scope = useChatScope();
199
215
  const { id: scopeId, keyOf } = scope;
216
+
200
217
  // Read when the menu opens rather than subscribed to, for the same reason the rows use the structure.
201
218
  const fullItems = () => useChats.getState().byKey[keyOf(chatId)]?.items;
219
+
202
220
  const scrollRef = useRef<HTMLDivElement>(null);
203
221
  const threadRef = useRef<HTMLDivElement>(null);
204
222
  const contentRef = useRef<HTMLDivElement>(null);
205
223
  const quoteTakerRef = useRef<QuoteTaker | null>(null);
224
+
206
225
  const registerQuoteTaker = useCallback((take: QuoteTaker) => {
207
226
  quoteTakerRef.current = take;
227
+
208
228
  return () => {
209
229
  if (quoteTakerRef.current === take) {
210
230
  quoteTakerRef.current = null;
211
231
  }
212
232
  };
213
233
  }, []);
234
+
214
235
  const followRef = useRef(true);
236
+
215
237
  const stopFollowing = useCallback(() => {
216
238
  followRef.current = false;
217
239
  }, []);
240
+
218
241
  const groups = useToggleSet(stopFollowing);
219
242
  const turns = useToggleSet(stopFollowing);
220
243
  const subagents = useToggleSet(stopFollowing);
@@ -234,8 +257,10 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
234
257
  if (cursor === null || loadingEarlierRef.current) {
235
258
  return;
236
259
  }
260
+
237
261
  loadingEarlierRef.current = true;
238
262
  setLoadingEarlier(true);
263
+
239
264
  // A page that failed is asked for again on the next scroll; the thread keeps what it has.
240
265
  void scope.chats
241
266
  .loadEarlier(chatId, cursor)
@@ -250,6 +275,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
250
275
  if (!order || !items) {
251
276
  return [];
252
277
  }
278
+
253
279
  return deriveTimelineRows(
254
280
  order.map((id) => items[id]!),
255
281
  {
@@ -262,14 +288,17 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
262
288
  }
263
289
  );
264
290
  }, [order, items, groups.ids, turns.ids, subagents.ids, activeTurnId, forkedTurns, host.commandLooks]);
291
+
265
292
  const cards = useHostThreadCards(scopeId, chatId);
266
293
  const drawsVisuals = host.visuals !== null;
267
294
  // Where the part of the thread this client holds begins, while there is a page before it.
268
295
  const heldFrom = cursor === null ? null : (items?.[order?.[0] ?? '']?.createdAt ?? null);
296
+
269
297
  const rows = useMemo(
270
298
  () => withTimedRows(threadRows, timedRowsOf(cards, drawsVisuals ? (visuals ?? []) : null, heldFrom)),
271
299
  [threadRows, cards, drawsVisuals, visuals, heldFrom]
272
300
  );
301
+
273
302
  const welcomes = useHostWelcome(chatId);
274
303
 
275
304
  const loadingHistory = historyPending && ((info !== null && (info.usage.turns > 0 || info.activeTurnId !== null)) || (order?.length ?? 0) > 0);
@@ -284,6 +313,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
284
313
  // A turn a jump is on its way to, which waits for the page its messages are on to come in.
285
314
  const [revealing, setRevealing] = useState<{ turnId: string; target: TurnTarget } | null>(null);
286
315
  const flash = useTimelineFlash((s) => s.target);
316
+
287
317
  const revealBlock = useCallback(
288
318
  (block: UiReplyTarget) => {
289
319
  setLanding(null);
@@ -291,6 +321,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
291
321
  },
292
322
  [keyOf, chatId]
293
323
  );
324
+
294
325
  const uiNavigation = useMemo(
295
326
  () => ({
296
327
  chatId,
@@ -303,31 +334,40 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
303
334
  // Whether the thread's text clears the strip depends on the width of the view, which only the thread's own frame tells.
304
335
  useLayoutEffect(() => {
305
336
  const element = frameRef.current;
337
+
306
338
  if (element === null || typeof ResizeObserver === 'undefined') {
307
339
  return;
308
340
  }
341
+
309
342
  const column = element.closest('.chat-column') !== null;
343
+
310
344
  const measure = (): void =>
311
345
  setFrame((current) =>
312
346
  current.width === element.offsetWidth && current.height === element.offsetHeight && current.column === column
313
347
  ? current
314
348
  : { width: element.offsetWidth, height: element.offsetHeight, column }
315
349
  );
350
+
316
351
  const observer = new ResizeObserver(measure);
317
352
  observer.observe(element);
318
353
  measure();
354
+
319
355
  return () => observer.disconnect();
320
356
  }, []);
321
357
 
322
358
  useLayoutEffect(() => {
323
359
  const element = composerRef.current;
360
+
324
361
  if (element === null) {
325
362
  return;
326
363
  }
364
+
327
365
  const measure = () => setComposerHeight(element.offsetHeight);
366
+
328
367
  const observer = new ResizeObserver(measure);
329
368
  observer.observe(element);
330
369
  measure();
370
+
331
371
  return () => observer.disconnect();
332
372
  }, []);
333
373
 
@@ -369,10 +409,13 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
369
409
  // Include the end before the first pin, so its actual heights are known before revealing it.
370
410
  rangeExtractor: (range) => {
371
411
  const indexes = defaultRangeExtractor(range);
412
+
372
413
  if (!initialLayout) {
373
414
  return indexes;
374
415
  }
416
+
375
417
  const tail = Math.max(8, Math.ceil(frame.height / ESTIMATED_ROW_PX));
418
+
376
419
  return [...new Set([...indexes, ...Array.from({ length: Math.min(tail, range.count) }, (_, index) => range.count - 1 - index)])].sort(
377
420
  (left, right) => left - right
378
421
  );
@@ -389,28 +432,47 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
389
432
  */
390
433
  const totalSize = virtualizer.getTotalSize();
391
434
  const virtualRows = virtualizer.getVirtualItems();
435
+
392
436
  useLayoutEffect(() => {
393
437
  if (!initialLayout || empty || loadingHistory || frame.height === 0) {
394
438
  return;
395
439
  }
440
+
396
441
  // Ref measurements can change offsets after render. Commit those offsets before the first paint.
397
442
  const measured = virtualizer.getVirtualItems();
443
+
398
444
  if (
399
445
  virtualRows.length !== measured.length ||
400
446
  virtualRows.some((row, index) => row.key !== measured[index]?.key || row.start !== measured[index]?.start || row.size !== measured[index]?.size)
401
447
  ) {
402
448
  settleLayout();
449
+
403
450
  return;
404
451
  }
452
+
405
453
  if (measured.at(-1)?.index === rows.length - 1) {
406
454
  setMeasuredThread(keyOf(chatId));
407
455
  }
408
456
  }, [initialLayout, empty, loadingHistory, frame.height, virtualRows, virtualizer, rows.length, keyOf, chatId]);
457
+
458
+ useLayoutEffect(() => {
459
+ const thread = threadRef.current;
460
+
461
+ if (onContentHeight === undefined || initialLayout || empty || loadingHistory || thread === null) {
462
+ return;
463
+ }
464
+
465
+ const style = getComputedStyle(thread);
466
+ onContentHeight(totalSize + (Number.parseFloat(style.paddingTop) || 0) + (Number.parseFloat(style.paddingBottom) || 0));
467
+ }, [onContentHeight, initialLayout, empty, loadingHistory, totalSize]);
468
+
409
469
  useLayoutEffect(() => {
410
470
  const element = scrollRef.current;
471
+
411
472
  if (!followRef.current || element === null) {
412
473
  return;
413
474
  }
475
+
414
476
  // The composer is in the scroll flow, so its measured height is part of this end position.
415
477
  element.scrollTop = element.scrollHeight;
416
478
  scrollGeometry.current = { top: element.scrollTop, height: element.scrollHeight, viewport: element.clientHeight };
@@ -422,16 +484,21 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
422
484
  const head = order?.[0];
423
485
  const moved = head !== headRef.current;
424
486
  headRef.current = head;
487
+
425
488
  if (element === null) {
426
489
  return;
427
490
  }
491
+
428
492
  const anchor = anchorRef.current;
429
493
  const index = moved && anchor !== null && !followRef.current ? rows.findIndex((row) => row.id === anchor.id) : -1;
494
+
430
495
  if (anchor !== null && index !== -1) {
431
496
  virtualizer.getTotalSize();
432
497
  element.scrollTop = virtualizer.measurementsCache[index]!.start - anchor.offset;
498
+
433
499
  return;
434
500
  }
501
+
435
502
  const row = virtualizer.getVirtualItemForOffset(element.scrollTop);
436
503
  anchorRef.current = row === undefined ? null : { id: String(row.key), offset: row.start - element.scrollTop };
437
504
  });
@@ -439,6 +506,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
439
506
  // Close to the top, the page before goes in; again after each page that left too little above the reader.
440
507
  useEffect(() => {
441
508
  const element = scrollRef.current;
509
+
442
510
  if (element !== null && wantsEarlier(element)) {
443
511
  loadEarlier();
444
512
  }
@@ -453,13 +521,16 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
453
521
 
454
522
  // The virtualizer materializes measurements on access; binary searches need only a few message starts.
455
523
  const measurements = virtualizer.measurementsCache;
524
+
456
525
  const starts = {
457
526
  length: ticks.length,
458
527
  at: (index: number): number | undefined => {
459
528
  const tick = ticks.at(index);
529
+
460
530
  return tick === undefined ? undefined : (measurements[tick.rowIndex]?.start ?? tick.rowIndex * ESTIMATED_ROW_PX);
461
531
  }
462
532
  };
533
+
463
534
  const scrollTop = virtualizer.scrollOffset ?? 0;
464
535
  const viewport = virtualizer.scrollRect?.height ?? 0;
465
536
  const visibleHeight = Math.max(0, viewport - coveredHeight);
@@ -473,12 +544,16 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
473
544
  const [, redrawStrip] = useReducer((count: number) => count + 1, 0);
474
545
  const stripRef = useRef({ starts, inView });
475
546
  stripRef.current = { starts, inView };
547
+
476
548
  const followStrip = (element: HTMLElement): void => {
477
549
  const { starts: current, inView: shown } = stripRef.current;
550
+
478
551
  if (shown === null) {
479
552
  return;
480
553
  }
554
+
481
555
  const next = messagesInView({ starts: current, scrollTop: element.scrollTop, visibleHeight: visibleHeightRef.current });
556
+
482
557
  if (next?.first !== shown.first || next?.last !== shown.last) {
483
558
  redrawStrip();
484
559
  }
@@ -497,6 +572,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
497
572
  order,
498
573
  firstRowInView: () => {
499
574
  const top = scrollRef.current?.scrollTop ?? 0;
575
+
500
576
  return virtualizer.getVirtualItems().find((virtualRow) => virtualRow.end > top)?.index ?? 0;
501
577
  },
502
578
  openTurn: turns.add,
@@ -506,6 +582,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
506
582
  scrollToRow: (index) => {
507
583
  const top = scrollRef.current?.scrollTop ?? 0;
508
584
  const bottom = top + visibleHeightRef.current;
585
+
509
586
  if (!virtualizer.getVirtualItems().some((virtualRow) => virtualRow.index === index && virtualRow.end > top && virtualRow.start < bottom)) {
510
587
  virtualizer.scrollToIndex(index, { align: 'center' });
511
588
  }
@@ -515,27 +592,34 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
515
592
  }),
516
593
  [chatId, onMainAgent, rows, items, order, virtualizer, turns.add, groups.add, subagents.add, stopFollowing]
517
594
  );
595
+
518
596
  const chatFind = useHostTimelineFind(findOptions);
519
597
  const searching = chatFind.open;
598
+
520
599
  // A find covers the whole thread, so every page before this one comes in while it is open.
521
600
  useEffect(() => {
522
601
  if (searching) {
523
602
  loadEarlier();
524
603
  }
525
604
  }, [searching, loadEarlier]);
605
+
526
606
  const found = useMemo(() => (searching ? ticksWithHits(ticks, chatFind.hitRows) : []), [searching, ticks, chatFind.hitRows]);
527
607
  const foundCurrent = chatFind.currentRow === null ? null : tickOfRow(ticks, chatFind.currentRow);
528
608
 
529
609
  const jumpTo = (index: number): void => {
530
610
  const tick = ticks[index];
611
+
531
612
  if (!tick) {
532
613
  return;
533
614
  }
615
+
534
616
  if (tick.bookmark !== null) {
535
617
  setLanding(null);
536
618
  setSeeking({ itemId: tick.bookmark.itemId, key: keyOf(chatId) });
619
+
537
620
  return;
538
621
  }
622
+
539
623
  showRowAtTop(virtualizer, followRef, tick.rowIndex);
540
624
  };
541
625
 
@@ -543,65 +627,89 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
543
627
  if (seeking === null) {
544
628
  return;
545
629
  }
630
+
546
631
  if (seeking.key !== keyOf(chatId)) {
547
632
  setSeeking(null);
633
+
548
634
  return;
549
635
  }
636
+
550
637
  if (loadingHistory) {
551
638
  return;
552
639
  }
640
+
553
641
  if (seeking.block) {
554
642
  const jump = uiReplyJump(seeking.block, rows, items, turns.ids, cursor !== null);
643
+
555
644
  if (jump.kind === 'earlier') {
556
645
  loadEarlier();
557
646
  } else if (jump.kind === 'turn') {
558
647
  turns.add(jump.turnId);
559
648
  } else {
560
649
  setSeeking(null);
650
+
561
651
  if (jump.kind === 'row') {
562
652
  showRowAtTop(virtualizer, followRef, jump.index);
563
653
  setLanding({ target: seeking.block, key: seeking.key });
564
654
  }
565
655
  }
656
+
566
657
  return;
567
658
  }
659
+
568
660
  const index = rows.findIndex((row) => row.id === seeking.itemId);
661
+
569
662
  if (index !== -1) {
570
663
  setSeeking(null);
571
664
  showRowAtTop(virtualizer, followRef, index);
665
+
572
666
  if (seeking.options?.flash) {
573
667
  useTimelineFlash.getState().flash(seeking.key, seeking.itemId, false);
574
668
  }
669
+
575
670
  return;
576
671
  }
672
+
577
673
  // The thread has not loaded yet, so nothing says the item is missing; the effect below bounds the wait.
578
674
  if (items === undefined) {
579
675
  return;
580
676
  }
677
+
581
678
  const item = items[seeking.itemId];
679
+
582
680
  if (item === undefined && cursor !== null) {
583
681
  loadEarlier();
682
+
584
683
  return;
585
684
  }
685
+
586
686
  const turnId = item?.turnId ?? null;
687
+
587
688
  if (turnId !== null && !turns.ids.has(turnId)) {
588
689
  turns.add(turnId);
690
+
589
691
  return;
590
692
  }
693
+
591
694
  setSeeking(null);
592
695
  seeking.options?.onMissing?.();
593
696
  }, [seeking, rows, items, turns, virtualizer, cursor, loadEarlier, keyOf, chatId, loadingHistory]);
697
+
594
698
  const waitingForThread = seeking !== null && (items === undefined || loadingHistory);
699
+
595
700
  useEffect(() => {
596
701
  if (!waitingForThread) {
597
702
  return;
598
703
  }
704
+
599
705
  const timer = window.setTimeout(() => {
600
706
  setSeeking(null);
601
707
  seeking?.options?.onMissing?.();
602
708
  }, JUMP_THREAD_WAIT_MS);
709
+
603
710
  return () => window.clearTimeout(timer);
604
711
  }, [waitingForThread, seeking]);
712
+
605
713
  useEffect(
606
714
  () =>
607
715
  registerItemJumper(keyOf(chatId), (itemId, options) => {
@@ -610,14 +718,18 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
610
718
  }),
611
719
  [keyOf, chatId]
612
720
  );
721
+
613
722
  useLayoutEffect(() => {
614
723
  if (!landing || landing.key !== keyOf(chatId)) {
615
724
  return;
616
725
  }
726
+
617
727
  let frame = 0;
618
728
  let attempts = 0;
729
+
619
730
  const reveal = () => {
620
731
  const scroller = scrollRef.current;
732
+
621
733
  if (scroller && revealUiReplyBlock(scroller, landing.target)) {
622
734
  useTimelineFlash.getState().flash(keyOf(chatId), landing.target.itemId, false, landing.target);
623
735
  setLanding(null);
@@ -628,7 +740,9 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
628
740
  setLanding(null);
629
741
  }
630
742
  };
743
+
631
744
  frame = requestAnimationFrame(reveal);
745
+
632
746
  return () => cancelAnimationFrame(frame);
633
747
  }, [landing, keyOf, chatId]);
634
748
 
@@ -636,28 +750,36 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
636
750
  if (revealing === null) {
637
751
  return;
638
752
  }
753
+
639
754
  const { turnId, target } = revealing;
640
755
  const changes = target === 'changes' ? rows.findIndex((row) => row.kind === 'changed-files' && row.turnId === turnId) : -1;
641
756
  const opening = order?.map((id) => items?.[id]).find((item) => item?.kind === 'user' && item.turnId === turnId);
642
757
  const found = changes !== -1 ? changes : rows.findIndex((row) => row.id === opening?.id || row.id === `start-${turnId}` || row.id === `fold-${turnId}`);
758
+
643
759
  if (found !== -1) {
644
760
  setRevealing(null);
645
761
  showRowAtTop(virtualizer, followRef, found);
646
762
  useTimelineFlash.getState().flash(keyOf(chatId), rows[found]!.id, changes !== -1);
763
+
647
764
  return;
648
765
  }
766
+
649
767
  // The turn is before the page this thread holds; a thread that holds it all has no such turn.
650
768
  if (cursor === null) {
651
769
  setRevealing(null);
770
+
652
771
  return;
653
772
  }
773
+
654
774
  loadEarlier();
655
775
  }, [revealing, rows, order, items, virtualizer, cursor, loadEarlier, keyOf, chatId]);
776
+
656
777
  useEffect(() => registerTurnJumper(keyOf(chatId), (turnId, target) => setRevealing({ turnId, target })), [keyOf, chatId]);
657
778
  // The strip is memoized and draws on every scroll, so it gets one callback for its life.
658
779
  const jumpRef = useRef(jumpTo);
659
780
  jumpRef.current = jumpTo;
660
781
  const pick = useCallback((index: number) => jumpRef.current(index), []);
782
+
661
783
  // Read when the card asks rather than on every render, so the strip's memo holds while the thread streams.
662
784
  const cardChat = useMemo<CardChat>(
663
785
  () => ({
@@ -666,7 +788,9 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
666
788
  if (fork === null) {
667
789
  return false;
668
790
  }
791
+
669
792
  const row = useChats.getState().byKey[keyOf(chatId)];
793
+
670
794
  return forkRefusal(row?.info ?? null, row?.structure[turnId]) === null;
671
795
  },
672
796
  fork: (turnId) => fork?.(chatId, turnId)
@@ -675,40 +799,51 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
675
799
  );
676
800
 
677
801
  const stepRef = useRef<(direction: -1 | 1) => boolean>(() => false);
802
+
678
803
  stepRef.current = (direction) => {
679
804
  if (!onMainAgent) {
680
805
  return false;
681
806
  }
807
+
682
808
  const index = stepMessage(starts, (i) => ticks[i]!.kind === 'person', scrollTop, direction);
809
+
683
810
  if (index === null) {
684
811
  return false;
685
812
  }
813
+
686
814
  jumpTo(index);
815
+
687
816
  return true;
688
817
  };
818
+
689
819
  useEffect(() => registerMessageStepper(keyOf(chatId), (direction) => stepRef.current(direction)), [keyOf, chatId]);
690
820
 
691
821
  // Read through a ref, so the rows keep one callback for their life and their memo holds.
692
822
  const opensRef = useRef({ rows, show });
693
823
  opensRef.current = { rows, show };
824
+
694
825
  /* The whole of what a sub-agent did, in the thread's place. Its row only keeps the beginning. */
695
826
  const openConversation = useCallback((step: SubagentStep): void => {
696
827
  opensRef.current.show(openFromMain(step));
697
828
  }, []);
698
829
 
699
830
  const addSubagent = subagents.add;
831
+
700
832
  /* The header of a turn a sub-agent woke. It points at the row that agent worked in, or the one it hangs under. */
701
833
  const openSubagent = useCallback(
702
834
  (toolUseId: string): void => {
703
835
  const found = findSubagentBranch(opensRef.current.rows, toolUseId);
836
+
704
837
  if (!found) {
705
838
  return;
706
839
  }
840
+
707
841
  addSubagent(found.branch.id);
708
842
  showRowAtTop(virtualizer, followRef, found.index);
709
843
  },
710
844
  [addSubagent, virtualizer]
711
845
  );
846
+
712
847
  const flashedRow = flash !== null && flash.key === keyOf(chatId) && !flash.block ? flash.rowId : null;
713
848
 
714
849
  return (
@@ -737,16 +872,20 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
737
872
  const previous = scrollGeometry.current;
738
873
  const layoutChanged = previous.height !== el.scrollHeight || previous.viewport !== el.clientHeight;
739
874
  const atEnd = el.scrollHeight - el.scrollTop - el.clientHeight < FOLLOW_THRESHOLD_PX;
875
+
740
876
  if (!layoutChanged && el.scrollTop < previous.top) {
741
877
  followRef.current = false;
742
878
  } else if (atEnd) {
743
879
  followRef.current = true;
744
880
  }
881
+
745
882
  scrollGeometry.current = { top: el.scrollTop, height: el.scrollHeight, viewport: el.clientHeight };
746
883
  setTimelineAtEnd(chatId, followRef.current);
884
+
747
885
  if (wantsEarlier(el)) {
748
886
  loadEarlier();
749
887
  }
888
+
750
889
  followStrip(el);
751
890
  }}
752
891
  >
@@ -782,6 +921,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
782
921
  <FindRevealContext.Provider value={chatFind.reveal}>
783
922
  {virtualRows.map((virtualRow) => {
784
923
  const row = rows[virtualRow.index]!;
924
+
785
925
  return (
786
926
  <ThreadRow
787
927
  key={row.id}
@@ -42,6 +42,7 @@ export function TimelineMenuPopup({
42
42
  const lookup = useReferenceLookup(chatId ?? '');
43
43
  const { fork, fileLinks } = useChatHost();
44
44
  const openFileLink = useOpenFileLink();
45
+
45
46
  const openInPreview = (): void => {
46
47
  if (target.file != null) {
47
48
  openFileLink(target.file.cwd, target.file.ref, target.file.scopeId);
@@ -49,6 +50,7 @@ export function TimelineMenuPopup({
49
50
  openFileLink(cwd, { path: target.path, ...(target.line === null ? {} : { line: target.line }), directory: false }, scopeId);
50
51
  }
51
52
  };
53
+
52
54
  return (
53
55
  <ContextMenu.Popup>
54
56
  <ContextMenu.Item disabled={target.selection === ''} onClick={() => copyText(target.selection)}>
@@ -108,6 +110,7 @@ export function BookmarkMenuItems({ chatId, itemId, onName }: { chatId: string;
108
110
  const scope = useChatScope();
109
111
  const host = useChatHost();
110
112
  const bookmark = useChatRow(chatId, (row) => row?.bookmarks?.find((candidate) => candidate.itemId === itemId) ?? null);
113
+
111
114
  if (bookmark === null) {
112
115
  return (
113
116
  <ContextMenu.Item
@@ -120,6 +123,7 @@ export function BookmarkMenuItems({ chatId, itemId, onName }: { chatId: string;
120
123
  </ContextMenu.Item>
121
124
  );
122
125
  }
126
+
123
127
  return (
124
128
  <>
125
129
  <ContextMenu.Item
@@ -141,6 +145,7 @@ export function BookmarkMenuItems({ chatId, itemId, onName }: { chatId: string;
141
145
  function ReadingWidthSubmenu() {
142
146
  const { t } = useTranslation('agent-chat');
143
147
  const readingWidth = useChatPreferences((preferences) => preferences.readingWidth);
148
+
144
149
  return (
145
150
  <ContextMenu.SubmenuRoot>
146
151
  <ContextMenu.SubmenuTrigger>
@@ -25,6 +25,7 @@ export default function UnifiedDiff(props: UnifiedDiffProps) {
25
25
  const legacyAllowed = legacyMode && legacyDiffBudget(oldText, newText, change.diff);
26
26
  const rawAllowed = hasHunks || legacyDiffBudget('', '', change.diff);
27
27
  const omitted = <DiffOmitted path={change.path} />;
28
+
28
29
  const fallback = legacyAllowed ? (
29
30
  <DiffFallbackBoundary fallback={omitted} resetKeys={[change.path, change.diff, oldText, newText, diffStyle]}>
30
31
  <LazyBoundary fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
@@ -34,16 +35,21 @@ export default function UnifiedDiff(props: UnifiedDiffProps) {
34
35
  ) : (
35
36
  omitted
36
37
  );
38
+
37
39
  if (!sourceAllowed) {
38
40
  return omitted;
39
41
  }
42
+
40
43
  if (!hasHunks) {
41
44
  return rawAllowed ? <RawDiff text={change.diff} overflow={overflow} fill={fill} maxHeight={maxHeight} /> : omitted;
42
45
  }
46
+
43
47
  if (legacyMode) {
44
48
  return fallback;
45
49
  }
50
+
46
51
  const unavailable = <DiffUnavailable path={change.path} />;
52
+
47
53
  return (
48
54
  <DiffFallbackBoundary fallback={unavailable} resetKeys={[change.path, change.diff, oldText, newText, ignoreWhitespace, diffStyle, loadEngine]}>
49
55
  <LazyBoundary fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
@@ -6,8 +6,10 @@ const BOX = 'h-14 w-14 rounded-lg border border-border';
6
6
  /* An image the composer holds, drawn from its thumbnail; an empty box of the same size stands in while that is made. */
7
7
  export function UploadThumb({ upload }: { upload: ChatAttachmentUpload }) {
8
8
  const url = useUploadThumbnail(upload);
9
+
9
10
  if (url === null) {
10
11
  return <span role="img" aria-label={upload.name} className={`block bg-surface-sunken ${BOX}`} />;
11
12
  }
13
+
12
14
  return <img src={url} alt={upload.name} className={`object-cover ${BOX}`} />;
13
15
  }
@@ -20,9 +20,11 @@ export function VisualDialogs({ chatId }: { chatId: string }) {
20
20
  const visual = useChatRow(chatId, (row) => (visualId === null ? null : (row?.visuals?.find((candidate) => candidate.id === visualId) ?? null)));
21
21
  // The dialog fades out after it closes, and draws what it showed until then.
22
22
  const [shown, setShown] = useState<ChatVisual | null>(visual);
23
+
23
24
  if (visual !== null && visual !== shown) {
24
25
  setShown(visual);
25
26
  }
27
+
26
28
  const close = (): void => useVisualDialog.getState().close();
27
29
 
28
30
  return (
@@ -55,6 +57,7 @@ export function VisualDialogs({ chatId }: { chatId: string }) {
55
57
  if (visualId !== null) {
56
58
  await scope.chats.removeVisual(chatId, visualId);
57
59
  }
60
+
58
61
  close();
59
62
  }}
60
63
  onOpenChange={close}