@assistant-ui/core 0.2.11 → 0.2.14

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 (212) hide show
  1. package/dist/adapters/thread-history.d.ts +3 -1
  2. package/dist/adapters/thread-history.d.ts.map +1 -1
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/react/AssistantProvider.js +6 -1
  6. package/dist/react/AssistantProvider.js.map +1 -1
  7. package/dist/react/RuntimeAdapter.d.ts +1 -1
  8. package/dist/react/RuntimeAdapter.d.ts.map +1 -1
  9. package/dist/react/RuntimeAdapter.js +16 -6
  10. package/dist/react/RuntimeAdapter.js.map +1 -1
  11. package/dist/react/client/DataRenderers.d.ts +1 -8
  12. package/dist/react/client/DataRenderers.d.ts.map +1 -1
  13. package/dist/react/client/DataRenderers.js +3 -2
  14. package/dist/react/client/DataRenderers.js.map +1 -1
  15. package/dist/react/client/Interactables.d.ts +1 -1
  16. package/dist/react/client/Interactables.d.ts.map +1 -1
  17. package/dist/react/client/Interactables.js +4 -3
  18. package/dist/react/client/Interactables.js.map +1 -1
  19. package/dist/react/client/Tools.d.ts +2 -13
  20. package/dist/react/client/Tools.d.ts.map +1 -1
  21. package/dist/react/client/Tools.js +4 -3
  22. package/dist/react/client/Tools.js.map +1 -1
  23. package/dist/react/primitives/message/MessageGroupedParts.d.ts +3 -2
  24. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  25. package/dist/react/primitives/message/MessageGroupedParts.js +4 -4
  26. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  27. package/dist/react/primitives/message/MessageParts.d.ts +28 -1
  28. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  29. package/dist/react/primitives/message/MessageParts.js +43 -9
  30. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  31. package/dist/react/providers/TextMessagePartProvider.d.ts.map +1 -1
  32. package/dist/react/providers/TextMessagePartProvider.js +3 -2
  33. package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
  34. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +2 -0
  35. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  36. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +2 -0
  37. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  38. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +1 -0
  39. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  40. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  41. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +6 -0
  42. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  43. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  44. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +2 -0
  45. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  46. package/dist/react/utils/groupParts.d.ts +13 -1
  47. package/dist/react/utils/groupParts.d.ts.map +1 -1
  48. package/dist/react/utils/groupParts.js +17 -5
  49. package/dist/react/utils/groupParts.js.map +1 -1
  50. package/dist/runtime/api/bindings.d.ts +1 -0
  51. package/dist/runtime/api/bindings.d.ts.map +1 -1
  52. package/dist/runtime/api/message-runtime.d.ts +2 -0
  53. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  54. package/dist/runtime/api/message-runtime.js +5 -0
  55. package/dist/runtime/api/message-runtime.js.map +1 -1
  56. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  57. package/dist/runtime/api/thread-list-runtime.js +1 -0
  58. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  59. package/dist/runtime/api/thread-runtime.d.ts +3 -0
  60. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  61. package/dist/runtime/api/thread-runtime.js +4 -0
  62. package/dist/runtime/api/thread-runtime.js.map +1 -1
  63. package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -0
  64. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  65. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  66. package/dist/runtime/branch/external-thread-branch-adapter.d.ts +30 -0
  67. package/dist/runtime/branch/external-thread-branch-adapter.d.ts.map +1 -0
  68. package/dist/runtime/branch/external-thread-branch-adapter.js +0 -0
  69. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +1 -0
  70. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  71. package/dist/runtime/interfaces/thread-runtime-core.d.ts +2 -0
  72. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  73. package/dist/runtimes/external-store/external-store-adapter.d.ts +1 -0
  74. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  75. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +1 -0
  76. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  77. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +13 -0
  78. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  79. package/dist/runtimes/local/local-runtime-options.d.ts +1 -1
  80. package/dist/runtimes/local/local-thread-runtime-core.d.ts +8 -1
  81. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  82. package/dist/runtimes/local/local-thread-runtime-core.js +63 -5
  83. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  84. package/dist/runtimes/local/should-continue.js +4 -2
  85. package/dist/runtimes/local/should-continue.js.map +1 -1
  86. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +2 -0
  87. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  88. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +4 -0
  89. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  90. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  91. package/dist/runtimes/remote-thread-list/empty-thread-core.js +4 -0
  92. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  93. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +1 -0
  94. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  95. package/dist/runtimes/remote-thread-list/remote-thread-state.js +1 -0
  96. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  97. package/dist/runtimes/remote-thread-list/types.d.ts +1 -0
  98. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  99. package/dist/store/clients/chain-of-thought-client.d.ts +2 -7
  100. package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
  101. package/dist/store/clients/chain-of-thought-client.js +3 -2
  102. package/dist/store/clients/chain-of-thought-client.js.map +1 -1
  103. package/dist/store/clients/model-context-client.d.ts +1 -1
  104. package/dist/store/clients/model-context-client.d.ts.map +1 -1
  105. package/dist/store/clients/model-context-client.js +3 -2
  106. package/dist/store/clients/model-context-client.js.map +1 -1
  107. package/dist/store/clients/no-op-composer-client.d.ts +2 -4
  108. package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
  109. package/dist/store/clients/no-op-composer-client.js +3 -2
  110. package/dist/store/clients/no-op-composer-client.js.map +1 -1
  111. package/dist/store/clients/runtime-adapter.d.ts +1 -3
  112. package/dist/store/clients/runtime-adapter.d.ts.map +1 -1
  113. package/dist/store/clients/runtime-adapter.js +2 -15
  114. package/dist/store/clients/runtime-adapter.js.map +1 -1
  115. package/dist/store/clients/suggestions.d.ts +1 -4
  116. package/dist/store/clients/suggestions.d.ts.map +1 -1
  117. package/dist/store/clients/suggestions.js +6 -4
  118. package/dist/store/clients/suggestions.js.map +1 -1
  119. package/dist/store/clients/thread-message-client.d.ts +1 -1
  120. package/dist/store/clients/thread-message-client.d.ts.map +1 -1
  121. package/dist/store/clients/thread-message-client.js +14 -10
  122. package/dist/store/clients/thread-message-client.js.map +1 -1
  123. package/dist/store/internal.d.ts +2 -2
  124. package/dist/store/internal.js +2 -2
  125. package/dist/store/runtime-clients/attachment-runtime-client.d.ts +2 -4
  126. package/dist/store/runtime-clients/attachment-runtime-client.d.ts.map +1 -1
  127. package/dist/store/runtime-clients/attachment-runtime-client.js +3 -2
  128. package/dist/store/runtime-clients/attachment-runtime-client.js.map +1 -1
  129. package/dist/store/runtime-clients/composer-runtime-client.d.ts +2 -10
  130. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  131. package/dist/store/runtime-clients/composer-runtime-client.js +9 -6
  132. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  133. package/dist/store/runtime-clients/message-part-runtime-client.d.ts +2 -4
  134. package/dist/store/runtime-clients/message-part-runtime-client.d.ts.map +1 -1
  135. package/dist/store/runtime-clients/message-part-runtime-client.js +3 -2
  136. package/dist/store/runtime-clients/message-part-runtime-client.js.map +1 -1
  137. package/dist/store/runtime-clients/message-runtime-client.d.ts +2 -7
  138. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  139. package/dist/store/runtime-clients/message-runtime-client.js +10 -6
  140. package/dist/store/runtime-clients/message-runtime-client.js.map +1 -1
  141. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +2 -4
  142. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  143. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +3 -2
  144. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  145. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts +2 -5
  146. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  147. package/dist/store/runtime-clients/thread-list-runtime-client.js +6 -4
  148. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  149. package/dist/store/runtime-clients/thread-runtime-client.d.ts +2 -4
  150. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  151. package/dist/store/runtime-clients/thread-runtime-client.js +7 -4
  152. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  153. package/dist/store/scopes/message.d.ts +1 -0
  154. package/dist/store/scopes/message.d.ts.map +1 -1
  155. package/dist/store/scopes/thread-list-item.d.ts +1 -0
  156. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  157. package/dist/store/scopes/thread.d.ts +1 -0
  158. package/dist/store/scopes/thread.d.ts.map +1 -1
  159. package/package.json +4 -4
  160. package/src/adapters/thread-history.ts +2 -0
  161. package/src/index.ts +1 -0
  162. package/src/react/AssistantProvider.tsx +3 -1
  163. package/src/react/RuntimeAdapter.ts +25 -8
  164. package/src/react/client/DataRenderers.ts +42 -45
  165. package/src/react/client/Interactables.ts +261 -261
  166. package/src/react/client/Tools.ts +6 -4
  167. package/src/react/primitives/message/MessageGroupedParts.tsx +19 -7
  168. package/src/react/primitives/message/MessageParts.tsx +64 -13
  169. package/src/react/providers/TextMessagePartProvider.tsx +5 -3
  170. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +1 -0
  171. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +11 -0
  172. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +6 -0
  173. package/src/react/utils/groupParts.ts +27 -0
  174. package/src/runtime/api/bindings.ts +1 -0
  175. package/src/runtime/api/message-runtime.ts +7 -0
  176. package/src/runtime/api/thread-list-runtime.ts +1 -0
  177. package/src/runtime/api/thread-runtime.ts +7 -0
  178. package/src/runtime/base/base-thread-runtime-core.ts +1 -0
  179. package/src/runtime/branch/external-thread-branch-adapter.ts +26 -0
  180. package/src/runtime/interfaces/thread-list-runtime-core.ts +1 -0
  181. package/src/runtime/interfaces/thread-runtime-core.ts +2 -0
  182. package/src/runtimes/external-store/external-store-adapter.ts +1 -0
  183. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +24 -0
  184. package/src/runtimes/local/local-runtime-options.ts +1 -1
  185. package/src/runtimes/local/local-thread-runtime-core.test.ts +311 -0
  186. package/src/runtimes/local/local-thread-runtime-core.ts +104 -7
  187. package/src/runtimes/local/should-continue.ts +23 -13
  188. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +5 -0
  189. package/src/runtimes/remote-thread-list/empty-thread-core.ts +5 -0
  190. package/src/runtimes/remote-thread-list/remote-thread-state.ts +2 -0
  191. package/src/runtimes/remote-thread-list/types.ts +1 -0
  192. package/src/store/clients/chain-of-thought-client.ts +5 -3
  193. package/src/store/clients/model-context-client.test.ts +5 -4
  194. package/src/store/clients/model-context-client.ts +21 -21
  195. package/src/store/clients/no-op-composer-client.ts +5 -3
  196. package/src/store/clients/runtime-adapter.ts +0 -24
  197. package/src/store/clients/suggestions.ts +9 -18
  198. package/src/store/clients/thread-message-client.ts +29 -26
  199. package/src/store/internal.ts +1 -4
  200. package/src/store/runtime-clients/attachment-runtime-client.ts +14 -14
  201. package/src/store/runtime-clients/composer-runtime-client.ts +30 -24
  202. package/src/store/runtime-clients/message-part-runtime-client.ts +5 -3
  203. package/src/store/runtime-clients/message-runtime-client.ts +26 -19
  204. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +5 -3
  205. package/src/store/runtime-clients/thread-list-runtime-client.ts +10 -6
  206. package/src/store/runtime-clients/thread-runtime-client.ts +11 -6
  207. package/src/store/scopes/message.ts +1 -0
  208. package/src/store/scopes/thread-list-item.ts +1 -0
  209. package/src/store/scopes/thread.ts +1 -0
  210. package/src/tests/external-store-thread-runtime-core.test.ts +57 -0
  211. package/src/tests/groupMessageParts.test.ts +84 -0
  212. package/src/tests/groupParts.test.ts +55 -0
