@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,358 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createTapRoot, useResource } from "@assistant-ui/tap";
3
+ import type {
4
+ Unstable_InteractablePersistedState,
5
+ Unstable_InteractablePersistenceAdapter,
6
+ Unstable_InteractableRegistration,
7
+ } from "../types/scopes/interactables";
8
+ import type { ThreadMessage } from "../../types/message";
9
+
10
+ const clientHolder: { client: unknown } = { client: null };
11
+
12
+ vi.mock("@assistant-ui/store", async (importOriginal) => {
13
+ const actual = await importOriginal<typeof import("@assistant-ui/store")>();
14
+ return {
15
+ ...actual,
16
+ useAssistantClientRef: () => ({
17
+ get current() {
18
+ return clientHolder.client;
19
+ },
20
+ }),
21
+ };
22
+ });
23
+
24
+ const { unstable_Interactables: Interactables } =
25
+ await import("./Interactables");
26
+
27
+ const missingScope = (name: string) =>
28
+ Object.assign(
29
+ () => {
30
+ throw new Error(`${name} scope not available`);
31
+ },
32
+ { source: null },
33
+ );
34
+
35
+ const makeClient = (
36
+ threadMessages?: ThreadMessage[],
37
+ setToolUI?: (...args: unknown[]) => () => void,
38
+ threadId?: string,
39
+ ) => ({
40
+ modelContext: () => ({ register: () => () => {} }),
41
+ thread: threadMessages
42
+ ? Object.assign(
43
+ () => ({ getState: () => ({ messages: threadMessages }) }),
44
+ {
45
+ source: "root",
46
+ },
47
+ )
48
+ : missingScope("thread"),
49
+ threadListItem: threadId
50
+ ? Object.assign(() => ({ getState: () => ({ id: threadId }) }), {
51
+ source: "root",
52
+ })
53
+ : missingScope("threadListItem"),
54
+ threads: threadId
55
+ ? Object.assign(() => ({ getState: () => ({ mainThreadId: threadId }) }), {
56
+ source: "root",
57
+ })
58
+ : missingScope("threads"),
59
+ ...(setToolUI
60
+ ? { tools: Object.assign(() => ({ setToolUI }), { source: "root" }) }
61
+ : {}),
62
+ });
63
+
64
+ const mount = (config?: {
65
+ persistence?: Unstable_InteractablePersistenceAdapter;
66
+ threadMessages?: ThreadMessage[];
67
+ setToolUI?: (...args: unknown[]) => () => void;
68
+ threadId?: string;
69
+ }) => {
70
+ clientHolder.client = makeClient(
71
+ config?.threadMessages,
72
+ config?.setToolUI,
73
+ config?.threadId,
74
+ );
75
+ const root = createTapRoot(function InteractablesRoot() {
76
+ return useResource(
77
+ Interactables(
78
+ config?.persistence ? { persistence: config.persistence } : {},
79
+ ),
80
+ );
81
+ });
82
+ return root;
83
+ };
84
+
85
+ const reg = (
86
+ id: string,
87
+ overrides: Partial<Unstable_InteractableRegistration> = {},
88
+ ): Unstable_InteractableRegistration => ({
89
+ id,
90
+ name: "note",
91
+ description: "a note",
92
+ stateSchema: { type: "object", properties: {} } as never,
93
+ initialState: { v: 0 },
94
+ ...overrides,
95
+ });
96
+
97
+ const stateOf = (root: ReturnType<typeof mount>, id: string) =>
98
+ root.getValue().getState().definitions[id]?.state;
99
+
100
+ const createCall = (
101
+ id: string,
102
+ args: Record<string, unknown> = { v: 0 },
103
+ name = "note",
104
+ ) =>
105
+ ({
106
+ role: "assistant",
107
+ content: [
108
+ {
109
+ type: "tool-call",
110
+ toolCallId: id,
111
+ toolName: name,
112
+ args,
113
+ result: { success: true },
114
+ },
115
+ ],
116
+ }) as unknown as ThreadMessage;
117
+
118
+ const flushMicrotasks = () => vi.advanceTimersByTimeAsync(0);
119
+
120
+ let root: ReturnType<typeof mount> | undefined;
121
+
122
+ beforeEach(() => {
123
+ vi.useFakeTimers();
124
+ });
125
+
126
+ afterEach(() => {
127
+ root?.unmount();
128
+ root = undefined;
129
+ vi.useRealTimers();
130
+ vi.restoreAllMocks();
131
+ });
132
+
133
+ describe("Interactables registration", () => {
134
+ it("seeds a new registration with initialState", () => {
135
+ root = mount();
136
+ root.getValue().register(reg("n1", { initialState: { v: 7 } }));
137
+ expect(stateOf(root, "n1")).toEqual({ v: 7 });
138
+ });
139
+
140
+ it("restores detached state when an instance re-registers in-session", async () => {
141
+ root = mount();
142
+ const unregister = root.getValue().register(reg("n1"));
143
+ await flushMicrotasks();
144
+ root.getValue().setState("n1", () => ({ v: 5 }));
145
+ unregister();
146
+ await flushMicrotasks();
147
+ expect(stateOf(root, "n1")).toBeUndefined();
148
+
149
+ root.getValue().register(reg("n1"));
150
+ expect(stateOf(root, "n1")).toEqual({ v: 5 });
151
+ });
152
+
153
+ it("restores a tool-created registration from the model-known thread state", () => {
154
+ const snapshot = {
155
+ role: "user",
156
+ metadata: {
157
+ custom: {
158
+ interactables: [{ id: "n1", name: "note", state: { v: 42 } }],
159
+ },
160
+ },
161
+ } as unknown as ThreadMessage;
162
+ root = mount({ threadMessages: [createCall("n1"), snapshot] });
163
+ root.getValue().register(reg("n1"));
164
+ expect(stateOf(root, "n1")).toEqual({ v: 42 });
165
+ });
166
+
167
+ it("does not infer thread ownership from snapshots alone", () => {
168
+ const snapshot = {
169
+ role: "user",
170
+ metadata: {
171
+ custom: {
172
+ interactables: [{ id: "n1", name: "note", state: { v: 42 } }],
173
+ },
174
+ },
175
+ } as unknown as ThreadMessage;
176
+ root = mount({ threadMessages: [snapshot] });
177
+ root.getValue().register(reg("n1"));
178
+ expect(stateOf(root, "n1")).toEqual({ v: 0 });
179
+ });
180
+
181
+ it("only restores detached tool-created state in the same thread", async () => {
182
+ root = mount({
183
+ threadId: "thread-a",
184
+ threadMessages: [createCall("shared")],
185
+ });
186
+ const unregister = root.getValue().register(reg("shared"));
187
+ await flushMicrotasks();
188
+ root.getValue().setState("shared", () => ({ v: 5 }));
189
+ unregister();
190
+ await flushMicrotasks();
191
+
192
+ clientHolder.client = makeClient(
193
+ [createCall("shared")],
194
+ undefined,
195
+ "thread-b",
196
+ );
197
+ root.getValue().register(reg("shared"));
198
+ expect(stateOf(root, "shared")).toEqual({ v: 0 });
199
+ });
200
+
201
+ it("restores a tool-created registration from its creating call's args", () => {
202
+ root = mount({ threadMessages: [createCall("n1", { v: 7 })] });
203
+ root.getValue().register(reg("n1"));
204
+ expect(stateOf(root, "n1")).toEqual({ v: 7 });
205
+ });
206
+
207
+ it("keeps the definition alive until the last of several anchors unregisters", async () => {
208
+ root = mount({ threadMessages: [createCall("n1")] });
209
+ const first = root.getValue().register(reg("n1"));
210
+ const second = root.getValue().register(reg("n1"));
211
+ await flushMicrotasks();
212
+ root.getValue().setState("n1", () => ({ v: 5 }));
213
+
214
+ first();
215
+ await flushMicrotasks();
216
+ expect(stateOf(root, "n1")).toEqual({ v: 5 });
217
+
218
+ second();
219
+ await flushMicrotasks();
220
+ expect(stateOf(root, "n1")).toBeUndefined();
221
+ });
222
+
223
+ it("installs the update tool UI once per name and removes it with the last anchor", () => {
224
+ const removeToolUI = vi.fn();
225
+ const setToolUI = vi.fn(() => removeToolUI);
226
+ root = mount({ setToolUI });
227
+
228
+ const render = () => null;
229
+ const first = root.getValue().register(reg("n1", { updateRender: render }));
230
+ const second = root
231
+ .getValue()
232
+ .register(reg("n2", { updateRender: render }));
233
+
234
+ expect(setToolUI).toHaveBeenCalledTimes(1);
235
+ expect(setToolUI).toHaveBeenCalledWith("update_note", render, {
236
+ standalone: true,
237
+ });
238
+
239
+ first();
240
+ expect(removeToolUI).not.toHaveBeenCalled();
241
+ second();
242
+ expect(removeToolUI).toHaveBeenCalledTimes(1);
243
+ });
244
+ });
245
+
246
+ describe("Interactables persistence save", () => {
247
+ it("debounces saves and excludes tool-created items from the payload", async () => {
248
+ const save = vi.fn();
249
+ root = mount({
250
+ persistence: { save },
251
+ threadMessages: [createCall("t1")],
252
+ });
253
+ await flushMicrotasks();
254
+ root.getValue().register(reg("n1"));
255
+ root.getValue().register(reg("t1"));
256
+
257
+ root.getValue().setState("n1", () => ({ v: 1 }));
258
+ root.getValue().setState("t1", () => ({ v: 9 }));
259
+ expect(save).not.toHaveBeenCalled();
260
+
261
+ await vi.advanceTimersByTimeAsync(500);
262
+ expect(save).toHaveBeenCalledTimes(1);
263
+ expect(save.mock.calls[0]![0]).toEqual({
264
+ n1: { name: "note", state: { v: 1 } },
265
+ });
266
+ });
267
+
268
+ it("records a per-id error when save rejects, and clears pending on success", async () => {
269
+ const save = vi.fn().mockRejectedValueOnce(new Error("boom"));
270
+ root = mount({ persistence: { save } });
271
+ await flushMicrotasks();
272
+ root.getValue().register(reg("n1"));
273
+
274
+ root.getValue().setState("n1", () => ({ v: 1 }));
275
+ await vi.advanceTimersByTimeAsync(500);
276
+ const failed = root.getValue().getState().persistence["n1"];
277
+ expect(failed?.isPending).toBe(false);
278
+ expect(failed?.error).toBeInstanceOf(Error);
279
+
280
+ root.getValue().setState("n1", () => ({ v: 2 }));
281
+ await vi.advanceTimersByTimeAsync(500);
282
+ expect(root.getValue().getState().persistence["n1"]).toBeUndefined();
283
+ });
284
+
285
+ it("flush() skips the debounce delay and resolves once the save completed", async () => {
286
+ const save = vi.fn();
287
+ root = mount({ persistence: { save } });
288
+ await flushMicrotasks();
289
+ root.getValue().register(reg("n1"));
290
+ root.getValue().setState("n1", () => ({ v: 1 }));
291
+
292
+ let resolved = false;
293
+ const p = root
294
+ .getValue()
295
+ .flush()
296
+ .then(() => {
297
+ resolved = true;
298
+ });
299
+ await flushMicrotasks();
300
+ expect(save).toHaveBeenCalledTimes(1);
301
+ await p;
302
+ expect(resolved).toBe(true);
303
+ });
304
+ });
305
+
306
+ describe("Interactables persistence load", () => {
307
+ const adapter = (
308
+ saved: Unstable_InteractablePersistedState,
309
+ delayMs = 0,
310
+ ) => ({
311
+ save: vi.fn(),
312
+ load: vi.fn(
313
+ () =>
314
+ new Promise<Unstable_InteractablePersistedState>((resolve) =>
315
+ setTimeout(() => resolve(saved), delayMs),
316
+ ),
317
+ ),
318
+ });
319
+
320
+ it("seeds an app-scoped interactable that registers after the load resolved", async () => {
321
+ root = mount({
322
+ persistence: adapter({ n1: { name: "note", state: { v: 3 } } }),
323
+ });
324
+ await flushMicrotasks();
325
+ root.getValue().register(reg("n1"));
326
+ expect(stateOf(root, "n1")).toEqual({ v: 3 });
327
+ });
328
+
329
+ it("applies loaded state to already-registered app items but never to tool-created ones", async () => {
330
+ root = mount({
331
+ persistence: adapter(
332
+ {
333
+ n1: { name: "note", state: { v: 3 } },
334
+ t1: { name: "note", state: { v: 9 } },
335
+ },
336
+ 100,
337
+ ),
338
+ threadMessages: [createCall("t1")],
339
+ });
340
+ root.getValue().register(reg("n1"));
341
+ root.getValue().register(reg("t1"));
342
+
343
+ await vi.advanceTimersByTimeAsync(100);
344
+ expect(stateOf(root, "n1")).toEqual({ v: 3 });
345
+ expect(stateOf(root, "t1")).toEqual({ v: 0 });
346
+ });
347
+
348
+ it("lets a local edit made while the load was in flight win over the loaded state", async () => {
349
+ root = mount({
350
+ persistence: adapter({ n1: { name: "note", state: { v: 3 } } }, 100),
351
+ });
352
+ root.getValue().register(reg("n1"));
353
+ root.getValue().setState("n1", () => ({ v: 99 }));
354
+
355
+ await vi.advanceTimersByTimeAsync(600);
356
+ expect(stateOf(root, "n1")).toEqual({ v: 99 });
357
+ });
358
+ });