basalt-ui 1.41.0 → 2.0.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 (914) hide show
  1. package/AGENTS.md +46 -43
  2. package/CHANGELOG.md +535 -518
  3. package/MIGRATING.md +945 -37
  4. package/README.md +171 -366
  5. package/agent/rules/basalt-batteries.md +19 -74
  6. package/agent/rules/basalt-charts.md +21 -95
  7. package/agent/rules/basalt-controls.md +23 -131
  8. package/agent/rules/basalt-mantine.md +26 -99
  9. package/agent/rules/basalt-state.md +21 -103
  10. package/agent/rules/basalt-tokens.md +33 -114
  11. package/agent/skills/basalt-app/SKILL.md +229 -72
  12. package/agent/skills/basalt-charts/SKILL.md +23 -57
  13. package/agent/skills/basalt-design/SKILL.md +12 -70
  14. package/agent/templates/AGENTS-block.md.tpl +16 -26
  15. package/agent/templates/DESIGN.md.tpl +1 -1
  16. package/agent/templates/__root.tpl +9 -16
  17. package/agent/templates/main.tpl +21 -0
  18. package/agent/templates/nav.tpl +18 -0
  19. package/configs/oxlint-basalt.json +4 -10
  20. package/configs/oxlint-plugin.js +465 -844
  21. package/configs/oxlint-plugin.test.ts +661 -709
  22. package/configs/oxlint.json +10 -2
  23. package/dist/agent/ai-sdk-transport.d.ts +19 -7
  24. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  25. package/dist/agent/ai-sdk-transport.js +10 -7
  26. package/dist/{agent-chat → agent/chat}/composer.d.ts +16 -16
  27. package/dist/agent/chat/composer.d.ts.map +1 -0
  28. package/dist/{agent-chat → agent/chat}/composer.js +10 -9
  29. package/dist/{agent-chat → agent/chat}/message-affordances.d.ts +7 -6
  30. package/dist/agent/chat/message-affordances.d.ts.map +1 -0
  31. package/dist/agent/chat/relative-time.d.ts.map +1 -0
  32. package/dist/{agent-chat → agent/chat}/thread-feed-row.d.ts +18 -16
  33. package/dist/agent/chat/thread-feed-row.d.ts.map +1 -0
  34. package/dist/{agent-chat → agent/chat}/thread-feed-row.js +12 -11
  35. package/dist/{agent-chat → agent/chat}/thread-feed.d.ts +8 -8
  36. package/dist/agent/chat/thread-feed.d.ts.map +1 -0
  37. package/dist/{agent-chat → agent/chat}/thread-feed.js +12 -10
  38. package/dist/{agent-chat → agent/chat}/thread-message.d.ts +12 -9
  39. package/dist/agent/chat/thread-message.d.ts.map +1 -0
  40. package/dist/{agent-chat → agent/chat}/thread-message.js +15 -13
  41. package/dist/{agent-chat → agent/chat}/thread-outcome-card.d.ts +2 -2
  42. package/dist/agent/chat/thread-outcome-card.d.ts.map +1 -0
  43. package/dist/{agent-chat → agent/chat}/thread-outcome-card.js +3 -3
  44. package/dist/{agent-chat → agent/chat}/tool-chip.d.ts +5 -5
  45. package/dist/agent/chat/tool-chip.d.ts.map +1 -0
  46. package/dist/{agent-chat → agent/chat}/tool-chip.js +4 -4
  47. package/dist/agent/chat/use-keyboard-inset.d.ts.map +1 -0
  48. package/dist/{agent-chat → agent/chat}/use-keyboard-inset.js +1 -1
  49. package/dist/{agent-chat → agent/chat}/virtualize.d.ts +7 -7
  50. package/dist/agent/chat/virtualize.d.ts.map +1 -0
  51. package/dist/agent/error.d.ts +31 -0
  52. package/dist/agent/error.d.ts.map +1 -0
  53. package/dist/agent/error.js +18 -0
  54. package/dist/agent/history.d.ts +2 -36
  55. package/dist/agent/history.d.ts.map +1 -1
  56. package/dist/agent/history.js +0 -32
  57. package/dist/agent/id.d.ts +4 -5
  58. package/dist/agent/id.d.ts.map +1 -1
  59. package/dist/agent/index.d.ts +19 -52
  60. package/dist/agent/index.d.ts.map +1 -1
  61. package/dist/agent/index.js +10 -32
  62. package/dist/agent/outcome.d.ts +3 -14
  63. package/dist/agent/outcome.d.ts.map +1 -1
  64. package/dist/agent/outcome.js +0 -30
  65. package/dist/agent/part-list.d.ts +3 -3
  66. package/dist/agent/part-list.d.ts.map +1 -1
  67. package/dist/agent/parts.d.ts +23 -24
  68. package/dist/agent/parts.d.ts.map +1 -1
  69. package/dist/agent/run-committer.d.ts +23 -0
  70. package/dist/agent/run-committer.d.ts.map +1 -0
  71. package/dist/agent/run-committer.js +41 -0
  72. package/dist/agent/run-records.d.ts +77 -0
  73. package/dist/agent/run-records.d.ts.map +1 -0
  74. package/dist/agent/run-records.js +69 -0
  75. package/dist/agent/thread.d.ts +23 -47
  76. package/dist/agent/thread.d.ts.map +1 -1
  77. package/dist/agent/thread.js +3 -134
  78. package/dist/agent/transport.d.ts +6 -69
  79. package/dist/agent/transport.d.ts.map +1 -1
  80. package/dist/agent/transport.js +0 -18
  81. package/dist/agent/use-agent-thread-runs.d.ts +63 -26
  82. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  83. package/dist/agent/use-agent-thread-runs.js +202 -66
  84. package/dist/charts/BandStrip.d.ts +46 -0
  85. package/dist/charts/BandStrip.d.ts.map +1 -0
  86. package/dist/charts/BandStrip.js +151 -0
  87. package/dist/charts/Bars.d.ts +19 -0
  88. package/dist/charts/Bars.d.ts.map +1 -0
  89. package/dist/charts/Bars.js +77 -0
  90. package/dist/charts/CartesianChart.d.ts +57 -0
  91. package/dist/charts/CartesianChart.d.ts.map +1 -0
  92. package/dist/charts/CartesianChart.js +121 -0
  93. package/dist/charts/ChartCard.d.ts +34 -0
  94. package/dist/charts/ChartCard.d.ts.map +1 -0
  95. package/dist/charts/ChartCard.js +115 -0
  96. package/dist/charts/Donut.d.ts +25 -0
  97. package/dist/charts/Donut.d.ts.map +1 -0
  98. package/dist/charts/Donut.js +191 -0
  99. package/dist/charts/DualPanel.d.ts +47 -0
  100. package/dist/charts/DualPanel.d.ts.map +1 -0
  101. package/dist/charts/DualPanel.js +214 -0
  102. package/dist/charts/Heatmap.d.ts +41 -0
  103. package/dist/charts/Heatmap.d.ts.map +1 -0
  104. package/dist/charts/Heatmap.js +216 -0
  105. package/dist/charts/Lines.d.ts +11 -0
  106. package/dist/charts/Lines.d.ts.map +1 -0
  107. package/dist/charts/Lines.js +114 -0
  108. package/dist/charts/MirroredBars.d.ts +44 -0
  109. package/dist/charts/MirroredBars.d.ts.map +1 -0
  110. package/dist/charts/MirroredBars.js +190 -0
  111. package/dist/charts/Sparkline.d.ts +22 -0
  112. package/dist/charts/Sparkline.d.ts.map +1 -0
  113. package/dist/charts/Sparkline.js +129 -0
  114. package/dist/charts/StackedArea.d.ts +15 -0
  115. package/dist/charts/StackedArea.d.ts.map +1 -0
  116. package/dist/charts/StackedArea.js +46 -0
  117. package/dist/charts/box.d.ts +38 -0
  118. package/dist/charts/box.d.ts.map +1 -0
  119. package/dist/charts/box.js +72 -0
  120. package/dist/charts/card-context.d.ts +3 -0
  121. package/dist/charts/card-context.d.ts.map +1 -0
  122. package/dist/charts/card-context.js +7 -0
  123. package/dist/charts/chart-box.module.css +64 -0
  124. package/{src/content/article-card.module.css.d.ts → dist/charts/chart-box.module.css.d.ts} +5 -9
  125. package/dist/charts/chart-card.module.css +25 -0
  126. package/dist/{dashboard/widget-grid.module.css.d.ts → charts/chart-card.module.css.d.ts} +1 -3
  127. package/dist/charts/chart-legend.module.css +38 -0
  128. package/{src/dashboard/widget-grid.module.css.d.ts → dist/charts/chart-legend.module.css.d.ts} +3 -3
  129. package/dist/charts/compile.d.ts +62 -0
  130. package/dist/charts/compile.d.ts.map +1 -0
  131. package/dist/charts/compile.js +386 -0
  132. package/dist/charts/cursor.d.ts +50 -0
  133. package/dist/charts/cursor.d.ts.map +1 -0
  134. package/dist/charts/cursor.js +157 -0
  135. package/dist/charts/curves.d.ts +12 -0
  136. package/dist/charts/curves.d.ts.map +1 -0
  137. package/dist/charts/curves.js +86 -0
  138. package/dist/charts/donut.module.css +54 -0
  139. package/dist/{content/article-card.module.css.d.ts → charts/donut.module.css.d.ts} +2 -9
  140. package/dist/charts/host.d.ts +17 -0
  141. package/dist/charts/host.d.ts.map +1 -0
  142. package/dist/charts/host.js +35 -0
  143. package/dist/charts/index.d.ts +18 -53
  144. package/dist/charts/index.d.ts.map +1 -1
  145. package/dist/charts/index.js +17 -189
  146. package/dist/charts/legend.d.ts +23 -0
  147. package/dist/charts/legend.d.ts.map +1 -0
  148. package/dist/charts/legend.js +160 -0
  149. package/dist/charts/marks.d.ts +25 -0
  150. package/dist/charts/marks.d.ts.map +1 -0
  151. package/dist/charts/marks.js +128 -0
  152. package/dist/charts/panel.d.ts +56 -0
  153. package/dist/charts/panel.d.ts.map +1 -0
  154. package/dist/charts/panel.js +123 -0
  155. package/dist/charts/scales.d.ts +44 -0
  156. package/dist/charts/scales.d.ts.map +1 -0
  157. package/dist/charts/scales.js +163 -0
  158. package/dist/charts/sizing.d.ts +12 -0
  159. package/dist/charts/sizing.d.ts.map +1 -0
  160. package/dist/charts/sizing.js +33 -0
  161. package/dist/charts/state.d.ts +17 -0
  162. package/dist/charts/state.d.ts.map +1 -0
  163. package/dist/charts/state.js +53 -0
  164. package/dist/charts/theme.d.ts +38 -19
  165. package/dist/charts/theme.d.ts.map +1 -1
  166. package/dist/charts/theme.js +48 -26
  167. package/dist/charts/types.d.ts +227 -0
  168. package/dist/charts/types.d.ts.map +1 -0
  169. package/dist/charts/types.js +4 -0
  170. package/dist/cli/audit-allows.d.ts +4 -17
  171. package/dist/cli/audit-allows.d.ts.map +1 -1
  172. package/dist/cli/config.d.ts +14 -43
  173. package/dist/cli/config.d.ts.map +1 -1
  174. package/dist/cli/doctor-upgrade.d.ts +31 -0
  175. package/dist/cli/doctor-upgrade.d.ts.map +1 -0
  176. package/dist/cli/doctor-upgrade.js +269 -0
  177. package/dist/cli/doctor.d.ts +13 -29
  178. package/dist/cli/doctor.d.ts.map +1 -1
  179. package/dist/cli/doctor.js +51 -9
  180. package/dist/cli/help.d.ts.map +1 -1
  181. package/dist/cli/help.js +2 -4
  182. package/dist/cli/index.d.ts +48 -172
  183. package/dist/cli/index.d.ts.map +1 -1
  184. package/dist/cli/index.js +39 -132
  185. package/dist/cli/init.d.ts.map +1 -1
  186. package/dist/cli/init.js +43 -37
  187. package/dist/cli/sync.d.ts +5 -21
  188. package/dist/cli/sync.d.ts.map +1 -1
  189. package/dist/cli/sync.js +4 -23
  190. package/dist/cli/tokens-css.d.ts +7 -28
  191. package/dist/cli/tokens-css.d.ts.map +1 -1
  192. package/dist/commands/ShortcutsHelp.d.ts.map +1 -1
  193. package/dist/commands/ShortcutsHelp.js +4 -5
  194. package/dist/commands/define-commands.d.ts +15 -31
  195. package/dist/commands/define-commands.d.ts.map +1 -1
  196. package/dist/commands/define-commands.js +1 -11
  197. package/dist/commands/define-overlays.d.ts +5 -19
  198. package/dist/commands/define-overlays.d.ts.map +1 -1
  199. package/dist/commands/define-overlays.js +3 -11
  200. package/dist/commands/index.d.ts +10 -40
  201. package/dist/commands/index.d.ts.map +1 -1
  202. package/dist/commands/index.js +9 -46
  203. package/dist/{notifications → commands/notifications}/bell.d.ts +1 -1
  204. package/dist/commands/notifications/bell.d.ts.map +1 -0
  205. package/dist/{notifications → commands/notifications}/center.d.ts +2 -2
  206. package/dist/commands/notifications/center.d.ts.map +1 -0
  207. package/dist/{notifications → commands/notifications}/center.js +2 -2
  208. package/dist/{notifications → commands/notifications}/define-notifications.d.ts +3 -3
  209. package/dist/commands/notifications/define-notifications.d.ts.map +1 -0
  210. package/dist/commands/notifications/notify.d.ts +44 -0
  211. package/dist/commands/notifications/notify.d.ts.map +1 -0
  212. package/dist/commands/notifications/notify.js +79 -0
  213. package/dist/{notifications → commands/notifications}/store.d.ts +2 -0
  214. package/dist/commands/notifications/store.d.ts.map +1 -0
  215. package/dist/{notifications → commands/notifications}/store.js +4 -0
  216. package/dist/commands/projectors.d.ts +1 -131
  217. package/dist/commands/projectors.d.ts.map +1 -1
  218. package/dist/commands/projectors.js +2 -73
  219. package/dist/commands/shell-bridge.d.ts +3 -3
  220. package/dist/commands/shortcut-format.d.ts +5 -4
  221. package/dist/commands/shortcut-format.d.ts.map +1 -1
  222. package/dist/common/errors.d.ts +3 -3
  223. package/dist/common/use-measured-widths.d.ts.map +1 -1
  224. package/dist/common/use-measured-widths.js +5 -3
  225. package/dist/common/validate.d.ts +2 -2
  226. package/dist/content/article-layout.d.ts.map +1 -1
  227. package/dist/content/article-layout.js +3 -3
  228. package/dist/content/article-model.d.ts +1 -46
  229. package/dist/content/article-model.d.ts.map +1 -1
  230. package/dist/content/article-model.js +1 -29
  231. package/dist/content/index.d.ts +11 -41
  232. package/dist/content/index.d.ts.map +1 -1
  233. package/dist/content/index.js +3 -35
  234. package/dist/content/markdown.d.ts +1 -1
  235. package/dist/content/markdown.d.ts.map +1 -1
  236. package/dist/content/markdown.js +1 -1
  237. package/dist/content/mermaid.d.ts.map +1 -1
  238. package/dist/content/mermaid.js +7 -7
  239. package/dist/content/slug.d.ts +1 -8
  240. package/dist/content/slug.d.ts.map +1 -1
  241. package/dist/content/slug.js +1 -8
  242. package/dist/content/url-hardening.d.ts +2 -2
  243. package/dist/content/url-hardening.d.ts.map +1 -1
  244. package/dist/content/url-hardening.js +2 -0
  245. package/dist/controls/actions.d.ts +16 -1
  246. package/dist/controls/actions.d.ts.map +1 -1
  247. package/dist/controls/enum-filter.d.ts +1 -1
  248. package/dist/controls/enum-filter.d.ts.map +1 -1
  249. package/dist/controls/filter-context.d.ts.map +1 -1
  250. package/dist/controls/filter-context.js +3 -1
  251. package/dist/controls/filter-set.js +4 -6
  252. package/dist/controls/form-layout.d.ts +1 -1
  253. package/dist/controls/form-layout.d.ts.map +1 -1
  254. package/dist/controls/form-layout.js +1 -1
  255. package/dist/controls/glyphs.d.ts +0 -2
  256. package/dist/controls/glyphs.d.ts.map +1 -1
  257. package/dist/controls/glyphs.js +0 -7
  258. package/dist/controls/index.d.ts +12 -17
  259. package/dist/controls/index.d.ts.map +1 -1
  260. package/dist/controls/index.js +5 -39
  261. package/dist/controls/multi-select-filter.d.ts +4 -3
  262. package/dist/controls/multi-select-filter.d.ts.map +1 -1
  263. package/dist/controls/number-filter.d.ts +4 -3
  264. package/dist/controls/number-filter.d.ts.map +1 -1
  265. package/dist/controls/number-filter.js +3 -1
  266. package/dist/controls/range-filter.d.ts +4 -3
  267. package/dist/controls/range-filter.d.ts.map +1 -1
  268. package/dist/controls/search-filter.d.ts +4 -3
  269. package/dist/controls/search-filter.d.ts.map +1 -1
  270. package/dist/controls/search-filter.js +19 -14
  271. package/dist/controls/select-filter.d.ts +4 -3
  272. package/dist/controls/select-filter.d.ts.map +1 -1
  273. package/dist/controls/sync-button.d.ts.map +1 -1
  274. package/dist/controls/sync-button.js +12 -15
  275. package/dist/controls/toggle-filter.d.ts +4 -3
  276. package/dist/controls/toggle-filter.d.ts.map +1 -1
  277. package/dist/controls/uncontrolled.d.ts +12 -0
  278. package/dist/controls/uncontrolled.d.ts.map +1 -0
  279. package/dist/controls/view-tabs.d.ts +2 -2
  280. package/dist/controls/view-tabs.d.ts.map +1 -1
  281. package/dist/controls-dates/index.d.ts +0 -1
  282. package/dist/controls-dates/index.d.ts.map +1 -1
  283. package/dist/dashboard/index.d.ts +7 -9
  284. package/dist/dashboard/index.d.ts.map +1 -1
  285. package/dist/dashboard/index.js +3 -23
  286. package/dist/dashboard/section.d.ts +4 -6
  287. package/dist/dashboard/section.d.ts.map +1 -1
  288. package/dist/dashboard/section.js +4 -10
  289. package/dist/dashboard/stat-card.d.ts +4 -4
  290. package/dist/dashboard/stat-card.d.ts.map +1 -1
  291. package/dist/dashboard/stat-card.js +3 -3
  292. package/dist/dashboard/widget-header.module.css +9 -7
  293. package/dist/dev-dock.d.ts.map +1 -1
  294. package/dist/dev-dock.js +6 -1
  295. package/dist/format/date.d.ts +0 -34
  296. package/dist/format/date.d.ts.map +1 -1
  297. package/dist/format/date.js +0 -54
  298. package/dist/format/index.d.ts +7 -22
  299. package/dist/format/index.d.ts.map +1 -1
  300. package/dist/format/index.js +2 -43
  301. package/dist/format/number.d.ts +3 -14
  302. package/dist/format/number.d.ts.map +1 -1
  303. package/dist/format/number.js +0 -13
  304. package/dist/format/shared.d.ts +2 -3
  305. package/dist/format/shared.d.ts.map +1 -1
  306. package/dist/forms/index.d.ts +0 -11
  307. package/dist/forms/index.d.ts.map +1 -1
  308. package/dist/forms/index.js +1 -14
  309. package/dist/guard/index.d.ts +6 -21
  310. package/dist/guard/index.d.ts.map +1 -1
  311. package/dist/guard/index.js +27 -115
  312. package/dist/guard/types.d.ts +7 -18
  313. package/dist/guard/types.d.ts.map +1 -1
  314. package/dist/index.d.ts +16 -48
  315. package/dist/index.d.ts.map +1 -1
  316. package/dist/index.js +21 -117
  317. package/dist/provider/create-router.d.ts +44 -0
  318. package/dist/provider/create-router.d.ts.map +1 -0
  319. package/dist/provider/create-router.js +62 -0
  320. package/dist/provider/hotkeys.d.ts +8 -0
  321. package/dist/provider/hotkeys.d.ts.map +1 -0
  322. package/dist/provider/hotkeys.js +50 -0
  323. package/dist/provider/index.d.ts +33 -110
  324. package/dist/provider/index.d.ts.map +1 -1
  325. package/dist/provider/index.js +182 -82
  326. package/dist/provider/lab-theme.d.ts.map +1 -1
  327. package/dist/provider/route-transition.d.ts +76 -0
  328. package/dist/provider/route-transition.d.ts.map +1 -0
  329. package/dist/provider/route-transition.js +98 -0
  330. package/dist/provider/spotlight-actions.d.ts +10 -0
  331. package/dist/provider/spotlight-actions.d.ts.map +1 -0
  332. package/dist/provider/spotlight-actions.js +55 -0
  333. package/dist/provider/test-router.d.ts +6 -0
  334. package/dist/provider/test-router.d.ts.map +1 -0
  335. package/dist/provider/test-router.js +18 -0
  336. package/dist/query-client.d.ts +4 -20
  337. package/dist/query-client.d.ts.map +1 -1
  338. package/dist/query-client.js +2 -2
  339. package/dist/router-tanstack/index.d.ts +18 -64
  340. package/dist/router-tanstack/index.d.ts.map +1 -1
  341. package/dist/router-tanstack/index.js +2 -16
  342. package/dist/router-tanstack/nav.d.ts +12 -1
  343. package/dist/router-tanstack/nav.d.ts.map +1 -1
  344. package/dist/router-tanstack/nav.js +8 -1
  345. package/dist/router-tanstack/search-store.d.ts +1 -1
  346. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  347. package/dist/router-tanstack/use-nav.d.ts +8 -9
  348. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  349. package/dist/router-tanstack/use-nav.js +20 -1
  350. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  351. package/dist/shell/app-sidebar.js +1 -1
  352. package/dist/shell/index.d.ts +31 -10
  353. package/dist/shell/index.d.ts.map +1 -1
  354. package/dist/shell/index.js +14 -3
  355. package/dist/shell/page-aside.d.ts +2 -2
  356. package/dist/shell/page-aside.d.ts.map +1 -1
  357. package/dist/shell/page-aside.js +31 -12
  358. package/dist/shell/page-bar.d.ts +2 -2
  359. package/dist/shell/page-bar.d.ts.map +1 -1
  360. package/dist/shell/page-bar.js +1 -1
  361. package/dist/state.d.ts +10 -12
  362. package/dist/state.d.ts.map +1 -1
  363. package/dist/state.js +4 -6
  364. package/dist/styles.css +107 -7
  365. package/dist/surfaces.d.ts +28 -103
  366. package/dist/surfaces.d.ts.map +1 -1
  367. package/dist/surfaces.js +54 -143
  368. package/dist/{data → table}/data-table-state.d.ts +1 -1
  369. package/dist/table/data-table-state.d.ts.map +1 -0
  370. package/dist/{data → table}/data-table-state.js +4 -3
  371. package/dist/{data → table}/data-table.d.ts +28 -49
  372. package/dist/table/data-table.d.ts.map +1 -0
  373. package/dist/{data → table}/data-table.js +74 -60
  374. package/dist/table/index.d.ts +18 -0
  375. package/dist/table/index.d.ts.map +1 -0
  376. package/dist/{data/table.js → table/index.js} +1 -1
  377. package/dist/table/query-branch.d.ts.map +1 -0
  378. package/dist/table/table-features.d.ts +69 -0
  379. package/dist/table/table-features.d.ts.map +1 -0
  380. package/dist/table/table-features.js +49 -0
  381. package/dist/telemetry.d.ts +77 -0
  382. package/dist/telemetry.d.ts.map +1 -0
  383. package/dist/telemetry.js +33 -0
  384. package/dist/theme/index.d.ts +10 -21
  385. package/dist/theme/index.d.ts.map +1 -1
  386. package/dist/theme/index.js +3 -14
  387. package/dist/theme/theme-toggle.d.ts.map +1 -1
  388. package/dist/theme/theme-toggle.js +7 -33
  389. package/dist/theme/theme-toggle.module.css +30 -0
  390. package/dist/theme/theme-toggle.module.css.d.ts +10 -0
  391. package/dist/theme-lab/derive-controls.d.ts.map +1 -1
  392. package/dist/theme-lab/derive-controls.js +5 -3
  393. package/dist/theme-lab/derive-state.d.ts.map +1 -1
  394. package/dist/theme-lab/derive-state.js +2 -1
  395. package/dist/tokens/core.d.ts +360 -0
  396. package/dist/tokens/core.d.ts.map +1 -0
  397. package/dist/tokens/core.js +708 -0
  398. package/dist/tokens/derive.d.ts +1 -1
  399. package/dist/tokens/derive.d.ts.map +1 -1
  400. package/dist/tokens/index.d.ts +5 -373
  401. package/dist/tokens/index.d.ts.map +1 -1
  402. package/dist/tokens/index.js +8 -700
  403. package/dist/tokens/palette.d.ts +2 -6
  404. package/dist/tokens/palette.d.ts.map +1 -1
  405. package/dist/tokens/palette.js +1 -4
  406. package/dist/vite.d.ts +4 -3
  407. package/dist/vite.d.ts.map +1 -1
  408. package/llms.txt +43 -85
  409. package/package.json +77 -170
  410. package/src/agent/ai-sdk-transport.ts +57 -37
  411. package/src/{agent-chat → agent/chat}/composer.tsx +29 -30
  412. package/src/{agent-chat → agent/chat}/message-affordances.ts +8 -5
  413. package/src/{agent-chat → agent/chat}/thread-feed-row.tsx +30 -35
  414. package/src/{agent-chat → agent/chat}/thread-feed.tsx +40 -35
  415. package/src/{agent-chat → agent/chat}/thread-message.tsx +37 -35
  416. package/src/{agent-chat → agent/chat}/thread-outcome-card.tsx +5 -5
  417. package/src/{agent-chat → agent/chat}/tool-chip.tsx +12 -12
  418. package/src/{agent-chat → agent/chat}/use-keyboard-inset.ts +1 -1
  419. package/src/{agent-chat → agent/chat}/virtualize.ts +5 -5
  420. package/src/agent/error.ts +43 -0
  421. package/src/agent/history.ts +2 -107
  422. package/src/agent/id.ts +4 -5
  423. package/src/agent/index.ts +23 -112
  424. package/src/agent/outcome.ts +3 -56
  425. package/src/agent/part-list.tsx +3 -3
  426. package/src/agent/parts.ts +23 -22
  427. package/src/agent/run-committer.ts +64 -0
  428. package/src/agent/run-records.ts +147 -0
  429. package/src/agent/thread.ts +24 -231
  430. package/src/agent/transport.ts +6 -96
  431. package/src/agent/use-agent-thread-runs.ts +364 -199
  432. package/src/charts/BandStrip.tsx +276 -0
  433. package/src/charts/Bars.tsx +141 -0
  434. package/src/charts/CartesianChart.tsx +206 -0
  435. package/src/charts/ChartCard.tsx +178 -0
  436. package/src/charts/Donut.tsx +256 -0
  437. package/src/charts/DualPanel.tsx +326 -0
  438. package/src/charts/Heatmap.tsx +282 -0
  439. package/src/charts/Lines.tsx +151 -0
  440. package/src/charts/MirroredBars.tsx +303 -0
  441. package/src/charts/Sparkline.tsx +176 -0
  442. package/src/charts/StackedArea.tsx +80 -0
  443. package/src/charts/box.tsx +96 -0
  444. package/src/charts/card-context.ts +11 -0
  445. package/src/charts/chart-box.module.css +64 -0
  446. package/src/charts/chart-box.module.css.d.ts +12 -0
  447. package/src/charts/chart-card.module.css +25 -0
  448. package/src/charts/chart-card.module.css.d.ts +8 -0
  449. package/src/charts/chart-legend.module.css +38 -0
  450. package/src/charts/chart-legend.module.css.d.ts +10 -0
  451. package/src/charts/compile.ts +585 -0
  452. package/src/charts/cursor.ts +282 -0
  453. package/src/charts/curves.ts +111 -0
  454. package/src/charts/donut.module.css +54 -0
  455. package/src/charts/donut.module.css.d.ts +9 -0
  456. package/src/charts/host.tsx +60 -0
  457. package/src/charts/index.ts +41 -178
  458. package/src/charts/legend.tsx +204 -0
  459. package/src/charts/marks.ts +201 -0
  460. package/src/charts/panel.tsx +227 -0
  461. package/src/charts/scales.ts +237 -0
  462. package/src/charts/sizing.ts +47 -0
  463. package/src/charts/state.tsx +80 -0
  464. package/src/charts/theme.ts +65 -0
  465. package/src/charts/types.ts +242 -0
  466. package/src/cli/audit-allows.ts +8 -30
  467. package/src/cli/config.ts +14 -43
  468. package/src/cli/doctor-upgrade.ts +392 -0
  469. package/src/cli/doctor.ts +81 -72
  470. package/src/cli/help.ts +2 -4
  471. package/src/cli/index.ts +100 -384
  472. package/src/cli/init.ts +60 -51
  473. package/src/cli/sync.ts +13 -68
  474. package/src/cli/tokens-css.ts +7 -28
  475. package/src/commands/ShortcutsHelp.tsx +8 -9
  476. package/src/commands/define-commands.ts +16 -43
  477. package/src/commands/define-overlays.ts +12 -36
  478. package/src/commands/index.ts +11 -94
  479. package/src/{notifications → commands/notifications}/bell.tsx +2 -2
  480. package/src/{notifications → commands/notifications}/center.tsx +7 -5
  481. package/src/{notifications → commands/notifications}/define-notifications.ts +3 -3
  482. package/src/commands/notifications/notify.tsx +158 -0
  483. package/src/{notifications → commands/notifications}/store.ts +10 -2
  484. package/src/commands/projectors.ts +5 -225
  485. package/src/commands/shell-bridge.ts +5 -5
  486. package/src/commands/shortcut-format.ts +5 -4
  487. package/src/common/errors.ts +3 -3
  488. package/src/common/use-measured-widths.ts +6 -3
  489. package/src/common/validate.ts +2 -2
  490. package/src/content/article-layout.tsx +37 -38
  491. package/src/content/article-model.ts +1 -84
  492. package/src/content/index.ts +12 -83
  493. package/src/content/markdown.tsx +4 -3
  494. package/src/content/mermaid.tsx +9 -7
  495. package/src/content/slug.ts +1 -16
  496. package/src/content/url-hardening.ts +8 -2
  497. package/src/controls/actions.tsx +24 -1
  498. package/src/controls/enum-filter.tsx +1 -1
  499. package/src/controls/filter-context.tsx +3 -1
  500. package/src/controls/filter-set.tsx +5 -7
  501. package/src/controls/form-layout.tsx +2 -2
  502. package/src/controls/glyphs.tsx +0 -10
  503. package/src/controls/index.ts +13 -64
  504. package/src/controls/multi-select-filter.tsx +5 -4
  505. package/src/controls/number-filter.tsx +8 -4
  506. package/src/controls/range-filter.tsx +5 -4
  507. package/src/controls/search-filter.tsx +25 -19
  508. package/src/controls/select-filter.tsx +5 -4
  509. package/src/controls/sync-button.tsx +18 -20
  510. package/src/controls/toggle-filter.tsx +5 -4
  511. package/src/controls/uncontrolled.ts +12 -0
  512. package/src/controls/view-tabs.tsx +3 -3
  513. package/src/controls-dates/date-range-picker.tsx +1 -1
  514. package/src/controls-dates/index.ts +0 -1
  515. package/src/dashboard/index.ts +7 -48
  516. package/src/dashboard/section.tsx +11 -40
  517. package/src/dashboard/stat-card.tsx +14 -16
  518. package/src/dashboard/widget-header.module.css +9 -7
  519. package/src/dev-dock.tsx +14 -27
  520. package/src/format/date.ts +2 -92
  521. package/src/format/index.ts +7 -53
  522. package/src/format/number.ts +3 -26
  523. package/src/format/shared.ts +2 -3
  524. package/src/forms/index.ts +0 -23
  525. package/src/guard/index.ts +36 -191
  526. package/src/guard/types.ts +6 -19
  527. package/src/index.ts +30 -282
  528. package/src/provider/create-router.tsx +138 -0
  529. package/src/provider/hotkeys.ts +70 -0
  530. package/src/provider/index.tsx +260 -230
  531. package/src/provider/lab-theme.ts +3 -2
  532. package/src/provider/route-transition.ts +199 -0
  533. package/src/provider/spotlight-actions.tsx +69 -0
  534. package/src/provider/test-router.tsx +25 -0
  535. package/src/query-client.ts +4 -22
  536. package/src/router-tanstack/index.ts +18 -112
  537. package/src/router-tanstack/nav.ts +28 -2
  538. package/src/router-tanstack/search-store.ts +9 -2
  539. package/src/router-tanstack/use-nav.ts +47 -13
  540. package/src/shell/app-mobile-nav.tsx +3 -2
  541. package/src/shell/app-sidebar.tsx +1 -1
  542. package/src/shell/aside-docking.ts +1 -1
  543. package/src/shell/index.tsx +61 -20
  544. package/src/shell/page-aside.tsx +47 -23
  545. package/src/shell/page-bar.tsx +3 -3
  546. package/src/shell/page-title.tsx +1 -1
  547. package/src/state.ts +11 -37
  548. package/src/styles.css +107 -7
  549. package/src/surfaces.ts +56 -151
  550. package/src/{data → table}/data-table-state.ts +5 -4
  551. package/src/{data → table}/data-table.tsx +170 -161
  552. package/src/table/index.ts +18 -0
  553. package/src/table/table-features.ts +102 -0
  554. package/src/telemetry.ts +110 -0
  555. package/src/theme/index.ts +22 -47
  556. package/src/theme/theme-toggle.module.css +30 -0
  557. package/src/theme/theme-toggle.module.css.d.ts +10 -0
  558. package/src/theme/theme-toggle.tsx +15 -43
  559. package/src/theme-lab/derive-controls.tsx +5 -3
  560. package/src/theme-lab/derive-state.ts +2 -1
  561. package/src/tokens/core.ts +1163 -0
  562. package/src/tokens/derive.ts +1 -1
  563. package/src/tokens/index.ts +14 -1181
  564. package/src/tokens/palette.ts +3 -7
  565. package/src/vite.ts +3 -3
  566. package/agent/templates/query-client.ts.tpl +0 -26
  567. package/dist/agent/adapter.d.ts +0 -150
  568. package/dist/agent/adapter.d.ts.map +0 -1
  569. package/dist/agent/adapter.js +0 -482
  570. package/dist/agent/use-agent-stream.d.ts +0 -35
  571. package/dist/agent/use-agent-stream.d.ts.map +0 -1
  572. package/dist/agent/use-agent-stream.js +0 -59
  573. package/dist/agent-chat/composer.d.ts.map +0 -1
  574. package/dist/agent-chat/index.d.ts +0 -31
  575. package/dist/agent-chat/index.d.ts.map +0 -1
  576. package/dist/agent-chat/index.js +0 -19
  577. package/dist/agent-chat/message-affordances.d.ts.map +0 -1
  578. package/dist/agent-chat/relative-time.d.ts.map +0 -1
  579. package/dist/agent-chat/thread-detail-panel.d.ts +0 -28
  580. package/dist/agent-chat/thread-detail-panel.d.ts.map +0 -1
  581. package/dist/agent-chat/thread-detail-panel.js +0 -115
  582. package/dist/agent-chat/thread-feed-row.d.ts.map +0 -1
  583. package/dist/agent-chat/thread-feed.d.ts.map +0 -1
  584. package/dist/agent-chat/thread-message.d.ts.map +0 -1
  585. package/dist/agent-chat/thread-outcome-card.d.ts.map +0 -1
  586. package/dist/agent-chat/thread-workspace.d.ts +0 -44
  587. package/dist/agent-chat/thread-workspace.d.ts.map +0 -1
  588. package/dist/agent-chat/thread-workspace.js +0 -153
  589. package/dist/agent-chat/tool-chip.d.ts.map +0 -1
  590. package/dist/agent-chat/use-keyboard-inset.d.ts.map +0 -1
  591. package/dist/agent-chat/virtualize.d.ts.map +0 -1
  592. package/dist/charts/cursor/resolve.d.ts +0 -75
  593. package/dist/charts/cursor/resolve.d.ts.map +0 -1
  594. package/dist/charts/cursor/resolve.js +0 -91
  595. package/dist/charts/cursor/scope.d.ts +0 -20
  596. package/dist/charts/cursor/scope.d.ts.map +0 -1
  597. package/dist/charts/cursor/scope.js +0 -20
  598. package/dist/charts/cursor/store.d.ts +0 -35
  599. package/dist/charts/cursor/store.d.ts.map +0 -1
  600. package/dist/charts/cursor/store.js +0 -25
  601. package/dist/charts/cursor/touch-pin.d.ts +0 -45
  602. package/dist/charts/cursor/touch-pin.d.ts.map +0 -1
  603. package/dist/charts/cursor/touch-pin.js +0 -79
  604. package/dist/charts/hooks/use-discrete-cursor.d.ts +0 -66
  605. package/dist/charts/hooks/use-discrete-cursor.d.ts.map +0 -1
  606. package/dist/charts/hooks/use-discrete-cursor.js +0 -198
  607. package/dist/charts/hooks/use-hysteresis.d.ts +0 -18
  608. package/dist/charts/hooks/use-hysteresis.d.ts.map +0 -1
  609. package/dist/charts/hooks/use-hysteresis.js +0 -12
  610. package/dist/charts/hooks/useBandPlot.d.ts +0 -145
  611. package/dist/charts/hooks/useBandPlot.d.ts.map +0 -1
  612. package/dist/charts/hooks/useBandPlot.js +0 -178
  613. package/dist/charts/hooks/useChartCursor.d.ts +0 -75
  614. package/dist/charts/hooks/useChartCursor.d.ts.map +0 -1
  615. package/dist/charts/hooks/useChartCursor.js +0 -200
  616. package/dist/charts/hooks/useChartSize.d.ts +0 -29
  617. package/dist/charts/hooks/useChartSize.d.ts.map +0 -1
  618. package/dist/charts/hooks/useChartSize.js +0 -8
  619. package/dist/charts/kinds/BandStrip.d.ts +0 -132
  620. package/dist/charts/kinds/BandStrip.d.ts.map +0 -1
  621. package/dist/charts/kinds/BandStrip.js +0 -241
  622. package/dist/charts/kinds/Bars.d.ts +0 -141
  623. package/dist/charts/kinds/Bars.d.ts.map +0 -1
  624. package/dist/charts/kinds/Bars.js +0 -343
  625. package/dist/charts/kinds/Donut.d.ts +0 -77
  626. package/dist/charts/kinds/Donut.d.ts.map +0 -1
  627. package/dist/charts/kinds/Donut.js +0 -245
  628. package/dist/charts/kinds/DualPanel.d.ts +0 -111
  629. package/dist/charts/kinds/DualPanel.d.ts.map +0 -1
  630. package/dist/charts/kinds/DualPanel.js +0 -471
  631. package/dist/charts/kinds/Heatmap.d.ts +0 -85
  632. package/dist/charts/kinds/Heatmap.d.ts.map +0 -1
  633. package/dist/charts/kinds/Heatmap.js +0 -243
  634. package/dist/charts/kinds/MirroredBars.d.ts +0 -98
  635. package/dist/charts/kinds/MirroredBars.d.ts.map +0 -1
  636. package/dist/charts/kinds/MirroredBars.js +0 -292
  637. package/dist/charts/kinds/MultiLine.d.ts +0 -87
  638. package/dist/charts/kinds/MultiLine.d.ts.map +0 -1
  639. package/dist/charts/kinds/MultiLine.js +0 -176
  640. package/dist/charts/kinds/StackedArea.d.ts +0 -73
  641. package/dist/charts/kinds/StackedArea.d.ts.map +0 -1
  642. package/dist/charts/kinds/StackedArea.js +0 -135
  643. package/dist/charts/kinds/ZonedLine.d.ts +0 -97
  644. package/dist/charts/kinds/ZonedLine.d.ts.map +0 -1
  645. package/dist/charts/kinds/ZonedLine.js +0 -155
  646. package/dist/charts/kinds/band-chrome.d.ts +0 -31
  647. package/dist/charts/kinds/band-chrome.d.ts.map +0 -1
  648. package/dist/charts/kinds/band-chrome.js +0 -71
  649. package/dist/charts/kinds/donut-layout.d.ts +0 -47
  650. package/dist/charts/kinds/donut-layout.d.ts.map +0 -1
  651. package/dist/charts/kinds/donut-layout.js +0 -52
  652. package/dist/charts/layout/auto-margin.d.ts +0 -122
  653. package/dist/charts/layout/auto-margin.d.ts.map +0 -1
  654. package/dist/charts/layout/auto-margin.js +0 -81
  655. package/dist/charts/layout/end-labels.d.ts +0 -64
  656. package/dist/charts/layout/end-labels.d.ts.map +0 -1
  657. package/dist/charts/layout/end-labels.js +0 -76
  658. package/dist/charts/layout/log-ticks.d.ts +0 -27
  659. package/dist/charts/layout/log-ticks.d.ts.map +0 -1
  660. package/dist/charts/layout/log-ticks.js +0 -42
  661. package/dist/charts/primitives/AreaGradient.d.ts +0 -18
  662. package/dist/charts/primitives/AreaGradient.d.ts.map +0 -1
  663. package/dist/charts/primitives/AreaGradient.js +0 -13
  664. package/dist/charts/primitives/Axes.d.ts +0 -83
  665. package/dist/charts/primitives/Axes.d.ts.map +0 -1
  666. package/dist/charts/primitives/Axes.js +0 -168
  667. package/dist/charts/primitives/CartesianChart.d.ts +0 -257
  668. package/dist/charts/primitives/CartesianChart.d.ts.map +0 -1
  669. package/dist/charts/primitives/CartesianChart.js +0 -549
  670. package/dist/charts/primitives/ChartCard.d.ts +0 -66
  671. package/dist/charts/primitives/ChartCard.d.ts.map +0 -1
  672. package/dist/charts/primitives/ChartCard.js +0 -143
  673. package/dist/charts/primitives/ChartFrame.d.ts +0 -156
  674. package/dist/charts/primitives/ChartFrame.d.ts.map +0 -1
  675. package/dist/charts/primitives/ChartFrame.js +0 -241
  676. package/dist/charts/primitives/ChartLegend.d.ts +0 -67
  677. package/dist/charts/primitives/ChartLegend.d.ts.map +0 -1
  678. package/dist/charts/primitives/ChartLegend.js +0 -292
  679. package/dist/charts/primitives/ChartPending.d.ts +0 -113
  680. package/dist/charts/primitives/ChartPending.d.ts.map +0 -1
  681. package/dist/charts/primitives/ChartPending.js +0 -117
  682. package/dist/charts/primitives/ChartTooltip.d.ts +0 -59
  683. package/dist/charts/primitives/ChartTooltip.d.ts.map +0 -1
  684. package/dist/charts/primitives/ChartTooltip.js +0 -197
  685. package/dist/charts/primitives/Crosshair.d.ts +0 -26
  686. package/dist/charts/primitives/Crosshair.d.ts.map +0 -1
  687. package/dist/charts/primitives/Crosshair.js +0 -23
  688. package/dist/charts/primitives/HatchPattern.d.ts +0 -31
  689. package/dist/charts/primitives/HatchPattern.d.ts.map +0 -1
  690. package/dist/charts/primitives/HatchPattern.js +0 -31
  691. package/dist/charts/primitives/HoverOverlay.d.ts +0 -45
  692. package/dist/charts/primitives/HoverOverlay.d.ts.map +0 -1
  693. package/dist/charts/primitives/HoverOverlay.js +0 -72
  694. package/dist/charts/primitives/HoverOverlay.module.css +0 -13
  695. package/dist/charts/primitives/HoverOverlay.module.css.d.ts +0 -8
  696. package/dist/charts/primitives/XZoneRects.d.ts +0 -54
  697. package/dist/charts/primitives/XZoneRects.d.ts.map +0 -1
  698. package/dist/charts/primitives/XZoneRects.js +0 -65
  699. package/dist/charts/primitives/ZoneRects.d.ts +0 -38
  700. package/dist/charts/primitives/ZoneRects.d.ts.map +0 -1
  701. package/dist/charts/primitives/ZoneRects.js +0 -30
  702. package/dist/charts/primitives/chart-card-context.d.ts +0 -33
  703. package/dist/charts/primitives/chart-card-context.d.ts.map +0 -1
  704. package/dist/charts/primitives/chart-card-context.js +0 -18
  705. package/dist/charts/primitives/chart-frame-layout.d.ts +0 -185
  706. package/dist/charts/primitives/chart-frame-layout.d.ts.map +0 -1
  707. package/dist/charts/primitives/chart-frame-layout.js +0 -175
  708. package/dist/charts/primitives/chart-layout.d.ts +0 -112
  709. package/dist/charts/primitives/chart-layout.d.ts.map +0 -1
  710. package/dist/charts/primitives/chart-layout.js +0 -175
  711. package/dist/charts/primitives/chart-tier.d.ts +0 -16
  712. package/dist/charts/primitives/chart-tier.d.ts.map +0 -1
  713. package/dist/charts/primitives/chart-tier.js +0 -44
  714. package/dist/charts/primitives/legend-disclosure.d.ts +0 -27
  715. package/dist/charts/primitives/legend-disclosure.d.ts.map +0 -1
  716. package/dist/charts/primitives/legend-disclosure.js +0 -153
  717. package/dist/charts/series.d.ts +0 -189
  718. package/dist/charts/series.d.ts.map +0 -1
  719. package/dist/charts/series.js +0 -84
  720. package/dist/charts/sparklines/BarSparkline.d.ts +0 -37
  721. package/dist/charts/sparklines/BarSparkline.d.ts.map +0 -1
  722. package/dist/charts/sparklines/BarSparkline.js +0 -53
  723. package/dist/charts/sparklines/LineSparkline.d.ts +0 -15
  724. package/dist/charts/sparklines/LineSparkline.d.ts.map +0 -1
  725. package/dist/charts/sparklines/LineSparkline.js +0 -51
  726. package/dist/charts/sparklines/index.d.ts +0 -3
  727. package/dist/charts/sparklines/index.d.ts.map +0 -1
  728. package/dist/charts/sparklines/index.js +0 -6
  729. package/dist/charts/utils/domain.d.ts +0 -35
  730. package/dist/charts/utils/domain.d.ts.map +0 -1
  731. package/dist/charts/utils/domain.js +0 -12
  732. package/dist/charts/utils/format.d.ts +0 -8
  733. package/dist/charts/utils/format.d.ts.map +0 -1
  734. package/dist/charts/utils/format.js +0 -20
  735. package/dist/charts/utils/measure-text.d.ts +0 -25
  736. package/dist/charts/utils/measure-text.d.ts.map +0 -1
  737. package/dist/charts/utils/measure-text.js +0 -64
  738. package/dist/charts/utils/ticks.d.ts +0 -103
  739. package/dist/charts/utils/ticks.d.ts.map +0 -1
  740. package/dist/charts/utils/ticks.js +0 -92
  741. package/dist/commands/overlays-mount.d.ts +0 -122
  742. package/dist/commands/overlays-mount.d.ts.map +0 -1
  743. package/dist/commands/overlays-mount.js +0 -125
  744. package/dist/commands/useCommandHotkeys.d.ts +0 -14
  745. package/dist/commands/useCommandHotkeys.d.ts.map +0 -1
  746. package/dist/commands/useCommandHotkeys.js +0 -45
  747. package/dist/content/article-actions.d.ts +0 -39
  748. package/dist/content/article-actions.d.ts.map +0 -1
  749. package/dist/content/article-actions.js +0 -28
  750. package/dist/content/article-card.d.ts +0 -58
  751. package/dist/content/article-card.d.ts.map +0 -1
  752. package/dist/content/article-card.js +0 -79
  753. package/dist/content/article-card.module.css +0 -111
  754. package/dist/content/mdx.d.ts +0 -20
  755. package/dist/content/mdx.d.ts.map +0 -1
  756. package/dist/content/mdx.js +0 -64
  757. package/dist/controls/compare-filter.d.ts +0 -42
  758. package/dist/controls/compare-filter.d.ts.map +0 -1
  759. package/dist/controls/compare-filter.js +0 -38
  760. package/dist/controls/slider-control.d.ts +0 -36
  761. package/dist/controls/slider-control.d.ts.map +0 -1
  762. package/dist/controls/slider-control.js +0 -64
  763. package/dist/dashboard/widget-grid.d.ts +0 -27
  764. package/dist/dashboard/widget-grid.d.ts.map +0 -1
  765. package/dist/dashboard/widget-grid.js +0 -40
  766. package/dist/dashboard/widget-grid.module.css +0 -69
  767. package/dist/data/data-table-state.d.ts.map +0 -1
  768. package/dist/data/data-table.d.ts.map +0 -1
  769. package/dist/data/query-branch.d.ts.map +0 -1
  770. package/dist/data/table.d.ts +0 -24
  771. package/dist/data/table.d.ts.map +0 -1
  772. package/dist/data/virtual-list.d.ts +0 -133
  773. package/dist/data/virtual-list.d.ts.map +0 -1
  774. package/dist/data/virtual-list.js +0 -157
  775. package/dist/data/virtual.d.ts +0 -21
  776. package/dist/data/virtual.d.ts.map +0 -1
  777. package/dist/data/virtual.js +0 -4
  778. package/dist/forms/FormErrorSummary.d.ts +0 -30
  779. package/dist/forms/FormErrorSummary.d.ts.map +0 -1
  780. package/dist/forms/FormErrorSummary.js +0 -30
  781. package/dist/forms/use-field-array.d.ts +0 -60
  782. package/dist/forms/use-field-array.d.ts.map +0 -1
  783. package/dist/forms/use-field-array.js +0 -20
  784. package/dist/forms/use-form-draft.d.ts +0 -76
  785. package/dist/forms/use-form-draft.d.ts.map +0 -1
  786. package/dist/forms/use-form-draft.js +0 -92
  787. package/dist/forms/use-form-submit.d.ts +0 -88
  788. package/dist/forms/use-form-submit.d.ts.map +0 -1
  789. package/dist/forms/use-form-submit.js +0 -63
  790. package/dist/notifications/bell.d.ts.map +0 -1
  791. package/dist/notifications/center.d.ts.map +0 -1
  792. package/dist/notifications/define-notifications.d.ts.map +0 -1
  793. package/dist/notifications/index.d.ts +0 -40
  794. package/dist/notifications/index.d.ts.map +0 -1
  795. package/dist/notifications/index.js +0 -54
  796. package/dist/notifications/mount-guard.d.ts +0 -8
  797. package/dist/notifications/mount-guard.d.ts.map +0 -1
  798. package/dist/notifications/mount-guard.js +0 -18
  799. package/dist/notifications/notify.d.ts +0 -149
  800. package/dist/notifications/notify.d.ts.map +0 -1
  801. package/dist/notifications/notify.js +0 -171
  802. package/dist/notifications/store.d.ts.map +0 -1
  803. package/dist/provider/connectivity-indicator.d.ts +0 -13
  804. package/dist/provider/connectivity-indicator.d.ts.map +0 -1
  805. package/dist/provider/connectivity-indicator.js +0 -205
  806. package/dist/provider/connectivity-provider.d.ts +0 -7
  807. package/dist/provider/connectivity-provider.d.ts.map +0 -1
  808. package/dist/provider/connectivity-provider.js +0 -128
  809. package/dist/provider/connectivity-types.d.ts +0 -36
  810. package/dist/provider/connectivity-types.d.ts.map +0 -1
  811. package/dist/provider/use-connectivity.d.ts +0 -3
  812. package/dist/provider/use-connectivity.d.ts.map +0 -1
  813. package/dist/provider/use-connectivity.js +0 -14
  814. package/dist/query-devtools.d.ts +0 -21
  815. package/dist/query-devtools.d.ts.map +0 -1
  816. package/dist/query-devtools.js +0 -13
  817. package/src/agent/adapter.ts +0 -945
  818. package/src/agent/use-agent-stream.ts +0 -154
  819. package/src/agent-chat/index.ts +0 -54
  820. package/src/agent-chat/thread-detail-panel.tsx +0 -201
  821. package/src/agent-chat/thread-workspace.tsx +0 -279
  822. package/src/charts/cursor/resolve.ts +0 -198
  823. package/src/charts/cursor/scope.tsx +0 -35
  824. package/src/charts/cursor/store.ts +0 -58
  825. package/src/charts/cursor/touch-pin.ts +0 -138
  826. package/src/charts/hooks/use-discrete-cursor.ts +0 -335
  827. package/src/charts/hooks/use-hysteresis.ts +0 -26
  828. package/src/charts/hooks/useBandPlot.ts +0 -383
  829. package/src/charts/hooks/useChartCursor.ts +0 -367
  830. package/src/charts/hooks/useChartSize.ts +0 -35
  831. package/src/charts/kinds/BandStrip.tsx +0 -449
  832. package/src/charts/kinds/Bars.tsx +0 -589
  833. package/src/charts/kinds/Donut.tsx +0 -368
  834. package/src/charts/kinds/DualPanel.tsx +0 -712
  835. package/src/charts/kinds/Heatmap.tsx +0 -396
  836. package/src/charts/kinds/MirroredBars.tsx +0 -469
  837. package/src/charts/kinds/MultiLine.tsx +0 -322
  838. package/src/charts/kinds/StackedArea.tsx +0 -267
  839. package/src/charts/kinds/ZonedLine.tsx +0 -301
  840. package/src/charts/kinds/band-chrome.tsx +0 -91
  841. package/src/charts/kinds/donut-layout.ts +0 -82
  842. package/src/charts/layout/auto-margin.ts +0 -242
  843. package/src/charts/layout/end-labels.ts +0 -135
  844. package/src/charts/layout/log-ticks.ts +0 -90
  845. package/src/charts/primitives/AreaGradient.tsx +0 -26
  846. package/src/charts/primitives/Axes.tsx +0 -271
  847. package/src/charts/primitives/CartesianChart.tsx +0 -1002
  848. package/src/charts/primitives/ChartCard.tsx +0 -280
  849. package/src/charts/primitives/ChartFrame.tsx +0 -493
  850. package/src/charts/primitives/ChartLegend.tsx +0 -422
  851. package/src/charts/primitives/ChartPending.tsx +0 -243
  852. package/src/charts/primitives/ChartTooltip.tsx +0 -265
  853. package/src/charts/primitives/Crosshair.tsx +0 -43
  854. package/src/charts/primitives/HatchPattern.tsx +0 -55
  855. package/src/charts/primitives/HoverOverlay.module.css +0 -13
  856. package/src/charts/primitives/HoverOverlay.module.css.d.ts +0 -8
  857. package/src/charts/primitives/HoverOverlay.tsx +0 -109
  858. package/src/charts/primitives/XZoneRects.tsx +0 -132
  859. package/src/charts/primitives/ZoneRects.tsx +0 -67
  860. package/src/charts/primitives/chart-card-context.ts +0 -46
  861. package/src/charts/primitives/chart-frame-layout.ts +0 -430
  862. package/src/charts/primitives/chart-layout.ts +0 -318
  863. package/src/charts/primitives/chart-tier.tsx +0 -76
  864. package/src/charts/primitives/legend-disclosure.tsx +0 -178
  865. package/src/charts/series.ts +0 -276
  866. package/src/charts/sparklines/BarSparkline.tsx +0 -94
  867. package/src/charts/sparklines/LineSparkline.tsx +0 -71
  868. package/src/charts/sparklines/index.ts +0 -2
  869. package/src/charts/theme.tsx +0 -53
  870. package/src/charts/utils/domain.ts +0 -41
  871. package/src/charts/utils/format.ts +0 -19
  872. package/src/charts/utils/measure-text.ts +0 -109
  873. package/src/charts/utils/ticks.ts +0 -239
  874. package/src/commands/overlays-mount.tsx +0 -333
  875. package/src/commands/useCommandHotkeys.ts +0 -114
  876. package/src/content/article-actions.ts +0 -72
  877. package/src/content/article-card.module.css +0 -111
  878. package/src/content/article-card.tsx +0 -174
  879. package/src/content/mdx.tsx +0 -127
  880. package/src/controls/compare-filter.tsx +0 -81
  881. package/src/controls/slider-control.tsx +0 -159
  882. package/src/dashboard/widget-grid.module.css +0 -69
  883. package/src/dashboard/widget-grid.tsx +0 -146
  884. package/src/data/table.ts +0 -39
  885. package/src/data/virtual-list.tsx +0 -356
  886. package/src/data/virtual.ts +0 -26
  887. package/src/forms/FormErrorSummary.tsx +0 -71
  888. package/src/forms/use-field-array.ts +0 -94
  889. package/src/forms/use-form-draft.ts +0 -262
  890. package/src/forms/use-form-submit.ts +0 -199
  891. package/src/notifications/index.ts +0 -88
  892. package/src/notifications/mount-guard.ts +0 -38
  893. package/src/notifications/notify.tsx +0 -379
  894. package/src/provider/connectivity-indicator.tsx +0 -267
  895. package/src/provider/connectivity-provider.tsx +0 -189
  896. package/src/provider/connectivity-types.ts +0 -42
  897. package/src/provider/use-connectivity.ts +0 -26
  898. package/src/query-devtools.tsx +0 -40
  899. /package/dist/{agent-chat → agent/chat}/message-affordances.js +0 -0
  900. /package/dist/{agent-chat → agent/chat}/relative-time.d.ts +0 -0
  901. /package/dist/{agent-chat → agent/chat}/relative-time.js +0 -0
  902. /package/dist/{agent-chat → agent/chat}/use-keyboard-inset.d.ts +0 -0
  903. /package/dist/{agent-chat → agent/chat}/virtualize.js +0 -0
  904. /package/dist/{notifications → commands/notifications}/bell.js +0 -0
  905. /package/dist/{notifications → commands/notifications}/define-notifications.js +0 -0
  906. /package/dist/{provider/connectivity-types.js → controls/uncontrolled.js} +0 -0
  907. /package/dist/{data → table}/data-table.module.css +0 -0
  908. /package/dist/{data → table}/data-table.module.css.d.ts +0 -0
  909. /package/dist/{data → table}/query-branch.d.ts +0 -0
  910. /package/dist/{data → table}/query-branch.js +0 -0
  911. /package/src/{agent-chat → agent/chat}/relative-time.ts +0 -0
  912. /package/src/{data → table}/data-table.module.css +0 -0
  913. /package/src/{data → table}/data-table.module.css.d.ts +0 -0
  914. /package/src/{data → table}/query-branch.ts +0 -0
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * useAgentThreadRuns — concurrent multi-thread stream run manager.
3
3
  *
