basalt-ui 1.28.0 → 1.29.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 (455) hide show
  1. package/AGENTS.md +11 -18
  2. package/CHANGELOG.md +13 -0
  3. package/MIGRATING.md +151 -7
  4. package/README.md +23 -66
  5. package/agent/rules/basalt-batteries.md +59 -182
  6. package/agent/rules/basalt-charts.md +56 -75
  7. package/agent/rules/basalt-controls.md +38 -69
  8. package/agent/rules/basalt-mantine.md +58 -103
  9. package/agent/rules/basalt-state.md +42 -60
  10. package/agent/rules/basalt-tokens.md +46 -67
  11. package/agent/skills/basalt-app/SKILL.md +2 -0
  12. package/agent/templates/CLAUDE-block.md.tpl +14 -14
  13. package/configs/oxlint-plugin.js +70 -167
  14. package/configs/oxlint-plugin.test.ts +20 -186
  15. package/configs/oxlint.json +0 -1
  16. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  17. package/dist/agent/ai-sdk-transport.js +7 -4
  18. package/dist/agent/coalesce.js +1 -1
  19. package/dist/agent/merge.js +1 -1
  20. package/dist/agent/transport.d.ts +13 -2
  21. package/dist/agent/transport.d.ts.map +1 -1
  22. package/dist/agent/use-agent-thread-runs.d.ts +12 -1
  23. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  24. package/dist/agent/use-agent-thread-runs.js +20 -6
  25. package/dist/agent-chat/index.d.ts +1 -1
  26. package/dist/agent-chat/index.d.ts.map +1 -1
  27. package/dist/agent-chat/relative-time.js +1 -1
  28. package/dist/agent-chat/thread-detail-panel.js +1 -1
  29. package/dist/agent-chat/thread-feed-row.d.ts +32 -7
  30. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
  31. package/dist/agent-chat/thread-feed-row.js +121 -86
  32. package/dist/agent-chat/thread-feed.d.ts +11 -1
  33. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  34. package/dist/agent-chat/thread-feed.js +10 -6
  35. package/dist/agent-chat/thread-message.js +1 -1
  36. package/dist/agent-chat/virtualize.d.ts +39 -0
  37. package/dist/agent-chat/virtualize.d.ts.map +1 -1
  38. package/dist/agent-chat/virtualize.js +15 -0
  39. package/dist/charts/index.d.ts +3 -3
  40. package/dist/charts/index.d.ts.map +1 -1
  41. package/dist/charts/index.js +8 -4
  42. package/dist/charts/kinds/BandStrip.js +1 -1
  43. package/dist/charts/kinds/Bars.d.ts +1 -3
  44. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  45. package/dist/charts/kinds/Bars.js +1 -1
  46. package/dist/charts/kinds/DualPanel.d.ts +3 -1
  47. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  48. package/dist/charts/kinds/DualPanel.js +1 -1
  49. package/dist/charts/kinds/StackedArea.js +1 -1
  50. package/dist/charts/kinds/ZonedLine.d.ts +1 -3
  51. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  52. package/dist/charts/primitives/Axes.d.ts +21 -2
  53. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  54. package/dist/charts/primitives/Axes.js +34 -2
  55. package/dist/charts/primitives/ChartCard.d.ts +20 -4
  56. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  57. package/dist/charts/primitives/ChartCard.js +62 -33
  58. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  59. package/dist/charts/primitives/ChartFrame.js +8 -1
  60. package/dist/charts/primitives/ChartPending.d.ts +12 -6
  61. package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
  62. package/dist/charts/primitives/ChartPending.js +1 -1
  63. package/dist/charts/utils/format.d.ts +5 -92
  64. package/dist/charts/utils/format.d.ts.map +1 -1
  65. package/dist/charts/utils/format.js +10 -80
  66. package/dist/cli/audit-allows.d.ts +24 -0
  67. package/dist/cli/audit-allows.d.ts.map +1 -0
  68. package/dist/cli/audit-allows.js +240 -0
  69. package/dist/cli/check-theme.d.ts +10 -0
  70. package/dist/cli/check-theme.d.ts.map +1 -0
  71. package/dist/cli/check-theme.js +139 -0
  72. package/dist/cli/doctor.d.ts +47 -0
  73. package/dist/cli/doctor.d.ts.map +1 -0
  74. package/dist/cli/doctor.js +197 -0
  75. package/dist/cli/guard-hook.d.ts +9 -0
  76. package/dist/cli/guard-hook.d.ts.map +1 -0
  77. package/dist/cli/guard-hook.js +93 -0
  78. package/dist/cli/help.d.ts +9 -0
  79. package/dist/cli/help.d.ts.map +1 -0
  80. package/dist/cli/help.js +53 -0
  81. package/dist/cli/index.d.ts +360 -270
  82. package/dist/cli/index.d.ts.map +1 -1
  83. package/dist/cli/index.js +85 -1901
  84. package/dist/cli/init.d.ts +4 -0
  85. package/dist/cli/init.d.ts.map +1 -0
  86. package/dist/cli/init.js +165 -0
  87. package/dist/cli/sync.d.ts +43 -0
  88. package/dist/cli/sync.d.ts.map +1 -0
  89. package/dist/cli/sync.js +206 -0
  90. package/dist/cli/tokens-css.d.ts +51 -0
  91. package/dist/cli/tokens-css.d.ts.map +1 -0
  92. package/dist/cli/tokens-css.js +165 -0
  93. package/dist/commands/define-commands.js +1 -1
  94. package/dist/commands/define-overlays.js +1 -1
  95. package/dist/commands/index.d.ts +2 -1
  96. package/dist/commands/index.d.ts.map +1 -1
  97. package/dist/commands/index.js +6 -0
  98. package/dist/commands/overlays-mount.d.ts +7 -9
  99. package/dist/commands/overlays-mount.d.ts.map +1 -1
  100. package/dist/commands/shell-bridge.d.ts +57 -0
  101. package/dist/commands/shell-bridge.d.ts.map +1 -0
  102. package/dist/commands/shell-bridge.js +32 -0
  103. package/dist/common/errors.d.ts +35 -0
  104. package/dist/common/errors.d.ts.map +1 -1
  105. package/dist/common/errors.js +29 -1
  106. package/dist/common/index.d.ts +2 -0
  107. package/dist/common/index.d.ts.map +1 -1
  108. package/dist/common/index.js +6 -0
  109. package/dist/common/is-dev.d.ts.map +1 -0
  110. package/dist/{motion/index.d.ts → common/motion.d.ts} +1 -1
  111. package/dist/common/motion.d.ts.map +1 -0
  112. package/dist/common/props.d.ts +1 -1
  113. package/dist/common/props.d.ts.map +1 -1
  114. package/dist/common/validate.js +1 -1
  115. package/dist/content/markdown.d.ts +3 -4
  116. package/dist/content/markdown.d.ts.map +1 -1
  117. package/dist/content/markdown.js +1 -4
  118. package/dist/controls/actions.d.ts +1 -1
  119. package/dist/controls/actions.d.ts.map +1 -1
  120. package/dist/controls/panel-row.js +1 -1
  121. package/dist/controls/range-filter.js +1 -1
  122. package/dist/dashboard/delta-badge.d.ts +18 -10
  123. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  124. package/dist/dashboard/delta-badge.js +33 -1
  125. package/dist/dashboard/empty-state.d.ts +1 -4
  126. package/dist/dashboard/empty-state.d.ts.map +1 -1
  127. package/dist/dashboard/empty-state.js +1 -2
  128. package/dist/dashboard/index.d.ts +4 -2
  129. package/dist/dashboard/index.d.ts.map +1 -1
  130. package/dist/dashboard/index.js +7 -1
  131. package/dist/dashboard/query-state.d.ts +4 -13
  132. package/dist/dashboard/query-state.d.ts.map +1 -1
  133. package/dist/dashboard/query-state.js +4 -7
  134. package/dist/{section → dashboard}/section.d.ts +2 -2
  135. package/dist/{section → dashboard}/section.d.ts.map +1 -1
  136. package/dist/{section → dashboard}/section.js +2 -2
  137. package/dist/dashboard/settings-section.d.ts +1 -1
  138. package/dist/dashboard/settings-section.d.ts.map +1 -1
  139. package/dist/dashboard/settings-section.js +1 -1
  140. package/dist/dashboard/stat-card.d.ts +1 -1
  141. package/dist/dashboard/stat-card.d.ts.map +1 -1
  142. package/dist/dashboard/stat-card.js +1 -1
  143. package/dist/dashboard/widget-header.d.ts.map +1 -0
  144. package/dist/data/data-table.js +2 -2
  145. package/dist/data/table.d.ts +1 -1
  146. package/dist/data/table.d.ts.map +1 -1
  147. package/dist/data/table.js +2 -16
  148. package/dist/data/virtual.d.ts +3 -3
  149. package/dist/dev-dock.d.ts +28 -0
  150. package/dist/dev-dock.d.ts.map +1 -0
  151. package/dist/dev-dock.js +85 -0
  152. package/dist/format/date.d.ts +62 -0
  153. package/dist/format/date.d.ts.map +1 -0
  154. package/dist/format/date.js +106 -0
  155. package/dist/format/index.d.ts +39 -0
  156. package/dist/format/index.d.ts.map +1 -0
  157. package/dist/format/index.js +54 -0
  158. package/dist/format/measure.d.ts +15 -0
  159. package/dist/format/measure.d.ts.map +1 -0
  160. package/dist/format/measure.js +16 -0
  161. package/dist/format/number.d.ts +69 -0
  162. package/dist/format/number.d.ts.map +1 -0
  163. package/dist/format/number.js +62 -0
  164. package/dist/format/shared.d.ts +27 -0
  165. package/dist/format/shared.d.ts.map +1 -0
  166. package/dist/format/shared.js +29 -0
  167. package/dist/forms/field.d.ts +4 -20
  168. package/dist/forms/field.d.ts.map +1 -1
  169. package/dist/forms/field.js +0 -4
  170. package/dist/forms/form-layout.d.ts +1 -1
  171. package/dist/forms/form-layout.d.ts.map +1 -1
  172. package/dist/forms/form-layout.js +1 -1
  173. package/dist/forms/index.d.ts +1 -1
  174. package/dist/forms/index.d.ts.map +1 -1
  175. package/dist/forms/index.js +1 -2
  176. package/dist/forms/use-form-submit.js +1 -1
  177. package/dist/guard/index.d.ts +2 -9
  178. package/dist/guard/index.d.ts.map +1 -1
  179. package/dist/guard/index.js +16 -26
  180. package/dist/guard/types.d.ts +8 -10
  181. package/dist/guard/types.d.ts.map +1 -1
  182. package/dist/index.d.ts +16 -11
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.js +23 -29
  185. package/dist/notifications/index.d.ts +5 -5
  186. package/dist/notifications/index.d.ts.map +1 -1
  187. package/dist/notifications/index.js +0 -2
  188. package/dist/notifications/mount-guard.d.ts.map +1 -1
  189. package/dist/notifications/mount-guard.js +2 -2
  190. package/dist/provider/connectivity-indicator.d.ts.map +1 -0
  191. package/dist/{connectivity → provider}/connectivity-provider.d.ts +1 -1
  192. package/dist/provider/connectivity-provider.d.ts.map +1 -0
  193. package/dist/{connectivity/types.d.ts → provider/connectivity-types.d.ts} +1 -1
  194. package/dist/provider/connectivity-types.d.ts.map +1 -0
  195. package/dist/provider/error-fallback.module.css +21 -0
  196. package/dist/provider/error-fallback.module.css.d.ts +9 -0
  197. package/dist/provider/index.d.ts +20 -36
  198. package/dist/provider/index.d.ts.map +1 -1
  199. package/dist/provider/index.js +42 -32
  200. package/dist/provider/lab-theme.js +1 -1
  201. package/dist/provider/use-connectivity.d.ts +3 -0
  202. package/dist/provider/use-connectivity.d.ts.map +1 -0
  203. package/dist/query-client.d.ts +63 -0
  204. package/dist/query-client.d.ts.map +1 -0
  205. package/dist/query-client.js +33 -0
  206. package/dist/{query/devtools.d.ts → query-devtools.d.ts} +2 -2
  207. package/dist/query-devtools.d.ts.map +1 -0
  208. package/dist/register.d.ts +17 -3
  209. package/dist/register.d.ts.map +1 -1
  210. package/dist/router-tanstack/index.d.ts +4 -7
  211. package/dist/router-tanstack/index.d.ts.map +1 -1
  212. package/dist/router-tanstack/index.js +1 -9
  213. package/dist/router-tanstack/nav.d.ts +1 -1
  214. package/dist/router-tanstack/nav.d.ts.map +1 -1
  215. package/dist/router-tanstack/search-store.d.ts +3 -6
  216. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  217. package/dist/router-tanstack/use-nav.d.ts +1 -1
  218. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  219. package/dist/shell/app-breadcrumbs.d.ts +1 -1
  220. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  221. package/dist/shell/app-main.module.css +11 -0
  222. package/dist/shell/app-mobile-nav.d.ts +1 -1
  223. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  224. package/dist/shell/app-mobile-nav.js +24 -2
  225. package/dist/shell/app-sidebar.d.ts +1 -1
  226. package/dist/shell/app-sidebar.d.ts.map +1 -1
  227. package/dist/shell/app-sidebar.js +31 -2
  228. package/dist/shell/app-sidebar.module.css +10 -0
  229. package/dist/shell/index.d.ts +10 -4
  230. package/dist/shell/index.d.ts.map +1 -1
  231. package/dist/shell/index.js +13 -17
  232. package/dist/shell/mobile-nav-model.d.ts +1 -1
  233. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  234. package/dist/{nav/types.d.ts → shell/nav-types.d.ts} +1 -1
  235. package/dist/shell/nav-types.d.ts.map +1 -0
  236. package/dist/shell/page-aside.d.ts.map +1 -1
  237. package/dist/shell/page-aside.js +2 -29
  238. package/dist/shell/page-title.d.ts +14 -0
  239. package/dist/shell/page-title.d.ts.map +1 -0
  240. package/dist/shell/page-title.js +25 -0
  241. package/dist/shell/page-title.module.css +33 -0
  242. package/dist/shell/page-title.module.css.d.ts +11 -0
  243. package/dist/shell/sidebar-block-model.d.ts +1 -1
  244. package/dist/shell/sidebar-block-model.d.ts.map +1 -1
  245. package/dist/shell/sidebar-blocks.d.ts +1 -1
  246. package/dist/shell/sidebar-blocks.d.ts.map +1 -1
  247. package/dist/shell/use-breakpoint.d.ts +11 -0
  248. package/dist/shell/use-breakpoint.d.ts.map +1 -0
  249. package/dist/shell/use-breakpoint.js +33 -0
  250. package/dist/state/fields.d.ts +3 -19
  251. package/dist/state/fields.d.ts.map +1 -1
  252. package/dist/state/fields.js +6 -16
  253. package/dist/styles.css +36 -0
  254. package/dist/surfaces.d.ts +18 -41
  255. package/dist/surfaces.d.ts.map +1 -1
  256. package/dist/surfaces.js +46 -59
  257. package/dist/theme/index.d.ts.map +1 -1
  258. package/dist/{theme-toggle/index.d.ts → theme/theme-toggle.d.ts} +1 -1
  259. package/dist/theme/theme-toggle.d.ts.map +1 -0
  260. package/dist/{theme-toggle/index.js → theme/theme-toggle.js} +1 -1
  261. package/dist/tokens/index.d.ts +1 -0
  262. package/dist/tokens/index.d.ts.map +1 -1
  263. package/dist/tokens/index.js +10 -0
  264. package/dist/tokens/palette.d.ts +12 -0
  265. package/dist/tokens/palette.d.ts.map +1 -1
  266. package/dist/tokens/palette.js +13 -1
  267. package/dist/tokens.css +1 -0
  268. package/llms.txt +9 -24
  269. package/package.json +12 -16
  270. package/src/agent/ai-sdk-transport.ts +11 -4
  271. package/src/agent/coalesce.ts +1 -1
  272. package/src/agent/merge.ts +1 -1
  273. package/src/agent/transport.ts +15 -2
  274. package/src/agent/use-agent-thread-runs.ts +65 -15
  275. package/src/agent-chat/index.ts +6 -4
  276. package/src/agent-chat/relative-time.ts +1 -1
  277. package/src/agent-chat/thread-detail-panel.tsx +1 -1
  278. package/src/agent-chat/thread-feed-row.tsx +105 -30
  279. package/src/agent-chat/thread-feed.tsx +51 -25
  280. package/src/agent-chat/thread-message.tsx +1 -1
  281. package/src/agent-chat/virtualize.ts +48 -0
  282. package/src/charts/index.ts +7 -10
  283. package/src/charts/kinds/BandStrip.tsx +1 -1
  284. package/src/charts/kinds/Bars.tsx +2 -5
  285. package/src/charts/kinds/DualPanel.tsx +4 -3
  286. package/src/charts/kinds/StackedArea.tsx +1 -1
  287. package/src/charts/kinds/ZonedLine.tsx +1 -4
  288. package/src/charts/primitives/Axes.tsx +49 -0
  289. package/src/charts/primitives/ChartCard.tsx +49 -6
  290. package/src/charts/primitives/ChartFrame.tsx +5 -1
  291. package/src/charts/primitives/ChartPending.tsx +14 -8
  292. package/src/charts/utils/format.ts +17 -186
  293. package/src/cli/audit-allows.ts +371 -0
  294. package/src/cli/check-theme.ts +197 -0
  295. package/src/cli/doctor.ts +331 -0
  296. package/src/cli/guard-hook.ts +115 -0
  297. package/src/cli/help.ts +101 -0
  298. package/src/cli/index.ts +269 -3439
  299. package/src/cli/init.ts +247 -0
  300. package/src/cli/sync.ts +324 -0
  301. package/src/cli/tokens-css.ts +252 -0
  302. package/src/commands/define-commands.ts +1 -1
  303. package/src/commands/define-overlays.ts +1 -1
  304. package/src/commands/index.ts +10 -1
  305. package/src/commands/overlays-mount.tsx +8 -10
  306. package/src/commands/shell-bridge.ts +86 -0
  307. package/src/common/errors.ts +70 -0
  308. package/src/common/index.ts +2 -0
  309. package/src/common/props.ts +1 -1
  310. package/src/common/validate.ts +1 -1
  311. package/src/content/markdown.tsx +4 -8
  312. package/src/controls/actions.tsx +1 -1
  313. package/src/controls/panel-row.tsx +1 -1
  314. package/src/controls/range-filter.tsx +1 -1
  315. package/src/dashboard/delta-badge.tsx +74 -7
  316. package/src/dashboard/empty-state.tsx +2 -6
  317. package/src/dashboard/index.ts +11 -2
  318. package/src/dashboard/query-state.tsx +5 -18
  319. package/src/{section → dashboard}/section.tsx +4 -4
  320. package/src/dashboard/settings-section.tsx +2 -2
  321. package/src/dashboard/stat-card.tsx +2 -2
  322. package/src/data/data-table.tsx +3 -3
  323. package/src/data/table.ts +6 -25
  324. package/src/data/virtual-list.tsx +1 -1
  325. package/src/data/virtual.ts +3 -3
  326. package/src/dev-dock.tsx +159 -0
  327. package/src/format/date.ts +184 -0
  328. package/src/format/index.ts +69 -0
  329. package/src/format/measure.ts +31 -0
  330. package/src/format/number.ts +129 -0
  331. package/src/format/shared.ts +63 -0
  332. package/src/forms/field.ts +4 -24
  333. package/src/forms/form-layout.tsx +2 -2
  334. package/src/forms/index.ts +1 -2
  335. package/src/forms/use-form-submit.ts +1 -1
  336. package/src/guard/index.ts +30 -56
  337. package/src/guard/types.ts +7 -10
  338. package/src/index.ts +38 -29
  339. package/src/notifications/index.ts +5 -7
  340. package/src/notifications/mount-guard.ts +7 -9
  341. package/src/{connectivity → provider}/connectivity-indicator.tsx +1 -1
  342. package/src/{connectivity → provider}/connectivity-provider.tsx +1 -1
  343. package/src/provider/error-fallback.module.css +21 -0
  344. package/src/provider/error-fallback.module.css.d.ts +9 -0
  345. package/src/provider/index.tsx +71 -71
  346. package/src/provider/lab-theme.ts +1 -1
  347. package/src/{connectivity → provider}/use-connectivity.ts +3 -3
  348. package/src/query-client.ts +100 -0
  349. package/src/{query/devtools.tsx → query-devtools.tsx} +5 -4
  350. package/src/register.ts +20 -4
  351. package/src/router-tanstack/index.ts +4 -15
  352. package/src/router-tanstack/nav.ts +1 -1
  353. package/src/router-tanstack/search-store.ts +3 -4
  354. package/src/router-tanstack/use-nav.ts +1 -1
  355. package/src/shell/app-breadcrumbs.tsx +1 -1
  356. package/src/shell/app-main.module.css +11 -0
  357. package/src/shell/app-mobile-nav.tsx +28 -1
  358. package/src/shell/app-sidebar.module.css +10 -0
  359. package/src/shell/app-sidebar.tsx +31 -2
  360. package/src/shell/index.tsx +23 -28
  361. package/src/shell/mobile-nav-model.ts +1 -1
  362. package/src/shell/page-aside.tsx +2 -52
  363. package/src/shell/page-title.module.css +33 -0
  364. package/src/shell/page-title.module.css.d.ts +11 -0
  365. package/src/shell/page-title.tsx +57 -0
  366. package/src/shell/sidebar-block-model.ts +1 -1
  367. package/src/shell/sidebar-blocks.tsx +1 -1
  368. package/src/shell/use-breakpoint.ts +73 -0
  369. package/src/state/fields.ts +7 -43
  370. package/src/styles.css +36 -0
  371. package/src/surfaces.ts +45 -60
  372. package/src/theme/index.ts +7 -0
  373. package/src/{theme-toggle/index.tsx → theme/theme-toggle.tsx} +1 -1
  374. package/src/tokens/index.ts +10 -0
  375. package/src/tokens/palette.ts +12 -0
  376. package/dist/connectivity/connectivity-indicator.d.ts.map +0 -1
  377. package/dist/connectivity/connectivity-provider.d.ts.map +0 -1
  378. package/dist/connectivity/index.d.ts +0 -14
  379. package/dist/connectivity/index.d.ts.map +0 -1
  380. package/dist/connectivity/index.js +0 -8
  381. package/dist/connectivity/types.d.ts.map +0 -1
  382. package/dist/connectivity/use-connectivity.d.ts +0 -3
  383. package/dist/connectivity/use-connectivity.d.ts.map +0 -1
  384. package/dist/data/index.d.ts +0 -38
  385. package/dist/data/index.d.ts.map +0 -1
  386. package/dist/data/index.js +0 -24
  387. package/dist/motion/index.d.ts.map +0 -1
  388. package/dist/nav/types.d.ts.map +0 -1
  389. package/dist/notifications/overlay.d.ts +0 -20
  390. package/dist/notifications/overlay.d.ts.map +0 -1
  391. package/dist/notifications/overlay.js +0 -14
  392. package/dist/query/devtools.d.ts.map +0 -1
  393. package/dist/query/error-message.d.ts +0 -41
  394. package/dist/query/error-message.d.ts.map +0 -1
  395. package/dist/query/error-message.js +0 -30
  396. package/dist/query/index.d.ts +0 -47
  397. package/dist/query/index.d.ts.map +0 -1
  398. package/dist/query/index.js +0 -56
  399. package/dist/router-tanstack/field.d.ts +0 -14
  400. package/dist/router-tanstack/field.d.ts.map +0 -1
  401. package/dist/router-tanstack/field.js +0 -4
  402. package/dist/router-tanstack/multi-search-param-store.d.ts +0 -43
  403. package/dist/router-tanstack/multi-search-param-store.d.ts.map +0 -1
  404. package/dist/router-tanstack/multi-search-param-store.js +0 -30
  405. package/dist/router-tanstack/search-param-store.d.ts +0 -42
  406. package/dist/router-tanstack/search-param-store.d.ts.map +0 -1
  407. package/dist/router-tanstack/search-param-store.js +0 -25
  408. package/dist/section/index.d.ts +0 -2
  409. package/dist/section/index.d.ts.map +0 -1
  410. package/dist/section/index.js +0 -4
  411. package/dist/theme-toggle/index.d.ts.map +0 -1
  412. package/dist/utils/is-dev.d.ts.map +0 -1
  413. package/dist/widget-header/delta-badge.d.ts +0 -19
  414. package/dist/widget-header/delta-badge.d.ts.map +0 -1
  415. package/dist/widget-header/delta-badge.js +0 -36
  416. package/dist/widget-header/index.d.ts +0 -3
  417. package/dist/widget-header/index.d.ts.map +0 -1
  418. package/dist/widget-header/index.js +0 -8
  419. package/dist/widget-header/widget-header.d.ts.map +0 -1
  420. package/src/connectivity/index.ts +0 -19
  421. package/src/data/index.ts +0 -69
  422. package/src/notifications/overlay.tsx +0 -66
  423. package/src/query/error-message.ts +0 -70
  424. package/src/query/index.ts +0 -97
  425. package/src/router-tanstack/field.ts +0 -36
  426. package/src/router-tanstack/multi-search-param-store.ts +0 -106
  427. package/src/router-tanstack/search-param-store.ts +0 -100
  428. package/src/section/index.ts +0 -1
  429. package/src/widget-header/delta-badge.tsx +0 -77
  430. package/src/widget-header/index.ts +0 -10
  431. /package/dist/{utils → common}/is-dev.d.ts +0 -0
  432. /package/dist/{utils → common}/is-dev.js +0 -0
  433. /package/dist/{motion/index.js → common/motion.js} +0 -0
  434. /package/dist/{section → dashboard}/section.module.css +0 -0
  435. /package/dist/{section → dashboard}/section.module.css.d.ts +0 -0
  436. /package/dist/{widget-header → dashboard}/widget-header.d.ts +0 -0
  437. /package/dist/{widget-header → dashboard}/widget-header.js +0 -0
  438. /package/dist/{widget-header → dashboard}/widget-header.module.css +0 -0
  439. /package/dist/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
  440. /package/dist/{connectivity → provider}/connectivity-indicator.d.ts +0 -0
  441. /package/dist/{connectivity → provider}/connectivity-indicator.js +0 -0
  442. /package/dist/{connectivity → provider}/connectivity-provider.js +0 -0
  443. /package/dist/{connectivity/types.js → provider/connectivity-types.js} +0 -0
  444. /package/dist/{connectivity → provider}/use-connectivity.js +0 -0
  445. /package/dist/{query/devtools.js → query-devtools.js} +0 -0
  446. /package/dist/{nav/types.js → shell/nav-types.js} +0 -0
  447. /package/src/{utils → common}/is-dev.ts +0 -0
  448. /package/src/{motion/index.ts → common/motion.ts} +0 -0
  449. /package/src/{section → dashboard}/section.module.css +0 -0
  450. /package/src/{section → dashboard}/section.module.css.d.ts +0 -0
  451. /package/src/{widget-header → dashboard}/widget-header.module.css +0 -0
  452. /package/src/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
  453. /package/src/{widget-header → dashboard}/widget-header.tsx +0 -0
  454. /package/src/{connectivity/types.ts → provider/connectivity-types.ts} +0 -0
  455. /package/src/{nav/types.ts → shell/nav-types.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -77,6 +77,11 @@
77
77
  "import": "./dist/guard/index.js",
78
78
  "default": "./dist/guard/index.js"
79
79
  },
