@assistant-ui/core 0.2.18 → 0.2.20

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 (231) hide show
  1. package/dist/index.d.ts +4 -2
  2. package/dist/index.js +3 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/internal.d.ts +2 -1
  5. package/dist/internal.js +2 -1
  6. package/dist/model-context/interactable-composer-metadata.d.ts +105 -0
  7. package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -0
  8. package/dist/model-context/interactable-composer-metadata.js +259 -0
  9. package/dist/model-context/interactable-composer-metadata.js.map +1 -0
  10. package/dist/model-context/registry.d.ts.map +1 -1
  11. package/dist/model-context/registry.js +1 -0
  12. package/dist/model-context/registry.js.map +1 -1
  13. package/dist/model-context/types.d.ts +6 -0
  14. package/dist/model-context/types.d.ts.map +1 -1
  15. package/dist/model-context/types.js +4 -0
  16. package/dist/model-context/types.js.map +1 -1
  17. package/dist/react/AssistantProvider.d.ts +1 -1
  18. package/dist/react/AssistantProvider.js +1 -1
  19. package/dist/react/AssistantRuntimeProvider.d.ts +1 -1
  20. package/dist/react/RuntimeAdapter.js +1 -1
  21. package/dist/react/client/Interactables.d.ts +8 -2
  22. package/dist/react/client/Interactables.d.ts.map +1 -1
  23. package/dist/react/client/Interactables.js +190 -94
  24. package/dist/react/client/Interactables.js.map +1 -1
  25. package/dist/react/client/Tools.js +1 -1
  26. package/dist/react/client/interactable-model-context.d.ts +9 -6
  27. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  28. package/dist/react/client/interactable-model-context.js +154 -32
  29. package/dist/react/client/interactable-model-context.js.map +1 -1
  30. package/dist/react/index.d.ts +15 -6
  31. package/dist/react/index.js +12 -5
  32. package/dist/react/interactables-legacy/Interactables.d.ts +12 -0
  33. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -0
  34. package/dist/react/interactables-legacy/Interactables.js +264 -0
  35. package/dist/react/interactables-legacy/Interactables.js.map +1 -0
  36. package/dist/react/interactables-legacy/interactable-model-context.d.ts +12 -0
  37. package/dist/react/interactables-legacy/interactable-model-context.d.ts.map +1 -0
  38. package/dist/react/interactables-legacy/interactable-model-context.js +54 -0
  39. package/dist/react/interactables-legacy/interactable-model-context.js.map +1 -0
  40. package/dist/react/interactables-legacy/scopes.d.ts +58 -0
  41. package/dist/react/interactables-legacy/scopes.d.ts.map +1 -0
  42. package/dist/react/interactables-legacy/scopes.js +0 -0
  43. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts +31 -0
  44. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -0
  45. package/dist/react/{model-context → interactables-legacy}/useAssistantInteractable.js +6 -2
  46. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -0
  47. package/dist/react/interactables-legacy/useInteractableState.d.ts +21 -0
  48. package/dist/react/interactables-legacy/useInteractableState.d.ts.map +1 -0
  49. package/dist/react/interactables-legacy/useInteractableState.js +84 -0
  50. package/dist/react/interactables-legacy/useInteractableState.js.map +1 -0
  51. package/dist/react/model-context/interactableTool.d.ts +53 -0
  52. package/dist/react/model-context/interactableTool.d.ts.map +1 -0
  53. package/dist/react/model-context/interactableTool.js +188 -0
  54. package/dist/react/model-context/interactableTool.js.map +1 -0
  55. package/dist/react/model-context/useAssistantContext.js +1 -1
  56. package/dist/react/model-context/useAssistantDataUI.js +1 -1
  57. package/dist/react/model-context/useAssistantInstructions.js +1 -1
  58. package/dist/react/model-context/useAssistantTool.js +1 -1
  59. package/dist/react/model-context/useAssistantToolUI.js +1 -1
  60. package/dist/react/model-context/useAuiToolOverrides.js +1 -1
  61. package/dist/react/model-context/useInteractable.d.ts +73 -0
  62. package/dist/react/model-context/useInteractable.d.ts.map +1 -0
  63. package/dist/react/model-context/useInteractable.js +100 -0
  64. package/dist/react/model-context/useInteractable.js.map +1 -0
  65. package/dist/react/model-context/useInteractableState.d.ts +8 -5
  66. package/dist/react/model-context/useInteractableState.d.ts.map +1 -1
  67. package/dist/react/model-context/useInteractableState.js +48 -48
  68. package/dist/react/model-context/useInteractableState.js.map +1 -1
  69. package/dist/react/model-context/useInteractableVersions.d.ts +19 -0
  70. package/dist/react/model-context/useInteractableVersions.d.ts.map +1 -0
  71. package/dist/react/model-context/useInteractableVersions.js +51 -0
  72. package/dist/react/model-context/useInteractableVersions.js.map +1 -0
  73. package/dist/react/primitive-hooks/useThreadMessageIds.d.ts +15 -0
  74. package/dist/react/primitive-hooks/useThreadMessageIds.d.ts.map +1 -0
  75. package/dist/react/primitive-hooks/useThreadMessageIds.js +26 -0
  76. package/dist/react/primitive-hooks/useThreadMessageIds.js.map +1 -0
  77. package/dist/react/primitive-hooks/useVoice.js +1 -1
  78. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
  79. package/dist/react/primitives/composer/ComposerAttachments.js +1 -1
  80. package/dist/react/primitives/composer/ComposerQueue.js +1 -1
  81. package/dist/react/primitives/generativeUI/GenerativeUI.d.ts.map +1 -1
  82. package/dist/react/primitives/generativeUI/GenerativeUI.js +2 -2
  83. package/dist/react/primitives/generativeUI/GenerativeUI.js.map +1 -1
  84. package/dist/react/primitives/message/MessageAttachments.js +1 -1
  85. package/dist/react/primitives/message/MessageGroupedParts.js +1 -1
  86. package/dist/react/primitives/message/MessageParts.js +1 -1
  87. package/dist/react/primitives/message/MessageQuote.js +1 -1
  88. package/dist/react/primitives/part/PartMessages.d.ts +5 -2
  89. package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
  90. package/dist/react/primitives/part/PartMessages.js +8 -8
  91. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  92. package/dist/react/primitives/thread/ThreadMessages.d.ts +31 -1
  93. package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
  94. package/dist/react/primitives/thread/ThreadMessages.js +64 -2
  95. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  96. package/dist/react/primitives/thread/ThreadSuggestions.js +1 -1
  97. package/dist/react/primitives/threadList/ThreadListItems.js +1 -1
  98. package/dist/react/providers/MessageByIdProvider.d.ts +9 -0
  99. package/dist/react/providers/MessageByIdProvider.d.ts.map +1 -0
  100. package/dist/react/providers/MessageByIdProvider.js +57 -0
  101. package/dist/react/providers/MessageByIdProvider.js.map +1 -0
  102. package/dist/react/providers/ReadonlyThreadProvider.js +1 -1
  103. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -1
  104. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +3 -0
  105. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  106. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +16 -1
  107. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  108. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +1 -1
  109. package/dist/react/runtimes/cloud/auiV0.js +1 -1
  110. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +1 -1
  111. package/dist/react/runtimes/createRuntimeExtras.d.ts +20 -0
  112. package/dist/react/runtimes/createRuntimeExtras.d.ts.map +1 -0
  113. package/dist/react/runtimes/createRuntimeExtras.js +43 -0
  114. package/dist/react/runtimes/createRuntimeExtras.js.map +1 -0
  115. package/dist/react/runtimes/useLocalRuntime.js +1 -1
  116. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  117. package/dist/react/runtimes/useRemoteThreadListRuntime.js +6 -2
  118. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  119. package/dist/react/runtimes/useStreamingTiming.d.ts +26 -0
  120. package/dist/react/runtimes/useStreamingTiming.d.ts.map +1 -0
  121. package/dist/react/runtimes/useStreamingTiming.js +43 -0
  122. package/dist/react/runtimes/useStreamingTiming.js.map +1 -0
  123. package/dist/react/types/scopes/interactables.d.ts +40 -24
  124. package/dist/react/types/scopes/interactables.d.ts.map +1 -1
  125. package/dist/react/types/store-augmentation.d.ts +3 -1
  126. package/dist/react/types/store-augmentation.d.ts.map +1 -1
  127. package/dist/react/utils/useJSONEqual.d.ts +11 -0
  128. package/dist/react/utils/useJSONEqual.d.ts.map +1 -0
  129. package/dist/react/utils/useJSONEqual.js +30 -0
  130. package/dist/react/utils/useJSONEqual.js.map +1 -0
  131. package/dist/runtime/base/base-composer-runtime-core.d.ts +5 -0
  132. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  133. package/dist/runtime/base/base-composer-runtime-core.js +13 -0
  134. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  135. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  136. package/dist/runtime/base/default-edit-composer-runtime-core.js +6 -1
  137. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  138. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  139. package/dist/runtime/base/default-thread-composer-runtime-core.js +4 -1
  140. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  141. package/dist/runtime/utils/message-repository.d.ts +1 -0
  142. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  143. package/dist/runtime/utils/message-repository.js +6 -3
  144. package/dist/runtime/utils/message-repository.js.map +1 -1
  145. package/dist/runtime/utils/streaming-timing.d.ts +59 -0
  146. package/dist/runtime/utils/streaming-timing.d.ts.map +1 -0
  147. package/dist/runtime/utils/streaming-timing.js +76 -0
  148. package/dist/runtime/utils/streaming-timing.js.map +1 -0
  149. package/dist/runtime/utils/thread-message-like.js +2 -1
  150. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  151. package/dist/runtimes/external-store/external-store-adapter.d.ts +26 -1
  152. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  153. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +9 -0
  154. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  155. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +19 -0
  156. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  157. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  158. package/dist/runtimes/local/local-thread-runtime-core.js +1 -1
  159. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  160. package/dist/runtimes/remote-thread-list/types.d.ts +12 -0
  161. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  162. package/dist/store/clients/suggestions.js +1 -1
  163. package/dist/store/clients/thread-message-client.js +1 -1
  164. package/dist/store/runtime-clients/composer-runtime-client.js +1 -1
  165. package/dist/store/runtime-clients/message-runtime-client.js +1 -1
  166. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
  167. package/dist/store/runtime-clients/thread-runtime-client.js +1 -1
  168. package/dist/tests/remote-thread-list-test-helpers.d.ts +1 -1
  169. package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
  170. package/dist/tests/remote-thread-list-test-helpers.js +3 -2
  171. package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
  172. package/package.json +9 -9
  173. package/src/index.ts +17 -0
  174. package/src/internal.ts +7 -0
  175. package/src/model-context/interactable-composer-metadata.test.ts +550 -0
  176. package/src/model-context/interactable-composer-metadata.ts +395 -0
  177. package/src/model-context/registry.ts +5 -0
  178. package/src/model-context/types.ts +12 -0
  179. package/src/react/client/Interactables.test.ts +358 -0
  180. package/src/react/client/Interactables.ts +339 -86
  181. package/src/react/client/interactable-model-context.test.ts +418 -0
  182. package/src/react/client/interactable-model-context.ts +252 -58
  183. package/src/react/index.ts +72 -3
  184. package/src/react/interactables-legacy/Interactables.ts +332 -0
  185. package/src/react/interactables-legacy/interactable-model-context.test.ts +54 -0
  186. package/src/react/interactables-legacy/interactable-model-context.ts +80 -0
  187. package/src/react/interactables-legacy/scopes.ts +66 -0
  188. package/src/react/{model-context → interactables-legacy}/useAssistantInteractable.ts +10 -1
  189. package/src/react/interactables-legacy/useInteractableState.ts +67 -0
  190. package/src/react/model-context/interactableTool.tsx +175 -0
  191. package/src/react/model-context/useInteractable.ts +233 -0
  192. package/src/react/model-context/useInteractableState.ts +31 -23
  193. package/src/react/model-context/useInteractableVersions.ts +48 -0
  194. package/src/react/primitive-hooks/useThreadMessageIds.ts +27 -0
  195. package/src/react/primitives/generativeUI/GenerativeUI.tsx +5 -1
  196. package/src/react/primitives/part/PartMessages.test.tsx +17 -0
  197. package/src/react/primitives/part/PartMessages.tsx +13 -8
  198. package/src/react/primitives/thread/ThreadMessages.tsx +67 -0
  199. package/src/react/providers/MessageByIdProvider.tsx +23 -0
  200. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +21 -1
  201. package/src/react/runtimes/createRuntimeExtras.test.ts +58 -0
  202. package/src/react/runtimes/createRuntimeExtras.ts +68 -0
  203. package/src/react/runtimes/createRuntimeExtras.use.test.ts +50 -0
  204. package/src/react/runtimes/useRemoteThreadListRuntime.ts +13 -1
  205. package/src/react/runtimes/useStreamingTiming.ts +68 -0
  206. package/src/react/types/scopes/interactables.ts +45 -23
  207. package/src/react/types/store-augmentation.ts +4 -2
  208. package/src/react/utils/useJSONEqual.ts +20 -0
  209. package/src/runtime/base/base-composer-runtime-core.ts +13 -0
  210. package/src/runtime/base/default-edit-composer-runtime-core.ts +18 -1
  211. package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
  212. package/src/runtime/utils/message-repository.ts +11 -7
  213. package/src/runtime/utils/streaming-timing.test.ts +301 -0
  214. package/src/runtime/utils/streaming-timing.ts +161 -0
  215. package/src/runtime/utils/thread-message-like.ts +2 -1
  216. package/src/runtimes/external-store/external-store-adapter.ts +28 -0
  217. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +33 -1
  218. package/src/runtimes/local/local-thread-runtime-core.ts +4 -1
  219. package/src/runtimes/remote-thread-list/types.ts +13 -0
  220. package/src/tests/MessageRepository.test.ts +2 -0
  221. package/src/tests/composer-can-send.test.ts +1 -0
  222. package/src/tests/default-edit-composer-runtime-core.test.ts +78 -1
  223. package/src/tests/default-thread-composer-runtime-core.test.ts +82 -0
  224. package/src/tests/external-store-thread-runtime-core.test.ts +337 -0
  225. package/src/tests/groupParts.test.ts +32 -0
  226. package/src/tests/remote-thread-list-reactive-threadId.test.ts +90 -0
  227. package/src/tests/remote-thread-list-test-helpers.ts +2 -1
  228. package/src/tests/thread-message-like.test.ts +50 -0
  229. package/dist/react/model-context/useAssistantInteractable.d.ts +0 -22
  230. package/dist/react/model-context/useAssistantInteractable.d.ts.map +0 -1
  231. package/dist/react/model-context/useAssistantInteractable.js.map +0 -1