4
- * Unlike useAgentStream (ONE in-flight turn — a new send() aborts the previous one),
5
4
  * useAgentThreadRuns runs N CONCURRENT streams, one per thread id, so a user can fire many
6
5
  * short chats that each stream and resolve independently in the background. Each thread's
7
6
  * turn is appended to an injected ThreadsStore and distilled into an AgentOutcome via an
@@ -14,61 +13,94 @@
14
13
  * per thread id and cached: `transport: (threadId) => aiSdkTransport(opts).forThread(threadId)`.
15
14
  *
16
15
  * @example
17
- * import { useAgentThreadRuns, createThreadsStore, edenTransport, heuristicOutcome } from 'basalt-ui/agent'
16
+ * import { useAgentThreadRuns, aiSdkTransport } from 'basalt-ui/agent'
18
17
  *
19
- * const useThreads = createThreadsStore({ key: 'main-threads', version: 1 })
20
- * const transport = edenTransport((input, signal) =>
21
- * api.chat.post({ body: { message: input }, fetch: { signal } }),
22
- * )
18
+ * const transport = aiSdkTransport({ api: '/api/chat' })
23
19
  *
24
20
  * function ThreadFeed() {
25
- * const store = useThreads()
21
+ * const store = useMyThreadsStore() // consumer-owned ThreadsStore
26
22
  * const { runs, start, stop } = useAgentThreadRuns({
27
23
  * transport,
28
24
  * store,
29
- * resolveOutcome: heuristicOutcome,
25
+ * resolveOutcome, // your OutcomeResolver
30
26
  * })
31
27
  * const id = store.create()
28
+ * const status = runs.get(id)?.status // 'submitted' | 'streaming' | 'error' | undefined
29
+ * const isLive = status === 'submitted' || status === 'streaming'
32
30
  * return (
33
31
  * <div>
34
32
  * <button onClick={() => start(id, 'Hello')}>Send</button>
35
- * {runs.get(id)?.status === 'streaming' && <button onClick={() => stop(id)}>Stop</button>}
33
+ * {isLive && <button onClick={() => stop(id)}>Stop</button>}
36
34
  * </div>
37
35
  * )
38
36
  * }
