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