80
+ "./format": {
81
+ "types": "./dist/format/index.d.ts",
82
+ "import": "./dist/format/index.js",
83
+ "default": "./dist/format/index.js"
84
+ },
80
85
  "./forms": {
81
86
  "types": "./dist/forms/index.d.ts",
82
87
  "import": "./dist/forms/index.js",
@@ -92,21 +97,11 @@
92
97
  "import": "./dist/notifications/index.js",
93
98
  "default": "./dist/notifications/index.js"
94
99
  },
95
- "./query": {
96
- "types": "./dist/query/index.d.ts",
97
- "import": "./dist/query/index.js",
98
- "default": "./dist/query/index.js"
99
- },
100
100
  "./router-tanstack": {
101
101
  "types": "./dist/router-tanstack/index.d.ts",
102
102
  "import": "./dist/router-tanstack/index.js",
103
103
  "default": "./dist/router-tanstack/index.js"
104
104
  },
105
- "./data": {
106
- "types": "./dist/data/index.d.ts",
107
- "import": "./dist/data/index.js",
108
- "default": "./dist/data/index.js"
109
- },
110
105
  "./data/table": {
111
106
  "types": "./dist/data/table.d.ts",
112
107
  "import": "./dist/data/table.js",
@@ -147,11 +142,6 @@
147
142
  "import": "./dist/state.js",
148
143
  "default": "./dist/state.js"
149
144
  },