@@ -18,201 +18,183 @@ import { buildInteractableModelContext } from "./interactable-model-context";
18
18
 
19
19
  const PERSISTENCE_DEBOUNCE_MS = 500;
20
20
 
21
- export const Interactables = resource(
22
- function Interactables(): ClientOutput<"interactables"> {
23
- const [state, setState] = useState<InteractablesState>(() => ({
24
- definitions: {},
25
- persistence: {},
26
- }));
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]);
27
33
 
28
- const clientRef = useAssistantClientRef();
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>());
29
39
 
30
- const stateRef = useRef(state);
31
- useEffect(() => {
32
- stateRef.current = state;
33
- }, [state]);
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>());
34
50
 
35
- const subscribersRef = useRef(new Set<() => void>());
36
- const partialSchemaCacheRef = useRef(
37
- new Map<string, InteractableStateSchema>(),
38
- );
39
- const detachedStateRef = useRef(new Map<string, unknown>());
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
+ }
40
58
 
41
- const adapterRef = useRef<InteractablePersistenceAdapter | undefined>(
42
- undefined,
43
- );
44
- const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
45
- undefined,
46
- );
47
- const syncSeqRef = useRef(0);
48
- const hasPendingLocalChangeRef = useRef(false);
49
- const flushResolversRef = useRef<Array<() => void>>([]);
50
- const dirtyIdsRef = useRef(new Set<string>());
59
+ const seq = ++syncSeqRef.current;
60
+ const dirtyIds = new Set(dirtyIdsRef.current);
61
+ dirtyIdsRef.current.clear();
62
+ hasPendingLocalChangeRef.current = true;
51
63
 