39
37
  */
40
- import { useCallback, useEffect, useRef, useState } from 'react'
38
+ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
41
39
  import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
42
40
  import type { ChatMessage } from './history'
41
+ import { toAgentError } from './error'
42
+ import type { AgentError } from './error'
43
43
  import { mintMessageId, mintThreadId, withPartIds } from './id'
44
44
  import { mergePart } from './merge'
45
45
  import type { PartLike } from './merge'
46
46
  import type { AgentOutcome, OutcomeResolver } from './outcome'
47
47
  import { isStartPart } from './parts'
48
48
  import type { AgentPart } from './parts'
49
- import type { AgentThread, ThreadStatus, ThreadsStore } from './thread'
49
+ import { createRunCommitter, isContentPart } from './run-committer'
50
+ import {
51
+ beginRun,
52
+ endRun,
53
+ failedThreadIds,
54
+ dismissRun,
55
+ hasLiveRun,
56
+ isLatestRun,
57
+ isLive,
58
+ planStopWithTombstone,
59
+ sweepFlushed,
60
+ } from './run-records'
61
+ import type { RunRecord, RunRecords } from './run-records'
62
+ import type { AgentThread, LiveRunStatus, ThreadStatus, ThreadsStore } from './thread'
50
63
  import { isResumable } from './transport'