150
- "./connectivity": {
151
- "types": "./dist/connectivity/index.d.ts",
152
- "import": "./dist/connectivity/index.js",
153
- "default": "./dist/connectivity/index.js"
154
- },
155
145
  "./styles.css": "./dist/styles.css",
156
146
  "./tokens.css": "./dist/tokens.css",
157
147
  "./configs/*": "./configs/*",
@@ -165,6 +155,7 @@
165
155
  "scripts": {
166
156
  "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly --declarationMap && bun scripts/copy-assets.mjs && bun scripts/gen-tokens-css.mjs && bun scripts/fix-esm-extensions.mjs",
167
157
  "typecheck": "tsc --noEmit",
158
+ "check-budgets": "bun scripts/check-budgets.ts",
168
159
  "test": "cd ../.. && bun test packages/basalt-ui",
169
160
  "pack-test": "bash scripts/pack-test.sh",
170
161
  "prepack": "bun run build",
@@ -179,6 +170,7 @@
179
170
  "@shikijs/langs": "^4.3.1",
180
171
  "@shikijs/themes": "^4.3.1",
181
172
  "@tanstack/react-hotkeys": "0.10.0",
173
+ "@tanstack/react-router-devtools": "^1.166.13",
182
174
  "@types/bun": "^1.3.14",
183
175
  "@types/hast": "^3.0.4",
184
176
  "@types/react": "^19.2.17",
@@ -224,6 +216,7 @@
224
216
  "@tanstack/react-query": "^5.101.0",
225
217
  "@tanstack/react-query-devtools": "^5.101.0",
226
218
  "@tanstack/react-router": "^1.170.0",
219
+ "@tanstack/react-router-devtools": "^1.166.13",
227
220
  "@tanstack/react-table": ">=8 <9",
228
221
  "@tanstack/react-virtual": ">=3.13.26 <4",
229
222
  "@visx/axis": "4.0.0",
@@ -298,6 +291,9 @@
298
291
  "@tanstack/react-router": {
299
292
  "optional": true
300
293
  },
294
+ "@tanstack/react-router-devtools": {
295
+ "optional": true
296
+ },
301
297
  "@tanstack/react-table": {
302
298
  "optional": true
303
299
  },
@@ -395,7 +395,11 @@ export function aiSdkTransport<TPart = AgentPart>(
395
395
  // diffPart), so a replayed run rewrites in place instead of duplicating — the literal
396
396
  // condition this assertion stands for.
397
397
  idempotentReplay: true,
398
- async *stream(input: string, signal?: AbortSignal): AsyncGenerator<TPart> {
398
+ async *stream(
399
+ input: string,
400
+ signal?: AbortSignal,
401
+ ctx?: { readonly messageId: string },
402
+ ): AsyncGenerator<TPart> {
399
403
  // Synthesized locally, first — we already know the chat id client-side, so there is no
400
404
  // need to wait on the server to hand us a run id (unlike a river-style protocol).
401
405
  yield { id: `${chatId}#start`, type: 'start', runId: chatId, resumeToken: chatId } as TPart
@@ -403,9 +407,12 @@ export function aiSdkTransport<TPart = AgentPart>(
403
407
  const { httpTransport, readUIMessageStream: readStream } = await resolveAiSdk()
404
408
  const userMessage: UIMessage = {
405
409
  // This id is what AI SDK's own backend persistence/dedup keys turns hang off — the same
406
- // idempotency-key cost as basalt's own ChatMessage.id (see mintMessageId's doc), so
407
- // mintMessageId, not mintThreadId, is the right helper here.
408
- id: mintMessageId(),
410
+ // idempotency-key cost as basalt's own ChatMessage.id (see mintMessageId's doc). Prefer
411
+ // the caller's OWN idempotency key (ctx.messageId — the id useAgentThreadRuns.start()
412
+ // already minted for this turn's ChatMessage) so the two never diverge for one turn; only
413
+ // a caller that omits ctx (useAgentStream, a hand-rolled stream() call) falls back to
414
+ // minting a fresh one here.
415
+ id: ctx?.messageId ?? mintMessageId(),
409
416
  role: 'user',
410
417
  parts: [{ type: 'text', text: input }],
411
418
  }
@@ -38,7 +38,7 @@
38
38
  * <PartList parts={coalesceParts(parts)} />
39
39
  */
40
40
  import type { AgentPart, ReasoningPart, TextPart, ToolCallPart } from './parts'
41
- import { isDev } from '../utils/is-dev'
41
+ import { isDev } from '../common/is-dev'
42
42
 
43
43
  type TextLike = TextPart | ReasoningPart
44
44
 
@@ -29,7 +29,7 @@
29
29
  * // parts === [{ id: 'p1', type: 'text', text: 'Hello' }]
30
30
  */
31
31
  import type { ReasoningPart, TextPart } from './parts'
32
- import { isDev } from '../utils/is-dev'
32
+ import { isDev } from '../common/is-dev'
33
33
 
34
34
  type TextLike = TextPart | ReasoningPart
35
35
 
@@ -34,7 +34,7 @@ import type { AgentPart, AgentPartDraft } from './parts'
34
34
  // ── AgentTransport ────────────────────────────────────────────────────────────
35
35
 
36
36
  /**
37
- * The injected transport seam. `stream(input, signal)` returns an async generator of parts.
37
+ * The injected transport seam. `stream(input, signal, ctx)` returns an async generator of parts.
38
38
  * Implement this interface for any backend — Eden, fetch-based SSE, AI SDK, or a mock.
39
39
  *
40
40
  * The default `TPart` is `AgentPartDraft` (not `AgentPart`): transports yield DRAFTS — `id`
@@ -42,6 +42,15 @@ import type { AgentPart, AgentPartDraft } from './parts'
42
42
  * part before it is ever accumulated. Pass an explicit `AgentPart` generic when a transport mints
43
43
  * its own ids directly (as `aiSdkTransport` does).
44
44
  *
45
+ * `ctx.messageId`, when present, is the id `useAgentThreadRuns.start()` already minted for this
46
+ * turn's user `ChatMessage` — the SAME idempotency key `appendMessage`/the backend's dedup keys off
47
+ * of (see `mintMessageId`'s doc). A transport that mints its OWN wire-level message id independent
48
+ * of that (as `aiSdkTransport` used to) forces the consumer to reconcile two ids for one turn; a
49
+ * transport MAY use `ctx.messageId` as its outbound message id instead of minting a fresh one. The
50
+ * param is optional and appended last so an existing two-arg transport keeps compiling unchanged —
51
+ * only `useAgentThreadRuns.start()`/`retry()` pass it; a bare `stream(input, signal)` call (e.g.
52
+ * `useAgentStream`, a hand-rolled caller) omits it and a transport falls back to minting its own.
53
+ *
45
54
  * @example
46
55
  * // Minimal mock transport for tests / playground:
47
56
  * const mockTransport: AgentTransport = {
@@ -51,7 +60,11 @@ import type { AgentPart, AgentPartDraft } from './parts'
51
60
  * }
52
61
  */
53
62
  export type AgentTransport<TPart = AgentPartDraft, TInput = string> = {
54
- stream: (input: TInput, signal?: AbortSignal) => AsyncGenerator<TPart>
63
+ stream: (
64
+ input: TInput,
65
+ signal?: AbortSignal,
66
+ ctx?: { readonly messageId: string },
67
+ ) => AsyncGenerator<TPart>
55
68
  /** Optional: resume a previously-started run from its resumeToken (see StartPart). Transports that don't support resumption simply omit this. */
56
69
  resume?: (resumeToken: string, signal?: AbortSignal) => AsyncGenerator<TPart>
57
70
  }
@@ -96,6 +96,14 @@ export type UseAgentThreadRunsArgs<TPart = AgentPart> = {
96
96
  * Required when TPart is not the default AgentPart union.
97
97
  */
98
98
  readonly toUserParts?: (input: string) => TPart[]
99
+ /**
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.
105
+ */
106
+ readonly onError?: (info: { readonly threadId: string; readonly error: unknown }) => void
99
107
  }
100
108
 
101
109
  // ── useAgentThreadRuns return type ────────────────────────────────────────────
@@ -207,6 +215,7 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
207
215
  resolveOutcome: OutcomeResolver<TPart>
208
216
  setRuns: Dispatch<SetStateAction<Map<string, ThreadRunState<TPart>>>>
209
217
  onFailureStatus: ThreadStatus
218
+ onError?: (info: { readonly threadId: string; readonly error: unknown }) => void
210
219
  }): Promise<void> {
211
220
  const {
212
221
  threadId,
@@ -219,6 +228,7 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
219
228
  resolveOutcome,
220
229
  setRuns,
221
230
  onFailureStatus,
231
+ onError,
222
232
  } = args
223
233
  let parts: TPart[] = []
224
234
  try {
@@ -314,6 +324,7 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
314
324
  next.delete(threadId)
315
325
  return next
316
326
  })
327
+ onError?.({ threadId, error: err })
317
328
  }
318
329
  }
319
330
 
@@ -435,6 +446,7 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
435
446
  store,
436
447
  resolveOutcome,
437
448
  toUserParts,
449
+ onError,
438
450
  }: UseAgentThreadRunsArgs<TPart>): UseAgentThreadRunsReturn<TPart> {
439
451
  const [runs, setRuns] = useState<Map<string, ThreadRunState<TPart>>>(new Map())
440
452
  // Mirrors the latest `runs` state every render so stop() (a plain callback, not a render) can
@@ -442,6 +454,10 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
442
454
  // updater — same mirroring pattern as storeRef/transportRef below.
443
455
  const runsRef = useRef(runs)
444
456
  runsRef.current = runs
457
+ // Mirrors the latest onError every render (same pattern as storeRef/transportRef) so a stale
458
+ // closure from an earlier render never fires instead of the consumer's current callback.
459
+ const onErrorRef = useRef(onError)
460
+ onErrorRef.current = onError
445
461
 
446
462
  // Refs: mutable per-render state that must not trigger re-renders.
447
463
  const controllersRef = useRef<Map<string, AbortController>>(new Map())
@@ -520,9 +536,12 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
520
536
  // seeing a phantom in-flight turn no controller is driving anymore, and `stop()` on it was a
521
537
  // permanent no-op (nothing left in `controllersRef` to abort) — see `stop()`'s own doc. Scoped to
522
538
  // exactly the threadIds this pass is aborting (not a blind `runs.clear()`): the mount-reconcile
523
- // effect below runs AFTER this cleanup completes, in the same synchronous commit (both are
524
- // `[]`-dep effects on this same fiber, so StrictMode/`<Activity>` destroy-then-recreate both
525
- // together, in declaration order) — a resumable thread's reconcile pass registers a FRESH
539
+ // effect below runs AFTER this cleanup completes, in the same synchronous commit — StrictMode's
540
+ // dev-only double-invoke and `<Activity>` hide/show both destroy-then-recreate EVERY mount effect
541
+ // on this fiber together, in declaration order, regardless of each effect's own dep array (this
542
+ // one stays `[]`; the reconcile effect below is `[store.hydrated]` as of R3 — the double-invoke
543
+ // guarantee is about the mount/unmount CYCLE, not per-effect dep memoization) — a resumable
544
+ // thread's reconcile pass registers a FRESH
526
545
  // controller and a fresh `runs` entry there, and this teardown must never clobber that. It
527
546
  // can't: by the time this cleanup runs, nothing has created a NEW entry for these threadIds yet
528
547
  // (that only happens in the reconcile effect, still to come), so scoping to the ids this pass is
@@ -547,17 +566,37 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
547
566
  [],
548
567
  )