52
- const runPersistence = useCallback(async () => {
53
- const adapter = adapterRef.current;
54
- if (!adapter) {
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 {
55
111
  for (const resolve of flushResolversRef.current) resolve();
56
112
  flushResolversRef.current = [];
57
- return;
58
113
  }
114
+ }
115
+ }, []);
59
116
 
60
- const seq = ++syncSeqRef.current;
61
- const dirtyIds = new Set(dirtyIdsRef.current);
62
- dirtyIdsRef.current.clear();
63
- hasPendingLocalChangeRef.current = true;
64
-
65
- // Snapshot before any await so unregistered definitions are still included.
66
- const exported = stateRef.current.definitions;
67
- const payload: InteractablePersistedState = {};
68
- for (const [id, def] of Object.entries(exported)) {
69
- payload[id] = { name: def.name, state: def.state };
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);
70
123
  }
71
-
72
- setState((prev) => ({
73
- ...prev,
74
- persistence: {
75
- ...prev.persistence,
76
- ...Object.fromEntries(
77
- [...dirtyIds].map((id) => [
78
- id,
79
- { isPending: true, error: undefined },
80
- ]),
81
- ),
82
- },
83
- }));
84
-
85
- try {
86
- await adapter.save(payload);
87
- if (syncSeqRef.current === seq) {
88
- hasPendingLocalChangeRef.current = false;
89
- setState((prev) => {
90
- const persistence = { ...prev.persistence };
91
- for (const id of dirtyIds) delete persistence[id];
92
- return { ...prev, persistence };
93
- });
94
- }
95
- } catch (e) {
96
- if (syncSeqRef.current === seq) {
97
- hasPendingLocalChangeRef.current = false;
98
- setState((prev) => ({
99
- ...prev,
100
- persistence: {
101
- ...prev.persistence,
102
- ...Object.fromEntries(
103
- [...dirtyIds].map((id) => [id, { isPending: false, error: e }]),
104
- ),
105
- },
106
- }));
107
- }
108
- } finally {
109
- if (dirtyIdsRef.current.size > 0 && adapterRef.current) {
124
+ debounceTimerRef.current = setTimeout(() => {
125
+ debounceTimerRef.current = undefined;
126
+ if (!hasPendingLocalChangeRef.current) {
110
127
  runPersistence();
111
128
  } else {
112
- for (const resolve of flushResolversRef.current) resolve();
113
- flushResolversRef.current = [];
114
- }
115
- }
116
- }, []);
117
-
118
- const schedulePersistence = useCallback(
119
- (id: string) => {
120
- if (!adapterRef.current) return;
121
- dirtyIdsRef.current.add(id);
122
- if (debounceTimerRef.current !== undefined) {
123
- clearTimeout(debounceTimerRef.current);
124
- }
125
- debounceTimerRef.current = setTimeout(() => {
126
- debounceTimerRef.current = undefined;
127
- if (!hasPendingLocalChangeRef.current) {
129
+ debounceTimerRef.current = setTimeout(() => {
130
+ debounceTimerRef.current = undefined;
128
131
  runPersistence();
129
- } else {
130
- debounceTimerRef.current = setTimeout(() => {
131
- debounceTimerRef.current = undefined;
132
- runPersistence();
133
- }, PERSISTENCE_DEBOUNCE_MS);
134
- }
135
- }, PERSISTENCE_DEBOUNCE_MS);
136
- },
137
- [runPersistence],
138
- );
132
+ }, PERSISTENCE_DEBOUNCE_MS);
133
+ }
134
+ }, PERSISTENCE_DEBOUNCE_MS);
135
+ },
136
+ [runPersistence],
137
+ );
139
138
 