51
64
  import type { AgentTransport } from './transport'
65
+ import { TelemetryContext, createEmitter, errorName, now } from '../telemetry'
66
+ import type { BasaltEvent, Telemetry } from '../telemetry'
52
67
 
53
68
  // ── ThreadRunState ────────────────────────────────────────────────────────────
54
69
 
55
70
  /**
56
- * The live stream state of a single thread's in-flight turn.
71
+ * The stream state of a single thread's most recent turn, while it is in flight or has failed.
57
72
  *
58
- * A thread only has an entry in the `runs` map while its turn is actively streaming — once a
59
- * turn finishes (success or error), its entry is deleted from the map (the outcome lives on the
60
- * persisted `AgentThread` instead). So `status` only ever holds `'streaming'`; there is no
61
- * `'done'`/`'idle'`/`'error'` to observe here.
73
+ * `status` mirrors AI SDK's `ChatStatus`: `'submitted'` from send until the first content part
74
+ * (text, reasoning or tool) arrives, then `'streaming'`, or `'error'` once the turn failed. A turn
75
+ * that settles successfully (or is stopped) has its entry deleted from the `runs` map — the
76
+ * outcome lives on the persisted `AgentThread` instead.
77
+ *
78
+ * A turn that FAILS keeps its entry — `status: 'error'`, `error` set, the parts received so far —
79
+ * until the next `start()`/`retry()` on that thread replaces it or `stop()` dismisses it. So
80
+ * `runs.has(id)` means "in flight or failed"; `run.status` alone says which, and
81
+ * `liveStatus={runs.get(id)?.status}` is correct for the chat rows (`'error'` is not live).
62
82
  *
63
83
  * @example
64
84
  * const state: ThreadRunState = { status: 'streaming', parts: [{ type: 'text', text: 'Hi' }] }
65
85
  */
