@assistant-ui/core 0.2.21 → 0.2.22
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/attachment.d.ts.map +1 -1
- package/dist/adapters/speech.d.ts.map +1 -1
- package/dist/adapters/speech.js +9 -2
- package/dist/adapters/speech.js.map +1 -1
- package/dist/adapters/voice.d.ts.map +1 -1
- package/dist/adapters/voice.js +13 -5
- package/dist/adapters/voice.js.map +1 -1
- package/dist/internal.d.ts +3 -1
- package/dist/internal.js +3 -1
- package/dist/model-context/frame/host.d.ts +1 -0
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +5 -0
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +1 -0
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/interactable-composer-metadata.js.map +1 -1
- package/dist/react/AssistantProvider.js.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +77 -50
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.d.ts.map +1 -1
- package/dist/react/model-context/useInteractable.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js +13 -13
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerAddAttachment.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.d.ts.map +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
- package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
- package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
- package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
- package/dist/react/primitives/generativeUI/GenerativeUI.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts +2 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +46 -36
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/primitives/part/PartMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadSuggestions.js.map +1 -1
- package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.d.ts.map +1 -1
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +18 -30
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js.map +1 -1
- package/dist/react/providers/PartByIndexProvider.d.ts.map +1 -1
- package/dist/react/providers/PartByIndexProvider.js +18 -30
- package/dist/react/providers/PartByIndexProvider.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +9 -2
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +49 -8
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +2 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
- package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +1 -1
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.d.ts +1 -0
- package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.js +9 -1
- package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
- package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
- package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
- package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +4 -4
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.js +2 -1
- package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +4 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +41 -14
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +2 -1
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.js +4 -2
- package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.js +2 -2
- package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +4 -1
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.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.map +1 -1
- package/dist/runtimes/local/local-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +5 -2
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +4 -4
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.js +22 -9
- package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +1 -2
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
- package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
- package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/attachment-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-part-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/subscribable/subscribable.d.ts +1 -1
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js +11 -3
- package/dist/subscribable/subscribable.js.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts +2 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.js +5 -2
- package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
- package/dist/types/attachment.d.ts +2 -1
- package/dist/types/attachment.d.ts.map +1 -1
- package/dist/types/attachment.js +6 -0
- package/dist/types/attachment.js.map +1 -0
- package/dist/types/message.d.ts +1 -0
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/data-url.d.ts +9 -0
- package/dist/utils/data-url.d.ts.map +1 -0
- package/dist/utils/data-url.js +14 -0
- package/dist/utils/data-url.js.map +1 -0
- package/dist/utils/notify-event-listeners.d.ts +6 -0
- package/dist/utils/notify-event-listeners.d.ts.map +1 -0
- package/dist/utils/notify-event-listeners.js +16 -0
- package/dist/utils/notify-event-listeners.js.map +1 -0
- package/package.json +10 -9
- package/src/adapters/speech.test.ts +88 -0
- package/src/adapters/speech.ts +17 -2
- package/src/adapters/voice.test.ts +130 -0
- package/src/adapters/voice.ts +26 -6
- package/src/internal.ts +5 -0
- package/src/model-context/frame/host.test.ts +100 -0
- package/src/model-context/frame/host.ts +10 -0
- package/src/model-context/frame/provider.test.ts +83 -0
- package/src/model-context/frame/provider.ts +1 -0
- package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +82 -0
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +94 -54
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +79 -0
- package/src/react/primitive-hooks/useActionBarCopy.ts +4 -3
- package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -3
- package/src/react/primitives/message/MessageParts.tsx +48 -35
- package/src/react/providers/ChainOfThoughtPartByIndexProvider.tsx +19 -2
- package/src/react/providers/PartByIndexProvider.tsx +16 -2
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +18 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +1 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +48 -7
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +69 -0
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +1 -1
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +16 -0
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +2 -1
- package/src/react/runtimes/external-message-converter.test.tsx +21 -0
- package/src/react/runtimes/external-message-converter.ts +1 -1
- package/src/react/runtimes/useLocalRuntime.test.tsx +56 -0
- package/src/react/runtimes/useLocalRuntime.ts +13 -1
- package/src/runtime/api/message-runtime.test.ts +81 -0
- package/src/runtime/api/message-runtime.ts +4 -6
- package/src/runtime/api/thread-list-item-runtime.ts +6 -1
- package/src/runtime/base/base-composer-runtime-core.ts +77 -31
- package/src/runtime/base/base-thread-runtime-core.ts +2 -1
- package/src/runtime/base/default-edit-composer-runtime-core.ts +4 -2
- package/src/runtime/base/default-thread-composer-runtime-core.ts +2 -2
- package/src/runtime/utils/thread-message-like.ts +1 -0
- package/src/runtimes/local/local-thread-runtime-core.test.ts +150 -1
- package/src/runtimes/local/local-thread-runtime-core.ts +4 -2
- package/src/runtimes/remote-thread-list/optimistic-state.test.ts +63 -0
- package/src/runtimes/remote-thread-list/optimistic-state.ts +49 -17
- package/src/runtimes/remote-thread-list/types.ts +1 -1
- package/src/runtimes/tool-invocations/EDGE_CASES.md +13 -19
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +127 -6
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +4 -2
- package/src/subscribable/subscribable.test.ts +58 -0
- package/src/subscribable/subscribable.ts +13 -3
- package/src/tests/OptimisticState-list-race.test.ts +33 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-order.test.ts +223 -0
- package/src/tests/base-composer-runtime-core-addAttachment.test.ts +44 -0
- package/src/tests/base-composer-runtime-core-send.test.ts +433 -5
- package/src/tests/default-edit-composer-runtime-core.test.ts +43 -5
- package/src/tests/event-subscription-listener-errors.test.ts +205 -0
- package/src/tests/remote-thread-list-test-helpers.ts +14 -7
- package/src/tests/thread-message-like.test.ts +57 -0
- package/src/types/attachment.ts +5 -0
- package/src/types/message.ts +1 -0
- package/src/utils/data-url.test.ts +50 -0
- package/src/utils/data-url.ts +9 -0
- package/src/utils/notify-event-listeners.ts +30 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createVoiceSession,
|
|
4
|
+
type VoiceSessionControls,
|
|
5
|
+
type VoiceSessionHelpers,
|
|
6
|
+
} from "./voice";
|
|
7
|
+
|
|
8
|
+
const createTestSession = () => {
|
|
9
|
+
let helpers: VoiceSessionHelpers | undefined;
|
|
10
|
+
const session = createVoiceSession({}, async (sessionHelpers) => {
|
|
11
|
+
helpers = sessionHelpers;
|
|
12
|
+
return {
|
|
13
|
+
disconnect: vi.fn(),
|
|
14
|
+
mute: vi.fn(),
|
|
15
|
+
unmute: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!helpers) throw new Error("Voice session setup did not start");
|
|
20
|
+
return { helpers, session };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const createPendingTestSession = () => {
|
|
24
|
+
let resolveControls!: (controls: VoiceSessionControls) => void;
|
|
25
|
+
const controlsPromise = new Promise<VoiceSessionControls>((resolve) => {
|
|
26
|
+
resolveControls = resolve;
|
|
27
|
+
});
|
|
28
|
+
const controls = {
|
|
29
|
+
disconnect: vi.fn(),
|
|
30
|
+
mute: vi.fn(),
|
|
31
|
+
unmute: vi.fn(),
|
|
32
|
+
};
|
|
33
|
+
const session = createVoiceSession({}, () => controlsPromise);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
controls,
|
|
37
|
+
controlsPromise,
|
|
38
|
+
resolveControls: () => resolveControls(controls),
|
|
39
|
+
session,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.restoreAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("createVoiceSession", () => {
|
|
48
|
+
it("applies mute requested while setup is pending", async () => {
|
|
49
|
+
const { controls, controlsPromise, resolveControls, session } =
|
|
50
|
+
createPendingTestSession();
|
|
51
|
+
|
|
52
|
+
session.mute();
|
|
53
|
+
|
|
54
|
+
expect(session.isMuted).toBe(true);
|
|
55
|
+
expect(controls.mute).not.toHaveBeenCalled();
|
|
56
|
+
|
|
57
|
+
resolveControls();
|
|
58
|
+
await controlsPromise;
|
|
59
|
+
|
|
60
|
+
expect(controls.mute).toHaveBeenCalledOnce();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not apply stale mute after unmuting during setup", async () => {
|
|
64
|
+
const { controls, controlsPromise, resolveControls, session } =
|
|
65
|
+
createPendingTestSession();
|
|
66
|
+
|
|
67
|
+
session.mute();
|
|
68
|
+
session.unmute();
|
|
69
|
+
resolveControls();
|
|
70
|
+
await controlsPromise;
|
|
71
|
+
|
|
72
|
+
expect(session.isMuted).toBe(false);
|
|
73
|
+
expect(controls.mute).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("continues notifying listeners when one throws", () => {
|
|
77
|
+
const listenerError = new Error("listener failed");
|
|
78
|
+
const consoleError = vi
|
|
79
|
+
.spyOn(console, "error")
|
|
80
|
+
.mockImplementation(() => {});
|
|
81
|
+
const { helpers, session } = createTestSession();
|
|
82
|
+
|
|
83
|
+
const transcriptListener = vi.fn();
|
|
84
|
+
session.onTranscript(() => {
|
|
85
|
+
throw listenerError;
|
|
86
|
+
});
|
|
87
|
+
session.onTranscript(transcriptListener);
|
|
88
|
+
|
|
89
|
+
const modeListener = vi.fn();
|
|
90
|
+
session.onModeChange(() => {
|
|
91
|
+
throw listenerError;
|
|
92
|
+
});
|
|
93
|
+
session.onModeChange(modeListener);
|
|
94
|
+
|
|
95
|
+
const volumeListener = vi.fn();
|
|
96
|
+
session.onVolumeChange(() => {
|
|
97
|
+
throw listenerError;
|
|
98
|
+
});
|
|
99
|
+
session.onVolumeChange(volumeListener);
|
|
100
|
+
|
|
101
|
+
const statusListener = vi.fn();
|
|
102
|
+
session.onStatusChange(() => {
|
|
103
|
+
throw listenerError;
|
|
104
|
+
});
|
|
105
|
+
session.onStatusChange(statusListener);
|
|
106
|
+
|
|
107
|
+
helpers.emitTranscript({ role: "assistant", text: "hello" });
|
|
108
|
+
helpers.emitMode("speaking");
|
|
109
|
+
helpers.emitVolume(0.5);
|
|
110
|
+
helpers.end("finished");
|
|
111
|
+
|
|
112
|
+
expect(transcriptListener).toHaveBeenCalledWith({
|
|
113
|
+
role: "assistant",
|
|
114
|
+
text: "hello",
|
|
115
|
+
});
|
|
116
|
+
expect(modeListener).toHaveBeenCalledWith("speaking");
|
|
117
|
+
expect(volumeListener).toHaveBeenCalledWith(0.5);
|
|
118
|
+
expect(statusListener).toHaveBeenCalledWith({
|
|
119
|
+
type: "ended",
|
|
120
|
+
reason: "finished",
|
|
121
|
+
error: undefined,
|
|
122
|
+
});
|
|
123
|
+
expect(helpers.isDisposed()).toBe(true);
|
|
124
|
+
expect(consoleError).toHaveBeenCalledTimes(4);
|
|
125
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
126
|
+
"[assistant-ui] Voice session listener threw an error",
|
|
127
|
+
listenerError,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
});
|
package/src/adapters/voice.ts
CHANGED
|
@@ -57,6 +57,22 @@ export type VoiceSessionHelpers = {
|
|
|
57
57
|
isDisposed: () => boolean;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
const notifyListeners = <T>(
|
|
61
|
+
listeners: ReadonlySet<(value: T) => void>,
|
|
62
|
+
value: T,
|
|
63
|
+
) => {
|
|
64
|
+
for (const listener of listeners) {
|
|
65
|
+
try {
|
|
66
|
+
listener(value);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(
|
|
69
|
+
"[assistant-ui] Voice session listener threw an error",
|
|
70
|
+
error,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
60
76
|
export function createVoiceSession(
|
|
61
77
|
options: { abortSignal?: AbortSignal },
|
|
62
78
|
setup: (helpers: VoiceSessionHelpers) => Promise<VoiceSessionControls>,
|
|
@@ -85,25 +101,25 @@ export function createVoiceSession(
|
|
|
85
101
|
setStatus: (status) => {
|
|
86
102
|
if (disposed) return;
|
|
87
103
|
currentStatus = status;
|
|
88
|
-
|
|
104
|
+
notifyListeners(statusCbs, status);
|
|
89
105
|
},
|
|
90
106
|
end: (reason, error?) => {
|
|
91
107
|
if (disposed) return;
|
|
92
108
|
currentStatus = { type: "ended", reason, error };
|
|
93
|
-
|
|
109
|
+
notifyListeners(statusCbs, currentStatus);
|
|
94
110
|
cleanup();
|
|
95
111
|
},
|
|
96
112
|
emitTranscript: (item) => {
|
|
97
113
|
if (disposed) return;
|
|
98
|
-
|
|
114
|
+
notifyListeners(transcriptCbs, item);
|
|
99
115
|
},
|
|
100
116
|
emitMode: (mode) => {
|
|
101
117
|
if (disposed) return;
|
|
102
|
-
|
|
118
|
+
notifyListeners(modeCbs, mode);
|
|
103
119
|
},
|
|
104
120
|
emitVolume: (volume) => {
|
|
105
121
|
if (disposed) return;
|
|
106
|
-
|
|
122
|
+
notifyListeners(volumeCbs, volume);
|
|
107
123
|
},
|
|
108
124
|
isDisposed: () => disposed,
|
|
109
125
|
};
|
|
@@ -155,7 +171,11 @@ export function createVoiceSession(
|
|
|
155
171
|
try {
|
|
156
172
|
if (disposed) return;
|
|
157
173
|
controls = await setup(helpers);
|
|
158
|
-
if (disposed)
|
|
174
|
+
if (disposed) {
|
|
175
|
+
controls.disconnect();
|
|
176
|
+
} else if (isMuted) {
|
|
177
|
+
controls.mute();
|
|
178
|
+
}
|
|
159
179
|
} catch (error) {
|
|
160
180
|
helpers.end("error", error);
|
|
161
181
|
}
|
package/src/internal.ts
CHANGED
|
@@ -41,6 +41,7 @@ export { CompositeContextProvider } from "./utils/composite-context-provider";
|
|
|
41
41
|
// Shared attachment data-URL encoder, reused by framework adapters so the
|
|
42
42
|
// FileReader fallback lives in one place.
|
|
43
43
|
export { getFileDataURL } from "./adapters/attachment";
|
|
44
|
+
export { isCreateAttachment } from "./types/attachment";
|
|
44
45
|
|
|
45
46
|
// Streaming-stable tool-args stringifier, reused by framework adapters so the
|
|
46
47
|
// key-order stabilization lives in one place.
|
|
@@ -53,6 +54,10 @@ export {
|
|
|
53
54
|
// validation lives in one place.
|
|
54
55
|
export { isJSONValue, isRecord } from "./utils/json/is-json";
|
|
55
56
|
|
|
57
|
+
// Data-URL decoder and http(s) matcher, reused by framework adapters so the
|
|
58
|
+
// outbound part conversion lives in one place.
|
|
59
|
+
export { httpUrlPattern, parseDataUrl } from "./utils/data-url";
|
|
60
|
+
|
|
56
61
|
// Runtime extras helper for external-store adapters. Internal because the
|
|
57
62
|
// tap-native runtime path replaces the `thread.extras` side-channel it wraps.
|
|
58
63
|
export {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { AssistantFrameHost } from "./host";
|
|
3
|
+
import { type FrameMessage, FRAME_MESSAGE_CHANNEL } from "./types";
|
|
4
|
+
|
|
5
|
+
const executionContext = {
|
|
6
|
+
toolCallId: "tool-call",
|
|
7
|
+
abortSignal: new AbortController().signal,
|
|
8
|
+
human: async () => undefined,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const createHost = () => {
|
|
12
|
+
let handleMessage: ((event: MessageEvent) => void) | undefined;
|
|
13
|
+
const addEventListener = vi.fn(
|
|
14
|
+
(_type: string, listener: EventListenerOrEventListenerObject) => {
|
|
15
|
+
handleMessage = listener as (event: MessageEvent) => void;
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
const removeEventListener = vi.fn();
|
|
19
|
+
vi.stubGlobal("window", { addEventListener, removeEventListener });
|
|
20
|
+
|
|
21
|
+
const postMessage = vi.fn();
|
|
22
|
+
const iframeWindow = { postMessage } as unknown as Window;
|
|
23
|
+
const host = new AssistantFrameHost(iframeWindow);
|
|
24
|
+
|
|
25
|
+
const dispatchMessage = (message: FrameMessage) =>
|
|
26
|
+
handleMessage?.({
|
|
27
|
+
source: iframeWindow,
|
|
28
|
+
data: {
|
|
29
|
+
channel: FRAME_MESSAGE_CHANNEL,
|
|
30
|
+
message,
|
|
31
|
+
},
|
|
32
|
+
} as unknown as MessageEvent);
|
|
33
|
+
|
|
34
|
+
dispatchMessage({
|
|
35
|
+
type: "model-context-update",
|
|
36
|
+
context: {
|
|
37
|
+
tools: {
|
|
38
|
+
search: {
|
|
39
|
+
type: "frontend",
|
|
40
|
+
parameters: { type: "object", properties: {} },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const execute = host.getModelContext().tools?.search?.execute;
|
|
47
|
+
if (!execute) throw new Error("Expected the search tool to be available");
|
|
48
|
+
|
|
49
|
+
return { dispatchMessage, execute, host, postMessage };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.useFakeTimers();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
vi.useRealTimers();
|
|
58
|
+
vi.unstubAllGlobals();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("AssistantFrameHost", () => {
|
|
62
|
+
it("resolves tool calls from frame results", async () => {
|
|
63
|
+
const { dispatchMessage, execute, host } = createHost();
|
|
64
|
+
const result = Promise.resolve(
|
|
65
|
+
execute({ query: "weather" }, executionContext),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
dispatchMessage({
|
|
69
|
+
type: "tool-result",
|
|
70
|
+
id: "tool-0",
|
|
71
|
+
result: "sunny",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await expect(result).resolves.toBe("sunny");
|
|
75
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
76
|
+
host.dispose();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("rejects pending tool calls when disposed", async () => {
|
|
80
|
+
const { execute, host } = createHost();
|
|
81
|
+
const result = Promise.resolve(execute({}, executionContext));
|
|
82
|
+
|
|
83
|
+
host.dispose();
|
|
84
|
+
|
|
85
|
+
await expect(result).rejects.toThrow(
|
|
86
|
+
"AssistantFrameHost has been disposed",
|
|
87
|
+
);
|
|
88
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("rejects tool calls made after disposal without posting a request", async () => {
|
|
92
|
+
const { execute, host, postMessage } = createHost();
|
|
93
|
+
host.dispose();
|
|
94
|
+
|
|
95
|
+
await expect(execute({}, executionContext)).rejects.toThrow(
|
|
96
|
+
"AssistantFrameHost has been disposed",
|
|
97
|
+
);
|
|
98
|
+
expect(postMessage).toHaveBeenCalledOnce();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -53,6 +53,7 @@ export class AssistantFrameHost implements ModelContextProvider {
|
|
|
53
53
|
private _requestCounter = 0;
|
|
54
54
|
private _iframeWindow: Window;
|
|
55
55
|
private _targetOrigin: string;
|
|
56
|
+
private _disposed = false;
|
|
56
57
|
|
|
57
58
|
constructor(iframeWindow: Window, targetOrigin: string = "*") {
|
|
58
59
|
this._iframeWindow = iframeWindow;
|
|
@@ -130,6 +131,10 @@ export class AssistantFrameHost implements ModelContextProvider {
|
|
|
130
131
|
timeout = 30000,
|
|
131
132
|
timeoutMessage = "Request timed out",
|
|
132
133
|
): Promise<any> {
|
|
134
|
+
if (this._disposed) {
|
|
135
|
+
return Promise.reject(new Error("AssistantFrameHost has been disposed"));
|
|
136
|
+
}
|
|
137
|
+
|
|
133
138
|
return new Promise((resolve, reject) => {
|
|
134
139
|
this._pendingRequests.set(message.id, { resolve, reject });
|
|
135
140
|
|
|
@@ -188,8 +193,13 @@ export class AssistantFrameHost implements ModelContextProvider {
|
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
dispose() {
|
|
196
|
+
this._disposed = true;
|
|
191
197
|
window.removeEventListener("message", this.handleMessage);
|
|
192
198
|
this._subscribers.clear();
|
|
199
|
+
const error = new Error("AssistantFrameHost has been disposed");
|
|
200
|
+
for (const pending of this._pendingRequests.values()) {
|
|
201
|
+
pending.reject(error);
|
|
202
|
+
}
|
|
193
203
|
this._pendingRequests.clear();
|
|
194
204
|
}
|
|
195
205
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { AssistantFrameProvider } from "./provider";
|
|
6
|
+
import { FRAME_MESSAGE_CHANNEL } from "./types";
|
|
7
|
+
|
|
8
|
+
describe("AssistantFrameProvider", () => {
|
|
9
|
+
let messageHandler: ((event: MessageEvent) => void) | undefined;
|
|
10
|
+
let parentWindow: Window;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
parentWindow = {
|
|
14
|
+
postMessage: vi.fn(),
|
|
15
|
+
} as unknown as Window;
|
|
16
|
+
|
|
17
|
+
Object.defineProperty(window, "parent", {
|
|
18
|
+
value: parentWindow,
|
|
19
|
+
configurable: true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
vi.spyOn(window, "addEventListener").mockImplementation(
|
|
23
|
+
(event, listener) => {
|
|
24
|
+
if (event === "message" && typeof listener === "function") {
|
|
25
|
+
messageHandler = listener as (event: MessageEvent) => void;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
vi.spyOn(window, "removeEventListener").mockImplementation(() => {});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
AssistantFrameProvider.dispose();
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("only accepts tool calls from the parent window", async () => {
|
|
38
|
+
const execute = vi.fn(async () => "result");
|
|
39
|
+
AssistantFrameProvider.addModelContextProvider(
|
|
40
|
+
{
|
|
41
|
+
getModelContext: () => ({
|
|
42
|
+
tools: {
|
|
43
|
+
sensitiveTool: { execute },
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
"https://parent.example",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const toolCall = {
|
|
51
|
+
channel: FRAME_MESSAGE_CHANNEL,
|
|
52
|
+
message: {
|
|
53
|
+
type: "tool-call",
|
|
54
|
+
id: "tool-call-1",
|
|
55
|
+
toolName: "sensitiveTool",
|
|
56
|
+
args: {},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const otherWindow = {
|
|
60
|
+
postMessage: vi.fn(),
|
|
61
|
+
} as unknown as Window;
|
|
62
|
+
|
|
63
|
+
messageHandler?.(
|
|
64
|
+
new MessageEvent("message", {
|
|
65
|
+
data: toolCall,
|
|
66
|
+
origin: "https://parent.example",
|
|
67
|
+
source: otherWindow,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(execute).not.toHaveBeenCalled();
|
|
72
|
+
|
|
73
|
+
messageHandler?.(
|
|
74
|
+
new MessageEvent("message", {
|
|
75
|
+
data: toolCall,
|
|
76
|
+
origin: "https://parent.example",
|
|
77
|
+
source: parentWindow,
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await vi.waitFor(() => expect(execute).toHaveBeenCalledOnce());
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -59,6 +59,7 @@ export class AssistantFrameProvider {
|
|
|
59
59
|
private handleMessage(event: MessageEvent) {
|
|
60
60
|
if (this._targetOrigin !== "*" && event.origin !== this._targetOrigin)
|
|
61
61
|
return;
|
|
62
|
+
if (event.source !== window.parent) return;
|
|
62
63
|
if (event.data?.channel !== FRAME_MESSAGE_CHANNEL) return;
|
|
63
64
|
|
|
64
65
|
const message = event.data.message as FrameMessage;
|
|
@@ -207,6 +207,88 @@ describe("createLocalStorageAdapter", () => {
|
|
|
207
207
|
]);
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
+
it("preserves concurrent metadata mutations across adapters", async () => {
|
|
211
|
+
const threadsKey = "@assistant-ui:threads";
|
|
212
|
+
const values = new Map<string, string>();
|
|
213
|
+
let metadataReads = 0;
|
|
214
|
+
let metadataWrites = 0;
|
|
215
|
+
let markFirstWriteStarted!: () => void;
|
|
216
|
+
let releaseFirstWrite!: () => void;
|
|
217
|
+
const firstWriteStarted = new Promise<void>((resolve) => {
|
|
218
|
+
markFirstWriteStarted = resolve;
|
|
219
|
+
});
|
|
220
|
+
const firstWriteCanFinish = new Promise<void>((resolve) => {
|
|
221
|
+
releaseFirstWrite = resolve;
|
|
222
|
+
});
|
|
223
|
+
const storage: AsyncStorageLike = {
|
|
224
|
+
getItem: async (key) => {
|
|
225
|
+
if (key === threadsKey) metadataReads += 1;
|
|
226
|
+
return values.get(key) ?? null;
|
|
227
|
+
},
|
|
228
|
+
setItem: async (key, value) => {
|
|
229
|
+
if (key === threadsKey) {
|
|
230
|
+
metadataWrites += 1;
|
|
231
|
+
if (metadataWrites === 1) {
|
|
232
|
+
markFirstWriteStarted();
|
|
233
|
+
await firstWriteCanFinish;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
values.set(key, value);
|
|
237
|
+
},
|
|
238
|
+
removeItem: async (key) => {
|
|
239
|
+
values.delete(key);
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
const firstAdapter = createLocalStorageAdapter({ storage });
|
|
243
|
+
const secondAdapter = createLocalStorageAdapter({ storage });
|
|
244
|
+
|
|
245
|
+
const firstInitialization = firstAdapter.initialize("thread-1");
|
|
246
|
+
await firstWriteStarted;
|
|
247
|
+
const secondInitialization = secondAdapter.initialize("thread-2");
|
|
248
|
+
const readsWhileFirstWritePending = metadataReads;
|
|
249
|
+
|
|
250
|
+
releaseFirstWrite();
|
|
251
|
+
await Promise.all([firstInitialization, secondInitialization]);
|
|
252
|
+
|
|
253
|
+
expect(readsWhileFirstWritePending).toBe(1);
|
|
254
|
+
expect(JSON.parse(values.get(threadsKey) ?? "")).toEqual([
|
|
255
|
+
{ remoteId: "thread-2", status: "regular" },
|
|
256
|
+
{ remoteId: "thread-1", status: "regular" },
|
|
257
|
+
]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("continues processing mutations after a storage failure", async () => {
|
|
261
|
+
const threadsKey = "@assistant-ui:threads";
|
|
262
|
+
const values = new Map<string, string>();
|
|
263
|
+
let shouldFail = true;
|
|
264
|
+
const storage: AsyncStorageLike = {
|
|
265
|
+
getItem: async (key) => values.get(key) ?? null,
|
|
266
|
+
setItem: async (key, value) => {
|
|
267
|
+
if (shouldFail) {
|
|
268
|
+
shouldFail = false;
|
|
269
|
+
throw new Error("Storage unavailable");
|
|
270
|
+
}
|
|
271
|
+
values.set(key, value);
|
|
272
|
+
},
|
|
273
|
+
removeItem: async (key) => {
|
|
274
|
+
values.delete(key);
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
const adapter = createLocalStorageAdapter({ storage });
|
|
278
|
+
|
|
279
|
+
await expect(adapter.initialize("thread-1")).rejects.toThrow(
|
|
280
|
+
"Storage unavailable",
|
|
281
|
+
);
|
|
282
|
+
await expect(adapter.initialize("thread-2")).resolves.toEqual({
|
|
283
|
+
remoteId: "thread-2",
|
|
284
|
+
externalId: undefined,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(JSON.parse(values.get(threadsKey) ?? "")).toEqual([
|
|
288
|
+
{ remoteId: "thread-2", status: "regular" },
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
|
|
210
292
|
it("includes the thread id when a stored thread cannot be fetched", async () => {
|
|
211
293
|
const storage = createStorage({
|
|
212
294
|
"@assistant-ui:threads": JSON.stringify([
|