140
- const exportState = useCallback((): InteractablePersistedState => {
141
- const result: InteractablePersistedState = {};
142
- for (const [id, def] of Object.entries(stateRef.current.definitions)) {
143
- result[id] = { name: def.name, state: def.state };
144
- }
145
- return result;
146
- }, []);
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
+ }, []);
147
146
 
148
- const importState = useCallback((saved: InteractablePersistedState) => {
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 };
149
154
  for (const [id, entry] of Object.entries(saved)) {
150
- detachedStateRef.current.set(id, entry.state);
151
- }
152
- setState((prev) => {
153
- let changed = false;
154
- const definitions = { ...prev.definitions };
155
- for (const [id, entry] of Object.entries(saved)) {
156
- if (definitions[id]) {
157
- definitions[id] = { ...definitions[id], state: entry.state };
158
- changed = true;
159
- }
155
+ if (definitions[id]) {
156
+ definitions[id] = { ...definitions[id], state: entry.state };
157
+ changed = true;
160
158
  }
161
- return changed ? { ...prev, definitions } : prev;
162
- });
163
- }, []);
164
-
165
- const setPersistenceAdapter = useCallback(
166
- (adapter: InteractablePersistenceAdapter | undefined) => {
167
- adapterRef.current = adapter;
168
- },
169
- [],
170
- );
171
-
172
- const flush = useCallback(async () => {
173
- if (debounceTimerRef.current !== undefined) {
174
- clearTimeout(debounceTimerRef.current);
175
- debounceTimerRef.current = undefined;
176
159
  }
177
- if (!adapterRef.current) return;
178
- if (!hasPendingLocalChangeRef.current && dirtyIdsRef.current.size === 0)
179
- return;
180
- const p = new Promise<void>((resolve) => {
181
- flushResolversRef.current.push(resolve);
182
- });
183
- if (!hasPendingLocalChangeRef.current) {
184
- runPersistence();
185
- }
186
- return p;
187
- }, [runPersistence]);
160
+ return changed ? { ...prev, definitions } : prev;
161
+ });
162
+ }, []);
188
163
 
