basalt-ui 1.10.0 → 1.12.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 (273) hide show
  1. package/AGENTS.md +21 -21
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +36 -27
  4. package/agent/rules/basalt-content.md +16 -3
  5. package/dist/agent/adapter.d.ts +146 -0
  6. package/dist/agent/adapter.d.ts.map +1 -0
  7. package/dist/agent/adapter.js +473 -0
  8. package/dist/agent/adapter.test.d.ts +2 -0
  9. package/dist/agent/adapter.test.d.ts.map +1 -0
  10. package/dist/agent/ai-sdk-transport.d.ts +3 -52
  11. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  12. package/dist/agent/ai-sdk-transport.js +108 -24
  13. package/dist/agent/ai-sdk-transport.test.d.ts +2 -0
  14. package/dist/agent/ai-sdk-transport.test.d.ts.map +1 -0
  15. package/dist/agent/coalesce.d.ts +42 -0
  16. package/dist/agent/coalesce.d.ts.map +1 -0
  17. package/dist/agent/coalesce.js +137 -0
  18. package/dist/agent/coalesce.test.d.ts +2 -0
  19. package/dist/agent/coalesce.test.d.ts.map +1 -0
  20. package/dist/agent/foreign.d.ts +97 -0
  21. package/dist/agent/foreign.d.ts.map +1 -0
  22. package/dist/agent/foreign.js +18 -0
  23. package/dist/agent/foreign.test.d.ts +2 -0
  24. package/dist/agent/foreign.test.d.ts.map +1 -0
  25. package/dist/agent/history.d.ts +2 -0
  26. package/dist/agent/history.d.ts.map +1 -1
  27. package/dist/agent/id.d.ts +65 -0
  28. package/dist/agent/id.d.ts.map +1 -0
  29. package/dist/agent/id.js +30 -0
  30. package/dist/agent/id.test.d.ts +2 -0
  31. package/dist/agent/id.test.d.ts.map +1 -0
  32. package/dist/agent/index.d.ts +11 -4
  33. package/dist/agent/index.d.ts.map +1 -1
  34. package/dist/agent/index.js +18 -3
  35. package/dist/agent/merge.d.ts +8 -0
  36. package/dist/agent/merge.d.ts.map +1 -0
  37. package/dist/agent/merge.js +35 -0
  38. package/dist/agent/merge.test.d.ts +2 -0
  39. package/dist/agent/merge.test.d.ts.map +1 -0
  40. package/dist/agent/part-list.d.ts +49 -34
  41. package/dist/agent/part-list.d.ts.map +1 -1
  42. package/dist/agent/part-list.js +53 -7
  43. package/dist/agent/part-list.test.d.ts +2 -0
  44. package/dist/agent/part-list.test.d.ts.map +1 -0
  45. package/dist/agent/parts.d.ts +135 -20
  46. package/dist/agent/parts.d.ts.map +1 -1
  47. package/dist/agent/parts.js +138 -11
  48. package/dist/agent/parts.test.d.ts +2 -0
  49. package/dist/agent/parts.test.d.ts.map +1 -0
  50. package/dist/agent/thread.d.ts +14 -0
  51. package/dist/agent/thread.d.ts.map +1 -1
  52. package/dist/agent/thread.js +8 -2
  53. package/dist/agent/thread.test.d.ts +2 -0
  54. package/dist/agent/thread.test.d.ts.map +1 -0
  55. package/dist/agent/transport.d.ts +39 -3
  56. package/dist/agent/transport.d.ts.map +1 -1
  57. package/dist/agent/transport.js +20 -17
  58. package/dist/agent/transport.test.d.ts +2 -0
  59. package/dist/agent/transport.test.d.ts.map +1 -0
  60. package/dist/agent/use-agent-stream.d.ts +2 -1
  61. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  62. package/dist/agent/use-agent-stream.js +5 -2
  63. package/dist/agent/use-agent-stream.test.d.ts +2 -0
  64. package/dist/agent/use-agent-stream.test.d.ts.map +1 -0
  65. package/dist/agent/use-agent-thread-runs.d.ts +9 -3
  66. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  67. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +2 -0
  68. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +1 -0
  69. package/dist/agent/use-agent-thread-runs.js +75 -23
  70. package/dist/agent/use-agent-thread-runs.resume.test.d.ts +2 -0
  71. package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +1 -0
  72. package/dist/agent/use-agent-thread-runs.test.d.ts +2 -0
  73. package/dist/agent/use-agent-thread-runs.test.d.ts.map +1 -0
  74. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +2 -0
  75. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +1 -0
  76. package/dist/agent-chat/composer.d.ts +130 -8
  77. package/dist/agent-chat/composer.d.ts.map +1 -1
  78. package/dist/agent-chat/composer.js +189 -23
  79. package/dist/agent-chat/composer.test.d.ts +2 -0
  80. package/dist/agent-chat/composer.test.d.ts.map +1 -0
  81. package/dist/agent-chat/index.d.ts +3 -1
  82. package/dist/agent-chat/index.d.ts.map +1 -1
  83. package/dist/agent-chat/index.js +2 -0
  84. package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
  85. package/dist/agent-chat/thread-detail-panel.js +1 -1
  86. package/dist/agent-chat/thread-message.d.ts +24 -7
  87. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  88. package/dist/agent-chat/thread-message.js +115 -45
  89. package/dist/agent-chat/thread-message.test.d.ts +2 -0
  90. package/dist/agent-chat/thread-message.test.d.ts.map +1 -0
  91. package/dist/agent-chat/thread-workspace.js +1 -1
  92. package/dist/agent-chat/tool-chip.d.ts +21 -0
  93. package/dist/agent-chat/tool-chip.d.ts.map +1 -0
  94. package/dist/agent-chat/tool-chip.js +120 -0
  95. package/dist/agent-chat/tool-chip.test.d.ts +2 -0
  96. package/dist/agent-chat/tool-chip.test.d.ts.map +1 -0
  97. package/dist/charts/primitives/ChartFrame.test.d.ts +2 -0
  98. package/dist/charts/primitives/ChartFrame.test.d.ts.map +1 -0
  99. package/dist/charts/utils/domain.test.d.ts +2 -0
  100. package/dist/charts/utils/domain.test.d.ts.map +1 -0
  101. package/dist/cli/check-theme.test.d.ts +2 -0
  102. package/dist/cli/check-theme.test.d.ts.map +1 -0
  103. package/dist/cli/help.test.d.ts +2 -0
  104. package/dist/cli/help.test.d.ts.map +1 -0
  105. package/dist/cli/index.d.ts.map +1 -1
  106. package/dist/cli/init-sync.test.d.ts +2 -0
  107. package/dist/cli/init-sync.test.d.ts.map +1 -0
  108. package/dist/cli/placement-engine.test.d.ts +2 -0
  109. package/dist/cli/placement-engine.test.d.ts.map +1 -0
  110. package/dist/cli/tarball-parity.test.d.ts +2 -0
  111. package/dist/cli/tarball-parity.test.d.ts.map +1 -0
  112. package/dist/cli/template-format.test.d.ts +2 -0
  113. package/dist/cli/template-format.test.d.ts.map +1 -0
  114. package/dist/cli/template-ledger.test.d.ts +2 -0
  115. package/dist/cli/template-ledger.test.d.ts.map +1 -0
  116. package/dist/cli/tokens-css.test.d.ts +2 -0
  117. package/dist/cli/tokens-css.test.d.ts.map +1 -0
  118. package/dist/content/article-actions.test.d.ts +2 -0
  119. package/dist/content/article-actions.test.d.ts.map +1 -0
  120. package/dist/content/article-model.test.d.ts +2 -0
  121. package/dist/content/article-model.test.d.ts.map +1 -0
  122. package/dist/content/block-split.test.d.ts +2 -0
  123. package/dist/content/block-split.test.d.ts.map +1 -0
  124. package/dist/content/fence-block.d.ts +37 -10
  125. package/dist/content/fence-block.d.ts.map +1 -1
  126. package/dist/content/fence-block.js +80 -14
  127. package/dist/content/heading-components.d.ts.map +1 -1
  128. package/dist/content/heading-components.js +7 -4
  129. package/dist/content/heading-components.test.d.ts +2 -0
  130. package/dist/content/heading-components.test.d.ts.map +1 -0
  131. package/dist/content/index.d.ts +6 -3
  132. package/dist/content/index.d.ts.map +1 -1
  133. package/dist/content/index.js +5 -1
  134. package/dist/content/markdown.d.ts +101 -14
  135. package/dist/content/markdown.d.ts.map +1 -1
  136. package/dist/content/markdown.js +141 -28
  137. package/dist/content/markdown.test.d.ts +2 -0
  138. package/dist/content/markdown.test.d.ts.map +1 -0
  139. package/dist/content/prose.module.css.test.d.ts +2 -0
  140. package/dist/content/prose.module.css.test.d.ts.map +1 -0
  141. package/dist/content/sanitize.d.ts +170 -0
  142. package/dist/content/sanitize.d.ts.map +1 -0
  143. package/dist/content/sanitize.js +178 -0
  144. package/dist/content/sanitize.test.d.ts +2 -0
  145. package/dist/content/sanitize.test.d.ts.map +1 -0
  146. package/dist/content/slug.test.d.ts +2 -0
  147. package/dist/content/slug.test.d.ts.map +1 -0
  148. package/dist/dashboard/stat-card.test.d.ts +2 -0
  149. package/dist/dashboard/stat-card.test.d.ts.map +1 -0
  150. package/dist/guard/check-source.test.d.ts +2 -0
  151. package/dist/guard/check-source.test.d.ts.map +1 -0
  152. package/dist/guard/guard-hook.test.d.ts +2 -0
  153. package/dist/guard/guard-hook.test.d.ts.map +1 -0
  154. package/dist/guard/index.d.ts.map +1 -1
  155. package/dist/guard/index.js +1 -3
  156. package/dist/index.d.ts +4 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +12 -2
  159. package/dist/provider/index.test.d.ts +2 -0
  160. package/dist/provider/index.test.d.ts.map +1 -0
  161. package/dist/provider/lab-theme.test.d.ts +2 -0
  162. package/dist/provider/lab-theme.test.d.ts.map +1 -0
  163. package/dist/router-tanstack/multi-search-param-store.test.d.ts +2 -0
  164. package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +1 -0
  165. package/dist/styles.floor.test.d.ts +2 -0
  166. package/dist/styles.floor.test.d.ts.map +1 -0
  167. package/dist/surfaces.d.ts +5 -5
  168. package/dist/surfaces.d.ts.map +1 -1
  169. package/dist/surfaces.js +13 -4
  170. package/dist/theme/border-coverage.test.d.ts +18 -0
  171. package/dist/theme/border-coverage.test.d.ts.map +1 -0
  172. package/dist/theme/contrast.test.d.ts +2 -0
  173. package/dist/theme/contrast.test.d.ts.map +1 -0
  174. package/dist/theme/density-option.test.d.ts +2 -0
  175. package/dist/theme/density-option.test.d.ts.map +1 -0
  176. package/dist/theme/density-relations.test.d.ts +2 -0
  177. package/dist/theme/density-relations.test.d.ts.map +1 -0
  178. package/dist/theme/derive-theme.test.d.ts +2 -0
  179. package/dist/theme/derive-theme.test.d.ts.map +1 -0
  180. package/dist/theme/fonts-option.test.d.ts +2 -0
  181. package/dist/theme/fonts-option.test.d.ts.map +1 -0
  182. package/dist/theme/radius-option.test.d.ts +2 -0
  183. package/dist/theme/radius-option.test.d.ts.map +1 -0
  184. package/dist/theme/radius.test.d.ts +2 -0
  185. package/dist/theme/radius.test.d.ts.map +1 -0
  186. package/dist/theme/shadow-surfaces.test.d.ts +26 -0
  187. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -0
  188. package/dist/theme/spacing.test.d.ts +2 -0
  189. package/dist/theme/spacing.test.d.ts.map +1 -0
  190. package/dist/theme/use-basalt-spacing.test.d.ts +2 -0
  191. package/dist/theme/use-basalt-spacing.test.d.ts.map +1 -0
  192. package/dist/theme-lab/derive-controls.test.d.ts +2 -0
  193. package/dist/theme-lab/derive-controls.test.d.ts.map +1 -0
  194. package/dist/theme-lab/derive-state.test.d.ts +2 -0
  195. package/dist/theme-lab/derive-state.test.d.ts.map +1 -0
  196. package/dist/theme-lab/theme-lab.test.d.ts +2 -0
  197. package/dist/theme-lab/theme-lab.test.d.ts.map +1 -0
  198. package/dist/tokens/build-fonts-css.test.d.ts +2 -0
  199. package/dist/tokens/build-fonts-css.test.d.ts.map +1 -0
  200. package/dist/tokens/build-radius-css.test.d.ts +2 -0
  201. package/dist/tokens/build-radius-css.test.d.ts.map +1 -0
  202. package/dist/tokens/density.test.d.ts +2 -0
  203. package/dist/tokens/density.test.d.ts.map +1 -0
  204. package/dist/tokens/derive-radius.test.d.ts +2 -0
  205. package/dist/tokens/derive-radius.test.d.ts.map +1 -0
  206. package/dist/tokens/derive.test.d.ts +2 -0
  207. package/dist/tokens/derive.test.d.ts.map +1 -0
  208. package/dist/tokens/palette.test.d.ts +2 -0
  209. package/dist/tokens/palette.test.d.ts.map +1 -0
  210. package/dist/vite-color-mix.test.d.ts +2 -0
  211. package/dist/vite-color-mix.test.d.ts.map +1 -0
  212. package/dist/vite.test.d.ts +2 -0
  213. package/dist/vite.test.d.ts.map +1 -0
  214. package/llms.txt +7 -7
  215. package/package.json +6 -1
  216. package/src/agent/adapter.test.ts +1403 -0
  217. package/src/agent/adapter.ts +887 -0
  218. package/src/agent/ai-sdk-transport.test.ts +380 -55
  219. package/src/agent/ai-sdk-transport.ts +206 -53
  220. package/src/agent/coalesce.test.ts +387 -0
  221. package/src/agent/coalesce.ts +229 -0
  222. package/src/agent/foreign.test.ts +51 -0
  223. package/src/agent/foreign.ts +140 -0
  224. package/src/agent/history.ts +2 -0
  225. package/src/agent/id.test.ts +75 -0
  226. package/src/agent/id.ts +91 -0
  227. package/src/agent/index.ts +34 -3
  228. package/src/agent/merge.test.ts +206 -0
  229. package/src/agent/merge.ts +114 -0
  230. package/src/agent/part-list.test.tsx +112 -0
  231. package/src/agent/part-list.tsx +108 -35
  232. package/src/agent/parts.test.ts +516 -57
  233. package/src/agent/parts.ts +323 -32
  234. package/src/agent/thread.test.ts +75 -2
  235. package/src/agent/thread.ts +21 -1
  236. package/src/agent/transport.test.ts +70 -0
  237. package/src/agent/transport.ts +72 -20
  238. package/src/agent/use-agent-stream.test.tsx +53 -19
  239. package/src/agent/use-agent-stream.ts +13 -3
  240. package/src/agent/use-agent-thread-runs.foreign.test.tsx +138 -0
  241. package/src/agent/use-agent-thread-runs.resume.test.tsx +44 -11
  242. package/src/agent/use-agent-thread-runs.test.tsx +418 -22
  243. package/src/agent/use-agent-thread-runs.ts +170 -26
  244. package/src/agent/use-agent-thread-runs.wedge.test.tsx +15 -7
  245. package/src/agent-chat/composer.test.tsx +924 -0
  246. package/src/agent-chat/composer.tsx +521 -52
  247. package/src/agent-chat/index.ts +5 -1
  248. package/src/agent-chat/thread-detail-panel.tsx +3 -1
  249. package/src/agent-chat/thread-message.test.tsx +351 -0
  250. package/src/agent-chat/thread-message.tsx +262 -64
  251. package/src/agent-chat/thread-workspace.tsx +1 -1
  252. package/src/agent-chat/tool-chip.test.tsx +139 -0
  253. package/src/agent-chat/tool-chip.tsx +219 -0
  254. package/src/cli/index.ts +0 -1
  255. package/src/cli/template-format.test.ts +2 -2
  256. package/src/content/fence-block.tsx +242 -15
  257. package/src/content/heading-components.test.tsx +65 -0
  258. package/src/content/heading-components.tsx +26 -5
  259. package/src/content/index.ts +19 -3
  260. package/src/content/markdown.test.tsx +603 -0
  261. package/src/content/markdown.tsx +355 -35
  262. package/src/content/sanitize.test.ts +659 -0
  263. package/src/content/sanitize.ts +510 -0
  264. package/src/dashboard/stat-card.test.tsx +6 -1
  265. package/src/guard/check-source.test.ts +8 -4
  266. package/src/guard/index.ts +1 -3
  267. package/src/index.ts +15 -1
  268. package/src/surfaces.ts +13 -4
  269. package/src/theme/contrast.test.ts +1 -4
  270. package/src/theme/density-relations.test.ts +4 -2
  271. package/src/theme/fonts-option.test.ts +7 -1
  272. package/src/theme/shadow-surfaces.test.ts +12 -6
  273. package/src/theme/use-basalt-spacing.test.tsx +2 -1