549
568
 
550
- // Reconcile orphaned in-flight threads on mount: a persisted thread can be stuck 'pending' or
551
- // 'streaming' after a reload/remount, since controllersRef and `runs` both start empty — nothing
552
- // would otherwise resolve that skeleton. If the transport supports resumption and the thread has
553
- // a resumeToken (from a StartPart emitted before the disconnect) plus a user message to resume
554
- // from, attempt to reconnect the run instead. Any other case — no resume(), no resumeToken, no
555
- // user message, or the resume itself failing — falls back to 'interrupted' so the UI renders a
556
- // resend prompt instead of an unresolving skeleton (today's behavior, unchanged as the fallback
557
- // path). Mount-only by design (empty deps): this is a one-time sweep of whatever was persisted
558
- // when this manager first attaches, not a recurring check — a thread only needs reconciling once
559
- // per stale mount.
569
+ // 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
571
+ // start empty — nothing would otherwise resolve that skeleton. If the transport supports
572
+ // resumption and the thread has a resumeToken (from a StartPart emitted before the disconnect)
573
+ // plus a user message to resume from, attempt to reconnect the run instead. Any other case — no
574
+ // resume(), no resumeToken, no user message, or the resume itself failing — falls back to
575
+ // 'interrupted' so the UI renders a resend prompt instead of an unresolving skeleton (today's
576
+ // behavior, unchanged as the fallback path).
577
+ //
578
+ // R3: keyed on `store.hydrated` rather than a bare `[]`-dep effect. `ThreadsStore.hydrated` is
579
+ // always `true` for the localStorage store (createPersistedState resolves synchronously), so this
580
+ // still fires on the very first commit for every consumer that predates this change — no behavior
581
+ // change there. It only matters for an ASYNC store (`createAdapterThreadsStore`): with `[]`, this
582
+ // ran on mount while `store.threads` was still the adapter's empty pre-load snapshot, so every
583
+ // orphaned thread from a previous session was invisible to the sweep — reconcile-after-reload was
584
+ // silently dead for exactly the store shape (server-backed, paginated) that most needs it. Firing
585
+ // again when `hydrated` flips true re-sweeps against the now-real `storeRef.current.threads`.
586
+ //
587
+ // Deliberately NO separate "has this already run" ref here — the sweep is naturally idempotent:
588
+ // a thread only qualifies as orphaned while its status is 'pending'/'streaming' AND it has no
589
+ // live controller, and this effect's own first pass either moves the status off that pair
590
+ // ('interrupted', or eventually 'done'/'error' once the resumed/started run settles) or registers
591
+ // a controller for it synchronously (before the first `await`) — so a second invocation with the
592
+ // SAME `store.hydrated` value (including React 19 StrictMode's dev-only double-invoke of every
593
+ // mount effect, and an `<Activity>` hide/show — see use-agent-thread-runs.wedge.test.tsx) finds
594
+ // nothing left to reconcile except whatever its own sibling invocation genuinely orphaned by
595
+ // aborting an in-flight controller, which is exactly the case that file's (b)/(c) tests require
596
+ // resume() to fire again for. An explicit "ran once" ref would suppress that second, legitimate
597
+ // sweep and reintroduce the F3 wedge for `store.hydrated`-keyed remounts.
560
598
  useEffect(() => {
599
+ if (!store.hydrated) return
561
600
  for (const thread of storeRef.current.threads) {
562
601
  const orphaned =
563
602
  (thread.status === 'pending' || thread.status === 'streaming') &&
@@ -606,10 +645,14 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
606
645
  resolveOutcome,
607
646
  setRuns,
608
647
  onFailureStatus: 'interrupted',
648
+ onError: (info) => onErrorRef.current?.(info),
609
649
  })
610
650
  }