189
- const flushIfPending = useCallback(() => {
190
- if (adapterRef.current && debounceTimerRef.current !== undefined) {
191
- clearTimeout(debounceTimerRef.current);
192
- debounceTimerRef.current = undefined;
193
- runPersistence();
194
- }
195
- }, [runPersistence]);
164
+ const setPersistenceAdapter = useCallback(
165
+ (adapter: InteractablePersistenceAdapter | undefined) => {
166
+ adapterRef.current = adapter;
167
+ },
168
+ [],
169
+ );
196
170
 
197
- const setDefState = useCallback(
198
- (id: string, updater: (prev: unknown) => unknown) => {
199
- setState((prev) => {
200
- const existing = prev.definitions[id];
201
- if (!existing) return prev;
202
- return {
203
- ...prev,
204
- definitions: {
205
- ...prev.definitions,
206
- [id]: { ...existing, state: updater(existing.state) },
207
- },
208
- };
209
- });
210
- if (stateRef.current.definitions[id]) schedulePersistence(id);
211
- },
212
- [schedulePersistence],
213
- );
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]);
214
195
 
215
- const setDefSelected = useCallback((id: string, selected: boolean) => {
196
+ const setDefState = useCallback(
197
+ (id: string, updater: (prev: unknown) => unknown) => {
216
198
  setState((prev) => {
217
199
  const existing = prev.definitions[id];
218
200
  if (!existing) return prev;
@@ -220,107 +202,125 @@ export const Interactables = resource(
220
202
  ...prev,
221
203
  definitions: {
222
204
  ...prev.definitions,
223
- [id]: { ...existing, selected },
205
+ [id]: { ...existing, state: updater(existing.state) },
224
206
  },
225
207
  };
226
208
  });
227
- }, []);
209
+ if (stateRef.current.definitions[id]) schedulePersistence(id);
210
+ },
211
+ [schedulePersistence],
212
+ );
228
213
 
