@assistant-ui/core 0.3.14 → 0.3.15
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/voice.js +11 -0
- package/dist/adapters/voice.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.js +2 -1
- package/dist/model-context/frame/host.d.ts +1 -1
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +17 -3
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts +4 -0
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +50 -3
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/frame/types.d.ts +4 -1
- package/dist/model-context/frame/types.d.ts.map +1 -1
- package/dist/model-context/frame/types.js.map +1 -1
- package/dist/react/client/RemoteThreadList.d.ts +1 -1
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +95 -23
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js +7 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts +3 -0
- package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsEmpty.js +3 -0
- package/dist/react/primitive-hooks/useThreadIsEmpty.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsRunning.d.ts +3 -0
- package/dist/react/primitive-hooks/useThreadIsRunning.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsRunning.js +3 -0
- package/dist/react/primitive-hooks/useThreadIsRunning.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +2 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +6 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +6 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +192 -50
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.js +2 -0
- package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
- package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js +1 -1
- package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +38 -5
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +27 -18
- package/dist/runtime/utils/message-repository.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 +2 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +102 -0
- package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -0
- package/dist/runtimes/inert/InertThreadRuntimeCore.js +156 -0
- package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +25 -6
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +12 -60
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +10 -145
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/adapter-changed.d.ts +9 -0
- package/dist/runtimes/remote-thread-list/adapter-changed.d.ts.map +1 -0
- package/dist/runtimes/remote-thread-list/adapter-changed.js +13 -0
- package/dist/runtimes/remote-thread-list/adapter-changed.js.map +1 -0
- package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +16 -155
- package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +2 -0
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.js +11 -0
- package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +3 -0
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +1 -1
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/runtime-clients/handle-runtime-action.d.ts.map +1 -1
- package/dist/store/runtime-clients/handle-runtime-action.js +2 -0
- package/dist/store/runtime-clients/handle-runtime-action.js.map +1 -1
- package/dist/types/message.d.ts +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/silent-runtime-action.d.ts +6 -0
- package/dist/utils/silent-runtime-action.d.ts.map +1 -0
- package/dist/utils/silent-runtime-action.js +7 -0
- package/dist/utils/silent-runtime-action.js.map +1 -0
- package/package.json +4 -4
- package/src/adapters/voice.test.ts +66 -1
- package/src/adapters/voice.ts +8 -0
- package/src/internal.ts +1 -0
- package/src/model-context/frame/host.test.ts +64 -5
- package/src/model-context/frame/host.ts +16 -3
- package/src/model-context/frame/provider.test.ts +347 -20
- package/src/model-context/frame/provider.ts +58 -2
- package/src/model-context/frame/types.ts +5 -0
- package/src/react/client/RemoteThreadList.test.ts +196 -0
- package/src/react/client/RemoteThreadList.ts +93 -24
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +15 -0
- package/src/react/primitive-hooks/useActionBarCopy.ts +13 -3
- package/src/react/primitive-hooks/useThreadIsEmpty.ts +3 -0
- package/src/react/primitive-hooks/useThreadIsRunning.ts +3 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +12 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.republish.test.ts +87 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +268 -63
- package/src/react/runtimes/RemoteThreadResource.ts +2 -0
- package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.test.tsx +58 -0
- package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.ts +3 -1
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +98 -3
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +68 -6
- package/src/react/runtimes/useLocalRuntime.test.tsx +5 -5
- package/src/runtime/utils/message-repository.ts +26 -18
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +4 -0
- package/src/runtimes/inert/InertThreadRuntimeCore.test.ts +253 -0
- package/src/runtimes/inert/InertThreadRuntimeCore.ts +232 -0
- package/src/runtimes/local/local-thread-runtime-core.test.ts +319 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +38 -8
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +17 -209
- package/src/runtimes/remote-thread-list/adapter-changed.ts +10 -0
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +28 -221
- package/src/runtimes/remote-thread-list/optimistic-state.test.ts +18 -0
- package/src/runtimes/remote-thread-list/optimistic-state.ts +13 -0
- package/src/runtimes/remote-thread-list/types.ts +4 -0
- package/src/store/clients/external-thread.ts +1 -1
- package/src/store/runtime-clients/handle-runtime-action.test.ts +40 -0
- package/src/store/runtime-clients/handle-runtime-action.ts +3 -0
- package/src/tests/MessageRepository.test.ts +48 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-errors.test.ts +2 -2
- package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +8 -6
- package/src/tests/empty-thread-core.test.ts +6 -0
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +20 -0
- package/src/tests/external-thread-isEmpty.test.tsx +100 -0
- package/src/tests/remote-thread-list-adapter-switch.test.ts +383 -0
- package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +67 -0
- package/src/types/message.ts +1 -1
- package/src/utils/silent-runtime-action.ts +6 -0
|
@@ -9,6 +9,45 @@ describe("AssistantFrameProvider", () => {
|
|
|
9
9
|
let messageHandler: ((event: MessageEvent) => void) | undefined;
|
|
10
10
|
let parentWindow: Window;
|
|
11
11
|
|
|
12
|
+
const dispatchToolCall = (
|
|
13
|
+
origin: string,
|
|
14
|
+
source: Window = parentWindow,
|
|
15
|
+
id = "tool-call-1",
|
|
16
|
+
) => {
|
|
17
|
+
messageHandler?.(
|
|
18
|
+
new MessageEvent("message", {
|
|
19
|
+
data: {
|
|
20
|
+
channel: FRAME_MESSAGE_CHANNEL,
|
|
21
|
+
message: {
|
|
22
|
+
type: "tool-call",
|
|
23
|
+
id,
|
|
24
|
+
toolName: "sensitiveTool",
|
|
25
|
+
args: {},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
origin,
|
|
29
|
+
source,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const dispatchToolCancel = (
|
|
35
|
+
origin: string,
|
|
36
|
+
source: Window = parentWindow,
|
|
37
|
+
id = "tool-call-1",
|
|
38
|
+
) => {
|
|
39
|
+
messageHandler?.(
|
|
40
|
+
new MessageEvent("message", {
|
|
41
|
+
data: {
|
|
42
|
+
channel: FRAME_MESSAGE_CHANNEL,
|
|
43
|
+
message: { type: "tool-cancel", id },
|
|
44
|
+
},
|
|
45
|
+
origin,
|
|
46
|
+
source,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
12
51
|
beforeEach(() => {
|
|
13
52
|
parentWindow = {
|
|
14
53
|
postMessage: vi.fn(),
|
|
@@ -47,37 +86,325 @@ describe("AssistantFrameProvider", () => {
|
|
|
47
86
|
"https://parent.example",
|
|
48
87
|
);
|
|
49
88
|
|
|
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
89
|
const otherWindow = {
|
|
60
90
|
postMessage: vi.fn(),
|
|
61
91
|
} as unknown as Window;
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
dispatchToolCall("https://parent.example", otherWindow);
|
|
94
|
+
|
|
95
|
+
expect(execute).not.toHaveBeenCalled();
|
|
96
|
+
|
|
97
|
+
dispatchToolCall("https://parent.example");
|
|
98
|
+
|
|
99
|
+
await vi.waitFor(() => expect(execute).toHaveBeenCalledOnce());
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("aborts in-flight tool calls when the parent cancels them", async () => {
|
|
103
|
+
let toolSignal: AbortSignal | undefined;
|
|
104
|
+
const execute = vi.fn(
|
|
105
|
+
async (_args: unknown, context: { abortSignal: AbortSignal }) => {
|
|
106
|
+
toolSignal = context.abortSignal;
|
|
107
|
+
await new Promise<never>((_resolve, reject) => {
|
|
108
|
+
context.abortSignal.addEventListener(
|
|
109
|
+
"abort",
|
|
110
|
+
() => reject(context.abortSignal.reason),
|
|
111
|
+
{ once: true },
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
AssistantFrameProvider.addModelContextProvider({
|
|
117
|
+
getModelContext: () => ({
|
|
118
|
+
tools: { sensitiveTool: { execute } },
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
dispatchToolCall("*");
|
|
123
|
+
await vi.waitFor(() => expect(toolSignal).toBeDefined());
|
|
124
|
+
|
|
125
|
+
dispatchToolCancel("*");
|
|
126
|
+
|
|
127
|
+
expect(toolSignal?.aborted).toBe(true);
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
129
|
+
expect(parentWindow.postMessage).not.toHaveBeenCalledWith(
|
|
130
|
+
expect.objectContaining({
|
|
131
|
+
message: expect.objectContaining({ type: "tool-result" }),
|
|
68
132
|
}),
|
|
133
|
+
expect.anything(),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("cancels only the matching in-flight tool call", async () => {
|
|
138
|
+
const signals = new Map<string, AbortSignal>();
|
|
139
|
+
const execute = vi.fn(
|
|
140
|
+
async (
|
|
141
|
+
_args: unknown,
|
|
142
|
+
context: { toolCallId: string; abortSignal: AbortSignal },
|
|
143
|
+
) => {
|
|
144
|
+
signals.set(context.toolCallId, context.abortSignal);
|
|
145
|
+
await new Promise<never>((_resolve, reject) => {
|
|
146
|
+
context.abortSignal.addEventListener(
|
|
147
|
+
"abort",
|
|
148
|
+
() => reject(context.abortSignal.reason),
|
|
149
|
+
{ once: true },
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
AssistantFrameProvider.addModelContextProvider({
|
|
155
|
+
getModelContext: () => ({ tools: { sensitiveTool: { execute } } }),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
dispatchToolCall("*", parentWindow, "tool-a");
|
|
159
|
+
dispatchToolCall("*", parentWindow, "tool-b");
|
|
160
|
+
await vi.waitFor(() => expect(signals.size).toBe(2));
|
|
161
|
+
|
|
162
|
+
dispatchToolCancel("*", parentWindow, "tool-a");
|
|
163
|
+
|
|
164
|
+
expect(signals.get("tool-a")?.aborted).toBe(true);
|
|
165
|
+
expect(signals.get("tool-b")?.aborted).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("aborts an earlier call when a duplicate ID arrives", async () => {
|
|
169
|
+
const signals: AbortSignal[] = [];
|
|
170
|
+
const execute = vi.fn(
|
|
171
|
+
async (_args: unknown, context: { abortSignal: AbortSignal }) => {
|
|
172
|
+
signals.push(context.abortSignal);
|
|
173
|
+
await new Promise<never>((_resolve, reject) => {
|
|
174
|
+
context.abortSignal.addEventListener(
|
|
175
|
+
"abort",
|
|
176
|
+
() => reject(context.abortSignal.reason),
|
|
177
|
+
{ once: true },
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
AssistantFrameProvider.addModelContextProvider({
|
|
183
|
+
getModelContext: () => ({ tools: { sensitiveTool: { execute } } }),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
dispatchToolCall("*", parentWindow, "duplicate");
|
|
187
|
+
await vi.waitFor(() => expect(signals).toHaveLength(1));
|
|
188
|
+
dispatchToolCall("*", parentWindow, "duplicate");
|
|
189
|
+
await vi.waitFor(() => expect(signals).toHaveLength(2));
|
|
190
|
+
|
|
191
|
+
expect(signals[0]?.aborted).toBe(true);
|
|
192
|
+
expect(signals[1]?.aborted).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("aborts in-flight tool calls when the provider is disposed", async () => {
|
|
196
|
+
let toolSignal: AbortSignal | undefined;
|
|
197
|
+
const execute = vi.fn(
|
|
198
|
+
async (_args: unknown, context: { abortSignal: AbortSignal }) => {
|
|
199
|
+
toolSignal = context.abortSignal;
|
|
200
|
+
await new Promise<never>((_resolve, reject) => {
|
|
201
|
+
context.abortSignal.addEventListener(
|
|
202
|
+
"abort",
|
|
203
|
+
() => reject(context.abortSignal.reason),
|
|
204
|
+
{ once: true },
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
AssistantFrameProvider.addModelContextProvider({
|
|
210
|
+
getModelContext: () => ({ tools: { sensitiveTool: { execute } } }),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
dispatchToolCall("*");
|
|
214
|
+
await vi.waitFor(() => expect(toolSignal).toBeDefined());
|
|
215
|
+
|
|
216
|
+
AssistantFrameProvider.dispose();
|
|
217
|
+
|
|
218
|
+
expect(toolSignal?.aborted).toBe(true);
|
|
219
|
+
expect(parentWindow.postMessage).toHaveBeenCalledWith(
|
|
220
|
+
{
|
|
221
|
+
channel: FRAME_MESSAGE_CHANNEL,
|
|
222
|
+
message: {
|
|
223
|
+
type: "tool-result",
|
|
224
|
+
id: "tool-call-1",
|
|
225
|
+
error: "AssistantFrameProvider has been disposed",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{ targetOrigin: "*" },
|
|
229
|
+
);
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
231
|
+
const toolResults = vi
|
|
232
|
+
.mocked(parentWindow.postMessage)
|
|
233
|
+
.mock.calls.filter(
|
|
234
|
+
([data]) =>
|
|
235
|
+
(data as { message?: { type?: string } }).message?.type ===
|
|
236
|
+
"tool-result",
|
|
237
|
+
);
|
|
238
|
+
expect(toolResults).toHaveLength(1);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("upgrades a wildcard origin policy when a strict provider registers", async () => {
|
|
242
|
+
AssistantFrameProvider.addModelContextProvider(
|
|
243
|
+
{ getModelContext: () => ({}) },
|
|
244
|
+
"*",
|
|
69
245
|
);
|
|
70
246
|
|
|
247
|
+
const execute = vi.fn(async () => "result");
|
|
248
|
+
AssistantFrameProvider.addModelContextProvider(
|
|
249
|
+
{
|
|
250
|
+
getModelContext: () => ({
|
|
251
|
+
tools: {
|
|
252
|
+
sensitiveTool: { execute },
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
256
|
+
"https://parent.example",
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
|
|
260
|
+
expect.anything(),
|
|
261
|
+
"https://parent.example",
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
dispatchToolCall("https://untrusted.example");
|
|
265
|
+
|
|
71
266
|
expect(execute).not.toHaveBeenCalled();
|
|
72
267
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
268
|
+
dispatchToolCall("https://parent.example");
|
|
269
|
+
|
|
270
|
+
await vi.waitFor(() => expect(execute).toHaveBeenCalledOnce());
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("does not downgrade a strict origin policy for a wildcard provider", async () => {
|
|
274
|
+
const execute = vi.fn(async () => "result");
|
|
275
|
+
AssistantFrameProvider.addModelContextProvider(
|
|
276
|
+
{
|
|
277
|
+
getModelContext: () => ({
|
|
278
|
+
tools: {
|
|
279
|
+
sensitiveTool: { execute },
|
|
280
|
+
},
|
|
281
|
+
}),
|
|
282
|
+
},
|
|
283
|
+
"https://parent.example",
|
|
284
|
+
);
|
|
285
|
+
AssistantFrameProvider.addModelContextProvider(
|
|
286
|
+
{ getModelContext: () => ({}) },
|
|
287
|
+
"*",
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
dispatchToolCall("https://untrusted.example");
|
|
291
|
+
|
|
292
|
+
expect(execute).not.toHaveBeenCalled();
|
|
293
|
+
|
|
294
|
+
dispatchToolCall("https://parent.example");
|
|
295
|
+
|
|
296
|
+
await vi.waitFor(() => expect(execute).toHaveBeenCalledOnce());
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("rejects conflicting strict origin policies", () => {
|
|
300
|
+
AssistantFrameProvider.addModelContextProvider(
|
|
301
|
+
{ getModelContext: () => ({}) },
|
|
302
|
+
"https://first.example",
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
expect(() =>
|
|
306
|
+
AssistantFrameProvider.addModelContextProvider(
|
|
307
|
+
{ getModelContext: () => ({}) },
|
|
308
|
+
"https://second.example",
|
|
309
|
+
),
|
|
310
|
+
).toThrow(
|
|
311
|
+
'AssistantFrameProvider cannot register conflicting target origins: "https://first.example" and "https://second.example"',
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("resets the origin policy after every provider unsubscribes", () => {
|
|
316
|
+
const unsubscribe = AssistantFrameProvider.addModelContextProvider(
|
|
317
|
+
{ getModelContext: () => ({}) },
|
|
318
|
+
"https://first.example",
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
unsubscribe();
|
|
322
|
+
|
|
323
|
+
expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
|
|
324
|
+
expect.anything(),
|
|
325
|
+
"*",
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(() =>
|
|
329
|
+
AssistantFrameProvider.addModelContextProvider(
|
|
330
|
+
{ getModelContext: () => ({}) },
|
|
331
|
+
"https://second.example",
|
|
332
|
+
),
|
|
333
|
+
).not.toThrow();
|
|
334
|
+
expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
|
|
335
|
+
expect.anything(),
|
|
336
|
+
"https://second.example",
|
|
79
337
|
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("treats a second unsubscribe as a no-op", () => {
|
|
341
|
+
const unsubscribe = AssistantFrameProvider.addModelContextProvider(
|
|
342
|
+
{ getModelContext: () => ({}) },
|
|
343
|
+
"https://first.example",
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
unsubscribe();
|
|
347
|
+
unsubscribe();
|
|
348
|
+
|
|
349
|
+
const unsubscribeSecond = AssistantFrameProvider.addModelContextProvider(
|
|
350
|
+
{ getModelContext: () => ({}) },
|
|
351
|
+
"https://second.example",
|
|
352
|
+
);
|
|
353
|
+
expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
|
|
354
|
+
expect.anything(),
|
|
355
|
+
"https://second.example",
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
unsubscribeSecond();
|
|
359
|
+
unsubscribeSecond();
|
|
360
|
+
|
|
361
|
+
expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
|
|
362
|
+
expect.anything(),
|
|
363
|
+
"*",
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("recomputes the origin policy from providers that remain", () => {
|
|
368
|
+
AssistantFrameProvider.addModelContextProvider(
|
|
369
|
+
{ getModelContext: () => ({}) },
|
|
370
|
+
"*",
|
|
371
|
+
);
|
|
372
|
+
const unsubscribeStrict = AssistantFrameProvider.addModelContextProvider(
|
|
373
|
+
{ getModelContext: () => ({}) },
|
|
374
|
+
"https://parent.example",
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
unsubscribeStrict();
|
|
378
|
+
|
|
379
|
+
expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
|
|
380
|
+
expect.anything(),
|
|
381
|
+
"*",
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("keeps a shared strict origin after one provider unsubscribes", async () => {
|
|
386
|
+
const unsubscribeFirst = AssistantFrameProvider.addModelContextProvider(
|
|
387
|
+
{ getModelContext: () => ({}) },
|
|
388
|
+
"https://parent.example",
|
|
389
|
+
);
|
|
390
|
+
const execute = vi.fn(async () => "result");
|
|
391
|
+
AssistantFrameProvider.addModelContextProvider(
|
|
392
|
+
{
|
|
393
|
+
getModelContext: () => ({
|
|
394
|
+
tools: {
|
|
395
|
+
sensitiveTool: { execute },
|
|
396
|
+
},
|
|
397
|
+
}),
|
|
398
|
+
},
|
|
399
|
+
"https://parent.example",
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
unsubscribeFirst();
|
|
403
|
+
|
|
404
|
+
dispatchToolCall("https://untrusted.example");
|
|
405
|
+
expect(execute).not.toHaveBeenCalled();
|
|
80
406
|
|
|
407
|
+
dispatchToolCall("https://parent.example");
|
|
81
408
|
await vi.waitFor(() => expect(execute).toHaveBeenCalledOnce());
|
|
82
409
|
});
|
|
83
410
|
});
|
|
@@ -37,7 +37,12 @@ export class AssistantFrameProvider {
|
|
|
37
37
|
ModelContextProvider,
|
|
38
38
|
Unsubscribe | undefined
|
|
39
39
|
>();
|
|
40
|
+
private _activeToolCalls = new Map<
|
|
41
|
+
string,
|
|
42
|
+
{ abortController: AbortController; event: MessageEvent }
|
|
43
|
+
>();
|
|
40
44
|
private _targetOrigin: string;
|
|
45
|
+
private _strictRegistrations = 0;
|
|
41
46
|
|
|
42
47
|
private constructor(targetOrigin: string = "*") {
|
|
43
48
|
this._targetOrigin = targetOrigin;
|
|
@@ -52,10 +57,25 @@ export class AssistantFrameProvider {
|
|
|
52
57
|
AssistantFrameProvider._instance = new AssistantFrameProvider(
|
|
53
58
|
targetOrigin,
|
|
54
59
|
);
|
|
60
|
+
} else {
|
|
61
|
+
AssistantFrameProvider._instance.reconcileTargetOrigin(targetOrigin);
|
|
55
62
|
}
|
|
56
63
|
return AssistantFrameProvider._instance;
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
private reconcileTargetOrigin(targetOrigin: string = "*") {
|
|
67
|
+
if (targetOrigin === "*" || targetOrigin === this._targetOrigin) return;
|
|
68
|
+
|
|
69
|
+
if (this._targetOrigin === "*") {
|
|
70
|
+
this._targetOrigin = targetOrigin;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new Error(
|
|
75
|
+
`AssistantFrameProvider cannot register conflicting target origins: "${this._targetOrigin}" and "${targetOrigin}"`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
59
79
|
private handleMessage(event: MessageEvent) {
|
|
60
80
|
if (this._targetOrigin !== "*" && event.origin !== this._targetOrigin)
|
|
61
81
|
return;
|
|
@@ -75,6 +95,10 @@ export class AssistantFrameProvider {
|
|
|
75
95
|
case "tool-call":
|
|
76
96
|
this.handleToolCall(message, event);
|
|
77
97
|
break;
|
|
98
|
+
|
|
99
|
+
case "tool-cancel":
|
|
100
|
+
this.cancelToolCall(message.id);
|
|
101
|
+
break;
|
|
78
102
|
}
|
|
79
103
|
}
|
|
80
104
|
|
|
@@ -83,6 +107,10 @@ export class AssistantFrameProvider {
|
|
|
83
107
|
event: MessageEvent,
|
|
84
108
|
) {
|
|
85
109
|
const tool = this.getModelContext().tools?.[message.toolName];
|
|
110
|
+
const abortController = new AbortController();
|
|
111
|
+
this._activeToolCalls.get(message.id)?.abortController.abort();
|
|
112
|
+
const activeCall = { abortController, event };
|
|
113
|
+
this._activeToolCalls.set(message.id, activeCall);
|
|
86
114
|
|
|
87
115
|
let result: any;
|
|
88
116
|
let error: string | undefined;
|
|
@@ -94,7 +122,7 @@ export class AssistantFrameProvider {
|
|
|
94
122
|
result = tool.execute
|
|
95
123
|
? await tool.execute(message.args, {
|
|
96
124
|
toolCallId: message.id,
|
|
97
|
-
abortSignal:
|
|
125
|
+
abortSignal: abortController.signal,
|
|
98
126
|
human: async () => {
|
|
99
127
|
throw new Error(
|
|
100
128
|
"Tool human input is not supported in frame context",
|
|
@@ -107,6 +135,9 @@ export class AssistantFrameProvider {
|
|
|
107
135
|
}
|
|
108
136
|
}
|
|
109
137
|
|
|
138
|
+
if (this._activeToolCalls.get(message.id) !== activeCall) return;
|
|
139
|
+
this._activeToolCalls.delete(message.id);
|
|
140
|
+
|
|
110
141
|
this.sendMessage(event, {
|
|
111
142
|
type: "tool-result",
|
|
112
143
|
id: message.id,
|
|
@@ -114,6 +145,13 @@ export class AssistantFrameProvider {
|
|
|
114
145
|
});
|
|
115
146
|
}
|
|
116
147
|
|
|
148
|
+
private cancelToolCall(id: string) {
|
|
149
|
+
const activeCall = this._activeToolCalls.get(id);
|
|
150
|
+
if (!activeCall) return;
|
|
151
|
+
this._activeToolCalls.delete(id);
|
|
152
|
+
activeCall.abortController.abort();
|
|
153
|
+
}
|
|
154
|
+
|
|
117
155
|
private sendMessage(event: MessageEvent, message: FrameMessage) {
|
|
118
156
|
event.source?.postMessage(
|
|
119
157
|
{ channel: FRAME_MESSAGE_CHANNEL, message },
|
|
@@ -157,8 +195,10 @@ export class AssistantFrameProvider {
|
|
|
157
195
|
provider: ModelContextProvider,
|
|
158
196
|
targetOrigin?: string,
|
|
159
197
|
): Unsubscribe {
|
|
160
|
-
const
|
|
198
|
+
const origin = targetOrigin ?? "*";
|
|
199
|
+
const instance = AssistantFrameProvider.getInstance(origin);
|
|
161
200
|
instance._providers.add(provider);
|
|
201
|
+
if (origin !== "*") instance._strictRegistrations += 1;
|
|
162
202
|
|
|
163
203
|
const unsubscribe = provider.subscribe?.(() => instance.broadcastUpdate());
|
|
164
204
|
if (unsubscribe) {
|
|
@@ -167,10 +207,17 @@ export class AssistantFrameProvider {
|
|
|
167
207
|
|
|
168
208
|
instance.broadcastUpdate();
|
|
169
209
|
|
|
210
|
+
let released = false;
|
|
170
211
|
return () => {
|
|
212
|
+
if (released) return;
|
|
213
|
+
released = true;
|
|
171
214
|
instance._providers.delete(provider);
|
|
172
215
|
instance._providerUnsubscribes.get(provider)?.();
|
|
173
216
|
instance._providerUnsubscribes.delete(provider);
|
|
217
|
+
if (origin !== "*") {
|
|
218
|
+
instance._strictRegistrations -= 1;
|
|
219
|
+
if (instance._strictRegistrations === 0) instance._targetOrigin = "*";
|
|
220
|
+
}
|
|
174
221
|
instance.broadcastUpdate();
|
|
175
222
|
};
|
|
176
223
|
}
|
|
@@ -183,6 +230,15 @@ export class AssistantFrameProvider {
|
|
|
183
230
|
instance._providerUnsubscribes.forEach((unsubscribe) => unsubscribe?.());
|
|
184
231
|
instance._providerUnsubscribes.clear();
|
|
185
232
|
instance._providers.clear();
|
|
233
|
+
instance._activeToolCalls.forEach(({ abortController, event }, id) => {
|
|
234
|
+
abortController.abort();
|
|
235
|
+
instance.sendMessage(event, {
|
|
236
|
+
type: "tool-result",
|
|
237
|
+
id,
|
|
238
|
+
error: "AssistantFrameProvider has been disposed",
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
instance._activeToolCalls.clear();
|
|
186
242
|
|
|
187
243
|
AssistantFrameProvider._instance = null;
|
|
188
244
|
}
|
|
@@ -14,6 +14,7 @@ export type FrameMessageType =
|
|
|
14
14
|
| "model-context-request"
|
|
15
15
|
| "model-context-update"
|
|
16
16
|
| "tool-call"
|
|
17
|
+
| "tool-cancel"
|
|
17
18
|
| "tool-result";
|
|
18
19
|
|
|
19
20
|
export type FrameMessage =
|
|
@@ -30,6 +31,10 @@ export type FrameMessage =
|
|
|
30
31
|
toolName: string;
|
|
31
32
|
args: unknown;
|
|
32
33
|
}
|
|
34
|
+
| {
|
|
35
|
+
type: "tool-cancel";
|
|
36
|
+
id: string;
|
|
37
|
+
}
|
|
33
38
|
| {
|
|
34
39
|
type: "tool-result";
|
|
35
40
|
id: string;
|