651
+ // Only `store.hydrated` — every other read in this body goes through a ref (storeRef,
652
+ // resolveTransport→transportRef/transportsRef, resolveOutcome closed over from the hook's own
653
+ // args) precisely so this effect's re-run trigger is `hydrated` alone. See the doc above.
611
654
  // eslint-disable-next-line react-hooks/exhaustive-deps
612
- }, [])
655
+ }, [store.hydrated])
613
656
 
614
657
  const start = useCallback(
615
658
  (threadId: string, input: string): void => {
@@ -643,7 +686,13 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
643
686
  threadId,
644
687
  controller,
645
688
  // Namespaced by threadId — same choice, same caveat, as the mount-time resume call above.
646
- generator: withPartIds(threadId, resolvedTransport.stream(input, controller.signal)),
689
+ // ctx.messageId is this turn's OWN idempotency key (userMessage.id, just minted above) —
690
+ // a transport that accepts it (aiSdkTransport) mints its wire-level message with the SAME
691
+ // id instead of a second, unrelated one (R1 — see AgentTransport.stream's doc).
692
+ generator: withPartIds(
693
+ threadId,
694
+ resolvedTransport.stream(input, controller.signal, { messageId: userMessage.id }),
695
+ ),
647
696
  userMessage,
648
697
  controllersRef,
649
698
  appendedRef,
@@ -651,6 +700,7 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
651
700
  resolveOutcome,
652
701
  setRuns,
653
702
  onFailureStatus: 'error',
703
+ onError: (info) => onErrorRef.current?.(info),
654
704
  })