@@ -0,0 +1,332 @@
1
+ import { useState, useEffect, useCallback, useRef, useMemo } from "react";
2
+ import { resource } from "@assistant-ui/tap";
3
+ import {
4
+ useAssistantClientRef,
5
+ type ClientOutput,
6
+ attachTransformScopes,
7
+ } from "@assistant-ui/store";
8
+ import type {
9
+ InteractablesState,
10
+ InteractableRegistration,
11
+ InteractableStateSchema,
12
+ InteractablePersistedState,
13
+ InteractablePersistenceAdapter,
14
+ } from "./scopes";
15
+ import { toJSONSchema, toPartialJSONSchema } from "assistant-stream";
16
+ import { ModelContext } from "../../store";
17
+ import { buildInteractableModelContext } from "./interactable-model-context";
18
+
19
+ const PERSISTENCE_DEBOUNCE_MS = 500;
20
+
21
+ const useInteractables = (): ClientOutput<"interactables"> => {
22
+ const [state, setState] = useState<InteractablesState>(() => ({
23
+ definitions: {},
24
+ persistence: {},
25
+ }));
26
+
27
+ const clientRef = useAssistantClientRef();
28
+
29
+ const stateRef = useRef(state);
30
+ useEffect(() => {
31
+ stateRef.current = state;
32
+ }, [state]);
33
+
34
+ const subscribersRef = useRef(new Set<() => void>());
35
+ const partialSchemaCacheRef = useRef(
36
+ new Map<string, InteractableStateSchema>(),
37
+ );
38
+ const detachedStateRef = useRef(new Map<string, unknown>());
39
+
40
+ const adapterRef = useRef<InteractablePersistenceAdapter | undefined>(
41
+ undefined,
42
+ );
43
+ const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
44
+ undefined,
45
+ );
46
+ const syncSeqRef = useRef(0);
47
+ const hasPendingLocalChangeRef = useRef(false);
48
+ const flushResolversRef = useRef<Array<() => void>>([]);
49
+ const dirtyIdsRef = useRef(new Set<string>());
50
+
51
+ const runPersistence = useCallback(async () => {
52
+ const adapter = adapterRef.current;
53
+ if (!adapter) {
54
+ for (const resolve of flushResolversRef.current) resolve();
55
+ flushResolversRef.current = [];
56
+ return;
57
+ }
58
+
59
+ const seq = ++syncSeqRef.current;
60
+ const dirtyIds = new Set(dirtyIdsRef.current);
61
+ dirtyIdsRef.current.clear();
62
+ hasPendingLocalChangeRef.current = true;
63
+
64
+ // Snapshot before any await so unregistered definitions are still included.
65
+ const exported = stateRef.current.definitions;
66
+ const payload: InteractablePersistedState = {};
67
+ for (const [id, def] of Object.entries(exported)) {
68
+ payload[id] = { name: def.name, state: def.state };
69
+ }
70
+
71
+ setState((prev) => ({
72
+ ...prev,
73
+ persistence: {
74
+ ...prev.persistence,
75
+ ...Object.fromEntries(
76
+ [...dirtyIds].map((id) => [
77
+ id,
78
+ { isPending: true, error: undefined },
79
+ ]),
80
+ ),
81
+ },
82
+ }));
83
+
84
+ try {
85
+ await adapter.save(payload);
86
+ if (syncSeqRef.current === seq) {
87
+ hasPendingLocalChangeRef.current = false;
88
+ setState((prev) => {
89
+ const persistence = { ...prev.persistence };
90
+ for (const id of dirtyIds) delete persistence[id];
91
+ return { ...prev, persistence };
92
+ });
93
+ }
94
+ } catch (e) {
95
+ if (syncSeqRef.current === seq) {
96
+ hasPendingLocalChangeRef.current = false;
97
+ setState((prev) => ({
98
+ ...prev,
99
+ persistence: {
100
+ ...prev.persistence,
101
+ ...Object.fromEntries(
102
+ [...dirtyIds].map((id) => [id, { isPending: false, error: e }]),
103
+ ),
104
+ },
105
+ }));
106
+ }
107
+ } finally {
108
+ if (dirtyIdsRef.current.size > 0 && adapterRef.current) {
109
+ runPersistence();
110
+ } else {
111
+ for (const resolve of flushResolversRef.current) resolve();
112
+ flushResolversRef.current = [];
113
+ }
114
+ }
115
+ }, []);
116
+
117
+ const schedulePersistence = useCallback(
118
+ (id: string) => {
119
+ if (!adapterRef.current) return;
120
+ dirtyIdsRef.current.add(id);
121
+ if (debounceTimerRef.current !== undefined) {
122
+ clearTimeout(debounceTimerRef.current);
123
+ }
124
+ debounceTimerRef.current = setTimeout(() => {
125
+ debounceTimerRef.current = undefined;
126
+ if (!hasPendingLocalChangeRef.current) {
127
+ runPersistence();
128
+ } else {
129
+ debounceTimerRef.current = setTimeout(() => {
130
+ debounceTimerRef.current = undefined;
131
+ runPersistence();
132
+ }, PERSISTENCE_DEBOUNCE_MS);
133
+ }
134
+ }, PERSISTENCE_DEBOUNCE_MS);
135
+ },
136
+ [runPersistence],
137
+ );
138
+
139
+ const exportState = useCallback((): InteractablePersistedState => {
140
+ const result: InteractablePersistedState = {};
141
+ for (const [id, def] of Object.entries(stateRef.current.definitions)) {
142
+ result[id] = { name: def.name, state: def.state };
143
+ }
144
+ return result;
145
+ }, []);
146
+
147
+ const importState = useCallback((saved: InteractablePersistedState) => {
148
+ for (const [id, entry] of Object.entries(saved)) {
149
+ detachedStateRef.current.set(id, entry.state);
150
+ }
151
+ setState((prev) => {
152
+ let changed = false;
153
+ const definitions = { ...prev.definitions };
154
+ for (const [id, entry] of Object.entries(saved)) {
155
+ if (definitions[id]) {
156
+ definitions[id] = { ...definitions[id], state: entry.state };
157
+ changed = true;
158
+ }
159
+ }
160
+ return changed ? { ...prev, definitions } : prev;
161
+ });
162
+ }, []);
163
+
164
+ const setPersistenceAdapter = useCallback(
165
+ (adapter: InteractablePersistenceAdapter | undefined) => {
166
+ adapterRef.current = adapter;
167
+ },
168
+ [],
169
+ );
170
+
171
+ const flush = useCallback(async () => {
172
+ if (debounceTimerRef.current !== undefined) {
173
+ clearTimeout(debounceTimerRef.current);
174
+ debounceTimerRef.current = undefined;
175
+ }
176
+ if (!adapterRef.current) return;
177
+ if (!hasPendingLocalChangeRef.current && dirtyIdsRef.current.size === 0)
178
+ return;
179
+ const p = new Promise<void>((resolve) => {
180
+ flushResolversRef.current.push(resolve);
181
+ });
182
+ if (!hasPendingLocalChangeRef.current) {
183
+ runPersistence();
184
+ }
185
+ return p;
186
+ }, [runPersistence]);
187
+
188
+ const flushIfPending = useCallback(() => {
189
+ if (adapterRef.current && debounceTimerRef.current !== undefined) {
190
+ clearTimeout(debounceTimerRef.current);
191
+ debounceTimerRef.current = undefined;
192
+ runPersistence();
193
+ }
194
+ }, [runPersistence]);
195
+
196
+ const setDefState = useCallback(
197
+ (id: string, updater: (prev: unknown) => unknown) => {
198
+ setState((prev) => {
199
+ const existing = prev.definitions[id];
200
+ if (!existing) return prev;
201
+ return {
202
+ ...prev,
203
+ definitions: {
204
+ ...prev.definitions,
205
+ [id]: { ...existing, state: updater(existing.state) },
206
+ },
207
+ };
208
+ });
209
+ if (stateRef.current.definitions[id]) schedulePersistence(id);
210
+ },
211
+ [schedulePersistence],
212
+ );
213
+
214
+ const setDefSelected = useCallback((id: string, selected: boolean) => {
215
+ setState((prev) => {
216
+ const existing = prev.definitions[id];
217
+ if (!existing) return prev;
218
+ return {
219
+ ...prev,
220
+ definitions: {
221
+ ...prev.definitions,
222
+ [id]: { ...existing, selected },
223
+ },
224
+ };
225
+ });
226
+ }, []);
227
+
228
+ const provider = useMemo(
229
+ () => ({
230
+ getModelContext: () => {
231
+ const defs = stateRef.current.definitions;
232
+ return (
233
+ buildInteractableModelContext(
234
+ defs,
235
+ partialSchemaCacheRef.current,
236
+ setDefState,
237
+ ) ?? {}
238
+ );
239
+ },
240
+ subscribe: (callback: () => void) => {
241
+ subscribersRef.current.add(callback);
242
+ return () => {
243
+ subscribersRef.current.delete(callback);
244
+ };
245
+ },
246
+ }),
247
+ [setDefState],
248
+ );
249
+
250
+ useEffect(() => {
251
+ for (const cb of subscribersRef.current) cb();
252
+ }, [state]);
253
+
254
+ useEffect(() => {
255
+ return clientRef.current!.modelContext().register(provider);
256
+ }, [clientRef, provider]);
257
+
258
+ const register = useCallback(
259
+ (def: InteractableRegistration) => {
260
+ try {
261
+ const jsonSchema = toJSONSchema(def.stateSchema);
262
+ partialSchemaCacheRef.current.set(
263
+ def.id,
264
+ toPartialJSONSchema(jsonSchema),
265
+ );
266
+ } catch (e) {
267
+ console.warn(
268
+ `[Interactables] Failed to create partial schema for "${def.name}". The update tool will require all fields.`,
269
+ e,
270
+ );
271
+ }
272
+
273
+ const detached = detachedStateRef.current.get(def.id);
274
+ detachedStateRef.current.delete(def.id);
275
+
276
+ setState((prev) => ({
277
+ ...prev,
278
+ definitions: {
279
+ ...prev.definitions,
280
+ [def.id]: {
281
+ id: def.id,
282
+ name: def.name,
283
+ description: def.description,
284
+ stateSchema: def.stateSchema,
285
+ state:
286
+ prev.definitions[def.id]?.state ?? detached ?? def.initialState,
287
+ selected: def.selected,
288
+ },
289
+ },
290
+ }));
291
+
292
+ return () => {
293
+ flushIfPending();
294
+ setState((prev) => {
295
+ const existing = prev.definitions[def.id];
296
+ if (existing) {
297
+ detachedStateRef.current.set(def.id, existing.state);
298
+ }
299
+ partialSchemaCacheRef.current.delete(def.id);
300
+ const { [def.id]: _, ...rest } = prev.definitions;
301
+ const { [def.id]: __, ...restPersistence } = prev.persistence;
302
+ return { ...prev, definitions: rest, persistence: restPersistence };
303
+ });
304
+ };
305
+ },
306
+ [flushIfPending],
307
+ );
308
+
309
+ return {
310
+ getState: () => state,
311
+ register,
312
+ setState: setDefState,
313
+ setSelected: setDefSelected,
314
+ exportState,
315
+ importState,
316
+ setPersistenceAdapter,
317
+ flush,
318
+ };
319
+ };
320
+
321
+ /**
322
+ * @deprecated Since 2026-06-14 — migrate to the Unstable / Experimental API.
323
+ * Scheduled for removal on/after 2026-09-14. See
324
+ * {@link https://www.assistant-ui.com/docs/tools/interactables#migrating-from-the-previous-api | Interactables migration guide}.
325
+ */
326
+ export const Interactables = resource(useInteractables);
327
+
328
+ attachTransformScopes(useInteractables, (scopes, parent) => {
329
+ if (!scopes.modelContext && parent.modelContext.source === null) {
330
+ scopes.modelContext = ModelContext();
331
+ }
332
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { buildInteractableModelContext } from "./interactable-model-context";
3
+ import type { InteractableDefinition } from "./scopes";
4
+
5
+ const def = (
6
+ id: string,
7
+ name: string,
8
+ state: unknown,
9
+ selected = false,
10
+ ): InteractableDefinition => ({
11
+ id,
12
+ name,
13
+ description: `desc of ${name}`,
14
+ stateSchema: { type: "object" as const, properties: {} },
15
+ state,
16
+ selected,
17
+ });
18
+
19
+ describe("legacy buildInteractableModelContext", () => {
20
+ it("preserves per-instance update tools and selected system context", async () => {
21
+ const definitions = {
22
+ "note-1": def("note-1", "note", { title: "one" }, true),
23
+ "note-2": def("note-2", "note", { title: "two" }),
24
+ };
25
+ const setDefState = vi.fn(
26
+ (id: string, updater: (prev: unknown) => unknown) => {
27
+ const current = definitions[id as keyof typeof definitions];
28
+ if (current) current.state = updater(current.state);
29
+ },
30
+ );
31
+
32
+ const ctx = buildInteractableModelContext(
33
+ definitions,
34
+ new Map(),
35
+ setDefState,
36
+ );
37
+
38
+ expect(Object.keys(ctx!.tools).sort()).toEqual([
39
+ "update_note_note-1",
40
+ "update_note_note-2",
41
+ ]);
42
+ expect(ctx!.system).toContain(
43
+ 'Interactable component "note" [id="note-1"] (SELECTED)',
44
+ );
45
+
46
+ await ctx!.tools["update_note_note-2"]!.execute!(
47
+ { title: "updated" },
48
+ {} as never,
49
+ );
50
+
51
+ expect(definitions["note-1"].state).toEqual({ title: "one" });
52
+ expect(definitions["note-2"].state).toEqual({ title: "updated" });
53
+ });
54
+ });
@@ -0,0 +1,80 @@
1
+ import type { Tool } from "assistant-stream";
2
+ import type { InteractableDefinition, InteractableStateSchema } from "./scopes";
3
+
4
+ export function shallowMerge(prev: unknown, partial: unknown): unknown {
5
+ if (
6
+ typeof prev !== "object" ||
7
+ prev === null ||
8
+ typeof partial !== "object" ||
9
+ partial === null ||
10
+ Array.isArray(prev) ||
11
+ Array.isArray(partial)
12
+ ) {
13
+ return partial;
14
+ }
15
+ return {
16
+ ...(prev as Record<string, unknown>),
17
+ ...(partial as Record<string, unknown>),
18
+ };
19
+ }
20
+
21
+ export function buildInteractableModelContext(
22
+ definitions: Record<string, InteractableDefinition>,
23
+ partialSchemaCache: Map<string, InteractableStateSchema>,
24
+ setDefState: (id: string, updater: (prev: unknown) => unknown) => void,
25
+ ): { system: string; tools: Record<string, Tool<any, any>> } | undefined {
26
+ const entries = Object.values(definitions);
27
+ if (entries.length === 0) return undefined;
28
+
29
+ const byName = new Map<string, InteractableDefinition[]>();
30
+ for (const def of entries) {
31
+ const list = byName.get(def.name) ?? [];
32
+ list.push(def);
33
+ byName.set(def.name, list);
34
+ }
35
+
36
+ const systemParts: string[] = [];
37
+ const tools: Record<string, Tool<any, any>> = {};
38
+
39
+ for (const [name, instances] of byName) {
40
+ const isMulti = instances.length > 1;
41
+
42
+ for (const def of instances) {
43
+ const selectedTag = def.selected ? " (SELECTED)" : "";
44
+ const idTag = isMulti ? ` [id="${def.id}"]` : "";
45
+
46
+ systemParts.push(
47
+ `Interactable component "${name}"${idTag}${selectedTag} (${def.description}). Current state: ${JSON.stringify(def.state)}`,
48
+ );
49
+
50
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
51
+ const safeId = def.id.replace(/[^a-zA-Z0-9_-]/g, "_");
52
+ const toolName = isMulti
53
+ ? `update_${safeName}_${safeId}`
54
+ : `update_${safeName}`;
55
+
56
+ const partialSchema = partialSchemaCache.get(def.id) ?? def.stateSchema;
57
+
58
+ tools[toolName] = {
59
+ type: "frontend" as const,
60
+ description: `Update the state of interactable component "${name}"${isMulti ? ` (id: ${def.id})` : ""}. Only include the fields you want to change; omitted fields keep their current values. ${def.description}`,
61
+ parameters: partialSchema,
62
+ streamCall: async (reader) => {
63
+ try {
64
+ for await (const partialArgs of reader.args.streamValues()) {
65
+ setDefState(def.id, (prev) => shallowMerge(prev, partialArgs));
66
+ }
67
+ } catch {
68
+ // Non-fatal: execute handles the final state
69
+ }
70
+ },
71
+ execute: async (partialState: unknown) => {
72
+ setDefState(def.id, (prev) => shallowMerge(prev, partialState));
73
+ return { success: true };
74
+ },
75
+ };
76
+ }
77
+ }
78
+
79
+ return { system: systemParts.join("\n"), tools };
80
+ }
@@ -0,0 +1,66 @@
1
+ import type { Tool } from "assistant-stream";
2
+ import type { Unsubscribe } from "../..";
3
+
4
+ /**
5
+ * Schema type matching Tool["parameters"] from assistant-stream.
6
+ * Accepts both StandardSchemaV1 and JSONSchema7.
7
+ */
8
+ export type InteractableStateSchema = NonNullable<
9
+ Extract<Tool, { parameters: unknown }>["parameters"]
10
+ >;
11
+
12
+ export type InteractableDefinition = {
13
+ id: string;
14
+ name: string;
15
+ description: string;
16
+ stateSchema: InteractableStateSchema;
17
+ state: unknown;
18
+ selected?: boolean | undefined;
19
+ };
20
+
21
+ export type InteractableRegistration = {
22
+ id: string;
23
+ name: string;
24
+ description: string;
25
+ stateSchema: InteractableStateSchema;
26
+ initialState: unknown;
27
+ selected?: boolean | undefined;
28
+ };
29
+
30
+ export type InteractablePersistenceStatus = {
31
+ isPending: boolean;
32
+ error: unknown;
33
+ };
34
+
35
+ export type InteractablesState = {
36
+ /** Keyed by instance id */
37
+ definitions: Record<string, InteractableDefinition>;
38
+ /** Per-id persistence sync status */
39
+ persistence: Record<string, InteractablePersistenceStatus>;
40
+ };
41
+
42
+ export type InteractablePersistedState = Record<
43
+ string,
44
+ { name: string; state: unknown }
45
+ >;
46
+
47
+ export type InteractablePersistenceAdapter = {
48
+ save(state: InteractablePersistedState): void | Promise<void>;
49
+ };
50
+
51
+ export type InteractablesMethods = {
52
+ getState(): InteractablesState;
53
+ register(def: InteractableRegistration): Unsubscribe;
54
+ setState(id: string, updater: (prev: unknown) => unknown): void;
55
+ setSelected(id: string, selected: boolean): void;
56
+ exportState(): InteractablePersistedState;
57
+ importState(saved: InteractablePersistedState): void;
58
+ setPersistenceAdapter(
59
+ adapter: InteractablePersistenceAdapter | undefined,
60
+ ): void;
61
+ flush(): Promise<void>;
62
+ };
63
+
64
+ export type InteractablesClientSchema = {
65
+ methods: InteractablesMethods;
66
+ };
@@ -1,7 +1,12 @@
1
1
  import { useEffect, useId, useRef } from "react";
2
2
  import { useAui } from "@assistant-ui/store";
3
- import type { InteractableStateSchema } from "../types/scopes/interactables";
3
+ import type { InteractableStateSchema } from "./scopes";
4
4
 
5
+ /**
6
+ * @deprecated Since 2026-06-14 — migrate to the Unstable / Experimental API.
7
+ * Scheduled for removal on/after 2026-09-14. See
8
+ * {@link https://www.assistant-ui.com/docs/tools/interactables#migrating-from-the-previous-api | Interactables migration guide}.
9
+ */
5
10
  export type AssistantInteractableProps = {
6
11
  description: string;
7
12
  stateSchema: InteractableStateSchema;
@@ -11,6 +16,10 @@ export type AssistantInteractableProps = {
11
16
  };
12
17
 
13
18
  /**
19
+ * @deprecated Since 2026-06-14 — migrate to the Unstable / Experimental API.
20
+ * Scheduled for removal on/after 2026-09-14. See
21
+ * {@link https://www.assistant-ui.com/docs/tools/interactables#migrating-from-the-previous-api | Interactables migration guide}.
22
+ *
14
23
  * Registers an interactable with the AI assistant.
15
24
  *
16
25
  * This hook handles registration only. To read and write the interactable's
@@ -0,0 +1,67 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ type StateUpdater<TState> = TState | ((prev: TState) => TState);
5
+
6
+ /**
7
+ * Reads and writes the state of a registered interactable.
8
+ *
9
+ * Pair with {@link useAssistantInteractable} which handles registration.
10
+ *
11
+ * @deprecated Since 2026-06-14 — migrate to the Unstable / Experimental API.
12
+ * Scheduled for removal on/after 2026-09-14. See
13
+ * {@link https://www.assistant-ui.com/docs/tools/interactables#migrating-from-the-previous-api | Interactables migration guide}.
14
+ */
15
+ export const useInteractableState = <TState>(
16
+ id: string,
17
+ fallback: TState,
18
+ ): [
19
+ TState,
20
+ {
21
+ setState: (updater: StateUpdater<TState>) => void;
22
+ setSelected: (selected: boolean) => void;
23
+ isPending: boolean;
24
+ error: unknown;
25
+ flush: () => Promise<void>;
26
+ },
27
+ ] => {
28
+ const aui = useAui();
29
+
30
+ const state =
31
+ (useAuiState((s) => s.interactables.definitions[id]?.state) as TState) ??
32
+ (fallback as TState);
33
+
34
+ const persistenceStatus = useAuiState((s) => s.interactables.persistence[id]);
35
+
36
+ const setState = useCallback(
37
+ (updater: StateUpdater<TState>) => {
38
+ aui.interactables().setState(id, (prev) => {
39
+ if (typeof updater === "function") {
40
+ return (updater as (prev: TState) => TState)(prev as TState);
41
+ }
42
+ return updater;
43
+ });
44
+ },
45
+ [aui, id],
46
+ );
47
+
48
+ const setSelected = useCallback(
49
+ (selected: boolean) => {
50
+ aui.interactables().setSelected(id, selected);
51
+ },
52
+ [aui, id],
53
+ );
54
+
55
+ const flush = useCallback(() => aui.interactables().flush(), [aui]);
56
+
57
+ return [
58
+ state,
59
+ {
60
+ setState,
61
+ setSelected,
62
+ isPending: persistenceStatus?.isPending ?? false,
63
+ error: persistenceStatus?.error,
64
+ flush,
65
+ },
66
+ ];
67
+ };