@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
@@ -6,20 +6,70 @@ import {
6
6
  attachTransformScopes,
7
7
  } from "@assistant-ui/store";
8
8
  import type {
9
- InteractablesState,
10
- InteractableRegistration,
11
- InteractableStateSchema,
12
- InteractablePersistedState,
13
- InteractablePersistenceAdapter,
9
+ Unstable_InteractablesState,
10
+ Unstable_InteractableRegistration,
11
+ Unstable_InteractablePersistedState,
12
+ Unstable_InteractablePersistenceAdapter,
13
+ Unstable_InteractablesConfig,
14
14
  } from "../types/scopes/interactables";
15
15
  import { toJSONSchema, toPartialJSONSchema } from "assistant-stream";
16
16
  import { ModelContext } from "../../store";
17
- import { buildInteractableModelContext } from "./interactable-model-context";
17
+ import {
18
+ buildInteractableModelContext,
19
+ type PartialJSONSchema,
20
+ } from "./interactable-model-context";
21
+ import {
22
+ findModelKnownState,
23
+ interactableToolName,
24
+ } from "../../model-context/interactable-composer-metadata";
18
25
 
19
26
  const PERSISTENCE_DEBOUNCE_MS = 500;
20
27
 
21
- const useInteractables = (): ClientOutput<"interactables"> => {
22
- const [state, setState] = useState<InteractablesState>(() => ({
28
+ type RestorePersistedStateOptions = {
29
+ stash: Map<string, unknown>;
30
+ shouldStash?: (id: string) => boolean;
31
+ shouldApply?: (
32
+ id: string,
33
+ def: Unstable_InteractablesState["definitions"][string],
34
+ ) => boolean;
35
+ };
36
+
37
+ type ToolCallLikePart = {
38
+ type?: string;
39
+ toolCallId?: string;
40
+ toolName?: string;
41
+ };
42
+
43
+ type MessageLike = {
44
+ role?: string;
45
+ content?: readonly unknown[] | undefined;
46
+ };
47
+
48
+ type InternalInteractableRegistration = Unstable_InteractableRegistration & {
49
+ scope?: "thread" | undefined;
50
+ };
51
+
52
+ const hasInteractableCreateCall = (
53
+ messages: readonly MessageLike[],
54
+ id: string,
55
+ name: string,
56
+ ) =>
57
+ messages.some(
58
+ (message) =>
59
+ message.role === "assistant" &&
60
+ message.content?.some((part) => {
61
+ if (!part || typeof part !== "object") return false;
62
+ const p = part as ToolCallLikePart;
63
+ return (
64
+ p.type === "tool-call" && p.toolCallId === id && p.toolName === name
65
+ );
66
+ }),
67
+ );
68
+
69
+ const useInteractablesResource = ({
70
+ persistence,
71
+ }: Unstable_InteractablesConfig = {}): ClientOutput<"unstable_interactables"> => {
72
+ const [state, setState] = useState<Unstable_InteractablesState>(() => ({
23
73
  definitions: {},
24
74
  persistence: {},
25
75
  }));
@@ -27,19 +77,32 @@ const useInteractables = (): ClientOutput<"interactables"> => {
27
77
  const clientRef = useAssistantClientRef();
28
78
 
29
79
  const stateRef = useRef(state);
30
- useEffect(() => {
31
- stateRef.current = state;
32
- }, [state]);
33
80
 
34
81
  const subscribersRef = useRef(new Set<() => void>());
35
- const partialSchemaCacheRef = useRef(
36
- new Map<string, InteractableStateSchema>(),
82
+ const partialSchemaCacheRef = useRef(new Map<string, PartialJSONSchema>());
83
+ const streamBaselinesRef = useRef(
84
+ new Map<string, { targetId: string; state: unknown }>(),
37
85
  );
38
- const detachedStateRef = useRef(new Map<string, unknown>());
39
-
40
- const adapterRef = useRef<InteractablePersistenceAdapter | undefined>(
41
- undefined,
86
+ const detachedAppStateRef = useRef(new Map<string, unknown>());
87
+ const detachedThreadStateRef = useRef(
88
+ new Map<string, Map<string, unknown>>(),
42
89
  );
90
+ // An instance may be registered from several anchors (its creating tool
91
+ // call plus update_* calls); the definition lives until the last one leaves.
92
+ const registrationCountsRef = useRef(new Map<string, number>());
93
+ // One update-tool UI per interactable name, alive while any registrant
94
+ // that supplied an updateRender is mounted.
95
+ const updateToolUIsRef = useRef(
96
+ new Map<string, { count: number; unsubscribe: () => void }>(),
97
+ );
98
+ // App-scoped state restored via adapter.load(), consumed as components register.
99
+ const loadedStateRef = useRef(new Map<string, unknown>());
100
+ // Ids edited locally this session — a local edit always wins over a slow load.
101
+ const touchedIdsRef = useRef(new Set<string>());
102
+
103
+ const adapterRef = useRef<
104
+ Unstable_InteractablePersistenceAdapter | undefined
105
+ >(undefined);
43
106
  const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
44
107
  undefined,
45
108
  );
@@ -48,6 +111,28 @@ const useInteractables = (): ClientOutput<"interactables"> => {
48
111
  const flushResolversRef = useRef<Array<() => void>>([]);
49
112
  const dirtyIdsRef = useRef(new Set<string>());
50
113
 
114
+ const setStateAndRef = useCallback(
115
+ (
116
+ updater: (
117
+ prev: Unstable_InteractablesState,
118
+ ) => Unstable_InteractablesState,
119
+ ) => {
120
+ const next = updater(stateRef.current);
121
+ stateRef.current = next;
122
+ setState(next);
123
+ },
124
+ [],
125
+ );
126
+
127
+ const exportState = useCallback((): Unstable_InteractablePersistedState => {
128
+ const result: Unstable_InteractablePersistedState = {};
129
+ for (const [id, def] of Object.entries(stateRef.current.definitions)) {
130
+ if (def.scope === "thread") continue; // thread items persist via snapshot, not the adapter
131
+ result[id] = { name: def.name, state: def.state };
132
+ }
133
+ return result;
134
+ }, []);
135
+
51
136
  const runPersistence = useCallback(async () => {
52
137
  const adapter = adapterRef.current;
53
138
  if (!adapter) {
@@ -62,13 +147,9 @@ const useInteractables = (): ClientOutput<"interactables"> => {
62
147
  hasPendingLocalChangeRef.current = true;
63
148
 
64
149
  // 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
- }
150
+ const payload = exportState();
70
151
 
71
- setState((prev) => ({
152
+ setStateAndRef((prev) => ({
72
153
  ...prev,
73
154
  persistence: {
74
155
  ...prev.persistence,
@@ -85,7 +166,7 @@ const useInteractables = (): ClientOutput<"interactables"> => {
85
166
  await adapter.save(payload);
86
167
  if (syncSeqRef.current === seq) {
87
168
  hasPendingLocalChangeRef.current = false;
88
- setState((prev) => {
169
+ setStateAndRef((prev) => {
89
170
  const persistence = { ...prev.persistence };
90
171
  for (const id of dirtyIds) delete persistence[id];
91
172
  return { ...prev, persistence };
@@ -94,7 +175,7 @@ const useInteractables = (): ClientOutput<"interactables"> => {
94
175
  } catch (e) {
95
176
  if (syncSeqRef.current === seq) {
96
177
  hasPendingLocalChangeRef.current = false;
97
- setState((prev) => ({
178
+ setStateAndRef((prev) => ({
98
179
  ...prev,
99
180
  persistence: {
100
181
  ...prev.persistence,
@@ -112,7 +193,7 @@ const useInteractables = (): ClientOutput<"interactables"> => {
112
193
  flushResolversRef.current = [];
113
194
  }
114
195
  }
115
- }, []);
196
+ }, [exportState, setStateAndRef]);
116
197
 
117
198
  const schedulePersistence = useCallback(
118
199
  (id: string) => {
@@ -136,38 +217,103 @@ const useInteractables = (): ClientOutput<"interactables"> => {
136
217
  [runPersistence],
137
218
  );
138
219
 
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
- }, []);
220
+ const restorePersistedState = useCallback(
221
+ (
222
+ saved: Unstable_InteractablePersistedState,
223
+ options: RestorePersistedStateOptions,
224
+ ) => {
225
+ const shouldStash = options.shouldStash ?? (() => true);
226
+ const shouldApply = options.shouldApply ?? (() => true);
146
227
 
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
228
  for (const [id, entry] of Object.entries(saved)) {
155
- if (definitions[id]) {
156
- definitions[id] = { ...definitions[id], state: entry.state };
229
+ if (shouldStash(id)) options.stash.set(id, entry.state);
230
+ }
231
+ setStateAndRef((prev) => {
232
+ let changed = false;
233
+ const definitions = { ...prev.definitions };
234
+ for (const [id, entry] of Object.entries(saved)) {
235
+ const def = definitions[id];
236
+ if (!def || !shouldApply(id, def)) continue;
237
+ definitions[id] = { ...def, state: entry.state };
157
238
  changed = true;
158
239
  }
240
+ if (!changed) return prev;
241
+ return { ...prev, definitions };
242
+ });
243
+ },
244
+ [setStateAndRef],
245
+ );
246
+
247
+ const importState = useCallback(
248
+ (saved: Unstable_InteractablePersistedState) => {
249
+ restorePersistedState(saved, { stash: detachedAppStateRef.current });
250
+ },
251
+ [restorePersistedState],
252
+ );
253
+
254
+ // Applies adapter.load() output: a local edit made while the load was in
255
+ // flight wins, and thread-scoped items never restore from the adapter.
256
+ const applyLoadedState = useCallback(
257
+ (saved: Unstable_InteractablePersistedState) => {
258
+ restorePersistedState(saved, {
259
+ stash: loadedStateRef.current,
260
+ shouldStash: (id) => !touchedIdsRef.current.has(id),
261
+ shouldApply: (id, def) =>
262
+ !touchedIdsRef.current.has(id) && def.scope !== "thread",
263
+ });
264
+ },
265
+ [restorePersistedState],
266
+ );
267
+
268
+ const loadFromAdapter = useCallback(
269
+ async (adapter: Unstable_InteractablePersistenceAdapter) => {
270
+ if (!adapter.load) return;
271
+ try {
272
+ const saved = await adapter.load();
273
+ if (!saved || adapterRef.current !== adapter) return;
274
+ applyLoadedState(saved);
275
+ } catch (e) {
276
+ console.warn("[Interactables] Persistence load failed.", e);
159
277
  }
160
- return changed ? { ...prev, definitions } : prev;
161
- });
162
- }, []);
278
+ },
279
+ [applyLoadedState],
280
+ );
163
281
 
164
282
  const setPersistenceAdapter = useCallback(
165
- (adapter: InteractablePersistenceAdapter | undefined) => {
283
+ (adapter: Unstable_InteractablePersistenceAdapter | undefined) => {
166
284
  adapterRef.current = adapter;
285
+ if (adapter) void loadFromAdapter(adapter);
167
286
  },
168
- [],
287
+ [loadFromAdapter],
169
288
  );
170
289
 
290
+ const getCurrentThreadId = useCallback((): string | undefined => {
291
+ const client = clientRef.current;
292
+ if (!client) return undefined;
293
+
294
+ const threadListItem = client.threadListItem;
295
+ if (threadListItem.source != null) {
296
+ return threadListItem().getState().id;
297
+ }
298
+
299
+ const threads = client.threads;
300
+ if (threads.source != null) {
301
+ return threads().getState().mainThreadId;
302
+ }
303
+
304
+ return undefined;
305
+ }, [clientRef]);
306
+
307
+ useEffect(() => {
308
+ if (!persistence) return;
309
+ setPersistenceAdapter(persistence);
310
+ return () => {
311
+ if (adapterRef.current === persistence) {
312
+ adapterRef.current = undefined;
313
+ }
314
+ };
315
+ }, [persistence, setPersistenceAdapter]);
316
+
171
317
  const flush = useCallback(async () => {
172
318
  if (debounceTimerRef.current !== undefined) {
173
319
  clearTimeout(debounceTimerRef.current);
@@ -195,7 +341,8 @@ const useInteractables = (): ClientOutput<"interactables"> => {
195
341
 
196
342
  const setDefState = useCallback(
197
343
  (id: string, updater: (prev: unknown) => unknown) => {
198
- setState((prev) => {
344
+ touchedIdsRef.current.add(id);
345
+ setStateAndRef((prev) => {
199
346
  const existing = prev.definitions[id];
200
347
  if (!existing) return prev;
201
348
  return {
@@ -206,25 +353,13 @@ const useInteractables = (): ClientOutput<"interactables"> => {
206
353
  },
207
354
  };
208
355
  });
209
- if (stateRef.current.definitions[id]) schedulePersistence(id);
356
+ if (stateRef.current.definitions[id]?.scope !== "thread") {
357
+ schedulePersistence(id);
358
+ }
210
359
  },
211
- [schedulePersistence],
360
+ [schedulePersistence, setStateAndRef],
212
361
  );
213
362
 
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
363
  const provider = useMemo(
229
364
  () => ({
230
365
  getModelContext: () => {
@@ -234,6 +369,7 @@ const useInteractables = (): ClientOutput<"interactables"> => {
234
369
  defs,
235
370
  partialSchemaCacheRef.current,
236
371
  setDefState,
372
+ streamBaselinesRef.current,
237
373
  ) ?? {}
238
374
  );
239
375
  },
@@ -256,24 +392,111 @@ const useInteractables = (): ClientOutput<"interactables"> => {
256
392
  }, [clientRef, provider]);
257
393
 
258
394
  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) {
395
+ (def: InternalInteractableRegistration) => {
396
+ const threadAccessor = clientRef.current?.thread;
397
+ const threadMessages =
398
+ threadAccessor && threadAccessor.source != null
399
+ ? (threadAccessor().getState().messages ?? [])
400
+ : [];
401
+ const scope =
402
+ def.scope ??
403
+ (hasInteractableCreateCall(threadMessages, def.id, def.name)
404
+ ? "thread"
405
+ : "app");
406
+
407
+ if (
408
+ process.env.NODE_ENV !== "production" &&
409
+ stateRef.current.definitions[def.id] &&
410
+ scope !== "thread"
411
+ ) {
267
412
  console.warn(
268
- `[Interactables] Failed to create partial schema for "${def.name}". The update tool will require all fields.`,
269
- e,
413
+ `[Interactables] "${def.name}" (${def.id}) is already registered. ` +
414
+ `Register an app-scoped interactable once (unstable_useInteractable) and ` +
415
+ `read it from other components with unstable_useInteractableState.`,
270
416
  );
271
417
  }
272
418
 
273
- const detached = detachedStateRef.current.get(def.id);
274
- detachedStateRef.current.delete(def.id);
419
+ registrationCountsRef.current.set(
420
+ def.id,
421
+ (registrationCountsRef.current.get(def.id) ?? 0) + 1,
422
+ );
275
423
 
276
- setState((prev) => ({
424
+ let releaseUpdateToolUI: (() => void) | undefined;
425
+ if (def.updateRender) {
426
+ const toolsAccessor = clientRef.current?.tools;
427
+ if (toolsAccessor && toolsAccessor.source != null) {
428
+ const toolName = interactableToolName(def.name);
429
+ const existing = updateToolUIsRef.current.get(def.name);
430
+ if (existing) {
431
+ existing.count++;
432
+ } else {
433
+ updateToolUIsRef.current.set(def.name, {
434
+ count: 1,
435
+ unsubscribe: toolsAccessor().setToolUI(
436
+ toolName,
437
+ def.updateRender,
438
+ { standalone: true },
439
+ ),
440
+ });
441
+ }
442
+ releaseUpdateToolUI = () => {
443
+ const entry = updateToolUIsRef.current.get(def.name);
444
+ if (!entry) return;
445
+ if (--entry.count === 0) {
446
+ updateToolUIsRef.current.delete(def.name);
447
+ entry.unsubscribe();
448
+ }
449
+ };
450
+ } else if (process.env.NODE_ENV !== "production") {
451
+ console.warn(
452
+ `[Interactables] "${def.name}" supplied an updateRender, but no ` +
453
+ `tools scope is available to install it into.`,
454
+ );
455
+ }
456
+ }
457
+
458
+ // The same id re-registers once per anchor (its create call + each update_*).
459
+ if (!partialSchemaCacheRef.current.has(def.id)) {
460
+ try {
461
+ const jsonSchema = toJSONSchema(def.stateSchema);
462
+ partialSchemaCacheRef.current.set(
463
+ def.id,
464
+ toPartialJSONSchema(jsonSchema),
465
+ );
466
+ } catch (e) {
467
+ console.warn(
468
+ `[Interactables] Failed to create partial schema for "${def.name}". The update tool will accept arbitrary fields without validation.`,
469
+ e,
470
+ );
471
+ }
472
+ }
473
+
474
+ const threadId = scope === "thread" ? getCurrentThreadId() : undefined;
475
+ const detached =
476
+ scope === "thread"
477
+ ? threadId
478
+ ? detachedThreadStateRef.current.get(threadId)?.get(def.id)
479
+ : undefined
480
+ : detachedAppStateRef.current.get(def.id);
481
+ if (scope === "thread") {
482
+ if (threadId)
483
+ detachedThreadStateRef.current.get(threadId)?.delete(def.id);
484
+ } else {
485
+ detachedAppStateRef.current.delete(def.id);
486
+ }
487
+ const loaded =
488
+ scope === "thread" ? undefined : loadedStateRef.current.get(def.id);
489
+
490
+ // Tool-created items restore from what the model already knows in this
491
+ // thread (the creating call's args, sent snapshots, and the model's own
492
+ // update_* calls) on a fresh reload; detached (in-session remount) still
493
+ // wins so an unsent edit survives a scroll/virtualization cycle.
494
+ const known =
495
+ scope === "thread"
496
+ ? findModelKnownState(threadMessages, def.id, def.name)
497
+ : undefined;
498
+
499
+ setStateAndRef((prev) => ({
277
500
  ...prev,
278
501
  definitions: {
279
502
  ...prev.definitions,
@@ -282,19 +505,45 @@ const useInteractables = (): ClientOutput<"interactables"> => {
282
505
  name: def.name,
283
506
  description: def.description,
284
507
  stateSchema: def.stateSchema,
508
+ initialState: def.initialState,
509
+ scope,
285
510
  state:
286
- prev.definitions[def.id]?.state ?? detached ?? def.initialState,
287
- selected: def.selected,
511
+ prev.definitions[def.id]?.state ??
512
+ detached ??
513
+ known?.state ??
514
+ loaded ??
515
+ def.initialState,
288
516
  },
289
517
  },
290
518
  }));
291
519
 
292
520
  return () => {
521
+ releaseUpdateToolUI?.();
522
+
523
+ const remaining = (registrationCountsRef.current.get(def.id) ?? 1) - 1;
524
+ if (remaining > 0) {
525
+ registrationCountsRef.current.set(def.id, remaining);
526
+ return;
527
+ }
528
+ registrationCountsRef.current.delete(def.id);
529
+
293
530
  flushIfPending();
294
- setState((prev) => {
531
+ setStateAndRef((prev) => {
295
532
  const existing = prev.definitions[def.id];
296
533
  if (existing) {
297
- detachedStateRef.current.set(def.id, existing.state);
534
+ if (existing.scope === "thread") {
535
+ const threadId = getCurrentThreadId();
536
+ if (threadId) {
537
+ let stateById = detachedThreadStateRef.current.get(threadId);
538
+ if (!stateById) {
539
+ stateById = new Map();
540
+ detachedThreadStateRef.current.set(threadId, stateById);
541
+ }
542
+ stateById.set(def.id, existing.state);
543
+ }
544
+ } else {
545
+ detachedAppStateRef.current.set(def.id, existing.state);
546
+ }
298
547
  }
299
548
  partialSchemaCacheRef.current.delete(def.id);
300
549
  const { [def.id]: _, ...rest } = prev.definitions;
@@ -303,14 +552,13 @@ const useInteractables = (): ClientOutput<"interactables"> => {
303
552
  });
304
553
  };
305
554
  },
306
- [flushIfPending],
555
+ [flushIfPending, clientRef, getCurrentThreadId, setStateAndRef],
307
556
  );
308
557
 
309
558
  return {
310
- getState: () => state,
559
+ getState: () => stateRef.current,
311
560
  register,
312
561
  setState: setDefState,
313
- setSelected: setDefSelected,
314
562
  exportState,
315
563
  importState,
316
564
  setPersistenceAdapter,
@@ -318,9 +566,14 @@ const useInteractables = (): ClientOutput<"interactables"> => {
318
566
  };
319
567
  };
320
568
 
321
- export const Interactables = resource(useInteractables);
569
+ /**
570
+ * Registers the unstable interactables store scope.
571
+ *
572
+ * @deprecated Unstable / Experimental (not actually removed).
573
+ */
574
+ export const unstable_Interactables = resource(useInteractablesResource);
322
575
 
323
- attachTransformScopes(useInteractables, (scopes, parent) => {
576
+ attachTransformScopes(useInteractablesResource, (scopes, parent) => {
324
577
  if (!scopes.modelContext && parent.modelContext.source === null) {
325
578
  scopes.modelContext = ModelContext();
326
579
  }