655
705
  },
656
706
  [resolveOutcome],
@@ -25,11 +25,13 @@ export type { ThreadFeedRowProps } from './thread-feed-row'
25
25
 
26
26
  // ── Shared transcript/row contracts ───────────────────────────────────────────
27
27
  // Type-only. Both are named in the PUBLIC props of components exported above
28
- // (`ThreadTranscriptProps.affordances`, `ThreadFeedRowProps`'s virtualize union), so a consumer
29
- // that wants to hold one in a typed variable needs to be able to name it. No runtime export here —
30
- // the resolution/defaults live inside the components.
28
+ // (`ThreadTranscriptProps.affordances`, `ThreadFeedRowProps`'s height union), so a consumer that
29
+ // wants to hold one in a typed variable needs to be able to name it. No runtime export here — the
30
+ // resolution/defaults live inside the components. `RowHeightProps` (B3) is `ThreadFeedRow`'s OWN
31
+ // height contract — a superset of `VirtualizeProps` that also allows `height` without
32
+ // `virtualize` — kept alongside it for the same reason.
31
33
  export type { MessageAffordances } from './message-affordances'
32
- export type { VirtualizeOptions, VirtualizeProps } from './virtualize'
34
+ export type { RowHeightProps, VirtualizeOptions, VirtualizeProps } from './virtualize'
33
35
 