229
- const provider = useMemo(
230
- () => ({
231
- getModelContext: () => {
232
- const defs = stateRef.current.definitions;
233
- return (
234
- buildInteractableModelContext(
235
- defs,
236
- partialSchemaCacheRef.current,
237
- setDefState,
238
- ) ?? {}
239
- );
240
- },
241
- subscribe: (callback: () => void) => {
242
- subscribersRef.current.add(callback);
243
- return () => {
244
- subscribersRef.current.delete(callback);
245
- };
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 },
246
223
  },
247
- }),
248
- [setDefState],
249
- );
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
+ );
250
249
 
251
- useEffect(() => {
252
- for (const cb of subscribersRef.current) cb();
253
- }, [state]);
250
+ useEffect(() => {
251
+ for (const cb of subscribersRef.current) cb();
252
+ }, [state]);
254
253
 
255
- useEffect(() => {
256
- return clientRef.current!.modelContext().register(provider);
257
- }, [clientRef, provider]);
254
+ useEffect(() => {
255
+ return clientRef.current!.modelContext().register(provider);
256
+ }, [clientRef, provider]);
258
257
 
259
- const register = useCallback(
260
- (def: InteractableRegistration) => {
261
- try {
262
- const jsonSchema = toJSONSchema(def.stateSchema);
263
- partialSchemaCacheRef.current.set(
264
- def.id,
265
- toPartialJSONSchema(jsonSchema),
266
- );
267
- } catch (e) {
268
- console.warn(
269
- `[Interactables] Failed to create partial schema for "${def.name}". The update tool will require all fields.`,
270
- e,
271
- );
272
- }
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
+ }
273
272
 
274
- const detached = detachedStateRef.current.get(def.id);
275
- detachedStateRef.current.delete(def.id);
273
+ const detached = detachedStateRef.current.get(def.id);
274
+ detachedStateRef.current.delete(def.id);
276
275
 
277
- setState((prev) => ({
278
- ...prev,
279
- definitions: {
280
- ...prev.definitions,
281
- [def.id]: {
282
- id: def.id,
283
- name: def.name,
284
- description: def.description,
285
- stateSchema: def.stateSchema,
286
- state:
287
- prev.definitions[def.id]?.state ?? detached ?? def.initialState,
288
- selected: def.selected,
289
- },
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,
290
288
  },
291
- }));
289
+ },
290
+ }));
292
291
 
293
- return () => {
294
- flushIfPending();
295
- setState((prev) => {
296
- const existing = prev.definitions[def.id];
297
- if (existing) {
298
- detachedStateRef.current.set(def.id, existing.state);
299
- }
300
- partialSchemaCacheRef.current.delete(def.id);
301
- const { [def.id]: _, ...rest } = prev.definitions;
302
- const { [def.id]: __, ...restPersistence } = prev.persistence;
303
- return { ...prev, definitions: rest, persistence: restPersistence };
304
- });
305
- };
306
- },
307
- [flushIfPending],
308
- );
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
+ };
309
320
 
310
- return {
311
- getState: () => state,
312
- register,
313
- setState: setDefState,
314
- setSelected: setDefSelected,
315
- exportState,
316
- importState,
317
- setPersistenceAdapter,
318
- flush,
319
- };
320
- },
321
- );
321
+ export const Interactables = resource(useInteractables);
322
322
 
