@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
@@ -20,6 +20,7 @@ export function TabsRenderer({ node, children, context }: UiRendererProps<UiProp
20
20
  const tabs = uiChildrenOf(node, 'Tab');
21
21
  const [picked, setPicked] = useBlockLocal<string | null>(context, node.id, () => null);
22
22
  const value = picked !== null && tabs.some((tab) => tab.id === picked) ? picked : (tabs[0]?.id ?? null);
23
+
23
24
  return (
24
25
  <Tabs.Root value={value} onValueChange={(next) => setPicked(String(next))} className="flex flex-col gap-3">
25
26
  <Tabs.List variant="pill" overflow="scroll" className="w-fit max-w-full rounded-xl bg-ui-tile p-0.75">
@@ -44,6 +45,7 @@ export function TabRenderer({ node, children }: UiRendererProps<UiProps<'Tab'>>)
44
45
 
45
46
  export function SectionsRenderer({ node, children, context }: UiRendererProps<UiProps<'Sections'>>) {
46
47
  const [touched, setTouched] = useBlockLocal(context, node.id, () => false);
48
+
47
49
  return (
48
50
  <SectionsContext value={{ touched, touch: () => setTouched(true) }}>
49
51
  <div className="chat-ui-nodes flex flex-col gap-1.5">{children}</div>
@@ -56,6 +58,7 @@ export function SectionRenderer({ node, children, context }: UiRendererProps<UiP
56
58
  const fold = useContext(SectionsContext);
57
59
  const [open, setOpen] = useBlockLocal(context, node.id, () => !fold.touched);
58
60
  const contentId = useId();
61
+
59
62
  return (
60
63
  <section className="rounded-xl bg-ui-tile">
61
64
  <button
@@ -49,6 +49,7 @@ export function SummaryRenderer({ node, children }: UiRendererProps<UiProps<'Sum
49
49
  const head = headId === undefined || headId === node.id;
50
50
  const { tone, badge } = node.props;
51
51
  const shown = tone ?? 'neutral';
52
+
52
53
  return (
53
54
  <div className={clsx(ICON_ROW, tone === undefined ? 'bg-ui-tile' : UI_TONE_SURFACE[tone])}>
54
55
  {head && (
@@ -69,6 +70,7 @@ export function SummaryRenderer({ node, children }: UiRendererProps<UiProps<'Sum
69
70
  /* A quiet tile whatever its tone: only the square of its icon wears the tone. */
70
71
  export function CalloutRenderer({ node, children }: UiRendererProps<UiProps<'Callout'>>) {
71
72
  const { tone, title } = node.props;
73
+
72
74
  return (
73
75
  <div className={clsx('chat-ui-flat bg-ui-tile text-xs', ICON_ROW)}>
74
76
  <span aria-hidden className={clsx(ICON_SQUARE, UI_TONE_SURFACE[tone], UI_TONE_TEXT[tone])}>
@@ -84,6 +86,7 @@ export function CalloutRenderer({ node, children }: UiRendererProps<UiProps<'Cal
84
86
 
85
87
  export function TagRenderer({ node, children }: UiRendererProps<UiProps<'Tag'>>) {
86
88
  const { tone } = node.props;
89
+
87
90
  return (
88
91
  <Pill shape="tag" size="sm" tone={tone === undefined ? 'muted' : UI_TONE_PILL[tone]}>
89
92
  {children}
@@ -96,9 +99,12 @@ export function ProgressRenderer({ node, children, context }: UiRendererProps<Ui
96
99
  const { t } = useTranslation('agent-chat');
97
100
  const { value, max } = node.props;
98
101
  const limit = max ?? 100;
102
+
99
103
  const reading =
100
104
  value === undefined ? null : limit === 100 ? formatPercent(value) : t('blocks.progress.of', { value: formatNumber(value), max: formatNumber(limit) });
105
+
101
106
  const label = uiNodeLabel(node);
107
+
102
108
  return (
103
109
  <div className="chat-ui-flat flex flex-col gap-2 rounded-xl bg-ui-tile p-3">
104
110
  <div className="flex items-baseline justify-between gap-3 text-xs">
@@ -123,6 +129,7 @@ export function StepRenderer({ node, children, context }: UiRendererProps<UiProp
123
129
  const { t } = useTranslation('agent-chat');
124
130
  const { state, detail } = node.props;
125
131
  const quiet = state === 'pending' || state === 'skipped';
132
+
126
133
  return (
127
134
  <li className="flex min-h-10 items-start gap-3 rounded-xl bg-ui-tile px-3 py-2.5 text-xs">
128
135
  <span className={clsx('mt-0.5 grid size-4 shrink-0 place-items-center', STEP_TONES[state])}>
@@ -162,6 +169,7 @@ export function EventRenderer({ node, children }: UiRendererProps<UiProps<'Event
162
169
  const label = [time, uiNodeLabel(node), tone === 'neutral' ? '' : t(`blocks.event.tone.${tone}`), detail ?? ''].filter((part) => part !== '').join(', ');
163
170
  const described = detail !== undefined && detail !== '';
164
171
  const tagged = described && detail.length <= DETAIL_AS_TAG;
172
+
165
173
  return (
166
174
  <li aria-label={label} className="flex min-h-9 items-start gap-3 py-2 text-xs">
167
175
  {/* A time nobody could read stays as written and is no `dateTime` for a machine to read. */}
@@ -5,18 +5,23 @@ export type UiReplyPart = { kind: 'text'; text: string } | { kind: 'ui'; block:
5
5
  export function uiReplyParts(text: string, blocks: readonly UiBlock[], streaming = false): UiReplyPart[] {
6
6
  const parts: UiReplyPart[] = [];
7
7
  let position = 0;
8
+
8
9
  for (const block of [...blocks].sort((left, right) => left.start - right.start)) {
9
10
  if (!Number.isInteger(block.start) || !Number.isInteger(block.end) || block.start < position || block.end < block.start || block.end > text.length) {
10
11
  continue;
11
12
  }
13
+
12
14
  if (block.start > position) {
13
15
  parts.push({ kind: 'text', text: text.slice(position, block.start) });
14
16
  }
17
+
15
18
  parts.push({ kind: 'ui', block });
16
19
  position = streaming && !block.complete ? text.length : block.end;
17
20
  }
21
+
18
22
  if (position < text.length) {
19
23
  parts.push({ kind: 'text', text: text.slice(position) });
20
24
  }
25
+
21
26
  return parts;
22
27
  }
@@ -21,6 +21,7 @@ export type UiTableCell =
21
21
 
22
22
  /* Rows shown before a person asks for all of them. */
23
23
  export const UI_TABLE_ROWS = 50;
24
+
24
25
  /* The furthest a JavaScript Date reaches from the epoch either way, in milliseconds. */
25
26
  const DATE_RANGE = 8.64e15;
26
27
  const NUMERIC_KINDS: ReadonlySet<UiColumnKind> = new Set(['number', 'bytes', 'duration']);
@@ -36,19 +37,27 @@ export function isNumericColumn(column: UiTableColumn): boolean {
36
37
  */
37
38
  export function uiTableColumns(node: UiViewNode<UiProps<'Table'>>): UiTableColumn[] {
38
39
  const rows = node.props.rows;
40
+
39
41
  const present = (key: string): boolean => rows.some((row) => Object.hasOwn(row, key));
42
+
40
43
  const declared = uiChildrenOf(node, 'Column');
44
+
41
45
  if (declared.length > 0) {
42
46
  const seen = new Set<string>();
47
+
43
48
  return declared.flatMap(({ props }) => {
44
49
  if (seen.has(props.key) || (rows.length > 0 && !present(props.key))) {
45
50
  return [];
46
51
  }
52
+
47
53
  seen.add(props.key);
54
+
48
55
  return [{ key: props.key, title: props.title ?? props.key, unit: props.unit, as: props.as ?? 'text' }];
49
56
  });
50
57
  }
58
+
51
59
  const keys: string[] = [];
60
+
52
61
  for (const row of rows) {
53
62
  for (const key of Object.keys(row)) {
54
63
  if (!keys.includes(key)) {
@@ -56,9 +65,11 @@ export function uiTableColumns(node: UiViewNode<UiProps<'Table'>>): UiTableColum
56
65
  }
57
66
  }
58
67
  }
68
+
59
69
  return keys.map((key) => {
60
70
  const values = rows.map((row) => row[key]).filter((value) => value !== undefined && value !== null);
61
71
  const numeric = values.length > 0 && values.every((value) => typeof value === 'number' && Number.isFinite(value));
72
+
62
73
  return { key, title: key, as: numeric ? 'number' : 'text' };
63
74
  });
64
75
  }
@@ -71,10 +82,13 @@ function numberOf(value: unknown): number | null {
71
82
  if (typeof value === 'number') {
72
83
  return Number.isFinite(value) ? value : null;
73
84
  }
85
+
74
86
  if (typeof value === 'string' && value.trim() !== '') {
75
87
  const parsed = Number(value);
88
+
76
89
  return Number.isFinite(parsed) ? parsed : null;
77
90
  }
91
+
78
92
  return null;
79
93
  }
80
94
 
@@ -83,15 +97,18 @@ export function uiTableCell(value: unknown, column: UiTableColumn): UiTableCell
83
97
  if (value === undefined || value === null || value === '') {
84
98
  return { kind: 'empty' };
85
99
  }
100
+
86
101
  switch (column.as) {
87
102
  case 'number':
88
103
  case 'bytes':
89
104
  case 'duration': {
90
105
  const number = numberOf(value);
106
+
91
107
  return number === null ? { kind: 'text', text: textOf(value) } : { kind: 'number', as: column.as, value: number };
92
108
  }
93
109
  case 'date': {
94
110
  const at = typeof value === 'number' ? value : typeof value === 'string' ? Date.parse(value) : Number.NaN;
111
+
95
112
  return Number.isFinite(at) && Math.abs(at) <= DATE_RANGE ? { kind: 'date', at } : { kind: 'text', text: textOf(value) };
96
113
  }
97
114
  case 'file':
@@ -6,13 +6,17 @@ import { useEffect, useState } from 'react';
6
6
  */
7
7
  export function useInView(element: Element | null, margin = '200px'): boolean {
8
8
  const [inView, setInView] = useState(typeof IntersectionObserver === 'undefined');
9
+
9
10
  useEffect(() => {
10
11
  if (element === null || typeof IntersectionObserver === 'undefined') {
11
12
  return;
12
13
  }
14
+
13
15
  const observer = new IntersectionObserver((entries) => setInView(entries.at(-1)?.isIntersecting ?? false), { rootMargin: margin });
14
16
  observer.observe(element);
17
+
15
18
  return () => observer.disconnect();
16
19
  }, [element, margin]);
20
+
17
21
  return inView;
18
22
  }
@@ -3,15 +3,19 @@ import { useEffect, useState } from 'react';
3
3
  /* True once `on` has held for `ms`, for a spinner that only shows when waiting takes long. */
4
4
  export function useLater(on: boolean, ms: number): boolean {
5
5
  const [late, setLate] = useState(false);
6
+
6
7
  useEffect(() => {
7
8
  if (!on) {
8
9
  return;
9
10
  }
11
+
10
12
  const timer = setTimeout(() => setLate(true), ms);
13
+
11
14
  return () => {
12
15
  clearTimeout(timer);
13
16
  setLate(false);
14
17
  };
15
18
  }, [on, ms]);
19
+
16
20
  return on && late;
17
21
  }
@@ -11,20 +11,25 @@ function cut(element: HTMLElement): boolean {
11
11
  */
12
12
  export function useTruncated<T extends HTMLElement>(within = false): [(element: T | null) => (() => void) | undefined, boolean] {
13
13
  const [truncated, setTruncated] = useState(false);
14
+
14
15
  const measure = (element: T | null) => {
15
16
  if (element === null || typeof ResizeObserver === 'undefined') {
16
17
  return undefined;
17
18
  }
19
+
18
20
  const check = () => setTruncated(within ? [...element.querySelectorAll<HTMLElement>('.truncate')].some(cut) : cut(element));
21
+
19
22
  check();
20
23
  const resize = new ResizeObserver(check);
21
24
  resize.observe(element);
22
25
  const text = new MutationObserver(check);
23
26
  text.observe(element, { characterData: true, childList: true, subtree: true });
27
+
24
28
  return () => {
25
29
  resize.disconnect();
26
30
  text.disconnect();
27
31
  };
28
32
  };
33
+
29
34
  return [measure, truncated];
30
35
  }
@@ -26,39 +26,49 @@ export function useUiLinks(
26
26
  const host = useChatHost().intelligentUi;
27
27
  const values = uiInputValues(block, state);
28
28
  const scope = state.scope();
29
+
29
30
  const queryValues = Object.fromEntries(
30
31
  Object.keys(block.queries)
31
32
  .filter((name) => Object.hasOwn(scope, name))
32
33
  .map((name) => [name, scope[name]])
33
34
  );
35
+
34
36
  let targets: Record<string, UiLinkTarget> = {};
37
+
35
38
  try {
36
39
  if (block.complete) {
37
40
  targets = uiLinkTargets(block, values, queryValues);
38
41
  }
39
42
  } catch {}
43
+
40
44
  const signature = JSON.stringify(targets);
41
45
  const input = JSON.stringify(values);
42
46
  const tickets = JSON.stringify(reads);
43
47
  const defaultInput = JSON.stringify(uiInputValues(block, new UiState(block)));
44
48
  const link = host?.link;
49
+
45
50
  useEffect(() => {
46
51
  asked.current.clear();
47
52
  }, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, link]);
53
+
48
54
  useEffect(() => {
49
55
  if (!link || !block.revision || context.phase !== 'final') {
50
56
  return;
51
57
  }
58
+
52
59
  let stopped = false;
60
+
53
61
  const validate = async () => {
54
62
  const moved = Object.entries(JSON.parse(signature) as Record<string, UiLinkTarget>)
55
63
  .slice(0, UI_HOST_LIMITS.links)
56
64
  .filter(([nodeId, target]) => asked.current.get(nodeId) !== JSON.stringify(target));
65
+
57
66
  for (const [nodeId, target] of moved) {
58
67
  const record = (reading: ChatUiLinkReading) => {
59
68
  asked.current.set(nodeId, JSON.stringify(target));
60
69
  setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading } }));
61
70
  };
71
+
62
72
  try {
63
73
  const reading = await link(context.scopeId, {
64
74
  chatId: context.chatId,
@@ -69,51 +79,66 @@ export function useUiLinks(
69
79
  values: JSON.parse(input),
70
80
  reads: JSON.parse(tickets)
71
81
  });
82
+
72
83
  if (stopped) {
73
84
  return;
74
85
  }
86
+
75
87
  record(reading);
76
88
  } catch {
77
89
  if (stopped) {
78
90
  return;
79
91
  }
92
+
80
93
  record({ state: 'plain' });
81
94
  }
82
95
  }
83
96
  };
97
+
84
98
  // The first check runs at once; a later one waits until the inputs rested.
85
99
  const timer = setTimeout(() => void validate(), asked.current.size === 0 ? 0 : LINK_SETTLE_MS);
100
+
86
101
  return () => {
87
102
  stopped = true;
88
103
  clearTimeout(timer);
89
104
  };
90
105
  }, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, context.phase, signature, input, tickets, link]);
106
+
91
107
  const idOf = (target: UiLinkTarget) => Object.entries(targets).find(([, entry]) => JSON.stringify(entry) === JSON.stringify(target))?.[0];
108
+
92
109
  const resolution = (target: UiLinkTarget): ChatUiLinkReading => {
93
110
  const id = idOf(target);
111
+
94
112
  if (id === undefined) {
95
113
  return { state: 'plain' };
96
114
  }
115
+
97
116
  if (checked[id]?.target === JSON.stringify(target)) {
98
117
  return checked[id].reading;
99
118
  }
119
+
100
120
  if (input === defaultInput && frozen && frozen.revision === block.revision) {
101
121
  const firstQueries = Object.fromEntries(
102
122
  Object.entries(frozen.readings)
103
123
  .filter(([, reading]) => reading.state === 'fresh')
104
124
  .map(([name, reading]) => [name, reading.value])
105
125
  );
126
+
106
127
  if (JSON.stringify(queryValues) === JSON.stringify(firstQueries)) {
107
128
  return frozen.links?.[id] ?? { state: 'plain' };
108
129
  }
109
130
  }
131
+
110
132
  return { state: 'plain' };
111
133
  };
134
+
112
135
  const open = (target: UiLinkTarget) => {
113
136
  const nodeId = idOf(target);
137
+
114
138
  if (!nodeId || !link || !block.revision) {
115
139
  return;
116
140
  }
141
+
117
142
  void link(context.scopeId, {
118
143
  chatId: context.chatId,
119
144
  itemId: context.itemId,
@@ -125,6 +150,7 @@ export function useUiLinks(
125
150
  })
126
151
  .then((reading) => {
127
152
  setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading } }));
153
+
128
154
  if (reading.state === 'chip' && reading.target) {
129
155
  host?.openLink?.(context.scopeId, reading);
130
156
  }
@@ -133,8 +159,11 @@ export function useUiLinks(
133
159
  setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading: { state: 'plain' } } }));
134
160
  });
135
161
  };
162
+
136
163
  const hostOpenUrl = host?.openUrl;
164
+
137
165
  const openUrl = (url: string) => hostOpenUrl?.(context.scopeId, url);
166
+
138
167
  return {
139
168
  link: link ? resolution : undefined,
140
169
  openLink: link && host?.openLink ? open : undefined,
@@ -25,6 +25,7 @@ function successfulReading(
25
25
  if (local?.identity === identity && local.successful?.inputs === signature) {
26
26
  return local.successful.reading;
27
27
  }
28
+
28
29
  return signature === defaultInputs && initial?.state === 'fresh' ? initial : undefined;
29
30
  }
30
31
 
@@ -54,34 +55,45 @@ export function useUiQueries(
54
55
  const reading = busyInput === inputKey;
55
56
  const defaultInputs = useMemo(() => JSON.stringify(uiInputValues(block, new UiState(block))), [block]);
56
57
  const initial = frozen && frozen.revision === block.revision ? frozen.readings : NO_READINGS;
58
+
57
59
  const successfulFor = (name: string, signature: string): ChatUiQueryReading | undefined =>
58
60
  successfulReading(readings[name], initial[name], identity, signature, defaultInputs);
61
+
59
62
  const readsFor = (values: Readonly<Record<string, unknown>>): Record<string, string> | null => {
60
63
  const signature = JSON.stringify(values);
61
64
  const reads: Record<string, string> = {};
65
+
62
66
  for (const name of names) {
63
67
  const local = readings[name];
64
68
  const successful = successfulFor(name, signature);
69
+
65
70
  if (!successful?.readId || (local?.identity === identity && local.reading.state === 'refused')) {
66
71
  return null;
67
72
  }
73
+
68
74
  reads[name] = successful.readId;
69
75
  }
76
+
70
77
  return reads;
71
78
  };
79
+
72
80
  const reads = readsFor(JSON.parse(inputs));
73
81
  const ready = reads !== null;
74
82
  const query = host.intelligentUi?.query;
75
83
  const subscribe = host.intelligentUi?.subscribe;
84
+
76
85
  useEffect(() => {
77
86
  const scope = state.scope();
87
+
78
88
  for (const name of Object.keys(block.queries)) {
79
89
  const successful = successfulReading(readings[name], initial[name], identity, inputs, defaultInputs);
90
+
80
91
  if (successful && JSON.stringify(scope[name]) !== JSON.stringify(successful.value)) {
81
92
  state.setQuery(name, successful.value, block);
82
93
  }
83
94
  }
84
95
  }, [state, block, readings, identity, inputs, defaultInputs, initial]);
96
+
85
97
  useEffect(() => {
86
98
  if (
87
99
  !query ||
@@ -94,6 +106,7 @@ export function useUiQueries(
94
106
  ) {
95
107
  return;
96
108
  }
109
+
97
110
  let stopped = false;
98
111
  let visible = typeof IntersectionObserver === 'undefined';
99
112
  let busy = false;
@@ -102,33 +115,41 @@ export function useUiQueries(
102
115
  // The first read of this block reads at once; a changed input waits until it rests.
103
116
  const changed = lastRead.current.inputKey !== '' && lastRead.current.inputKey !== inputKey;
104
117
  const settledAt = changed ? settleDeadline() : 0;
118
+
105
119
  const refresh = async () => {
106
120
  if (stopped || !visible || !documentVisible() || busy) {
107
121
  return;
108
122
  }
123
+
109
124
  const wait = Math.max(
110
125
  settledAt - Date.now(),
111
126
  lastRead.current.inputKey === inputKey ? UI_HOST_LIMITS.refreshMilliseconds - (Date.now() - lastRead.current.at) : 0
112
127
  );
128
+
113
129
  if (wait > 0) {
114
130
  if (timer === null) {
115
131
  if (!visible || !documentVisible()) {
116
132
  return;
117
133
  }
134
+
118
135
  timer = setTimeout(() => {
119
136
  timer = null;
120
137
  void refresh();
121
138
  }, wait);
122
139
  }
140
+
123
141
  return;
124
142
  }
143
+
125
144
  busy = true;
126
145
  lastRead.current = { inputKey, at: Date.now() };
127
146
  setBusyInput(inputKey);
147
+
128
148
  for (const name of Object.keys(block.queries).slice(0, UI_HOST_LIMITS.queries)) {
129
149
  if (!visible || !documentVisible()) {
130
150
  break;
131
151
  }
152
+
132
153
  try {
133
154
  const reading = await query(context.scopeId, {
134
155
  chatId: context.chatId,
@@ -138,12 +159,15 @@ export function useUiQueries(
138
159
  query: name,
139
160
  values: JSON.parse(inputs)
140
161
  });
162
+
141
163
  if (stopped) {
142
164
  return;
143
165
  }
166
+
144
167
  if (reading.state === 'fresh') {
145
168
  state.setQuery(name, reading.value, block);
146
169
  }
170
+
147
171
  setReadings((previous) => ({
148
172
  ...previous,
149
173
  [name]: {
@@ -157,6 +181,7 @@ export function useUiQueries(
157
181
  if (stopped) {
158
182
  return;
159
183
  }
184
+
160
185
  setReadings((previous) => ({
161
186
  ...previous,
162
187
  [name]: {
@@ -167,21 +192,27 @@ export function useUiQueries(
167
192
  }));
168
193
  }
169
194
  }
195
+
170
196
  busy = false;
197
+
171
198
  if (stopped) {
172
199
  return;
173
200
  }
201
+
174
202
  setBusyInput(null);
203
+
175
204
  timer = setTimeout(() => {
176
205
  timer = null;
177
206
  void refresh();
178
207
  }, UI_HOST_LIMITS.refreshMilliseconds);
179
208
  };
209
+
180
210
  const observer =
181
211
  typeof IntersectionObserver === 'undefined'
182
212
  ? null
183
213
  : new IntersectionObserver((entries) => {
184
214
  visible = entries.some((entry) => entry.isIntersecting);
215
+
185
216
  if (visible) {
186
217
  void refresh();
187
218
  } else if (timer !== null) {
@@ -189,15 +220,19 @@ export function useUiQueries(
189
220
  timer = null;
190
221
  }
191
222
  });
223
+
192
224
  if (element) {
193
225
  observer?.observe(element);
194
226
  }
227
+
195
228
  const focus = () => {
196
229
  void refresh();
197
230
  };
231
+
198
232
  document.addEventListener('visibilitychange', focus);
199
233
  const unsubscribe = subscribe?.(context.scopeId, context.chatId, focus);
200
234
  void refresh();
235
+
201
236
  return () => {
202
237
  stopped = true;
203
238
  // A read cut short by this cleanup never reaches its own reset, which would leave the choices closed.
@@ -205,19 +240,24 @@ export function useUiQueries(
205
240
  observer?.disconnect();
206
241
  unsubscribe?.();
207
242
  document.removeEventListener('visibilitychange', focus);
243
+
208
244
  if (timer !== null) {
209
245
  clearTimeout(timer);
210
246
  }
211
247
  };
212
248
  }, [block, state, context.scopeId, context.chatId, context.itemId, context.phase, inputs, query, subscribe, element, identity, inputKey]);
249
+
213
250
  const currentReadings = Object.fromEntries(
214
251
  names.flatMap((name) => {
215
252
  const local = readings[name];
216
253
  const reading = local?.identity === identity ? local.reading : initial[name];
254
+
217
255
  return reading ? [[name, reading]] : [];
218
256
  })
219
257
  );
258
+
220
259
  const failed = Object.entries(currentReadings).find(([, value]) => value.state !== 'fresh');
260
+
221
261
  const live: UiLiveStatus | undefined = names.length
222
262
  ? {
223
263
  state: reading || (!ready && !failed) ? 'reading' : (failed?.[1].state ?? 'fresh'),
@@ -230,6 +270,7 @@ export function useUiQueries(
230
270
  code: failed?.[1].code
231
271
  }
232
272
  : undefined;
273
+
233
274
  return { live, reads: reads ?? {}, reading, ready, readsFor, currentReadings };
234
275
  }
235
276
 
@@ -241,16 +282,21 @@ export function useUiLiveValues(block: UiBlock, nodes: readonly UiViewNode[], re
241
282
  previous: new Map()
242
283
  }
243
284
  );
285
+
244
286
  const dependencies = new Map<string, Set<string>>();
245
287
  const names = new Set(Object.keys(block.queries));
288
+
246
289
  const references = (value: unknown, into: Set<string>) => {
247
290
  if (!value || typeof value !== 'object') {
248
291
  return;
249
292
  }
293
+
250
294
  const expression = value as Record<string, unknown>;
295
+
251
296
  if (expression.kind === 'reference' && names.has(String(expression.name))) {
252
297
  into.add(String(expression.name));
253
298
  }
299
+
254
300
  for (const child of Object.values(expression)) {
255
301
  if (Array.isArray(child)) {
256
302
  child.forEach((item) => references(item, into));
@@ -259,6 +305,7 @@ export function useUiLiveValues(block: UiBlock, nodes: readonly UiViewNode[], re
259
305
  }
260
306
  }
261
307
  };
308
+
262
309
  const written = (nodes: readonly UiNode[], inherited = new Set<string>()) => {
263
310
  for (const node of nodes) {
264
311
  const related = new Set(inherited);
@@ -267,39 +314,52 @@ export function useUiLiveValues(block: UiBlock, nodes: readonly UiViewNode[], re
267
314
  written(node.children, node.type === 'Each' ? related : inherited);
268
315
  }
269
316
  };
317
+
270
318
  written(block.nodes);
271
319
  const values = new Map<string, Record<string, unknown>>();
272
320
  const sources = new Map<string, Set<string>>();
321
+
273
322
  const collect = (nodes: readonly UiViewNode[]) => {
274
323
  for (const node of nodes) {
275
324
  const related = dependencies.get(node.sourceId ?? node.id);
325
+
276
326
  if (related?.size && !node.error) {
277
327
  values.set(node.id, node.props);
278
328
  sources.set(node.id, related);
279
329
  }
330
+
280
331
  collect(node.children);
281
332
  }
282
333
  };
334
+
283
335
  collect(nodes);
284
336
  const token = JSON.stringify([block.revision, Object.entries(readings).map(([name, reading]) => [name, reading.state, reading.readId, reading.readAt])]);
285
337
  const previous = committed.token === token ? committed.previous : committed.values;
338
+
286
339
  if (committed.token !== token) {
287
340
  setCommitted({ token, values, previous });
288
341
  }
342
+
289
343
  if (!names.size) {
290
344
  return undefined;
291
345
  }
346
+
292
347
  return (id: string, prop: string, path?: readonly [number, string]): UiLiveValue | undefined => {
293
348
  const related = sources.get(id);
349
+
294
350
  if (!related) {
295
351
  return undefined;
296
352
  }
353
+
297
354
  const get = (props?: Record<string, unknown>): unknown => {
298
355
  const value = props?.[prop];
356
+
299
357
  return path && Array.isArray(value) ? value[path[0]]?.[path[1]] : value;
300
358
  };
359
+
301
360
  const before = get(previous.get(id));
302
361
  const value = get(values.get(id));
362
+
303
363
  return {
304
364
  previous: before,
305
365
  changed: before !== undefined && JSON.stringify(before) !== JSON.stringify(value),