66
- export type ThreadRunState<TPart = AgentPart> = {
67
- /** Always `'streaming'` — see the type doc for why no other status is reachable here. */
68
- readonly status: 'streaming'
69
- /** Accumulated parts from the current stream for this thread. */
70
- readonly parts: TPart[]
71
- }
86
+ export type ThreadRunState<TPart = AgentPart> =
87
+ | {
88
+ /** `'submitted'` until the first content part arrives, then `'streaming'`. */
89
+ readonly status: LiveRunStatus
90
+ /** Accumulated parts from the current stream for this thread. */
91
+ readonly parts: TPart[]
92
+ readonly error?: undefined
93
+ }
94
+ | {
95
+ readonly status: 'error'
96
+ /**
97
+ * The parts received before the failure. Empty when the turn's final message already landed
98
+ * in the thread (a failure after `appendMessage`, e.g. in `resolveOutcome`), so the feed
99
+ * never renders the same content twice.
100
+ */
101
+ readonly parts: TPart[]
102
+ readonly error: AgentError
103
+ }
72
104
 
73
105
  // ── useAgentThreadRuns args ───────────────────────────────────────────────────
74
106
 
@@ -95,15 +127,33 @@ export type UseAgentThreadRunsArgs<TPart = AgentPart> = {
95
127
  * Converts raw user input into the thread's part shape. Defaults to a single text part.
96
128
  * Required when TPart is not the default AgentPart union.
97
129
  */
98
- readonly toUserParts?: (input: string) => TPart[]
130
+ readonly toUserParts?: ((input: string) => TPart[]) | undefined
131
+ /**
132
+ * Called with the RAW thrown value when a turn's stream throws — never for the aborts this hook
133
+ * issues itself (`stop()`, supersede, unmount). An abort that comes from elsewhere (the
134
+ * transport, the network layer) is a failure and fires, as `kind: 'aborted'` on the run. Fires
135
+ * from the SAME catch branch that sets `onFailureStatus` on the thread, so a caller wanting a
136
+ * toast/notification for a genuine transport error (a 409/503/401, argo's hermes-chat S19) no
137
+ * longer has to wrap every transport call in its own generator to intercept the throw — see
138
+ * `consumeAndFinalize`'s catch block.
139
+ */
140
+ readonly onError?:
141
+ | ((info: { readonly threadId: string; readonly error: unknown }) => void)
142
+ | undefined
143
+ /**
144
+ * Minimum ms between `runs` commits while a part stream is flowing — `useChat`'s stable
145
+ * `throttle` semantics: the first part commits immediately, later parts within the window are
146
+ * batched into one trailing commit. The `submitted → streaming` edge, a failure, the end of the
147
+ * stream and `stop()` never wait on it, so no final part is lost. Default `100`; `0` commits
148
+ * every part.
149
+ */
150
+ readonly throttle?: number | undefined
99
151
  /**
100
- * Called when a turn's stream throws — never for an `AbortError` (an intentional stop()/
101
- * supersede, not a failure worth surfacing). Fires from the SAME catch branch that sets
102
- * `onFailureStatus` on the thread, so a caller wanting a toast/notification for a genuine
103
- * transport error (a 409/503/401, argo's hermes-chat S19) no longer has to wrap every transport
104
- * call in its own generator to intercept the throw — see `consumeAndFinalize`'s catch block.
152
+ * Receives one `chat.turn` event when a turn completes or fails (V2-SPEC § 7); a `stop()`ped
153
+ * turn emits nothing. Omitted, it defaults to the enclosing `BasaltProvider`'s `telemetry` sink,
154
+ * and outside a provider nothing is delivered. A throwing sink never breaks the turn.
105
155
  */
106
- readonly onError?: (info: { readonly threadId: string; readonly error: unknown }) => void
156
+ readonly telemetry?: Telemetry | undefined
107
157
  }