34
36
  // ── ThreadOutcomeCard ─────────────────────────────────────────────────────────
35
37
  export { ThreadOutcomeCard } from './thread-outcome-card'
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Internal to `agent-chat/` — not part of the public barrel.
9
9
  */
10
- import { isDev } from '../utils/is-dev'
10
+ import { isDev } from '../common/is-dev'
11
11
 
12
12
  const RELATIVE_TIME_FORMAT = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
13
13
 
@@ -28,7 +28,7 @@ import type { AgentPart, AgentThread, StreamStatus } from '../agent'
28
28
  import { BasaltStickToBottom } from '../agent'
29
29
  import { cx } from '../common/props'
30
30
  import type { BasaltProps } from '../common/props'
31
- import { MOTION_SPRING } from '../motion'
31
+ import { MOTION_SPRING } from '../common/motion'
32
32
  import { Composer } from './composer'
33
33
  import { ThreadTranscript } from './thread-message'
34
34
  import { VX } from '../tokens'
@@ -33,18 +33,20 @@
33
33
  * />
34
34
  */
35
35
  import { Box, Group, Stack, Text, UnstyledButton } from '@mantine/core'
36
- import type { JSX } from 'react'
36
+ import type { JSX, ReactNode } from 'react'
37
37
  import { useState } from 'react'
38
38
  import type {
39
39
  AgentThread,
40
+ ChatMessage,
40
41
  ForeignPart,
41
42
  PartRenderer,
42
43
  PartRenderers,
43
44
  StreamStatus,
44
45
  TranscriptPart,
45
46
  } from '../agent'
47
+ import { BasaltStickToBottom } from '../agent'
46
48
  import { cx } from '../common/props'
47
- import type { BasaltProps } from '../common/props'
49
+ import type { BasaltProps, SlotStylesProps } from '../common/props'
48
50
  import { assertRequiredProps } from '../common/validate'
49
51
  import { VX } from '../tokens'
50
52
  import type { ComposerProps, ComposerSubmit } from './composer'
@@ -52,8 +54,8 @@ import { Composer } from './composer'
52
54
  import type { MessageAffordances } from './message-affordances'
53
55
  import { formatRelativeTime } from './relative-time'
54
56
  import { ThreadTranscript } from './thread-message'
55
- import { resolveVirtualize } from './virtualize'
56
- import type { VirtualizeProps } from './virtualize'
57
+ import { resolveRowHeight } from './virtualize'
58
+ import type { RowHeightProps, VirtualizeProps } from './virtualize'
57
59
 
58
60
  /** A minimal, dependency-free chevron — rotates 90deg when expanded (same inline-svg idiom as
59
61
  * `composer.tsx`'s `SendGlyph`/`StopGlyph`: no icon-library dependency for a one-off glyph). */