323
- attachTransformScopes(Interactables, (scopes, parent) => {
323
+ attachTransformScopes(useInteractables, (scopes, parent) => {
324
324
  if (!scopes.modelContext && parent.modelContext.source === null) {
325
325
  scopes.modelContext = ModelContext();
326
326
  }
@@ -30,7 +30,7 @@ export type { McpAppResourceOutput };
30
30
  * context, while each tool renderer is registered with the tools scope for
31
31
  * message rendering.
32
32
  */
33
- export const Tools = resource(function Tools({
33
+ const useTools = ({
34
34
  toolkit,
35
35
  mcpApp,
36
36
  }: {
@@ -38,7 +38,7 @@ export const Tools = resource(function Tools({
38
38
  toolkit?: Toolkit;
39
39
  /** Optional MCP app resource whose tools should be merged into context. */
40
40
  mcpApp?: ResourceElement<McpAppResourceOutput> | undefined;
41
- }): ClientOutput<"tools"> {
41
+ }): ClientOutput<"tools"> => {
42
42
  const mcpAppOutputs = useResources(
43
43
  () => (mcpApp ? [withKey("mcpApp", mcpApp)] : []),
44
44
  [mcpApp],
@@ -155,9 +155,11 @@ export const Tools = resource(function Tools({
155
155
  getState: () => state,
156
156
  setToolUI,
157
157
  };
158
- });
158
+ };
159
+
160
+ export const Tools = resource(useTools);
159
161
 
160
- attachTransformScopes(Tools, (scopes, parent) => {
162
+ attachTransformScopes(useTools, (scopes, parent) => {
161
163
  if (!scopes.modelContext && parent.modelContext.source === null) {
162
164
  scopes.modelContext = ModelContext();
163
165
  }
@@ -49,8 +49,9 @@ export namespace MessagePrimitiveGroupedParts {
49
49
  * which running states qualify:
50
50
  * - `"never"` — never.
51
51
  * - `"empty"` — only when the message has no parts yet.
52
- * - `"no-text"` (default) — when the last part isn't `text`/`reasoning`
53
- * (e.g. it ended on a tool call, so the assistant likely isn't done).
52
+ * - `"no-text"` (default) — when the message has no parts yet or the last
53
+ * part isn't `text`/`reasoning` (e.g. it ended on a tool call, so the
54
+ * assistant likely isn't done).
54
55
  * - `"always"` — whenever the message is running, regardless of parts.
55
56
  */
56
57
  export type IndicatorMode = "never" | "empty" | "no-text" | "always";
@@ -153,7 +154,8 @@ const shouldShowIndicator = (
153
154
  case "no-text": {
154
155
  const last = parts[parts.length - 1];
155
156
  return (
156
- last !== undefined && last.type !== "text" && last.type !== "reasoning"
157
+ last === undefined ||
158
+ (last.type !== "text" && last.type !== "reasoning")
157
159
  );
158
160
  }
159
161
  }
@@ -179,9 +181,14 @@ const renderNode = <TKey extends `group-${string}`>(
179
181
  render: (info: MessagePrimitiveGroupedParts.RenderInfo<TKey>) => ReactNode,
180
182
  ): ReactNode => {
181
183
  if (node.type === "part") {
182
- // Key by absolute part index, not structural nodeKey prevents zombie fiber subscriptions when parts reshape (#4051).
184
+ // Key by part identity when available, else absolute part index never
185
+ // the structural nodeKey, which leaves zombie fiber subscriptions when
186
+ // parts reshape (#4051).
183
187
  return (
184
- <MessagePartChildren key={`part-${node.index}`} index={node.index}>
188
+ <MessagePartChildren
189
+ key={node.idKey ? `part-${node.idKey}` : `part-${node.index}`}
190
+ index={node.index}
191
+ >
185
192
  {({ part }) => render({ part, children: <PartChildrenSentinel /> })}
186
193
  </MessagePartChildren>
187
194
  );
@@ -195,7 +202,7 @@ const renderNode = <TKey extends `group-${string}`>(
195
202
  };
196
203
 
197
204
  return (
198
- <Fragment key={node.nodeKey}>
205
+ <Fragment key={node.idKey ?? node.nodeKey}>
199
206
  {render({
200
207
  part: groupPart,
201
208
  children: (
@@ -264,7 +271,12 @@ export const MessagePrimitiveGroupedParts = <TKey extends `group-${string}`>({
264
271
  const memoDep = memoKey ?? groupBy;
265
272
  const tree = useMemo(() => {
266
273
  const context: GroupByContext = { toolUIs };
267
- return buildGroupTree(parts.map((part) => groupBy(part, context) ?? []));
274
+ return buildGroupTree(
275
+ parts.map((part) => groupBy(part, context) ?? []),
276
+ parts.map((part) =>
277
+ part.type === "tool-call" ? part.toolCallId : undefined,
278
+ ),
279
+ );
268
280
  // oxlint-disable-next-line react/exhaustive-deps -- groupBy is captured via memoDep (either its identity or the helper's memoKey fingerprint); listing it directly would defeat the helper-tagged memo path
269
281
  }, [parts, memoDep, toolUIs]);
270
282