108
158
 
109
159
  // ── useAgentThreadRuns return type ────────────────────────────────────────────
@@ -132,21 +182,25 @@ export type UseAgentThreadRunsReturn<TPart = AgentPart> = {
132
182
  */
133
183
  readonly retry: (threadId: string) => void
134
184
  /**
135
- * Abort the in-flight stream for `threadId` (true no-op only if BOTH `controllersRef` and
136
- * `runs` have nothing for it — i.e. the thread genuinely isn't live). Preserves whatever content
185
+ * Abort the in-flight stream for `threadId`. A no-op when the thread has no live run and no
186
+ * failed entry. Preserves whatever content
137
187
  * had already arrived: if the run had accumulated any parts, they're persisted as an assistant
138
188
  * ChatMessage with `finish: 'stopped'` (unless that message already landed — see finalizeStop),
139
189
  * then distilled into an outcome and the thread is settled to 'done'.
140
190
  *
141
- * Defense-in-depth: if `runs` reports `threadId` as `'streaming'` but no controller is
142
- * registered for it (a phantom entry — not reachable via any path in this hook today, since the
143
- * unmount-cleanup effect and `finalizeStop` both tear down `controllersRef` and `runs` together,
144
- * but guarded against here so nothing in this file can EVER wedge a thread the UI shows as live
145
- * with no way to reach a terminal state), `stop()` still settles it instead of silently
146
- * no-opping forever.
191
+ * A FAILED turn's entry (`status: 'error'`) has no controller: `stop()` just dismisses it and
192
+ * leaves the thread's `'error'` status alone. A turn that has already settled is a no-op.
193
+ *
194
+ * Defense-in-depth: if `runs` reports `threadId` as live but no run record exists for it (a
195
+ * phantom entry — not reachable via any path in this hook today, since the unmount-cleanup
196
+ * effect and `finalizeStop` both tear down the records and `runs` together), `stop()` still
197
+ * settles it, once, instead of silently no-opping forever.
147
198
  */
148
199
  readonly stop: (threadId: string) => void
149
- /** Abort every in-flight stream across all threads, settling each the same way stop() would. */
200
+ /**
201
+ * Abort every in-flight stream across all threads, settling each the same way stop() would, and
202
+ * dismiss every failed entry.
203
+ */
150
204
  readonly stopAll: () => void
151
205
  }
152
206
 