@@ -106,6 +108,30 @@ type ThreadFeedRowBase = {
106
108
  readonly expanded: boolean
107
109
  /** Called with the thread's id when the header is clicked/activated. */
108
110
  readonly onToggle: (id: string) => void
111
+ /**
112
+ * Header title override. Falls back to `thread.outcome?.title ?? 'Untitled thread'` (today's
113
+ * behavior, unchanged) when omitted. For a server-titled thread whose title is never distilled
114
+ * into a basalt `AgentOutcome` (argo's hermes-chat S4 — title/summary/type/pin are server-owned
115
+ * and never guessed client-side), pass it directly instead of forcing an outcome shape onto data
116
+ * that never had one.
117
+ */
118
+ readonly title?: string
119
+ /** Header summary override, same fallback rule as `title` (`thread.outcome?.summary ?? ''`). */
120
+ readonly summary?: string
121
+ /** Rendered at the START of the header row, before the title/summary stack — a pin icon, a type
122
+ * badge (argo's hermes-chat S5). Absent by default; renders nothing extra when omitted. */
123
+ readonly headerLeft?: ReactNode
124
+ /** Rendered at the END of the header row, before the timestamp and chevron — a collapsed-only
125
+ * streaming indicator (argo's hermes-chat S5). Absent by default; renders nothing extra when
126
+ * omitted. */
127
+ readonly headerRight?: ReactNode
128
+ /**
129
+ * Overrides the transcript source: when provided, the row renders THESE messages instead of
130
+ * reading `thread.messages` directly (argo's hermes-chat S6 — the confirmed transcript is an
131
+ * optimistic-overlay MERGE over the store's messages, not the store's messages verbatim).
132
+ * Defaults to `thread.messages`.
133
+ */
134
+ readonly messages?: readonly ChatMessage<TranscriptPart>[]
109
135
  /** The live (in-flight) assistant turn's parts, when a run is streaming for this thread. */
110
136
  readonly liveParts?: readonly TranscriptPart[]
111
137
  /** The live run's status — drives the in-progress indicator on the live block. */
@@ -137,16 +163,29 @@ type ThreadFeedRowBase = {
137
163
  * `virtualize`/`height` are forwarded straight to the row's `ThreadTranscript`, carrying the same
138
164
  * union guard: an inline row holding a very long thread can window it, and doing so REQUIRES a
139
165
  * `height` (the transcript then owns a fixed-height scroll node inside the expanded body, instead
140
- * of the body growing to the thread's full length). Omit both and the row's transcript is
141
- * content-sized, as before.
166
+ * of the body growing to the thread's full length). `height` alone (no `virtualize`) is B3's
167
+ * bounded-but-not-virtualized mode — see `RowHeightProps`'s own doc. Omit both and the row's
168
+ * transcript is content-sized, as before.
142
169
  */
143
- export type ThreadFeedRowProps = ThreadFeedRowBase & VirtualizeProps & BasaltProps
170
+ export type ThreadFeedRowProps = ThreadFeedRowBase &
171
+ RowHeightProps &
172
+ SlotStylesProps<'root' | 'header' | 'body'> &
173
+ BasaltProps
174
+
175
+ /** The row header's title: the `title` prop override, else the resolved outcome title, else a
176
+ * plain placeholder — this row never falls back to scanning the first user message the way
177
+ * `ThreadOutcomeCard`'s `promptOf` does, since an inline row's own expanded transcript already
178
+ * shows that prompt a scroll away. */
179
+ function rowTitle(thread: AgentThread<TranscriptPart>, titleOverride: string | undefined): string {
180
+ return titleOverride ?? thread.outcome?.title ?? 'Untitled thread'
181
+ }
144
182
 
145
- /** The row header's title: the resolved outcome title, else a plain placeholder — this row never
146
- * falls back to scanning the first user message the way `ThreadOutcomeCard`'s `promptOf` does,
147
- * since an inline row's own expanded transcript already shows that prompt a scroll away. */
148
- function rowTitle(thread: AgentThread<TranscriptPart>): string {
149
- return thread.outcome?.title ?? 'Untitled thread'
183
+ /** The row header's summary: the `summary` prop override, else the resolved outcome summary. */
184
+ function rowSummary(
185
+ thread: AgentThread<TranscriptPart>,
186
+ summaryOverride: string | undefined,
187
+ ): string {
188
+ return summaryOverride ?? thread.outcome?.summary ?? ''
150
189
  }
151
190
 
152
191
  /**
@@ -162,6 +201,11 @@ export function ThreadFeedRow(props: ThreadFeedRowProps): JSX.Element {
162
201
  thread,
163
202
  expanded,
164
203
  onToggle,
204
+ title,
205
+ summary: summaryOverride,
206
+ headerLeft,
207
+ headerRight,
208
+ messages,
165
209
  liveParts,
166
210
  liveStatus,
167
211
  renderers,
@@ -171,16 +215,21 @@ export function ThreadFeedRow(props: ThreadFeedRowProps): JSX.Element {
171
215
  onSend,
172
216
  onStop,
173
217
  composerProps,
218
+ classNames,
174
219
  className,
175
220
  style,
176
221
  } = props
177
222
  assertRequiredProps('ThreadFeedRow', { thread }, ['thread'])
178
223
 
179
224
  // Resolved through the shared narrowing point rather than by destructuring `virtualize`/`height`
180
- // apart — see `resolveVirtualize`'s doc for why the two must be read together.
181
- const virtualized = resolveVirtualize(props)
225
+ // apart — see `resolveRowHeight`'s doc for why the two must be read together, and why it is a
226
+ // three-way (virtualized / bounded / content-sized) resolve rather than `resolveVirtualize`'s
227
+ // binary one (B3 adds the 'bounded' branch).
228
+ const rowHeight = resolveRowHeight(props)
182
229
  const virtualizeProps: VirtualizeProps =
183
- virtualized === null ? {} : { virtualize: virtualized.options, height: virtualized.height }
230
+ rowHeight.kind === 'virtualized'
231
+ ? { virtualize: rowHeight.virtualize.options, height: rowHeight.virtualize.height }
232
+ : {}
184
233
 
185
234
  // Lazy-mount + keep-mounted: flips true on the row's first expand and never resets. Everything
186
235
  // below this line renders — once — the first time `expanded` becomes true, and stays in the tree
@@ -197,7 +246,9 @@ export function ThreadFeedRow(props: ThreadFeedRowProps): JSX.Element {
197
246
  setHasOpened(true)
198
247
  }
199
248
 
200
- const summary = thread.outcome?.summary ?? ''
249
+ const resolvedTitle = rowTitle(thread, title)
250
+ const resolvedSummary = rowSummary(thread, summaryOverride)
251
+ const transcriptMessages = messages ?? thread.messages
201
252
 
202
253
  // The composer half of `liveStatus`: `Composer` only shows its Stop action and gates the
203
254
  // textarea when it is TOLD a run is streaming (`composer.tsx`'s `showStop`/`inputDisabled`) — it
@@ -205,18 +256,37 @@ export function ThreadFeedRow(props: ThreadFeedRowProps): JSX.Element {
205
256
  // Stop never rendered and a second turn could still be typed into a live thread.
206
257
  const streaming = liveStatus === 'streaming'
207
258
 
259
+ const transcript = (
260
+ <ThreadTranscript
261
+ messages={transcriptMessages}
262
+ {...(liveParts !== undefined ? { liveParts } : {})}
263
+ {...(liveStatus !== undefined ? { liveStatus } : {})}
264
+ {...(renderers !== undefined ? { renderers } : {})}
265
+ {...(fallbackRenderer !== undefined ? { fallbackRenderer } : {})}
266
+ {...(affordances !== undefined ? { affordances } : {})}
267
+ {...(groupConsecutive !== undefined ? { groupConsecutive } : {})}
268
+ {...virtualizeProps}
269
+ />
270
+ )
271
+
208
272
  return (
209
- <Box className={cx(className)} style={style ? { ...CARD_STYLE, ...style } : CARD_STYLE}>
273
+ <Box
274
+ className={cx(classNames?.root, className)}
275
+ style={style ? { ...CARD_STYLE, ...style } : CARD_STYLE}
276
+ data-expanded={expanded}
277
+ >
210
278
  <UnstyledButton
211
279
  onClick={() => onToggle(thread.id)}
212
280
  w="100%"
213
281
  aria-expanded={expanded}
282
+ {...(classNames?.header !== undefined && { className: classNames.header })}
214
283
  style={{
215
284
  display: 'block',
216
285
  padding: 'var(--mantine-spacing-xs) var(--mantine-spacing-sm)',
217
286
  }}
218
287
  >
219
288
  <Group justify="space-between" gap="xs" wrap="nowrap" align="center">
289
+ {headerLeft}
220
290
  <Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
221
291
  <Text
222
292
  fw={550}
@@ -227,14 +297,15 @@ export function ThreadFeedRow(props: ThreadFeedRowProps): JSX.Element {
227
297
  fontStretch: '88%',
228
298
  }}
229
299
  >
230
- {rowTitle(thread)}
300
+ {resolvedTitle}
231
301
  </Text>
232
- {summary.length > 0 && (
302
+ {resolvedSummary.length > 0 && (
233
303
  <Text size="xs" c="dimmed" lineClamp={1}>
234
- {summary}
304
+ {resolvedSummary}
235
305
  </Text>
236
306
  )}
237
307
  </Stack>
308
+ {headerRight}
238
309
  <Text
239
310
  style={{
240
311
  fontFamily: 'var(--basalt-font-mono)',
@@ -251,6 +322,7 @@ export function ThreadFeedRow(props: ThreadFeedRowProps): JSX.Element {
251
322
  {hasOpened && (
252
323
  <Box
253
324
  data-testid="thread-feed-row-body"
325
+ {...(classNames?.body !== undefined && { className: classNames.body })}
254
326
  style={{
255
327
  display: expanded ? 'block' : 'none',
256
328
  padding: 'var(--mantine-spacing-sm)',
@@ -258,16 +330,19 @@ export function ThreadFeedRow(props: ThreadFeedRowProps): JSX.Element {
258
330
  }}
259
331
  >
260
332
  <Stack gap="sm">
261
- <ThreadTranscript
262
- messages={thread.messages}
263
- {...(liveParts !== undefined ? { liveParts } : {})}
264
- {...(liveStatus !== undefined ? { liveStatus } : {})}
265
- {...(renderers !== undefined ? { renderers } : {})}
266
- {...(fallbackRenderer !== undefined ? { fallbackRenderer } : {})}
267
- {...(affordances !== undefined ? { affordances } : {})}
268
- {...(groupConsecutive !== undefined ? { groupConsecutive } : {})}
269
- {...virtualizeProps}
270
- />
333
+ {rowHeight.kind === 'bounded' ? (
334
+ <BasaltStickToBottom
335
+ style={{
336
+ height: rowHeight.height,
337
+ // theme-allow raw-scroll-container — BasaltStickToBottom owns this scroll node (see stick-to-bottom.tsx).
338
+ overflowY: 'auto',
339
+ }}
340
+ >
341
+ {transcript}
342
+ </BasaltStickToBottom>
343
+ ) : (
344
+ transcript
345
+ )}
271
346
  <Composer
272
347
  onSubmit={onSend}
273
348
  streaming={streaming}