@@ -0,0 +1,473 @@
1
+ import { useEffect, useSyncExternalStore } from "react";
2
+ import { mintThreadId } from "./id.js";
3
+ function createAdapterThreadsStore(adapter, opts) {
4
+ const revalidateOnFocus = opts?.revalidateOnFocus ?? false;
5
+ const EMPTY = [];
6
+ const SERVER_SNAPSHOT = {
7
+ threads: EMPTY,
8
+ activeId: null,
9
+ hydrated: false,
10
+ error: void 0
11
+ };
12
+ let base = EMPTY;
13
+ let baseSeq = 0;
14
+ let activeId = null;
15
+ let hydrated = false;
16
+ let error;
17
+ let errorToken = 0;
18
+ let errorFromWrite = false;
19
+ const pending = [];
20
+ let snapshot = SERVER_SNAPSHOT;
21
+ const listeners = /* @__PURE__ */ new Set();
22
+ function recompute() {
23
+ let threads = base;
24
+ let active = activeId;
25
+ for (const patch of pending) {
26
+ threads = patch.apply(threads);
27
+ if (patch.applyActive !== void 0) active = patch.applyActive(active);
28
+ }
29
+ snapshot = { threads, activeId: active, hydrated, error };
30
+ for (const listener of listeners) listener();
31
+ }
32
+ function memoizeApply(apply) {
33
+ let lastIn;
34
+ let lastOut;
35
+ return (threads) => {
36
+ if (lastOut !== void 0 && threads === lastIn) return lastOut;
37
+ lastIn = threads;
38
+ lastOut = apply(threads);
39
+ return lastOut;
40
+ };
41
+ }
42
+ function subscribe(listener) {
43
+ listeners.add(listener);
44
+ return () => {
45
+ listeners.delete(listener);
46
+ };
47
+ }
48
+ function getSnapshot() {
49
+ return snapshot;
50
+ }
51
+ function getServerSnapshot() {
52
+ return SERVER_SNAPSHOT;
53
+ }
54
+ let loadController = null;
55
+ let loadSeq = 0;
56
+ let queuedSeq = null;
57
+ function prune() {
58
+ const kept = [];
59
+ for (const patch of pending) {
60
+ if (patch.confirmSeq !== null && patch.confirmSeq <= baseSeq) {
61
+ if (patch.applyActive !== void 0) activeId = patch.applyActive(activeId);
62
+ continue;
63
+ }
64
+ kept.push(patch);
65
+ }
66
+ if (kept.length !== pending.length) pending.splice(0, pending.length, ...kept);
67
+ }
68
+ function startLoad(seq) {
69
+ const controller = new AbortController();
70
+ loadController = controller;
71
+ void (async () => {
72
+ try {
73
+ const next = await adapter.listThreads(controller.signal);
74
+ if (loadController !== controller) return;
75
+ base = [...next];
76
+ baseSeq = seq;
77
+ hydrated = true;
78
+ if (!errorFromWrite) error = void 0;
79
+ prune();
80
+ recompute();
81
+ } catch (cause) {
82
+ if (controller.signal.aborted || loadController !== controller) return;
83
+ error = cause;
84
+ errorToken += 1;
85
+ errorFromWrite = false;
86
+ recompute();
87
+ } finally {
88
+ if (loadController === controller) {
89
+ loadController = null;
90
+ if (queuedSeq !== null) {
91
+ const next = queuedSeq;
92
+ queuedSeq = null;
93
+ startLoad(next);
94
+ }
95
+ }
96
+ }
97
+ })();
98
+ }
99
+ function revalidate() {
100
+ if (loadController !== null) {
101
+ queuedSeq ??= ++loadSeq;
102
+ return queuedSeq;
103
+ }
104
+ const seq = ++loadSeq;
105
+ startLoad(seq);
106
+ return seq;
107
+ }
108
+ function forceRevalidate() {
109
+ loadController?.abort();
110
+ queuedSeq = null;
111
+ const seq = ++loadSeq;
112
+ startLoad(seq);
113
+ return seq;
114
+ }
115
+ const chains = /* @__PURE__ */ new Map();
116
+ function enqueue(threadIds, task) {
117
+ const ids = [...new Set(threadIds)];
118
+ const prior = ids.map((id) => chains.get(id)).filter((p) => p !== void 0);
119
+ const after = prior.length === 0 ? Promise.resolve() : Promise.all(prior).then(() => void 0);
120
+ const run = after.then(task);
121
+ for (const id of ids) chains.set(id, run);
122
+ void run.finally(() => {
123
+ for (const id of ids) if (chains.get(id) === run) chains.delete(id);
124
+ });
125
+ }
126
+ function drop(patch) {
127
+ const index = pending.indexOf(patch);
128
+ if (index !== -1) pending.splice(index, 1);
129
+ }
130
+ function mutate(i) {
131
+ const patch = {
132
+ apply: memoizeApply(i.apply),
133
+ ...i.applyActive !== void 0 ? { applyActive: i.applyActive } : {},
134
+ confirmSeq: null
135
+ };
136
+ pending.push(patch);
137
+ const tokenAtIssue = errorToken;
138
+ recompute();
139
+ enqueue(i.threadIds, async () => {
140
+ try {
141
+ await i.commit();
142
+ } catch (cause) {
143
+ drop(patch);
144
+ error = cause;
145
+ errorToken += 1;
146
+ errorFromWrite = true;
147
+ recompute();
148
+ revalidate();
149
+ return;
150
+ }
151
+ if (errorFromWrite && errorToken === tokenAtIssue) {
152
+ error = void 0;
153
+ errorFromWrite = false;
154
+ recompute();
155
+ }
156
+ patch.confirmSeq = revalidate();
157
+ });
158
+ }
159
+ function patchThread(id, update) {
160
+ return (threads) => threads.map((thread) => thread.id === id ? update(thread) : thread);
161
+ }
162
+ let mounted = 0;
163
+ function onFocus() {
164
+ forceRevalidate();
165
+ }
166
+ function retain() {
167
+ mounted += 1;
168
+ if (mounted !== 1) return;
169
+ revalidate();
170
+ if (revalidateOnFocus && typeof window !== "undefined") {
171
+ window.addEventListener("focus", onFocus);
172
+ }
173
+ }
174
+ function release() {
175
+ mounted -= 1;
176
+ if (mounted !== 0) return;
177
+ loadController?.abort();
178
+ loadController = null;
179
+ queuedSeq = null;
180
+ if (revalidateOnFocus && typeof window !== "undefined") {
181
+ window.removeEventListener("focus", onFocus);
182
+ }
183
+ }
184
+ function select(id) {
185
+ activeId = id;
186
+ recompute();
187
+ }
188
+ function create(createOpts) {
189
+ const id = mintThreadId();
190
+ const now = Date.now();
191
+ const thread = {
192
+ id,
193
+ messages: [],
194
+ outcome: null,
195
+ status: "pending",
196
+ read: false,
197
+ createdAt: now,
198
+ updatedAt: now,
199
+ ...createOpts?.meta !== void 0 ? { meta: createOpts.meta } : {}
200
+ };
201
+ mutate({
202
+ threadIds: [id],
203
+ apply: (threads) => threads.some((t) => t.id === id) ? [...threads] : [thread, ...threads],
204
+ commit: () => adapter.createThread({
205
+ id,
206
+ ...createOpts?.meta !== void 0 ? { meta: createOpts.meta } : {}
207
+ })
208
+ });
209
+ return id;
210
+ }
211
+ function appendMessage(id, message2) {
212
+ const now = Date.now();
213
+ mutate({
214
+ threadIds: [id],
215
+ apply: patchThread(
216
+ id,
217
+ (thread) => thread.messages.some((m) => m.id === message2.id) ? thread : { ...thread, messages: [...thread.messages, message2], updatedAt: now }
218
+ ),
219
+ commit: () => adapter.appendMessage({ threadId: id, message: message2 })
220
+ });
221
+ }
222
+ function setOutcome(id, outcome) {
223
+ const now = Date.now();
224
+ mutate({
225
+ threadIds: [id],
226
+ apply: patchThread(id, (thread) => ({ ...thread, outcome, updatedAt: now })),
227
+ commit: () => adapter.setOutcome({ threadId: id, outcome })
228
+ });
229
+ }
230
+ function setStatus(id, status) {
231
+ const now = Date.now();
232
+ mutate({
233
+ threadIds: [id],
234
+ apply: patchThread(id, (thread) => ({ ...thread, status, updatedAt: now })),
235
+ commit: () => adapter.setStatus({ threadId: id, status })
236
+ });
237
+ }
238
+ function setResumeToken(id, token) {
239
+ const now = Date.now();
240
+ mutate({
241
+ threadIds: [id],
242
+ apply: patchThread(id, (thread) => {
243
+ const { resumeToken: _resumeToken, ...rest } = thread;
244
+ return {
245
+ ...rest,
246
+ ...token !== void 0 ? { resumeToken: token } : {},
247
+ updatedAt: now
248
+ };
249
+ }),
250
+ commit: () => adapter.setResumeToken({ threadId: id, token })
251
+ });
252
+ }
253
+ function markRead(id) {
254
+ mutate({
255
+ threadIds: [id],
256
+ // Identity-preserving when the server already agrees — markRead does not bump updatedAt.
257
+ apply: patchThread(id, (thread) => thread.read ? thread : { ...thread, read: true }),
258
+ commit: () => adapter.markRead(id)
259
+ });
260
+ }
261
+ function remove(id) {
262
+ mutate({
263
+ threadIds: [id],
264
+ apply: (threads) => threads.filter((thread) => thread.id !== id),
265
+ // Inside the patch, so a rejected delete restores the SELECTION along with the row. Written
266
+ // as a conditional clear rather than a flat `null` so a `select()` issued while the delete
267
+ // is in flight survives the patch being re-applied on top of it.
268
+ applyActive: (active) => active === id ? null : active,
269
+ commit: () => adapter.removeThread(id)
270
+ });
271
+ }
272
+ function clear() {
273
+ const ids = snapshot.threads.map((thread) => thread.id);
274
+ const clearedIds = new Set(ids);
275
+ mutate({
276
+ threadIds: ids,
277
+ apply: () => [],
278
+ // Same idiom as remove(): only null a selection pointing at one of the threads THIS clear
279
+ // captured, so a select() issued while the clear is in flight — e.g. into a thread created
280
+ // concurrently by create() — survives the patch being re-applied on top of it.
281
+ applyActive: (active) => active !== null && clearedIds.has(active) ? null : active,
282
+ commit: async () => {
283
+ const results = await Promise.allSettled(ids.map((id) => adapter.removeThread(id)));
284
+ const failures = results.filter((r) => r.status === "rejected");
285
+ if (failures.length === 0) return;
286
+ throw new AggregateError(
287
+ failures.map((f) => f.reason),
288
+ `removeThread rejected for ${failures.length} of ${ids.length} threads`
289
+ );
290
+ }
291
+ });
292
+ }
293
+ return function useAdapterThreadsStore() {
294
+ const current = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
295
+ useEffect(() => {
296
+ retain();
297
+ return () => {
298
+ release();
299
+ };
300
+ }, []);
301
+ return {
302
+ threads: current.threads,
303
+ activeId: current.activeId,
304
+ hydrated: current.hydrated,
305
+ error: current.error,
306
+ select,
307
+ create,
308
+ appendMessage,
309
+ setOutcome,
310
+ setStatus,
311
+ setResumeToken,
312
+ markRead,
313
+ remove,
314
+ clear
315
+ };
316
+ };
317
+ }
318
+ function assert(condition, message2) {
319
+ if (!condition) throw new Error(`ThreadsStoreAdapter contract: ${message2}`);
320
+ }
321
+ function message(id) {
322
+ return { id, role: "user", parts: [], createdAt: Date.now() };
323
+ }
324
+ function threadsStoreAdapterContract(makeAdapter) {
325
+ function define(name, run) {
326
+ return {
327
+ name,
328
+ run: async () => {
329
+ await run(await makeAdapter());
330
+ }
331
+ };
332
+ }
333
+ async function loadOrThrow(adapter, id) {
334
+ const thread = await adapter.loadThread(id);
335
+ assert(thread !== null, `loadThread('${id}') returned null for a thread that was created`);
336
+ return thread;
337
+ }
338
+ return [
339
+ define("createThread then listThreads returns the thread with empty defaults", async (a) => {
340
+ const id = crypto.randomUUID();
341
+ await a.createThread({ id });
342
+ const threads = await a.listThreads();
343
+ const found = threads.filter((t) => t.id === id);
344
+ assert(found.length === 1, `listThreads returned ${found.length} threads for one create`);
345
+ const thread = found[0];
346
+ assert(thread !== void 0, "listThreads entry was undefined");
347
+ assert(thread.messages.length === 0, "a new thread must start with no messages");
348
+ assert(thread.outcome === null, "a new thread's outcome must be null");
349
+ assert(thread.status === "pending", `a new thread's status must be 'pending'`);
350
+ assert(thread.read === false, "a new thread must be unread");
351
+ assert(typeof thread.createdAt === "number", "createdAt must be a number");
352
+ assert(typeof thread.updatedAt === "number", "updatedAt must be a number");
353
+ }),
354
+ define("createThread persists meta", async (a) => {
355
+ const id = crypto.randomUUID();
356
+ await a.createThread({ id, meta: { source: "contract" } });
357
+ const thread = await loadOrThrow(a, id);
358
+ assert(thread.meta?.["source"] === "contract", "meta did not round-trip");
359
+ }),
360
+ define("loadThread returns null for an unknown id", async (a) => {
361
+ const thread = await a.loadThread(crypto.randomUUID());
362
+ assert(thread === null, "loadThread must return null (not throw, not undefined) when absent");
363
+ }),
364
+ define("appendMessage is idempotent on message.id", async (a) => {
365
+ const id = crypto.randomUUID();
366
+ await a.createThread({ id });
367
+ const msg = message("contract-msg-1");
368
+ await a.appendMessage({ threadId: id, message: msg });
369
+ await a.appendMessage({ threadId: id, message: msg });
370
+ const thread = await loadOrThrow(a, id);
371
+ assert(
372
+ thread.messages.length === 1,
373
+ `appending the same message.id twice produced ${thread.messages.length} rows \u2014 the id is the only idempotency key`
374
+ );
375
+ assert(thread.messages[0]?.id === "contract-msg-1", "the retained message has the wrong id");
376
+ }),
377
+ define("appendMessage preserves order for distinct ids", async (a) => {
378
+ const id = crypto.randomUUID();
379
+ await a.createThread({ id });
380
+ await a.appendMessage({ threadId: id, message: message("contract-msg-1") });
381
+ await a.appendMessage({ threadId: id, message: message("contract-msg-2") });
382
+ const thread = await loadOrThrow(a, id);
383
+ assert(
384
+ thread.messages.map((m) => m.id).join(",") === "contract-msg-1,contract-msg-2",
385
+ "messages must read back oldest-first in append order"
386
+ );
387
+ }),
388
+ define("setStatus round-trips", async (a) => {
389
+ const id = crypto.randomUUID();
390
+ await a.createThread({ id });
391
+ await a.setStatus({ threadId: id, status: "streaming" });
392
+ assert((await loadOrThrow(a, id)).status === "streaming", "status did not become 'streaming'");
393
+ await a.setStatus({ threadId: id, status: "error" });
394
+ assert((await loadOrThrow(a, id)).status === "error", "status did not become 'error'");
395
+ }),
396
+ define("setOutcome round-trips", async (a) => {
397
+ const id = crypto.randomUUID();
398
+ await a.createThread({ id });
399
+ const outcome = { title: "Title", summary: "Summary", status: "done" };
400
+ await a.setOutcome({ threadId: id, outcome });
401
+ const thread = await loadOrThrow(a, id);
402
+ assert(thread.outcome?.title === "Title", "outcome.title did not round-trip");
403
+ assert(thread.outcome?.summary === "Summary", "outcome.summary did not round-trip");
404
+ assert(thread.outcome?.status === "done", "outcome.status did not round-trip");
405
+ }),
406
+ define("setResumeToken round-trips and clears with undefined", async (a) => {
407
+ const id = crypto.randomUUID();
408
+ await a.createThread({ id });
409
+ await a.setResumeToken({ threadId: id, token: "tok-1" });
410
+ assert((await loadOrThrow(a, id)).resumeToken === "tok-1", "resumeToken did not round-trip");
411
+ await a.setResumeToken({ threadId: id, token: void 0 });
412
+ assert(
413
+ (await loadOrThrow(a, id)).resumeToken === void 0,
414
+ "setResumeToken(undefined) must CLEAR the token"
415
+ );
416
+ }),
417
+ define("markRead marks the thread read", async (a) => {
418
+ const id = crypto.randomUUID();
419
+ await a.createThread({ id });
420
+ assert((await loadOrThrow(a, id)).read === false, "a new thread must start unread");
421
+ await a.markRead(id);
422
+ assert((await loadOrThrow(a, id)).read === true, "markRead did not set read");
423
+ }),
424
+ define("listThreads reflects a write that has already resolved", async (a) => {
425
+ const id = crypto.randomUUID();
426
+ await a.createThread({ id });
427
+ await a.appendMessage({ threadId: id, message: message("contract-msg-raw") });
428
+ await a.setStatus({ threadId: id, status: "streaming" });
429
+ const found = (await a.listThreads()).find((t) => t.id === id);
430
+ assert(found !== void 0, "listThreads omitted a thread created before the call");
431
+ assert(
432
+ found.status === "streaming",
433
+ "listThreads returned a STALE status \u2014 a list started after a write resolves must reflect it"
434
+ );
435
+ assert(
436
+ found.messages.length === 1,
437
+ `listThreads returned a STALE message list (${found.messages.length} rows) \u2014 a list started after a write resolves must reflect it`
438
+ );
439
+ }),
440
+ define(
441
+ "dependent writes issued straight after createThread resolves are honoured",
442
+ async (a) => {
443
+ const id = crypto.randomUUID();
444
+ await a.createThread({ id });
445
+ await a.markRead(id);
446
+ await a.appendMessage({ threadId: id, message: message("contract-msg-dep") });
447
+ await a.setStatus({ threadId: id, status: "streaming" });
448
+ const thread = await loadOrThrow(a, id);
449
+ assert(thread.read === true, "markRead did not apply to a just-created thread");
450
+ assert(
451
+ thread.messages.length === 1,
452
+ `appendMessage did not apply to a just-created thread (${thread.messages.length} rows)`
453
+ );
454
+ assert(thread.status === "streaming", "setStatus did not apply to a just-created thread");
455
+ }
456
+ ),
457
+ define("removeThread removes the thread", async (a) => {
458
+ const id = crypto.randomUUID();
459
+ await a.createThread({ id });
460
+ await a.removeThread(id);
461
+ assert(await a.loadThread(id).then((t) => t === null), "loadThread must be null after remove");
462
+ const threads = await a.listThreads();
463
+ assert(!threads.some((t) => t.id === id), "listThreads still contains the removed thread");
464
+ }),
465
+ define("removeThread of an unknown id does not throw", async (a) => {
466
+ await a.removeThread(crypto.randomUUID());
467
+ })
468
+ ];
469
+ }
470
+ export {
471
+ createAdapterThreadsStore,
472
+ threadsStoreAdapterContract
473
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adapter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.test.d.ts","sourceRoot":"","sources":["../../src/agent/adapter.test.ts"],"names":[],"mappings":""}
@@ -1,54 +1,5 @@
1
- /**
2
- * aiSdkTransport — an AgentTransport backed by Vercel AI SDK's native streaming + resumption
3
- * primitives (the `ai` npm package). This is the RECOMMENDED DEFAULT transport for LLM chat use
4
- * cases — edenTransport remains the zero-extra-dependency alternative (see agent/rules/basalt-agent.md).
5
- *
6
- * OPTIONAL PEER: `ai` is not a required dependency. Importing 'basalt-ui/agent' (and calling this
7
- * factory) does NOT eagerly resolve it — the package is loaded via a memoized dynamic import()
8
- * only when the first stream()/resume() call actually runs, mirroring the lazy-optional-peer
9
- * contract used by BasaltStickToBottom. Unlike that, there is no meaningful "plain text" fallback
10
- * for a missing transport: if 'ai' is not installed, the dynamic import
11
- * rejects and the error propagates through the async generator to the consumer's EXISTING error
12
- * handling (useAgentStream sets status: 'error'; useAgentThreadRuns falls back to its
13
- * onFailureStatus) — no separate crash path is introduced.
14
- *
15
- * Install the optional peer:
16
- * bun add ai
17
- *
18
- * WHY DIFFING IS NEEDED: AI SDK's `readUIMessageStream` yields the FULL accumulated `UIMessage`
19
- * snapshot (a growing `parts` array, with existing parts' content growing in place) on every
20
- * update — it is not itself a delta stream. basalt-ui's whole part-accumulation model
21
- * (useAgentStream / useAgentThreadRuns, and every consumer's own coalescing) is delta-based: many
22
- * small parts merged at render time. This module diffs consecutive snapshots and yields only the
23
- * new deltas as AgentParts, so nothing is double-rendered.
24
- *
25
- * CHAT ID BINDING: AI SDK's convention is that the chat id is known client-side before the
26
- * request (unlike a river/server-minted id) — `aiSdkTransport(options)` mints ONE stable chat id
27
- * at construction time, so the returned object is immediately usable with useAgentStream (a single
28
- * ongoing conversation). `.forThread(threadId)` returns a transport bound to an arbitrary
29
- * caller-supplied id instead — pass a basalt-ui thread id here to plug into
30
- * useAgentThreadRuns's per-thread transport-factory form, so a thread's turns (and its mount-time
31
- * resume) share one continuous AI SDK chat session:
32
- *
33
- * @example
34
- * import { useAgentStream, aiSdkTransport } from 'basalt-ui/agent'
35
- *
36
- * const transport = aiSdkTransport({ api: '/api/chat' })
37
- * const { parts, status, send } = useAgentStream({ transport })
38
- *
39
- * @example
40
- * import { useAgentThreadRuns, aiSdkTransport, createThreadsStore, heuristicOutcome } from 'basalt-ui/agent'
41
- *
42
- * const useThreads = createThreadsStore({ key: 'main-threads', version: 1 })
43
- * const transport = aiSdkTransport({ api: '/api/chat' })
44
- * const { runs, start } = useAgentThreadRuns({
45
- * transport: (threadId) => transport.forThread(threadId),
46
- * store: useThreads(),
47
- * resolveOutcome: heuristicOutcome,
48
- * })
49
- */
50
1
  import type { AgentPart } from './parts.js';
51
- import type { AgentTransport } from './transport.js';
2
+ import type { ResumableAgentTransport } from './transport.js';
52
3
  export type AiSdkTransportOptions = {
53
4
  /** The API URL AI SDK's transport POSTs new turns to and GETs reconnects from. */
54
5
  readonly api: string;
@@ -62,13 +13,13 @@ export type AiSdkTransportOptions = {
62
13
  */
63
14
  readonly fetch?: typeof globalThis.fetch;
64
15
  };
65
- export type AiSdkTransport<TPart = AgentPart> = AgentTransport<TPart, string> & {
16
+ export type AiSdkTransport<TPart = AgentPart> = ResumableAgentTransport<TPart, string> & {
66
17
  /**
67
18
  * Returns an AgentTransport bound to a stable chat id (e.g. a basalt-ui thread id) instead of
68
19
  * this transport's own fixed one. Use this for useAgentThreadRuns's per-thread transport-factory
69
20
  * form so each thread's turns share one continuous AI SDK chat session.
70
21
  */
71
- readonly forThread: (chatId: string) => AgentTransport<TPart, string>;
22
+ readonly forThread: (chatId: string) => ResumableAgentTransport<TPart, string>;
72
23
  };
73
24
  /**
74
25
  * Wraps AI SDK's `DefaultChatTransport` + `readUIMessageStream` into an AgentTransport.
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk-transport.d.ts","sourceRoot":"","sources":["../../src/agent/ai-sdk-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,KAAK,EAAE,SAAS,EAA4B,MAAM,SAAS,CAAA;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAYjD,MAAM,MAAM,qBAAqB,GAAG;IAClC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAA;IACnD,oFAAoF;IACpF,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;IACzC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CACzC,CAAA;AAID,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG;IAC9E;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CACtE,CAAA;AA8ID;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,KAAK,GAAG,SAAS,EAC9C,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,KAAK,CAAC,CAuCvB"}
1
+ {"version":3,"file":"ai-sdk-transport.d.ts","sourceRoot":"","sources":["../../src/agent/ai-sdk-transport.ts"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,SAAS,CAAA;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAmB1D,MAAM,MAAM,qBAAqB,GAAG;IAClC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAA;IACnD,oFAAoF;IACpF,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;IACzC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CACzC,CAAA;AAID,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG;IACvF;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CAC/E,CAAA;AAwRD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,KAAK,GAAG,SAAS,EAC9C,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,KAAK,CAAC,CA6CvB"}