@@ -199,56 +253,64 @@ function buildDoneSnapshot<TPart>(
199
253
  * completes. Shared by both start() (a fresh turn) and the mount-time resume path (reconnecting
200
254
  * an orphaned thread) so the ~40-line accumulate/finalize/error logic exists exactly once.
201
255
  *
202
- * `onFailureStatus` is the ThreadStatus to set when the generator throws (excluding AbortError,
203
- * which is always a no-op): start() passes 'error' (today's behavior for a fresh turn that fails);
256
+ * `onFailureStatus` is the ThreadStatus to set when the generator throws (excluding the aborts
257
+ * this hook issues itself — stop(), supersede, unmount — which are always a no-op; an abort from
258
+ * elsewhere is a failure): start() passes 'error' (today's behavior for a fresh turn that fails);
204
259
  * the resume path passes 'interrupted' (a failed resume falls back to the same terminal state a
205
260
  * non-resumable orphaned thread already lands in).
206
261
  */
207
262
  async function consumeAndFinalize<TPart extends PartLike>(args: {
208
263
  threadId: string
209
- controller: AbortController
264
+ record: RunRecord<TPart>
210
265
  generator: AsyncGenerator<TPart>
211
266
  userMessage: ChatMessage<TPart>
212
- controllersRef: MutableRefObject<Map<string, AbortController>>
213
- appendedRef: MutableRefObject<Map<string, AbortController>>
267
+ recordsRef: MutableRefObject<RunRecords<TPart>>
214
268
  storeRef: MutableRefObject<ThreadsStore<TPart>>
215
269
  resolveOutcome: OutcomeResolver<TPart>
216
270
  setRuns: Dispatch<SetStateAction<Map<string, ThreadRunState<TPart>>>>
217
271
  onFailureStatus: ThreadStatus
272
+ throttle: number
273
+ emit: Telemetry
218
274
  onError?: (info: { readonly threadId: string; readonly error: unknown }) => void
219
275
  }): Promise<void> {
220
276
  const {
221
277
  threadId,
222
- controller,
278
+ record,
223
279
  generator,
224
280
  userMessage,
225
- controllersRef,
226
- appendedRef,
281
+ recordsRef,
227
282
  storeRef,
228
283
  resolveOutcome,
229
284
  setRuns,
230
285
  onFailureStatus,
286
+ throttle,
287
+ emit,
231
288
  onError,
232
289
  } = args
290
+ const { controller } = record
291
+ const live = (): boolean => isLive(recordsRef.current, threadId, controller)
292
+ const owns = (): boolean => isLatestRun(record)
233
293
  let parts: TPart[] = []
294
+ const startedAt = now()
295
+ let firstContentAt: number | undefined
296
+ // Every commit re-checks run ownership: a trailing timer that fires after stop()/unmount/a newer
297
+ // run took the thread over must not resurrect or clobber its `runs` entry.
298
+ const committer = createRunCommitter<TPart>(throttle, (committed, status) => {
299
+ if (!live()) return
300
+ setRuns((prev) => {
301
+ const next = new Map(prev)
302
+ next.set(threadId, { status, parts: committed })
303
+ return next
304
+ })
305
+ })
234
306
  try {
235
307
  for await (const part of generator) {
236
- // Guard: a newer call superseded this stream's controller for this thread, or it
237
- // was aborted — stop updating state.
238
- //
239
- // Deliberately NOT a `runs` teardown site (considered and rejected): `controllersRef`
240
- // mismatch here is ambiguous between "this run was aborted and nothing replaced it" and "a
241
- // newer run (a mount-reconcile resume, keyed by the SAME threadId) has already taken over
242
- // and is live in `runs` right now". Only the first case should ever clear `runs[threadId]`,
243
- // and only the abort's OWN originator can tell the two apart without a race — the unmount-
244
- // cleanup effect (see its doc) does that teardown synchronously, in the same tick as the
245
- // abort, before any newer run could exist to be confused with. If this guard also deleted
246
- // `runs[threadId]`, a superseding resume's fresh entry would be clobbered out from under it
247
- // by its OWN predecessor's late-settling loop iteration — reintroducing a wedge instead of
248
- // fixing one. `stop()`'s own abort path has the same property (finalizeStop's teardown runs
249
- // synchronously before `stop()` returns, never racing a subsequent call).
250
- if (controllersRef.current.get(threadId) !== controller) return
251
- if (controller.signal.aborted) return
308
+ // Guard: the run ended (stop(), unmount) or a newer run replaced its record — stop updating
309
+ // state. Deliberately NOT a `runs` teardown site: a mismatch is ambiguous between "aborted
310
+ // and nothing replaced it" and "a newer run (a mount-reconcile resume on the SAME threadId)
311
+ // is live in `runs` right now", and only the ender can tell which; stop() and the unmount
312
+ // cleanup tear `runs` down synchronously with the end, before a newer run can exist.
313
+ if (!live()) return
252
314
  if (isStartPart(part)) {
253
315
  storeRef.current.setResumeToken(threadId, part.resumeToken)
254
316
  continue
@@ -257,16 +319,17 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
257
319
  // array it returns), so the local accumulator and the rendered `runs` snapshot below stay
258
320
  // identical: a replayed/resumed delta rewrites its part instead of duplicating it.
259
321
  parts = mergePart(parts, part)
260
- setRuns((prev) => {
261
- const next = new Map(prev)
262
- next.set(threadId, { status: 'streaming', parts })
263
- return next
264
- })
265
- }
266
- // Only proceed if this stream is still the current one and wasn't aborted.
267
- if (controllersRef.current.get(threadId) !== controller || controller.signal.aborted) {
268
- return
322
+ record.parts = parts
323
+ // The submitted → streaming edge commits at once (it is the TTFT mark and must not wait on
324
+ // the throttle window); later parts ride the throttle.
325
+ const firstContent = firstContentAt === undefined && isContentPart(part)
326
+ if (firstContent) firstContentAt = now()
327
+ committer.push(parts, firstContentAt === undefined ? 'submitted' : 'streaming', firstContent)
269
328
  }
329
+ // The throttle's trailing commit: the last parts must reach `runs` before the settle below.
330
+ committer.flush()
331
+ // Only proceed if this stream is still the current one.
332
+ if (!live()) return
270
333
 
271
334
  const assistantMessage: ChatMessage<TPart> = {
272
335
  // appendMessage's idempotency key — mintMessageId, never mintThreadId (see that helper's
@@ -278,53 +341,89 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
278
341
  finish: 'complete',
279
342
  }
280
343
  storeRef.current.appendMessage(threadId, assistantMessage)
281
- // Record the terminal-append fact for THIS run (identified by `controller`, mirroring
282
- // controllersRef's own per-thread/per-run keying) — see appendedRef's doc on why this, and not
283
- // array-reference equality, is what finalizeStop checks.
284
- appendedRef.current.set(threadId, controller)
344
+ // The terminal-append fact for THIS run, on its record: stop() reads it to avoid a second
345
+ // message, and the failure path reads it to avoid re-rendering the landed content as live.
346
+ record.appended = true
285
347
 
286
348
  const priorThread = storeRef.current.threads.find((thread) => thread.id === threadId)
287
349
  const snapshot = buildDoneSnapshot(priorThread, threadId, userMessage, assistantMessage)
288
- const outcome: AgentOutcome = await resolveOutcome(snapshot)
350
+ record.resolving = true
351
+ let outcome: AgentOutcome
352
+ try {
353
+ outcome = await resolveOutcome(snapshot)
354
+ } finally {
355
+ // Settled either way: the record may now be swept once a render shows its outcome.
356
+ record.resolving = false
357
+ }
289
358
 
290
- // Re-check: resolveOutcome above is the only await in this function's success path, and
291
- // stop() may have run while it was pending — aborting the controller, appending its own
292
- // 'stopped' message only if appendedRef didn't already carry this run's marker (see
293
- // finalizeStop), and already forcing the thread to 'done'. Without this recheck, a slow
294
- // resolveOutcome settling AFTER stop() has already finalized the thread would silently
295
- // overwrite that outcome with whatever THIS call resolved to.
296
- if (controllersRef.current.get(threadId) !== controller || controller.signal.aborted) {
359
+ // Re-check: resolveOutcome above is the only await in this success path. If stop() ran while
360
+ // it was pending, stop() already settled the thread to 'done' (appending nothing: the message
361
+ // had landed) and deliberately did NOT call resolveOutcome a second time; this result is the
362
+ // one outcome of the turn, so it is recorded here. Any other end (unmount, a newer run)
363
+ // leaves the thread to whoever ended it.
364
+ if (!live()) {
365
+ // Only while no newer run has taken the thread: a stale resolver must not overwrite it.
366
+ if (record.ended === 'stopped' && record.appended && owns()) {
367
+ storeRef.current.setOutcome(threadId, outcome)
368
+ }
297
369
  return
298
370
  }
299
371
 
300
372
  storeRef.current.setOutcome(threadId, outcome)
301
373
  storeRef.current.setStatus(threadId, outcome.status)
302
374
  storeRef.current.setResumeToken(threadId, undefined)
303
- controllersRef.current.delete(threadId)
304
- appendedRef.current.delete(threadId)
375
+ endRun(record, 'settled')
305
376
  setRuns((prev) => {
306
377
  if (!prev.has(threadId)) return prev
307
378
  const next = new Map(prev)
308
379
  next.delete(threadId)
309
380
  return next
310
381
  })
382
+ emit(
383
+ buildTurnEvent({
384
+ threadId,
385
+ startedAt,
386
+ firstContentAt,
387
+ partCount: parts.length,
388
+ status: outcome.status,
389
+ }),
390
+ )
311
391
  } catch (err) {
312
- // Ignore abort errors — they are intentional (stop() was called or superseded).
313
- if (err instanceof Error && err.name === 'AbortError') return
314
- // Guard: don't corrupt state if a newer stream has taken over this thread.
315
- if (controllersRef.current.get(threadId) !== controller) return
316
- // Guard: don't overwrite a user-cancelled state if the signal was aborted.
317
- if (controller.signal.aborted) return
392
+ // Guard: the run ended (stop(), unmount — which abort the controller, so their AbortError lands
393
+ // here) or a newer stream took the thread over. Only an abort from somewhere else gets past.
394
+ if (!live()) {
395
+ // A resolveOutcome that stop() handed the outcome to and that then REJECTED: the stopped turn
396
+ // stays 'done', but the failure still reaches the caller (raw, like any other).
397
+ if (record.ended === 'stopped' && record.appended) onError?.({ threadId, error: err })
398
+ return
399
+ }
400
+ const agentError = toAgentError(err)
401
+ // The turn's final message already landed in the thread (resolveOutcome or a store call failed
402
+ // AFTER appendMessage): the feed renders it from the thread, so the error entry keeps no parts.
403
+ const messageLanded = record.appended
404
+ endRun(record, 'failed')
318
405
  storeRef.current.setStatus(threadId, onFailureStatus)
319
- controllersRef.current.delete(threadId)
320
- appendedRef.current.delete(threadId)
406
+ // The failed entry stays visible, with the error and the parts received so far, until the next
407
+ // start()/retry() on this thread replaces it or stop() dismisses it.
408
+ const failedParts = messageLanded ? [] : parts
321
409
  setRuns((prev) => {
322
- if (!prev.has(threadId)) return prev
323
410
  const next = new Map(prev)
324
- next.delete(threadId)
411
+ next.set(threadId, { status: 'error', parts: failedParts, error: agentError })
325
412
  return next
326
413
  })
414
+ emit(
415
+ buildTurnEvent({
416
+ threadId,
417
+ startedAt,
418
+ firstContentAt,
419
+ partCount: parts.length,
420
+ status: onFailureStatus,
421
+ failure: { error: err, agentError },
422
+ }),
423
+ )
327
424
  onError?.({ threadId, error: err })
425
+ } finally {
426
+ committer.cancel()
328
427
  }
329
428
  }
330
429
 
@@ -332,15 +431,17 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
332
431
  * Finalizes a stop() call for `threadId` — the counterpart to consumeAndFinalize's success path,
333
432
  * reused so the persist/settle/resolve sequence exists exactly once. `parts` is the accumulated
334
433
  * (possibly partial) content read out of the run's `ThreadRunState` at the moment stop() was
335
- * called; the caller has already aborted the controller and cleared `controllersRef` before
336
- * invoking this. `alreadyAppended` is precomputed by the caller (stop()) from `appendedRef` — see
337
- * that ref's doc for why this is an explicit marker rather than an inferred comparison.
434
+ * called; the caller has already ended the run's record and aborted its controller before
435
+ * invoking this. `alreadyAppended` is read by the caller (stop()) off the record's `appended` flag
436
+ * — an explicit marker rather than an inferred comparison. `outcomeInFlight` is the record's
437
+ * `resolving` flag: `consumeAndFinalize`'s own `resolveOutcome` is pending and records the
438
+ * outcome, so this function must not resolve a second time.
338
439
  *
339
440
  * Ordering / the append-failure guard: the append is attempted FIRST (so a successful stop still
340
441
  * reads as 'done' with its partial content intact — this is the common case and the one that
341
442
  * matters most), but it is wrapped in its own try/catch that CANNOT prevent the terminal
342
443
  * transition below it. This is deliberate, not incidental: by the time this function runs, `stop()`
343
- * has already deleted `threadId` from `controllersRef` and `appendedRef` (see `stop()`'s own doc),
444
+ * has already ended the run's record (see `stop()`'s own doc),
344
445
  * so a second `stop()` call is unconditionally a no-op regardless of what happens here — if the
345
446
  * append (or `mintMessageId`, which now also throws on rung 3 — see `./id.ts`) throws and nothing
346
447
  * downstream ran, the thread would be wedged at 'streaming' FOREVER with no way for the user to
@@ -358,12 +459,12 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
358
459
  * distilling into an outcome when the turn's own final message never made it into the store.
359
460
  *
360
461
  * `setStatus`/`setResumeToken` are guarded for the SAME reason, and this is not belt-and-braces:
361
- * they are consumer code exactly as `appendMessage` is (a `createThreadsStore` adapter over a
462
+ * they are consumer code exactly as `appendMessage` is (an adapter over a
362
463
  * remote backend, a store that rejects a threadId removed mid-stream), and guarding only the
363
464
  * append leaves the identical wedge one line further down. The single thing that must ALWAYS run
364
465
  * is the `setRuns` teardown — that entry is the HOOK'S OWN state, it is what `runs.get(threadId)`
365
- * reports to the UI as "a turn is in flight", and by this point `stop()` has already deleted the
366
- * thread from `controllersRef`, so no later `stop()` can ever reach here again to clean it up. A
466
+ * reports to the UI as "a turn is in flight", and by this point `stop()` has already ENDED the
467
+ * run's record, so no later `stop()` can ever reach here again to clean it up. A
367
468
  * throw above it therefore strands a phantom run for the lifetime of the hook. Each store call
368
469
  * gets its OWN try so a failing `setStatus` cannot also skip clearing the resume token — a
369
470
  * surviving token is separately harmful (it is what a later resume replays from).
@@ -372,11 +473,26 @@ async function finalizeStop<TPart extends PartLike>(args: {
372
473
  threadId: string
373
474
  parts: TPart[]
374
475
  alreadyAppended: boolean
476
+ /** `consumeAndFinalize`'s own `resolveOutcome` is pending and will record the outcome. */
477
+ outcomeInFlight: boolean
478
+ /** The stopped run's record: held `resolving` while this function's own resolver is pending. */
479
+ record: RunRecord<TPart>
375
480
  storeRef: MutableRefObject<ThreadsStore<TPart>>
376
481
  resolveOutcome: OutcomeResolver<TPart>
377
482
  setRuns: Dispatch<SetStateAction<Map<string, ThreadRunState<TPart>>>>
483
+ onError?: (info: { readonly threadId: string; readonly error: unknown }) => void
378
484
  }): Promise<void> {
379
- const { threadId, parts, alreadyAppended, storeRef, resolveOutcome, setRuns } = args
485
+ const {
486
+ threadId,
487
+ parts,
488
+ alreadyAppended,
489
+ outcomeInFlight,
490
+ record,
491
+ onError,
492
+ storeRef,
493
+ resolveOutcome,
494
+ setRuns,
495
+ } = args
380
496
 
381
497
  let appendFailed = false
382
498
  if (!alreadyAppended && parts.length > 0) {
@@ -420,12 +536,57 @@ async function finalizeStop<TPart extends PartLike>(args: {
420
536
 
421
537
  // No outcome to resolve for a turn whose own terminal message never landed, or whose settle
422
538
  // never took — see this function's doc on why this mirrors consumeAndFinalize's catch path.
423
- if (settleFailed) return
539
+ if (settleFailed || outcomeInFlight) return
424
540
 
425
541
  const snapshot = storeRef.current.threads.find((thread) => thread.id === threadId)
426
542
  if (snapshot === undefined) return
427
- const outcome: AgentOutcome = await resolveOutcome(snapshot)
428
- storeRef.current.setOutcome(threadId, outcome)
543
+ // Held `resolving` like consumeAndFinalize's own call: `sweepFlushed` keeps the record while the
544
+ // resolver is pending, so a later start() still supersedes it and a stale outcome is dropped.
545
+ record.resolving = true
546
+ try {
547
+ const outcome: AgentOutcome = await resolveOutcome(snapshot)
548
+ if (isLatestRun(record)) storeRef.current.setOutcome(threadId, outcome)
549
+ } catch (error) {
550
+ // `stop()` calls this with `void`: a rejection must reach the caller, not become unhandled.
551
+ // The stopped turn stays 'done'.
552
+ onError?.({ threadId, error })
553
+ } finally {
554
+ record.resolving = false
555
+ }
556
+ }
557
+
558
+ /** The `chat.turn` event for a turn that completed (`failure` absent) or failed. */
559
+ function buildTurnEvent(args: {
560
+ threadId: string
561
+ startedAt: number
562
+ firstContentAt: number | undefined
563
+ partCount: number
564
+ status: ThreadStatus
565
+ failure?: { readonly error: unknown; readonly agentError: AgentError }
566
+ }): BasaltEvent {
567
+ const { threadId, startedAt, firstContentAt, partCount, status, failure } = args
568
+ const endedAt = now()
569
+ const base = {
570
+ start: startedAt,
571
+ duration: endedAt - startedAt,
572
+ }
573
+ const attributes = {
574
+ 'basalt.chat.thread_id': threadId,
575
+ 'basalt.chat.ttft_ms': (firstContentAt ?? endedAt) - startedAt,
576
+ 'basalt.chat.parts': partCount,
577
+ 'basalt.chat.status': status,
578
+ }
579
+ if (failure === undefined) return { ...base, type: 'chat.turn', failed: false, attributes }
580
+ return {
581
+ ...base,
582
+ type: 'chat.turn',
583
+ failed: true,
584
+ attributes: {
585
+ ...attributes,
586
+ 'exception.type': errorName(failure.error),
587
+ 'basalt.error.kind': failure.agentError.kind,
588
+ },
589
+ }
429
590
  }
430
591
 
431
592
  // ── useAgentThreadRuns ────────────────────────────────────────────────────────
@@ -447,6 +608,8 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
447
608
  resolveOutcome,
448
609
  toUserParts,
449
610
  onError,
611
+ throttle = 100,
612
+ telemetry,
450
613
  }: UseAgentThreadRunsArgs<TPart>): UseAgentThreadRunsReturn<TPart> {
451
614
  const [runs, setRuns] = useState<Map<string, ThreadRunState<TPart>>>(new Map())
452
615
  // Mirrors the latest `runs` state every render so stop() (a plain callback, not a render) can
@@ -458,31 +621,16 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
458
621
  // closure from an earlier render never fires instead of the consumer's current callback.
459
622
  const onErrorRef = useRef(onError)
460
623
  onErrorRef.current = onError
461
-
462
- // Refs: mutable per-render state that must not trigger re-renders.
463
- const controllersRef = useRef<Map<string, AbortController>>(new Map())
464
- // Explicit single-writer marker: has THIS run's terminal assistant message already been
465
- // appended? Keyed the same way as controllersRef (threadId -> the specific run's controller),
466
- // set by consumeAndFinalize the instant it appends, and cleared on every path that ends a run
467
- // (normal completion, the error path, stop(), and unmount) — same lifecycle discipline as
468
- // controllersRef, deliberately, since that's the existing coordination channel between
469
- // consumeAndFinalize and stop()/finalizeStop.
470
- //
471
- // This replaces an earlier reference-equality check (`lastMessage.parts === parts`) that
472
- // compared the array runsRef mirrors against the array the message actually persisted. That
473
- // comparison implicitly depended on THREE things staying true at once: the run-state mirror not
474
- // re-copying `parts` on every delta, the accumulator reassigning (never mutating) `parts`, and —
475
- // the one that actually broke it — React's flush timing: runsRef is only updated during a
476
- // render (`runsRef.current = runs`, assigned unconditionally on every render, not in an effect),
477
- // so a setRuns dispatch that hasn't yet flushed into a render leaves runsRef holding an EARLIER
478
- // parts array. A stop() landing between consumeAndFinalize's post-loop appendMessage and its
479
- // `await resolveOutcome` resolving could observe exactly that staleness and defeat the
480
- // reference-equality guard, double-appending — see the regression test with "resolveOutcome"
481
- // in its name. Keying by controller identity (an AbortController, freshly minted per run) makes
482
- // a false-positive match across two different runs on the same thread structurally
483
- // impossible — no render, no array-copy semantics, no accumulator-reassignment assumption is
484
- // load-bearing anymore.
485
- const appendedRef = useRef<Map<string, AbortController>>(new Map())
624
+ // Same for the telemetry sink, read at emit time so an inline function never re-subscribes.
625
+ const providerSink = useContext(TelemetryContext)
626
+ const telemetryRef = useRef(telemetry ?? providerSink)
627
+ telemetryRef.current = telemetry ?? providerSink
628
+ const throttleRef = useRef(throttle)
629
+ throttleRef.current = throttle
630
+ // One record per thread id holds the run's lifecycle — controller, unthrottled parts, whether its
631
+ // terminal message landed, how it ended — written together through `./run-records` (see there for
632
+ // why these are not separate maps). A run is live while its record's `ended` is undefined.
633
+ const recordsRef = useRef<RunRecords<TPart>>(new Map())
486
634
  // Caches the last user input per thread so retry() can replay a failed turn without the
487
635
  // caller having to hold onto (or re-collect) what was typed.
488
636
  const lastInputRef = useRef<Map<string, string>>(new Map())
@@ -505,6 +653,12 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
505
653
  const toUserPartsRef = useRef(resolvedToUserParts)
506
654
  toUserPartsRef.current = resolvedToUserParts
507
655
 
656
+ // Stable over the sink ref, so an inline `telemetry` function never changes what `start` closes over.
657
+ const emitTelemetry = useMemo<Telemetry>(
658
+ () => createEmitter((event) => telemetryRef.current?.(event)),
659
+ [],
660
+ )
661
+
508
662
  // Resolves the transport for `threadId`: the shared object as-is for the plain-object form
509
663
  // (unchanged behavior), or the factory's result — resolved once per thread id and cached in
510
664
  // transportsRef thereafter — for the factory form. Not itself memoized (cheap to recreate each
@@ -520,21 +674,21 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
520
674
  }
521
675
 
522
676
  // Abort every in-flight stream on unmount to stop the async generators. Also clears the map —
523
- // leaving aborted entries behind would make the mount-reconcile effect below see a
524
- // `controllersRef.current.has(id)` of `true` for a thread whose only consumer was just aborted,
677
+ // leaving aborted records behind would make the mount-reconcile effect below see a live run
678
+ // for a thread whose only consumer was just aborted,
525
679
  // skipping the orphan-resume path and wedging the thread in 'streaming' forever. Reachable
526
680
  // whenever this fiber's effects re-run without the fiber itself unmounting (React 19 StrictMode
527
681
  // double-invoke; `<Activity>` hide/show) — see this hook's `@example`-adjacent doc / the F3 note.
528
682
  //
529
683
  // Also tears down the matching `runs` entries — the F3 fix above stopped this from wedging
530
- // `controllersRef`/the *persisted* thread status, but left `runs` (this hook's own transient
684
+ // the run records/the *persisted* thread status, but left `runs` (this hook's own transient
531
685
  // state) untouched, and on a re-run-without-unmount the component (and its `runs` state) survive
532
686
  // this cleanup. Without this, the entry the aborted controller was updating stays in `runs`
533
687
  // reporting `status: 'streaming'` forever: the mount-reconcile effect below DOES correctly settle
534
688
  // the persisted thread (to a resumed run, or to 'interrupted' if not resumable), but a consumer
535
689
  // deriving "is this thread live" from `runs.has(id)` — the whole reason `runs` exists — keeps
536
690
  // seeing a phantom in-flight turn no controller is driving anymore, and `stop()` on it was a
537
- // permanent no-op (nothing left in `controllersRef` to abort) — see `stop()`'s own doc. Scoped to
691
+ // permanent no-op (no live record left to abort) — see `stop()`'s own doc. Scoped to
538
692
  // exactly the threadIds this pass is aborting (not a blind `runs.clear()`): the mount-reconcile
539
693
  // effect below runs AFTER this cleanup completes, in the same synchronous commit — StrictMode's
540
694
  // dev-only double-invoke and `<Activity>` hide/show both destroy-then-recreate EVERY mount effect
@@ -548,10 +702,13 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
548
702
  // itself aborting is exact, not merely defensive.
549
703
  useEffect(
550
704
  () => () => {
551
- const abortedThreadIds = Array.from(controllersRef.current.keys())
552
- controllersRef.current.forEach((controller) => controller.abort())
553
- controllersRef.current.clear()
554
- appendedRef.current.clear()
705
+ const abortedThreadIds: string[] = []
706
+ for (const [threadId, record] of recordsRef.current) {
707
+ if (record.ended !== undefined) continue
708
+ abortedThreadIds.push(threadId)
709
+ record.controller.abort()
710
+ }
711
+ recordsRef.current.clear()
555
712
  if (abortedThreadIds.length === 0) return
556
713
  setRuns((prev) => {
557
714
  let next: Map<string, ThreadRunState<TPart>> | undefined
@@ -566,8 +723,14 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
566
723
  [],
567
724
  )
568
725
 
726
+ // Once a render shows a run's outcome, its terminal record has done its job (covering the window
727
+ // before that render); drop it so thread ids are not retained for the life of the hook.
728
+ useEffect(() => {
729
+ sweepFlushed(recordsRef.current, runs)
730
+ })
731
+
569
732
  // Reconcile orphaned in-flight threads once the store is HYDRATED: a persisted thread can be
570
- // stuck 'pending' or 'streaming' after a reload/remount, since controllersRef and `runs` both
733
+ // stuck 'pending' or 'streaming' after a reload/remount, since the run records and `runs` both
571
734
  // start empty — nothing would otherwise resolve that skeleton. If the transport supports
572
735
  // resumption and the thread has a resumeToken (from a StartPart emitted before the disconnect)
573
736
  // plus a user message to resume from, attempt to reconnect the run instead. Any other case — no
@@ -600,7 +763,7 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
600
763
  for (const thread of storeRef.current.threads) {
601
764
  const orphaned =
602
765
  (thread.status === 'pending' || thread.status === 'streaming') &&
603
- !controllersRef.current.has(thread.id)
766
+ !hasLiveRun(recordsRef.current, thread.id)
604
767
  if (!orphaned) continue
605
768
 
606
769
  const resumeToken = thread.resumeToken
@@ -620,16 +783,16 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
620
783
  const resume = resolvedTransport.resume
621
784
 
622
785
  const controller = new AbortController()
623
- controllersRef.current.set(thread.id, controller)
786
+ const record = beginRun(recordsRef.current, thread.id, controller)
624
787
  setRuns((prev) => {
625
788
  const next = new Map(prev)
626
- next.set(thread.id, { status: 'streaming', parts: [] })
789
+ next.set(thread.id, { status: 'submitted', parts: [] })
627
790
  return next
628
791
  })
629
792
 
630
793
  void consumeAndFinalize({
631
794
  threadId: thread.id,
632
- controller,
795
+ record,
633
796
  // Namespaced by thread.id (stable across a thread's whole lifetime, including this
634
797
  // resume) rather than per-call — see withPartIds' own doc for why this is NOT a replay-
635
798
  // convergence guarantee on its own: it only protects against two id-less drafts within
@@ -639,12 +802,13 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
639
802
  // providing that safety.
640
803
  generator: withPartIds(thread.id, resume(resumeToken, controller.signal)),
641
804
  userMessage: lastUserMessage,
642
- controllersRef,
643
- appendedRef,
805
+ recordsRef,
644
806
  storeRef,
645
807
  resolveOutcome,
646
808
  setRuns,
647
809
  onFailureStatus: 'interrupted',
810
+ throttle: throttleRef.current,
811
+ emit: emitTelemetry,
648
812
  onError: (info) => onErrorRef.current?.(info),
649
813
  })
650
814
  }
@@ -657,7 +821,7 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
657
821
  const start = useCallback(
658
822
  (threadId: string, input: string): void => {
659
823
  // Ignore a second concurrent turn on the SAME thread; different threads run concurrently.
660
- if (controllersRef.current.has(threadId)) return
824
+ if (hasLiveRun(recordsRef.current, threadId)) return
661
825
 
662
826
  lastInputRef.current.set(threadId, input)
663
827
 
@@ -672,19 +836,19 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
672
836
  storeRef.current.setStatus(threadId, 'streaming')
673
837
  setRuns((prev) => {
674
838
  const next = new Map(prev)
675
- next.set(threadId, { status: 'streaming', parts: [] })
839
+ next.set(threadId, { status: 'submitted', parts: [] })
676
840
  return next
677
841
  })
678
842
 
679
843
  const controller = new AbortController()
680
- controllersRef.current.set(threadId, controller)
844
+ const record = beginRun(recordsRef.current, threadId, controller)
681
845
  const resolvedTransport = resolveTransport(threadId)
682
846
 
683
847
  // Non-awaited: this consumer runs in the background so start() returns immediately and
684
848
  // multiple threads can stream at once.
685
849
  void consumeAndFinalize({
686
850
  threadId,
687
- controller,
851
+ record,
688
852
  // Namespaced by threadId — same choice, same caveat, as the mount-time resume call above.
689
853
  // ctx.messageId is this turn's OWN idempotency key (userMessage.id, just minted above) —
690
854
  // a transport that accepts it (aiSdkTransport) mints its wire-level message with the SAME
@@ -694,16 +858,17 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
694
858
  resolvedTransport.stream(input, controller.signal, { messageId: userMessage.id }),
695
859
  ),
696
860
  userMessage,
697
- controllersRef,
698
- appendedRef,
861
+ recordsRef,
699
862
  storeRef,
700
863
  resolveOutcome,
701
864
  setRuns,
702
865
  onFailureStatus: 'error',
866
+ throttle: throttleRef.current,
867
+ emit: emitTelemetry,
703
868
  onError: (info) => onErrorRef.current?.(info),
704
869
  })
705
870
  },
706
- [resolveOutcome],
871
+ [emitTelemetry, resolveOutcome],
707
872
  )
708
873
 
709
874
  const retry = useCallback(
@@ -715,61 +880,61 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
715
880
  [start],
716
881
  )
717
882
 
883
+ const dismiss = useCallback((threadId: string): void => {
884
+ dismissRun(recordsRef.current, threadId)
885
+ setRuns((prev) => {
886
+ if (!prev.has(threadId)) return prev
887
+ const next = new Map(prev)
888
+ next.delete(threadId)
889
+ return next
890
+ })
891
+ }, [])
892
+
718
893
  const stop = useCallback(
719
894
  (threadId: string): void => {
720
- const controller = controllersRef.current.get(threadId)
721
- // No in-flight run for this thread — true no-op, matching this hook's stop() JSDoc. Decided
722
- // from controllersRef, NOT runsRef: controllersRef is written synchronously by start() (line
723
- // ~509, before consumeAndFinalize is even invoked), while runsRef only mirrors `runs` as of
724
- // the last FLUSHED RENDER (`runsRef.current = runs`, assigned during render, above). A
725
- // start() immediately followed by stop() in the same synchronous tick/act() batch — e.g.
726
- // `onClick={() => { start(id, text); stop(id) }}` — has controllersRef already populated but
727
- // runsRef still holding the PREVIOUS render's (empty) map. Gating on runsRef here would read
728
- // `undefined`, wrongly treat this as a no-op, leave the controller registered and never
729
- // abort it — stranding the run in 'streaming' (see the F4 regression test).
730
- if (controller === undefined) {
731
- // No controller — but if `runs` still reports this thread as live, that's a phantom entry
732
- // (see this hook's `stop()` JSDoc): nothing is driving it, and with no controller to key
733
- // off of, this is the only remaining path that can ever settle it. Not reachable via any
734
- // path in THIS file today (the unmount-cleanup effect and finalizeStop both tear down
735
- // `controllersRef` and `runs` in the same synchronous step — see their docs), but a true
736
- // no-op here would mean "the UI shows this thread streaming forever, and Stop can never
737
- // clear it" the instant that invariant is ever broken by a future change. Settle it via the
738
- // same finalizeStop() the normal path uses, so there is still exactly one teardown routine
739
- // for "a run is ending" regardless of how it got here.
740
- if (!runsRef.current.has(threadId)) return
741
- const parts = runsRef.current.get(threadId)?.parts ?? []
742
- const alreadyAppended = appendedRef.current.has(threadId)
743
- appendedRef.current.delete(threadId)
744
- void finalizeStop({ threadId, parts, alreadyAppended, storeRef, resolveOutcome, setRuns })
895
+ // Decided from the run's record, which start() writes synchronously — NOT from `runsRef`,
896
+ // which only mirrors the last flushed render. `onClick={() => { start(id, text); stop(id) }}`
897
+ // has a live record but no rendered entry yet, and a settle's `setRuns` leaves the entry
898
+ // visible to `runsRef` until the next render; reading `runs` here stranded the first and
899
+ // double-appended the second. `planStop` falls back to the rendered entry only when no
900
+ // record exists.
901
+ const plan = planStopWithTombstone(
902
+ recordsRef.current,
903
+ threadId,
904
+ runsRef.current.get(threadId),
905
+ )
906
+ if (plan.kind === 'noop') return
907
+ if (plan.kind === 'dismiss') {
908
+ dismiss(threadId)
745
909
  return
746
910
  }
747
-
748
- controllersRef.current.delete(threadId)
749
- // Read the accumulated parts straight out of the live run entry (runsRef mirrors `runs` as of
750
- // the last flushed render) — falls back to [] for the same synchronous start()-then-stop()
751
- // case above, where no render has flushed yet; finalizeStop already treats an empty `parts`
752
- // as "nothing to persist".
753
- const parts = runsRef.current.get(threadId)?.parts ?? []
754
-
755
- controller.abort()
756
-
757
- // Was THIS run's terminal message already appended by consumeAndFinalize's own success path
758
- // (it can race this call — see appendedRef's doc)? Matched by controller identity, not by
759
- // comparing `parts` against anything React-observed, so this is correct regardless of
760
- // whether runsRef has flushed the latest delta yet.
761
- const alreadyAppended = appendedRef.current.get(threadId) === controller
762
- appendedRef.current.delete(threadId)
763
-
764
- void finalizeStop({ threadId, parts, alreadyAppended, storeRef, resolveOutcome, setRuns })
911
+ const { record, parts } = plan
912
+ // Capture before endRun empties the record.
913
+ const { appended, resolving } = record
914
+ endRun(record, 'stopped')
915
+ record.controller.abort()
916
+ void finalizeStop({
917
+ threadId,
918
+ parts,
919
+ alreadyAppended: appended,
920
+ outcomeInFlight: resolving,
921
+ record,
922
+ storeRef,
923
+ resolveOutcome,
924
+ setRuns,
925
+ onError: (info) => onErrorRef.current?.(info),
926
+ })
765
927
  },
766
- [resolveOutcome],
928
+ [dismiss, resolveOutcome],
767
929
  )
768
930
 
769
931
  const stopAll = useCallback((): void => {
770
- for (const threadId of Array.from(controllersRef.current.keys())) {
771
- stop(threadId)
932
+ for (const [threadId, record] of Array.from(recordsRef.current)) {
933
+ if (record.ended === undefined) stop(threadId)
772
934
  }
935
+ // Failed entries have no live run. Sweep the records AND what `runs` shows: an unmount re-run
936
+ // (StrictMode, `<Activity>`) clears the records while the failed entries survive.
937
+ for (const threadId of failedThreadIds(recordsRef.current, runsRef.current)) stop(threadId)
773
938
  }, [stop])
774
939
 
775
940
  return { runs, start, retry, stop, stopAll }