@assistant-ui/core 0.3.18 → 0.3.19
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.
- package/dist/adapters/speech.d.ts.map +1 -1
- package/dist/adapters/speech.js +13 -3
- package/dist/adapters/speech.js.map +1 -1
- package/dist/adapters/thread-history.d.ts +4 -0
- package/dist/adapters/thread-history.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +26 -11
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/interactable-composer-metadata.js +11 -9
- package/dist/model-context/interactable-composer-metadata.js.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +45 -7
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/DataRenderers.d.ts.map +1 -1
- package/dist/react/client/DataRenderers.js +20 -15
- package/dist/react/client/DataRenderers.js.map +1 -1
- package/dist/react/client/Interactables.d.ts.map +1 -1
- package/dist/react/client/Interactables.js +11 -5
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +3 -2
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/client/interactable-model-context.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.js +3 -2
- package/dist/react/client/interactable-model-context.js.map +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -2
- package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -1
- package/dist/react/interactables-legacy/useAssistantInteractable.js +3 -0
- package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -1
- package/dist/react/model-context/useInteractable.d.ts.map +1 -1
- package/dist/react/model-context/useInteractable.js +4 -1
- package/dist/react/model-context/useInteractable.js.map +1 -1
- package/dist/react/model-context/useInteractableVersions.js +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +11 -2
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +3 -2
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/part/PartMessages.d.ts +1 -0
- package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
- package/dist/react/primitives/part/PartMessages.js +1 -0
- package/dist/react/primitives/part/PartMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.js +6 -5
- package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +2 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +2 -24
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +135 -374
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -0
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +22 -2
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +40 -15
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +62 -15
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +2 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +4 -0
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/sdkIdentity.d.ts +6 -0
- package/dist/react/runtimes/cloud/sdkIdentity.d.ts.map +1 -0
- package/dist/react/runtimes/cloud/sdkIdentity.js +9 -0
- package/dist/react/runtimes/cloud/sdkIdentity.js.map +1 -0
- package/dist/react/runtimes/external-message-converter.d.ts +12 -2
- package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +26 -61
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/utils/groupParts.d.ts +6 -1
- package/dist/react/utils/groupParts.d.ts.map +1 -1
- package/dist/react/utils/groupParts.js +6 -0
- package/dist/react/utils/groupParts.js.map +1 -1
- package/dist/react/utils/useJSONEqual.d.ts +3 -1
- package/dist/react/utils/useJSONEqual.d.ts.map +1 -1
- package/dist/react/utils/useJSONEqual.js +6 -1
- package/dist/react/utils/useJSONEqual.js.map +1 -1
- package/dist/react/utils/useJSONSchemaDependency.d.ts +7 -0
- package/dist/react/utils/useJSONSchemaDependency.d.ts.map +1 -0
- package/dist/react/utils/useJSONSchemaDependency.js +20 -0
- package/dist/react/utils/useJSONSchemaDependency.js.map +1 -0
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +2 -5
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +46 -10
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -0
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +155 -64
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +6 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/queue/message-queue.d.ts +5 -0
- package/dist/runtime/queue/message-queue.d.ts.map +1 -1
- package/dist/runtime/queue/message-queue.js +61 -6
- package/dist/runtime/queue/message-queue.js.map +1 -1
- package/dist/runtime/utils/auto-status.d.ts +6 -2
- package/dist/runtime/utils/auto-status.d.ts.map +1 -1
- package/dist/runtime/utils/auto-status.js +12 -4
- package/dist/runtime/utils/auto-status.js.map +1 -1
- package/dist/runtime/utils/external-message-conversion.d.ts +16 -4
- package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
- package/dist/runtime/utils/external-message-conversion.js +48 -12
- package/dist/runtime/utils/external-message-conversion.js.map +1 -1
- package/dist/runtime/utils/message-repository.js +3 -3
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.d.ts +2 -1
- package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +4 -2
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +8 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/internal.d.ts +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +4 -1
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
- package/dist/store/clients/external-thread.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +241 -200
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/clients/thread-tasks.d.ts +13 -0
- package/dist/store/clients/thread-tasks.d.ts.map +1 -0
- package/dist/store/clients/thread-tasks.js +69 -0
- package/dist/store/clients/thread-tasks.js.map +1 -0
- package/dist/store/index.d.ts +3 -2
- package/dist/store/index.js +3 -3
- package/dist/store/primitive-predicates.d.ts.map +1 -1
- package/dist/store/primitive-predicates.js +4 -4
- package/dist/store/primitive-predicates.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +176 -128
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scope-registration.d.ts +2 -0
- package/dist/store/scope-registration.d.ts.map +1 -1
- package/dist/store/scopes/task.d.ts +36 -0
- package/dist/store/scopes/task.d.ts.map +1 -0
- package/dist/store/scopes/task.js +0 -0
- package/dist/store/scopes/thread.d.ts +20 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/subscribable/subscribable.d.ts +2 -1
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js +23 -15
- package/dist/subscribable/subscribable.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/message.d.ts +8 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/abortable-iterable.d.ts.map +1 -1
- package/dist/utils/abortable-iterable.js +30 -14
- package/dist/utils/abortable-iterable.js.map +1 -1
- package/dist/utils/getGroupStatus.d.ts +18 -2
- package/dist/utils/getGroupStatus.d.ts.map +1 -1
- package/dist/utils/getGroupStatus.js +32 -7
- package/dist/utils/getGroupStatus.js.map +1 -1
- package/dist/utils/wire-media.d.ts.map +1 -1
- package/dist/utils/wire-media.js +2 -2
- package/dist/utils/wire-media.js.map +1 -1
- package/package.json +6 -6
- package/src/adapters/speech.test.ts +236 -0
- package/src/adapters/speech.ts +17 -10
- package/src/adapters/thread-history.ts +3 -0
- package/src/index.ts +1 -0
- package/src/model-context/frame/host.test.ts +113 -1
- package/src/model-context/frame/host.ts +39 -12
- package/src/model-context/interactable-composer-metadata.test.ts +30 -0
- package/src/model-context/interactable-composer-metadata.ts +10 -10
- package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +244 -0
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +104 -7
- package/src/react/client/DataRenderers.test.tsx +45 -0
- package/src/react/client/DataRenderers.ts +13 -9
- package/src/react/client/Interactables.test.ts +44 -1
- package/src/react/client/Interactables.ts +7 -1
- package/src/react/client/RemoteThreadList.test.ts +41 -1
- package/src/react/client/RemoteThreadList.ts +15 -2
- package/src/react/client/interactable-model-context.test.ts +44 -0
- package/src/react/client/interactable-model-context.ts +3 -2
- package/src/react/index.ts +5 -1
- package/src/react/interactables-legacy/useAssistantInteractable.test.tsx +132 -0
- package/src/react/interactables-legacy/useAssistantInteractable.ts +10 -1
- package/src/react/model-context/useInteractable.test.tsx +177 -0
- package/src/react/model-context/useInteractable.ts +3 -0
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +0 -1
- package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +0 -1
- package/src/react/primitives/message/MessageGroupedParts.test.tsx +96 -0
- package/src/react/primitives/message/MessageGroupedParts.tsx +15 -4
- package/src/react/primitives/part/PartMessages.test.tsx +104 -1
- package/src/react/primitives/part/PartMessages.tsx +1 -0
- package/src/react/primitives/thread/ThreadMessages.test.tsx +142 -0
- package/src/react/primitives/thread/ThreadMessages.tsx +8 -5
- package/src/react/providers/ReadonlyThreadProvider.test.tsx +56 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +7 -1
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +20 -14
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +221 -1
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +166 -512
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +77 -0
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +27 -1
- package/src/react/runtimes/cloud/auiV0.ts +160 -25
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +21 -0
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +7 -1
- package/src/react/runtimes/cloud/sdkIdentity.ts +9 -0
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +18 -4
- package/src/react/runtimes/external-message-converter.test.ts +136 -1
- package/src/react/runtimes/external-message-converter.ts +39 -93
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +1 -1
- package/src/react/utils/groupParts.ts +12 -1
- package/src/react/utils/useJSONEqual.ts +9 -0
- package/src/react/utils/useJSONSchemaDependency.ts +22 -0
- package/src/runtime/api/thread-runtime.test.ts +58 -0
- package/src/runtime/api/thread-runtime.ts +2 -4
- package/src/runtime/base/base-composer-runtime-core.ts +69 -16
- package/src/runtime/base/base-thread-runtime-core.test.ts +1008 -1
- package/src/runtime/base/base-thread-runtime-core.ts +222 -83
- package/src/runtime/interfaces/thread-runtime-core.ts +6 -0
- package/src/runtime/queue/message-queue.ts +68 -6
- package/src/runtime/utils/auto-status.test.ts +112 -1
- package/src/runtime/utils/auto-status.ts +31 -5
- package/src/runtime/utils/external-message-conversion.test.ts +134 -0
- package/src/runtime/utils/external-message-conversion.ts +122 -31
- package/src/runtime/utils/message-repository.test.ts +49 -0
- package/src/runtime/utils/message-repository.ts +3 -3
- package/src/runtime/utils/thread-message-like.test.ts +52 -0
- package/src/runtime/utils/thread-message-like.ts +4 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +31 -1
- package/src/runtimes/local/local-thread-runtime-core.test.ts +21 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +11 -1
- package/src/runtimes/tool-invocations/EDGE_CASES.md +2 -6
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +133 -0
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +10 -6
- package/src/store/clients/external-thread.ts +19 -1
- package/src/store/clients/thread-tasks.test.ts +274 -0
- package/src/store/clients/thread-tasks.ts +138 -0
- package/src/store/index.ts +8 -0
- package/src/store/primitive-predicates.test.ts +48 -5
- package/src/store/primitive-predicates.ts +7 -2
- package/src/store/runtime-clients/thread-runtime-client.ts +31 -1
- package/src/store/scope-registration.ts +2 -0
- package/src/store/scopes/task.ts +34 -0
- package/src/store/scopes/thread.ts +16 -0
- package/src/store/thread-tasks.test.tsx +209 -0
- package/src/subscribable/subscribable.test.ts +156 -1
- package/src/subscribable/subscribable.ts +25 -15
- package/src/tests/MessageRepository.test.ts +1 -5
- package/src/tests/auiV0Encode.test.ts +300 -0
- package/src/tests/base-composer-runtime-core-send.test.ts +51 -0
- package/src/tests/base-composer-runtime-core.test.ts +314 -2
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +151 -0
- package/src/tests/external-thread-attachments.test.tsx +69 -0
- package/src/tests/external-thread-parity.test.tsx +34 -0
- package/src/tests/groupParts.test.ts +71 -0
- package/src/tests/message-queue.test.ts +253 -0
- package/src/tests/remote-thread-list-reactive-threadId.test.ts +51 -0
- package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +43 -0
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +8 -0
- package/src/utils/abortable-iterable.test.ts +115 -0
- package/src/utils/abortable-iterable.ts +34 -23
- package/src/utils/getGroupStatus.test.ts +70 -4
- package/src/utils/getGroupStatus.ts +35 -12
- package/src/utils/wire-media.test.ts +10 -0
- package/src/utils/wire-media.ts +6 -2
|
@@ -104,6 +104,18 @@ describe("interactableToolName", () => {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
describe("shallowMergeInteractableState", () => {
|
|
107
|
+
it("adds prototype-named fields as own properties", () => {
|
|
108
|
+
const value = { enabled: true };
|
|
109
|
+
const result = shallowMergeInteractableState(
|
|
110
|
+
{ title: "Example" },
|
|
111
|
+
Object.fromEntries([["__proto__", value]]),
|
|
112
|
+
) as Record<string, unknown>;
|
|
113
|
+
|
|
114
|
+
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
|
|
115
|
+
expect(Object.hasOwn(result, "__proto__")).toBe(true);
|
|
116
|
+
expect(result["__proto__"]).toBe(value);
|
|
117
|
+
});
|
|
118
|
+
|
|
107
119
|
it("applies array operations from a baseline", () => {
|
|
108
120
|
const prev = {
|
|
109
121
|
tasks: [
|
|
@@ -453,6 +465,24 @@ describe("gateInteractableComposerMetadata", () => {
|
|
|
453
465
|
expect(gated?.interactables).toEqual([entry("a", { v: 1 })]);
|
|
454
466
|
});
|
|
455
467
|
|
|
468
|
+
it.each(["__proto__", "toString"])(
|
|
469
|
+
"treats a removed prototype-named field %s as a full snapshot",
|
|
470
|
+
(field) => {
|
|
471
|
+
const known = Object.fromEntries([
|
|
472
|
+
[field, { enabled: true }],
|
|
473
|
+
["title", "draft"],
|
|
474
|
+
["stable", true],
|
|
475
|
+
]);
|
|
476
|
+
const current = { title: "edited", stable: true };
|
|
477
|
+
const meta = { interactables: [entry("a", current)] };
|
|
478
|
+
const history = [userMsg([entry("a", known)])];
|
|
479
|
+
|
|
480
|
+
const gated = gateInteractableComposerMetadata(meta, history);
|
|
481
|
+
|
|
482
|
+
expect(gated?.interactables).toEqual([entry("a", current)]);
|
|
483
|
+
},
|
|
484
|
+
);
|
|
485
|
+
|
|
456
486
|
it("omits an interactable the model already knows via its own update_* call", () => {
|
|
457
487
|
const meta = { interactables: [entry("a", { v: 2 }, "note")] };
|
|
458
488
|
const history = [
|
|
@@ -149,7 +149,7 @@ export function shallowMergeInteractableState(
|
|
|
149
149
|
const baseline = isRecord(options?.arrayBaseline)
|
|
150
150
|
? options.arrayBaseline
|
|
151
151
|
: prev;
|
|
152
|
-
const next =
|
|
152
|
+
const next = Object.entries(prev);
|
|
153
153
|
for (const [key, value] of Object.entries(partial)) {
|
|
154
154
|
const baseValue = baseline[key];
|
|
155
155
|
if (Array.isArray(baseValue) && isRecord(value)) {
|
|
@@ -158,12 +158,12 @@ export function shallowMergeInteractableState(
|
|
|
158
158
|
(options.idKeyedFields === undefined || options.idKeyedFields.has(key))
|
|
159
159
|
? () => options.idFactory?.(key)
|
|
160
160
|
: undefined;
|
|
161
|
-
next[key
|
|
161
|
+
next.push([key, applyArrayUpdate(baseValue, value, mintId)]);
|
|
162
162
|
} else {
|
|
163
|
-
next[key
|
|
163
|
+
next.push([key, value]);
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
-
return next;
|
|
166
|
+
return Object.fromEntries(next);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/**
|
|
@@ -179,17 +179,17 @@ function shallowDiffInteractableState(
|
|
|
179
179
|
): Record<string, unknown> | undefined {
|
|
180
180
|
if (!isRecord(known) || !isRecord(next)) return undefined;
|
|
181
181
|
for (const key of Object.keys(known)) {
|
|
182
|
-
if (!(key
|
|
182
|
+
if (!Object.hasOwn(next, key)) return undefined;
|
|
183
183
|
}
|
|
184
|
-
const diff:
|
|
184
|
+
const diff: [string, unknown][] = [];
|
|
185
185
|
for (const [key, value] of Object.entries(next)) {
|
|
186
|
-
if (!(key
|
|
187
|
-
diff[key
|
|
186
|
+
if (!Object.hasOwn(known, key) || !isJSONValueEqual(known[key], value)) {
|
|
187
|
+
diff.push([key, value]);
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
const changed =
|
|
190
|
+
const changed = diff.length;
|
|
191
191
|
if (changed === 0 || changed === Object.keys(next).length) return undefined;
|
|
192
|
-
return diff;
|
|
192
|
+
return Object.fromEntries(diff);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
type ToolCallLikePart = {
|
|
@@ -139,6 +139,250 @@ describe("parseStoredMessageRepository", () => {
|
|
|
139
139
|
expect(repo.messages.map((item) => item.message.id)).toEqual(["valid"]);
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it("drops unreadable parts and attachments while keeping their messages", () => {
|
|
143
|
+
const attachment = {
|
|
144
|
+
id: "attachment-1",
|
|
145
|
+
type: "document",
|
|
146
|
+
name: "notes.txt",
|
|
147
|
+
status: { type: "complete" },
|
|
148
|
+
content: [null, { type: "text", text: "notes" }],
|
|
149
|
+
};
|
|
150
|
+
const repo = parseStoredMessageRepository(
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
headId: "answer",
|
|
153
|
+
messages: [
|
|
154
|
+
{
|
|
155
|
+
message: {
|
|
156
|
+
...storedMessage("question"),
|
|
157
|
+
content: [
|
|
158
|
+
null,
|
|
159
|
+
"text",
|
|
160
|
+
{ type: 1 },
|
|
161
|
+
{ type: "text", text: "hi" },
|
|
162
|
+
],
|
|
163
|
+
attachments: [
|
|
164
|
+
null,
|
|
165
|
+
attachment,
|
|
166
|
+
{ ...attachment, id: 2 },
|
|
167
|
+
{ ...attachment, id: "uploading", status: { type: "running" } },
|
|
168
|
+
{ ...attachment, id: "no-content", content: undefined },
|
|
169
|
+
{ ...attachment, id: "no-name", name: undefined },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
parentId: null,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
message: {
|
|
176
|
+
...storedMessage("answer", "assistant"),
|
|
177
|
+
content: [null, { type: "future-part", value: 1 }],
|
|
178
|
+
},
|
|
179
|
+
parentId: "question",
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const [question, answer] = repo.messages.map((item) => item.message);
|
|
186
|
+
expect(repo.headId).toBe("answer");
|
|
187
|
+
expect(question?.content).toEqual([{ type: "text", text: "hi" }]);
|
|
188
|
+
expect(question?.attachments).toEqual([
|
|
189
|
+
{ ...attachment, content: [{ type: "text", text: "notes" }] },
|
|
190
|
+
]);
|
|
191
|
+
expect(answer?.content).toEqual([{ type: "future-part", value: 1 }]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("drops known parts that are missing a required field", () => {
|
|
195
|
+
const parts = {
|
|
196
|
+
text: { type: "text", text: "hi" },
|
|
197
|
+
reasoning: { type: "reasoning", text: "because" },
|
|
198
|
+
summary: { type: "reasoning", unstable_summary: "Searching the docs" },
|
|
199
|
+
image: { type: "image", image: "https://example.com/a.png" },
|
|
200
|
+
file: { type: "file", data: "SGk=", mimeType: "text/plain" },
|
|
201
|
+
audio: { type: "audio", audio: { data: "SGk=", format: "mp3" } },
|
|
202
|
+
data: { type: "data", name: "weather", data: { sunny: true } },
|
|
203
|
+
url: {
|
|
204
|
+
type: "source",
|
|
205
|
+
sourceType: "url",
|
|
206
|
+
id: "source-1",
|
|
207
|
+
url: "https://example.com",
|
|
208
|
+
},
|
|
209
|
+
document: {
|
|
210
|
+
type: "source",
|
|
211
|
+
sourceType: "document",
|
|
212
|
+
id: "source-2",
|
|
213
|
+
title: "Notes",
|
|
214
|
+
mediaType: "text/plain",
|
|
215
|
+
},
|
|
216
|
+
generativeUI: { type: "generative-ui", spec: { root: "hi" } },
|
|
217
|
+
toolCall: {
|
|
218
|
+
type: "tool-call",
|
|
219
|
+
toolCallId: "call-1",
|
|
220
|
+
toolName: "search",
|
|
221
|
+
args: {},
|
|
222
|
+
argsText: "{}",
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const repo = parseStoredMessageRepository(
|
|
226
|
+
JSON.stringify({
|
|
227
|
+
messages: [
|
|
228
|
+
{
|
|
229
|
+
message: {
|
|
230
|
+
...storedMessage("assistant", "assistant"),
|
|
231
|
+
content: [
|
|
232
|
+
{ type: "text" },
|
|
233
|
+
{ ...parts.reasoning, text: 1 },
|
|
234
|
+
{ type: "image" },
|
|
235
|
+
{ ...parts.file, mimeType: undefined },
|
|
236
|
+
{ type: "audio", audio: null },
|
|
237
|
+
{ type: "data", data: {} },
|
|
238
|
+
{ ...parts.url, url: undefined },
|
|
239
|
+
{ ...parts.document, sourceType: "unknown" },
|
|
240
|
+
{ type: "generative-ui" },
|
|
241
|
+
{ ...parts.toolCall, argsText: undefined },
|
|
242
|
+
...Object.values(parts),
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
parentId: null,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(repo.messages[0]?.message.content).toEqual(Object.values(parts));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("applies the same rules to nested tool call messages", () => {
|
|
255
|
+
const toolCall = {
|
|
256
|
+
type: "tool-call",
|
|
257
|
+
toolCallId: "call-1",
|
|
258
|
+
toolName: "delegate",
|
|
259
|
+
args: {},
|
|
260
|
+
argsText: "{}",
|
|
261
|
+
};
|
|
262
|
+
const repo = parseStoredMessageRepository(
|
|
263
|
+
JSON.stringify({
|
|
264
|
+
messages: [
|
|
265
|
+
{
|
|
266
|
+
message: {
|
|
267
|
+
...storedMessage("parent", "assistant"),
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
...toolCall,
|
|
271
|
+
messages: [
|
|
272
|
+
null,
|
|
273
|
+
{ id: "missing-shell" },
|
|
274
|
+
{
|
|
275
|
+
...storedMessage("nested", "assistant"),
|
|
276
|
+
content: [null, { type: "text", text: "nested" }],
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
{ ...toolCall, toolCallId: "call-2", messages: "invalid" },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
parentId: null,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
expect(repo.messages[0]?.message.content).toEqual([
|
|
290
|
+
{
|
|
291
|
+
...toolCall,
|
|
292
|
+
messages: [
|
|
293
|
+
{
|
|
294
|
+
...storedMessage("nested", "assistant"),
|
|
295
|
+
content: [{ type: "text", text: "nested" }],
|
|
296
|
+
createdAt: new Date("2026-01-01T00:00:00.000Z"),
|
|
297
|
+
metadata: {
|
|
298
|
+
unstable_state: null,
|
|
299
|
+
unstable_annotations: [],
|
|
300
|
+
unstable_data: [],
|
|
301
|
+
steps: [],
|
|
302
|
+
custom: {},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
{ ...toolCall, toolCallId: "call-2" },
|
|
308
|
+
]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("keeps a system message when exactly one readable part remains", () => {
|
|
312
|
+
const repo = parseStoredMessageRepository(
|
|
313
|
+
JSON.stringify({
|
|
314
|
+
messages: [
|
|
315
|
+
{
|
|
316
|
+
message: {
|
|
317
|
+
...storedMessage("unreadable", "system"),
|
|
318
|
+
content: [null],
|
|
319
|
+
},
|
|
320
|
+
parentId: null,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
message: {
|
|
324
|
+
...storedMessage("non-text", "system"),
|
|
325
|
+
content: [
|
|
326
|
+
{ type: "text" },
|
|
327
|
+
{ type: "image", image: "https://example.com/a.png" },
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
parentId: null,
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
message: {
|
|
334
|
+
...storedMessage("recovered", "system"),
|
|
335
|
+
content: [null, { type: "text", text: "Be brief." }],
|
|
336
|
+
},
|
|
337
|
+
parentId: null,
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
expect(
|
|
344
|
+
repo.messages.map(({ message }) => [message.id, message.content]),
|
|
345
|
+
).toEqual([
|
|
346
|
+
["non-text", [{ type: "image", image: "https://example.com/a.png" }]],
|
|
347
|
+
["recovered", [{ type: "text", text: "Be brief." }]],
|
|
348
|
+
]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("stops parsing nested tool call messages past the depth limit", () => {
|
|
352
|
+
let stored: unknown = storedMessage("leaf", "assistant");
|
|
353
|
+
for (let level = 0; level < 150; level += 1) {
|
|
354
|
+
stored = {
|
|
355
|
+
...storedMessage(`level-${level}`, "assistant"),
|
|
356
|
+
content: [
|
|
357
|
+
{
|
|
358
|
+
type: "tool-call",
|
|
359
|
+
toolCallId: `call-${level}`,
|
|
360
|
+
toolName: "delegate",
|
|
361
|
+
args: {},
|
|
362
|
+
argsText: "{}",
|
|
363
|
+
messages: [stored],
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const repo = parseStoredMessageRepository(
|
|
370
|
+
JSON.stringify({ messages: [{ message: stored, parentId: null }] }),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
let message = repo.messages[0]?.message;
|
|
374
|
+
let depth = 0;
|
|
375
|
+
while (message?.role === "assistant") {
|
|
376
|
+
const part = message.content[0];
|
|
377
|
+
const nested =
|
|
378
|
+
part?.type === "tool-call" ? part.messages?.[0] : undefined;
|
|
379
|
+
if (!nested) break;
|
|
380
|
+
message = nested;
|
|
381
|
+
depth += 1;
|
|
382
|
+
}
|
|
383
|
+
expect(depth).toBe(100);
|
|
384
|
+
});
|
|
385
|
+
|
|
142
386
|
it("skips messages whose parent is missing, skipped, or appears later", () => {
|
|
143
387
|
const repo = parseStoredMessageRepository(
|
|
144
388
|
JSON.stringify({
|
|
@@ -9,12 +9,15 @@ import {
|
|
|
9
9
|
} from "react";
|
|
10
10
|
import { useAui } from "@assistant-ui/store";
|
|
11
11
|
import type {
|
|
12
|
+
CompleteAttachment,
|
|
12
13
|
RemoteThreadInitializeResponse,
|
|
13
14
|
RemoteThreadListAdapter,
|
|
14
15
|
RemoteThreadListResponse,
|
|
15
16
|
RemoteThreadMetadata,
|
|
17
|
+
ThreadAssistantMessagePart,
|
|
16
18
|
ThreadHistoryAdapter,
|
|
17
19
|
ThreadMessage,
|
|
20
|
+
ThreadUserMessagePart,
|
|
18
21
|
RunConfig,
|
|
19
22
|
} from "../../index";
|
|
20
23
|
import type {
|
|
@@ -120,7 +123,91 @@ const parseDate = (value: unknown): Date | null => {
|
|
|
120
123
|
const isMessageRole = (value: unknown): value is ThreadMessage["role"] =>
|
|
121
124
|
value === "system" || value === "user" || value === "assistant";
|
|
122
125
|
|
|
123
|
-
const
|
|
126
|
+
const MAX_STORED_MESSAGE_DEPTH = 100;
|
|
127
|
+
|
|
128
|
+
const storedPartGuards = {
|
|
129
|
+
text: (part) => typeof part.text === "string",
|
|
130
|
+
reasoning: (part) =>
|
|
131
|
+
typeof part.text === "string" || typeof part.unstable_summary === "string",
|
|
132
|
+
image: (part) => typeof part.image === "string",
|
|
133
|
+
file: (part) =>
|
|
134
|
+
typeof part.data === "string" && typeof part.mimeType === "string",
|
|
135
|
+
audio: (part) =>
|
|
136
|
+
isRecord(part.audio) &&
|
|
137
|
+
typeof part.audio.data === "string" &&
|
|
138
|
+
typeof part.audio.format === "string",
|
|
139
|
+
data: (part) => typeof part.name === "string",
|
|
140
|
+
source: (part) =>
|
|
141
|
+
typeof part.id === "string" &&
|
|
142
|
+
(part.sourceType === "url"
|
|
143
|
+
? typeof part.url === "string"
|
|
144
|
+
: part.sourceType === "document" &&
|
|
145
|
+
typeof part.title === "string" &&
|
|
146
|
+
typeof part.mediaType === "string"),
|
|
147
|
+
"generative-ui": (part) => isRecord(part.spec),
|
|
148
|
+
"tool-call": (part) =>
|
|
149
|
+
typeof part.toolCallId === "string" &&
|
|
150
|
+
typeof part.toolName === "string" &&
|
|
151
|
+
isRecord(part.args) &&
|
|
152
|
+
typeof part.argsText === "string",
|
|
153
|
+
} satisfies Record<
|
|
154
|
+
(ThreadUserMessagePart | ThreadAssistantMessagePart)["type"],
|
|
155
|
+
(part: Record<string, unknown>) => boolean
|
|
156
|
+
>;
|
|
157
|
+
|
|
158
|
+
const isStoredMessagePart = (
|
|
159
|
+
value: unknown,
|
|
160
|
+
): value is Record<string, unknown> & { type: string } =>
|
|
161
|
+
isRecord(value) &&
|
|
162
|
+
typeof value.type === "string" &&
|
|
163
|
+
(!Object.hasOwn(storedPartGuards, value.type) ||
|
|
164
|
+
storedPartGuards[value.type as keyof typeof storedPartGuards](value));
|
|
165
|
+
|
|
166
|
+
const parseStoredMessageParts = (
|
|
167
|
+
content: unknown[],
|
|
168
|
+
depth: number,
|
|
169
|
+
): unknown[] =>
|
|
170
|
+
content.flatMap((part) => {
|
|
171
|
+
if (!isStoredMessagePart(part)) return [];
|
|
172
|
+
if (part.type !== "tool-call" || part.messages === undefined) return [part];
|
|
173
|
+
|
|
174
|
+
const { messages, ...toolCall } = part;
|
|
175
|
+
if (!Array.isArray(messages)) return [toolCall];
|
|
176
|
+
return [
|
|
177
|
+
{
|
|
178
|
+
...toolCall,
|
|
179
|
+
messages: messages.flatMap((item) => {
|
|
180
|
+
const message = parseStoredThreadMessage(item, depth + 1);
|
|
181
|
+
return message ? [message] : [];
|
|
182
|
+
}),
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const parseStoredAttachment = (value: unknown): CompleteAttachment | null => {
|
|
188
|
+
if (
|
|
189
|
+
!isRecord(value) ||
|
|
190
|
+
typeof value.id !== "string" ||
|
|
191
|
+
typeof value.type !== "string" ||
|
|
192
|
+
typeof value.name !== "string" ||
|
|
193
|
+
!isRecord(value.status) ||
|
|
194
|
+
value.status.type !== "complete" ||
|
|
195
|
+
!Array.isArray(value.content)
|
|
196
|
+
) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
...value,
|
|
202
|
+
content: value.content.filter(isStoredMessagePart),
|
|
203
|
+
} as CompleteAttachment;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const parseStoredThreadMessage = (
|
|
207
|
+
value: unknown,
|
|
208
|
+
depth: number,
|
|
209
|
+
): ThreadMessage | null => {
|
|
210
|
+
if (depth > MAX_STORED_MESSAGE_DEPTH) return null;
|
|
124
211
|
if (!isRecord(value) || typeof value.id !== "string") return null;
|
|
125
212
|
if (!isMessageRole(value.role)) return null;
|
|
126
213
|
if (!Array.isArray(value.content)) return null;
|
|
@@ -143,7 +230,10 @@ const parseStoredThreadMessage = (value: unknown): ThreadMessage | null => {
|
|
|
143
230
|
return {
|
|
144
231
|
id: value.id,
|
|
145
232
|
role: "assistant",
|
|
146
|
-
content:
|
|
233
|
+
content: parseStoredMessageParts(
|
|
234
|
+
value.content,
|
|
235
|
+
depth,
|
|
236
|
+
) as StoredAssistantMessage["content"],
|
|
147
237
|
status: status as StoredAssistantMessage["status"],
|
|
148
238
|
createdAt,
|
|
149
239
|
metadata: {
|
|
@@ -185,9 +275,15 @@ const parseStoredThreadMessage = (value: unknown): ThreadMessage | null => {
|
|
|
185
275
|
return {
|
|
186
276
|
id: value.id,
|
|
187
277
|
role: "user",
|
|
188
|
-
content:
|
|
278
|
+
content: parseStoredMessageParts(
|
|
279
|
+
value.content,
|
|
280
|
+
depth,
|
|
281
|
+
) as StoredUserMessage["content"],
|
|
189
282
|
attachments: Array.isArray(value.attachments)
|
|
190
|
-
?
|
|
283
|
+
? value.attachments.flatMap((item) => {
|
|
284
|
+
const attachment = parseStoredAttachment(item);
|
|
285
|
+
return attachment ? [attachment] : [];
|
|
286
|
+
})
|
|
191
287
|
: [],
|
|
192
288
|
createdAt,
|
|
193
289
|
metadata: {
|
|
@@ -196,12 +292,13 @@ const parseStoredThreadMessage = (value: unknown): ThreadMessage | null => {
|
|
|
196
292
|
};
|
|
197
293
|
}
|
|
198
294
|
|
|
199
|
-
|
|
295
|
+
const content = parseStoredMessageParts(value.content, depth);
|
|
296
|
+
if (content.length !== 1) return null;
|
|
200
297
|
|
|
201
298
|
return {
|
|
202
299
|
id: value.id,
|
|
203
300
|
role: "system",
|
|
204
|
-
content: [
|
|
301
|
+
content: [content[0] as StoredSystemMessage["content"][0]],
|
|
205
302
|
createdAt,
|
|
206
303
|
metadata: {
|
|
207
304
|
custom: metadata.custom,
|
|
@@ -226,7 +323,7 @@ const parseStoredMessageRepositoryItem = (
|
|
|
226
323
|
): ExportedMessageRepositoryItem | null => {
|
|
227
324
|
if (!isRecord(value)) return null;
|
|
228
325
|
|
|
229
|
-
const message = parseStoredThreadMessage(value.message);
|
|
326
|
+
const message = parseStoredThreadMessage(value.message, 0);
|
|
230
327
|
if (!message) return null;
|
|
231
328
|
|
|
232
329
|
const parentId = value.parentId;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, cleanup, render } from "@testing-library/react";
|
|
4
|
+
import { useAui } from "@assistant-ui/store";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { DataRenderers } from "./DataRenderers";
|
|
7
|
+
|
|
8
|
+
type AnyClient = Record<string, any>;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
cleanup();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("DataRenderers", () => {
|
|
15
|
+
it.each(["__proto__", "constructor"])(
|
|
16
|
+
"registers and removes a renderer named %s",
|
|
17
|
+
async (name) => {
|
|
18
|
+
let aui!: AnyClient;
|
|
19
|
+
const Harness = () => {
|
|
20
|
+
aui = useAui({ dataRenderers: DataRenderers() } as never);
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
render(<Harness />);
|
|
24
|
+
|
|
25
|
+
let remove!: () => void;
|
|
26
|
+
await act(async () => {
|
|
27
|
+
remove = aui.dataRenderers().setDataUI(name, () => null);
|
|
28
|
+
await vi.waitFor(() => {
|
|
29
|
+
const renderers = aui.dataRenderers().getState().renderers;
|
|
30
|
+
expect(Object.hasOwn(renderers, name)).toBe(true);
|
|
31
|
+
expect(renderers[name]).toHaveLength(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await act(async () => {
|
|
36
|
+
remove();
|
|
37
|
+
await vi.waitFor(() =>
|
|
38
|
+
expect(
|
|
39
|
+
Object.hasOwn(aui.dataRenderers().getState().renderers, name),
|
|
40
|
+
).toBe(false),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
});
|
|
@@ -3,6 +3,7 @@ import { resource } from "@assistant-ui/tap";
|
|
|
3
3
|
import type { ClientOutput } from "@assistant-ui/store";
|
|
4
4
|
import type { DataRenderersState } from "../types/scopes/dataRenderers";
|
|
5
5
|
import type { DataMessagePartComponent } from "../types/MessagePartComponentTypes";
|
|
6
|
+
import { nullProtoRecord } from "../../utils/record";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Registers renderers for `data` message parts.
|
|
@@ -13,30 +14,33 @@ import type { DataMessagePartComponent } from "../types/MessagePartComponentType
|
|
|
13
14
|
*/
|
|
14
15
|
const useDataRenderers = (): ClientOutput<"dataRenderers"> => {
|
|
15
16
|
const [state, setState] = useState<DataRenderersState>(() => ({
|
|
16
|
-
renderers:
|
|
17
|
+
renderers: nullProtoRecord(),
|
|
17
18
|
fallbacks: [],
|
|
18
19
|
}));
|
|
19
20
|
|
|
20
21
|
const setDataUI = useCallback(
|
|
21
22
|
(name: string, render: DataMessagePartComponent) => {
|
|
22
23
|
setState((prev) => {
|
|
24
|
+
const renderers = nullProtoRecord(prev.renderers);
|
|
25
|
+
renderers[name] = [...(renderers[name] ?? []), render];
|
|
23
26
|
return {
|
|
24
27
|
...prev,
|
|
25
|
-
renderers
|
|
26
|
-
...prev.renderers,
|
|
27
|
-
[name]: [...(prev.renderers[name] ?? []), render],
|
|
28
|
-
},
|
|
28
|
+
renderers,
|
|
29
29
|
};
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
return () => {
|
|
33
33
|
setState((prev) => {
|
|
34
|
+
const renderers = nullProtoRecord(prev.renderers);
|
|
35
|
+
const remaining = renderers[name]?.filter((r) => r !== render) ?? [];
|
|
36
|
+
if (remaining.length > 0) {
|
|
37
|
+
renderers[name] = remaining;
|
|
38
|
+
} else {
|
|
39
|
+
delete renderers[name];
|
|
40
|
+
}
|
|
34
41
|
return {
|
|
35
42
|
...prev,
|
|
36
|
-
renderers
|
|
37
|
-
...prev.renderers,
|
|
38
|
-
[name]: prev.renderers[name]?.filter((r) => r !== render) ?? [],
|
|
39
|
-
},
|
|
43
|
+
renderers,
|
|
40
44
|
};
|
|
41
45
|
});
|
|
42
46
|
};
|
|
@@ -10,7 +10,12 @@ import type { ThreadMessage } from "../../types/message";
|
|
|
10
10
|
const clientHolder: { client: unknown } = { client: null };
|
|
11
11
|
const clientListeners = new Set<() => void>();
|
|
12
12
|
let registeredModelContextProvider:
|
|
13
|
-
| {
|
|
13
|
+
| {
|
|
14
|
+
getModelContext?: () => {
|
|
15
|
+
tools?: Record<string, { parameters?: unknown }>;
|
|
16
|
+
};
|
|
17
|
+
subscribe?: (callback: () => void) => () => void;
|
|
18
|
+
}
|
|
14
19
|
| undefined;
|
|
15
20
|
|
|
16
21
|
const replaceClient = (client: unknown) => {
|
|
@@ -321,6 +326,44 @@ describe("Interactables registration", () => {
|
|
|
321
326
|
expect(stateOf(root, "n1")).toBeUndefined();
|
|
322
327
|
});
|
|
323
328
|
|
|
329
|
+
it("refreshes cached tool parameters while another anchor remains", async () => {
|
|
330
|
+
const schemaA = {
|
|
331
|
+
type: "object" as const,
|
|
332
|
+
properties: { first: { type: "string" } },
|
|
333
|
+
};
|
|
334
|
+
const schemaB = {
|
|
335
|
+
type: "object" as const,
|
|
336
|
+
properties: { second: { type: "number" } },
|
|
337
|
+
};
|
|
338
|
+
root = mount({ threadMessages: [createCall("n1")] });
|
|
339
|
+
const first = root.getValue().register(reg("n1", { stateSchema: schemaA }));
|
|
340
|
+
const second = root
|
|
341
|
+
.getValue()
|
|
342
|
+
.register(reg("n1", { stateSchema: schemaA }));
|
|
343
|
+
await flushMicrotasks();
|
|
344
|
+
|
|
345
|
+
first();
|
|
346
|
+
const replacement = root
|
|
347
|
+
.getValue()
|
|
348
|
+
.register(reg("n1", { stateSchema: schemaB }));
|
|
349
|
+
|
|
350
|
+
const parameters =
|
|
351
|
+
registeredModelContextProvider?.getModelContext?.().tools?.update_note
|
|
352
|
+
?.parameters;
|
|
353
|
+
expect(parameters).toMatchObject({
|
|
354
|
+
properties: {
|
|
355
|
+
id: { type: "string" },
|
|
356
|
+
second: { type: "number" },
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
expect(parameters).not.toMatchObject({
|
|
360
|
+
properties: { first: expect.anything() },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
second();
|
|
364
|
+
replacement();
|
|
365
|
+
});
|
|
366
|
+
|
|
324
367
|
it("installs the update tool UI once per name and removes it with the last anchor", () => {
|
|
325
368
|
const removeToolUI = vi.fn();
|
|
326
369
|
const setToolUI = vi.fn(() => removeToolUI);
|
|
@@ -88,6 +88,9 @@ const useInteractablesResource = ({
|
|
|
88
88
|
|
|
89
89
|
const subscribersRef = useRef(new Set<() => void>());
|
|
90
90
|
const partialSchemaCacheRef = useRef(new Map<string, PartialJSONSchema>());
|
|
91
|
+
const partialSchemaSourceRef = useRef(
|
|
92
|
+
new Map<string, Unstable_InteractableRegistration["stateSchema"]>(),
|
|
93
|
+
);
|
|
91
94
|
const streamBaselinesRef = useRef(
|
|
92
95
|
new Map<string, { targetId: string; state: unknown }>(),
|
|
93
96
|
);
|
|
@@ -409,7 +412,9 @@ const useInteractablesResource = ({
|
|
|
409
412
|
}
|
|
410
413
|
|
|
411
414
|
// The same id re-registers once per anchor (its create call + each update_*).
|
|
412
|
-
if (
|
|
415
|
+
if (partialSchemaSourceRef.current.get(def.id) !== def.stateSchema) {
|
|
416
|
+
partialSchemaSourceRef.current.set(def.id, def.stateSchema);
|
|
417
|
+
partialSchemaCacheRef.current.delete(def.id);
|
|
413
418
|
try {
|
|
414
419
|
const jsonSchema = toJSONSchema(def.stateSchema);
|
|
415
420
|
partialSchemaCacheRef.current.set(
|
|
@@ -497,6 +502,7 @@ const useInteractablesResource = ({
|
|
|
497
502
|
detachedAppStateRef.current.set(def.id, existing.state);
|
|
498
503
|
}
|
|
499
504
|
}
|
|
505
|
+
partialSchemaSourceRef.current.delete(def.id);
|
|
500
506
|
partialSchemaCacheRef.current.delete(def.id);
|
|
501
507
|
const definitions = nullProtoRecord(prev.definitions);
|
|
502
508
|
const persistence = nullProtoRecord(prev.persistence);
|