@ai-sdk/react 4.0.123 → 4.0.126
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/CHANGELOG.md +37 -0
- package/dist/index.d.ts +280 -276
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1171 -1346
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/use-chat.ts +37 -1
package/dist/index.js
CHANGED
|
@@ -1,1401 +1,1226 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
resolve,
|
|
5
|
-
normalizeHeaders,
|
|
6
|
-
safeValidateTypes
|
|
7
|
-
} from "@ai-sdk/provider-utils";
|
|
8
|
-
import {
|
|
9
|
-
asSchema,
|
|
10
|
-
isDeepEqualData,
|
|
11
|
-
parsePartialJson
|
|
12
|
-
} from "ai";
|
|
13
|
-
import { useCallback, useId, useRef, useState } from "react";
|
|
1
|
+
import { isAbortError, normalizeHeaders, resolve, safeValidateTypes, secureJsonParse } from "@ai-sdk/provider-utils";
|
|
2
|
+
import { AbstractChat, DefaultChatTransport, Experimental_AbstractRealtimeSession, asSchema, callCompletionApi, isDeepEqualData, parsePartialJson } from "ai";
|
|
3
|
+
import { useCallback, useEffect, useId, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
14
4
|
import useSWR from "swr";
|
|
15
|
-
var getOriginalFetch = () => fetch;
|
|
16
|
-
function useObject({
|
|
17
|
-
api,
|
|
18
|
-
id,
|
|
19
|
-
schema,
|
|
20
|
-
// required, in the future we will use it for validation
|
|
21
|
-
initialValue,
|
|
22
|
-
fetch: fetch2,
|
|
23
|
-
onError,
|
|
24
|
-
onFinish,
|
|
25
|
-
headers,
|
|
26
|
-
credentials
|
|
27
|
-
}) {
|
|
28
|
-
const hookId = useId();
|
|
29
|
-
const completionId = id ?? hookId;
|
|
30
|
-
const { data, mutate } = useSWR(
|
|
31
|
-
[completionId, "object"],
|
|
32
|
-
null,
|
|
33
|
-
{ fallbackData: { object: initialValue } }
|
|
34
|
-
);
|
|
35
|
-
const [error, setError] = useState(void 0);
|
|
36
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
37
|
-
const abortControllerRef = useRef(null);
|
|
38
|
-
const stop = useCallback(() => {
|
|
39
|
-
try {
|
|
40
|
-
abortControllerRef.current?.abort();
|
|
41
|
-
} catch {
|
|
42
|
-
} finally {
|
|
43
|
-
setIsLoading(false);
|
|
44
|
-
abortControllerRef.current = null;
|
|
45
|
-
}
|
|
46
|
-
}, []);
|
|
47
|
-
const submit = async (input) => {
|
|
48
|
-
const abortController = new AbortController();
|
|
49
|
-
try {
|
|
50
|
-
clearObject();
|
|
51
|
-
setIsLoading(true);
|
|
52
|
-
abortControllerRef.current = abortController;
|
|
53
|
-
const resolvedHeaders = await resolve(headers);
|
|
54
|
-
const actualFetch = fetch2 ?? getOriginalFetch();
|
|
55
|
-
const response = await actualFetch(api, {
|
|
56
|
-
method: "POST",
|
|
57
|
-
headers: {
|
|
58
|
-
"Content-Type": "application/json",
|
|
59
|
-
...normalizeHeaders(resolvedHeaders)
|
|
60
|
-
},
|
|
61
|
-
credentials,
|
|
62
|
-
signal: abortController.signal,
|
|
63
|
-
body: JSON.stringify(input)
|
|
64
|
-
});
|
|
65
|
-
if (!response.ok) {
|
|
66
|
-
throw new Error(
|
|
67
|
-
await response.text() || "Failed to fetch the response."
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
if (response.body == null) {
|
|
71
|
-
throw new Error("The response body is empty.");
|
|
72
|
-
}
|
|
73
|
-
let accumulatedText = "";
|
|
74
|
-
let latestObject = void 0;
|
|
75
|
-
await response.body.pipeThrough(new TextDecoderStream()).pipeTo(
|
|
76
|
-
new WritableStream({
|
|
77
|
-
async write(chunk) {
|
|
78
|
-
accumulatedText += chunk;
|
|
79
|
-
const { value } = await parsePartialJson(accumulatedText);
|
|
80
|
-
const currentObject = value;
|
|
81
|
-
if (!isDeepEqualData(latestObject, currentObject)) {
|
|
82
|
-
latestObject = currentObject;
|
|
83
|
-
mutate({ object: currentObject });
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
async close() {
|
|
87
|
-
if (abortControllerRef.current === abortController) {
|
|
88
|
-
setIsLoading(false);
|
|
89
|
-
}
|
|
90
|
-
if (onFinish != null) {
|
|
91
|
-
const validationResult = await safeValidateTypes({
|
|
92
|
-
value: latestObject,
|
|
93
|
-
schema: asSchema(schema)
|
|
94
|
-
});
|
|
95
|
-
await onFinish(
|
|
96
|
-
validationResult.success ? { object: validationResult.value, error: void 0 } : { object: void 0, error: validationResult.error }
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (abortControllerRef.current === abortController) {
|
|
100
|
-
abortControllerRef.current = null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
);
|
|
105
|
-
} catch (error2) {
|
|
106
|
-
if (isAbortError(error2)) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (onError && error2 instanceof Error) {
|
|
110
|
-
onError(error2);
|
|
111
|
-
}
|
|
112
|
-
if (abortControllerRef.current === abortController) {
|
|
113
|
-
setIsLoading(false);
|
|
114
|
-
abortControllerRef.current = null;
|
|
115
|
-
setError(error2 instanceof Error ? error2 : new Error(String(error2)));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
const clear = () => {
|
|
120
|
-
stop();
|
|
121
|
-
clearObject();
|
|
122
|
-
};
|
|
123
|
-
const clearObject = () => {
|
|
124
|
-
setError(void 0);
|
|
125
|
-
setIsLoading(false);
|
|
126
|
-
mutate({ object: void 0 });
|
|
127
|
-
};
|
|
128
|
-
return {
|
|
129
|
-
submit,
|
|
130
|
-
object: data?.object,
|
|
131
|
-
error,
|
|
132
|
-
isLoading,
|
|
133
|
-
stop,
|
|
134
|
-
clear
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// src/use-chat.ts
|
|
139
|
-
import {
|
|
140
|
-
DefaultChatTransport
|
|
141
|
-
} from "ai";
|
|
142
|
-
import {
|
|
143
|
-
useCallback as useCallback2,
|
|
144
|
-
useEffect,
|
|
145
|
-
useMemo,
|
|
146
|
-
useRef as useRef2,
|
|
147
|
-
useSyncExternalStore
|
|
148
|
-
} from "react";
|
|
149
|
-
|
|
150
|
-
// src/chat.react.ts
|
|
151
|
-
import {
|
|
152
|
-
AbstractChat
|
|
153
|
-
} from "ai";
|
|
154
|
-
|
|
155
|
-
// src/throttle.ts
|
|
156
5
|
import throttleFunction from "throttleit";
|
|
6
|
+
import { isJSONObject } from "@ai-sdk/provider";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
//#region src/use-object.ts
|
|
9
|
+
const getOriginalFetch = () => fetch;
|
|
10
|
+
function useObject({ api, id, schema, initialValue, fetch, onError, onFinish, headers, credentials }) {
|
|
11
|
+
const hookId = useId();
|
|
12
|
+
const { data, mutate } = useSWR([id ?? hookId, "object"], null, { fallbackData: { object: initialValue } });
|
|
13
|
+
const [error, setError] = useState(void 0);
|
|
14
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
15
|
+
const abortControllerRef = useRef(null);
|
|
16
|
+
const stop = useCallback(() => {
|
|
17
|
+
try {
|
|
18
|
+
abortControllerRef.current?.abort();
|
|
19
|
+
} catch {} finally {
|
|
20
|
+
setIsLoading(false);
|
|
21
|
+
abortControllerRef.current = null;
|
|
22
|
+
}
|
|
23
|
+
}, []);
|
|
24
|
+
const submit = async (input) => {
|
|
25
|
+
const abortController = new AbortController();
|
|
26
|
+
try {
|
|
27
|
+
clearObject();
|
|
28
|
+
setIsLoading(true);
|
|
29
|
+
abortControllerRef.current = abortController;
|
|
30
|
+
const resolvedHeaders = await resolve(headers);
|
|
31
|
+
const response = await (fetch ?? getOriginalFetch())(api, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
...normalizeHeaders(resolvedHeaders)
|
|
36
|
+
},
|
|
37
|
+
credentials,
|
|
38
|
+
signal: abortController.signal,
|
|
39
|
+
body: JSON.stringify(input)
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) throw new Error(await response.text() || "Failed to fetch the response.");
|
|
42
|
+
if (response.body == null) throw new Error("The response body is empty.");
|
|
43
|
+
let accumulatedText = "";
|
|
44
|
+
let latestObject = void 0;
|
|
45
|
+
await response.body.pipeThrough(new TextDecoderStream()).pipeTo(new WritableStream({
|
|
46
|
+
async write(chunk) {
|
|
47
|
+
accumulatedText += chunk;
|
|
48
|
+
const { value } = await parsePartialJson(accumulatedText);
|
|
49
|
+
const currentObject = value;
|
|
50
|
+
if (!isDeepEqualData(latestObject, currentObject)) {
|
|
51
|
+
latestObject = currentObject;
|
|
52
|
+
mutate({ object: currentObject });
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async close() {
|
|
56
|
+
if (abortControllerRef.current === abortController) setIsLoading(false);
|
|
57
|
+
if (onFinish != null) {
|
|
58
|
+
const validationResult = await safeValidateTypes({
|
|
59
|
+
value: latestObject,
|
|
60
|
+
schema: asSchema(schema)
|
|
61
|
+
});
|
|
62
|
+
await onFinish(validationResult.success ? {
|
|
63
|
+
object: validationResult.value,
|
|
64
|
+
error: void 0
|
|
65
|
+
} : {
|
|
66
|
+
object: void 0,
|
|
67
|
+
error: validationResult.error
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (abortControllerRef.current === abortController) abortControllerRef.current = null;
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (isAbortError(error)) return;
|
|
75
|
+
if (onError && error instanceof Error) onError(error);
|
|
76
|
+
if (abortControllerRef.current === abortController) {
|
|
77
|
+
setIsLoading(false);
|
|
78
|
+
abortControllerRef.current = null;
|
|
79
|
+
setError(error instanceof Error ? error : new Error(String(error)));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const clear = () => {
|
|
84
|
+
stop();
|
|
85
|
+
clearObject();
|
|
86
|
+
};
|
|
87
|
+
const clearObject = () => {
|
|
88
|
+
setError(void 0);
|
|
89
|
+
setIsLoading(false);
|
|
90
|
+
mutate({ object: void 0 });
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
submit,
|
|
94
|
+
object: data?.object,
|
|
95
|
+
error,
|
|
96
|
+
isLoading,
|
|
97
|
+
stop,
|
|
98
|
+
clear
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/throttle.ts
|
|
157
103
|
function throttle(fn, waitMs) {
|
|
158
|
-
|
|
104
|
+
return waitMs != null ? throttleFunction(fn, waitMs) : fn;
|
|
159
105
|
}
|
|
160
|
-
|
|
161
|
-
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/chat.react.ts
|
|
162
108
|
function cloneMetadata(metadata) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (metadata != null && typeof metadata === "object" && (Object.getPrototypeOf(metadata) === Object.prototype || Object.getPrototypeOf(metadata) === null)) {
|
|
167
|
-
return { ...metadata };
|
|
168
|
-
}
|
|
169
|
-
return metadata;
|
|
109
|
+
if (Array.isArray(metadata)) return [...metadata];
|
|
110
|
+
if (metadata != null && typeof metadata === "object" && (Object.getPrototypeOf(metadata) === Object.prototype || Object.getPrototypeOf(metadata) === null)) return { ...metadata };
|
|
111
|
+
return metadata;
|
|
170
112
|
}
|
|
171
113
|
var ReactChatState = class {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
#callMessagesCallbacks;
|
|
266
|
-
#callStatusCallbacks;
|
|
267
|
-
#callErrorCallbacks;
|
|
114
|
+
#messages;
|
|
115
|
+
#status;
|
|
116
|
+
#error;
|
|
117
|
+
#messagesCallbacks;
|
|
118
|
+
#statusCallbacks;
|
|
119
|
+
#errorCallbacks;
|
|
120
|
+
constructor(initialMessages = []) {
|
|
121
|
+
this.#status = "ready";
|
|
122
|
+
this.#error = void 0;
|
|
123
|
+
this.#messagesCallbacks = /* @__PURE__ */ new Set();
|
|
124
|
+
this.#statusCallbacks = /* @__PURE__ */ new Set();
|
|
125
|
+
this.#errorCallbacks = /* @__PURE__ */ new Set();
|
|
126
|
+
this.pushMessage = (message) => {
|
|
127
|
+
this.#messages = this.#messages.concat(message);
|
|
128
|
+
this.#callMessagesCallbacks();
|
|
129
|
+
};
|
|
130
|
+
this.popMessage = () => {
|
|
131
|
+
this.#messages = this.#messages.slice(0, -1);
|
|
132
|
+
this.#callMessagesCallbacks();
|
|
133
|
+
};
|
|
134
|
+
this.replaceMessage = (index, message) => {
|
|
135
|
+
this.#messages = [
|
|
136
|
+
...this.#messages.slice(0, index),
|
|
137
|
+
this.snapshot(message),
|
|
138
|
+
...this.#messages.slice(index + 1)
|
|
139
|
+
];
|
|
140
|
+
this.#callMessagesCallbacks();
|
|
141
|
+
};
|
|
142
|
+
this.snapshot = (value) => {
|
|
143
|
+
if (value == null || typeof value !== "object" || !("parts" in value) || !Array.isArray(value.parts)) return value;
|
|
144
|
+
const message = value;
|
|
145
|
+
const snapshot = {
|
|
146
|
+
...message,
|
|
147
|
+
parts: message.parts.map((part) => ({ ...part }))
|
|
148
|
+
};
|
|
149
|
+
if ("metadata" in message) snapshot.metadata = cloneMetadata(message.metadata);
|
|
150
|
+
return snapshot;
|
|
151
|
+
};
|
|
152
|
+
this["~registerMessagesCallback"] = (onChange, throttleWaitMs) => {
|
|
153
|
+
const callback = throttleWaitMs ? throttle(onChange, throttleWaitMs) : onChange;
|
|
154
|
+
this.#messagesCallbacks.add(callback);
|
|
155
|
+
return () => {
|
|
156
|
+
this.#messagesCallbacks.delete(callback);
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
this["~registerStatusCallback"] = (onChange) => {
|
|
160
|
+
this.#statusCallbacks.add(onChange);
|
|
161
|
+
return () => {
|
|
162
|
+
this.#statusCallbacks.delete(onChange);
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
this["~registerErrorCallback"] = (onChange) => {
|
|
166
|
+
this.#errorCallbacks.add(onChange);
|
|
167
|
+
return () => {
|
|
168
|
+
this.#errorCallbacks.delete(onChange);
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
this.#callMessagesCallbacks = () => {
|
|
172
|
+
this.#messagesCallbacks.forEach((callback) => callback());
|
|
173
|
+
};
|
|
174
|
+
this.#callStatusCallbacks = () => {
|
|
175
|
+
this.#statusCallbacks.forEach((callback) => callback());
|
|
176
|
+
};
|
|
177
|
+
this.#callErrorCallbacks = () => {
|
|
178
|
+
this.#errorCallbacks.forEach((callback) => callback());
|
|
179
|
+
};
|
|
180
|
+
this.#messages = initialMessages;
|
|
181
|
+
}
|
|
182
|
+
get status() {
|
|
183
|
+
return this.#status;
|
|
184
|
+
}
|
|
185
|
+
set status(newStatus) {
|
|
186
|
+
this.#status = newStatus;
|
|
187
|
+
this.#callStatusCallbacks();
|
|
188
|
+
}
|
|
189
|
+
get error() {
|
|
190
|
+
return this.#error;
|
|
191
|
+
}
|
|
192
|
+
set error(newError) {
|
|
193
|
+
this.#error = newError;
|
|
194
|
+
this.#callErrorCallbacks();
|
|
195
|
+
}
|
|
196
|
+
get messages() {
|
|
197
|
+
return this.#messages;
|
|
198
|
+
}
|
|
199
|
+
set messages(newMessages) {
|
|
200
|
+
this.#messages = [...newMessages];
|
|
201
|
+
this.#callMessagesCallbacks();
|
|
202
|
+
}
|
|
203
|
+
#callMessagesCallbacks;
|
|
204
|
+
#callStatusCallbacks;
|
|
205
|
+
#callErrorCallbacks;
|
|
268
206
|
};
|
|
269
207
|
var Chat = class extends AbstractChat {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
208
|
+
#state;
|
|
209
|
+
constructor({ messages, ...init }) {
|
|
210
|
+
const state = new ReactChatState(messages);
|
|
211
|
+
super({
|
|
212
|
+
...init,
|
|
213
|
+
state
|
|
214
|
+
});
|
|
215
|
+
this["~registerMessagesCallback"] = (onChange, throttleWaitMs) => this.#state["~registerMessagesCallback"](onChange, throttleWaitMs);
|
|
216
|
+
this["~registerStatusCallback"] = (onChange) => this.#state["~registerStatusCallback"](onChange);
|
|
217
|
+
this["~registerErrorCallback"] = (onChange) => this.#state["~registerErrorCallback"](onChange);
|
|
218
|
+
this.#state = state;
|
|
219
|
+
}
|
|
279
220
|
};
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
sendAutomaticallyWhen: options.sendAutomaticallyWhen,
|
|
297
|
-
transport: options.transport
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
let defaultTransport;
|
|
301
|
-
const getTransport = () => latestRef.current.transport ?? (defaultTransport ??= new DefaultChatTransport());
|
|
302
|
-
const chatOptions = {
|
|
303
|
-
...options,
|
|
304
|
-
transport: {
|
|
305
|
-
sendMessages: (sendOptions) => getTransport().sendMessages(sendOptions),
|
|
306
|
-
reconnectToStream: (reconnectOptions) => getTransport().reconnectToStream(reconnectOptions)
|
|
307
|
-
},
|
|
308
|
-
onToolCall: (arg) => latestRef.current.onToolCall?.(arg),
|
|
309
|
-
onData: (arg) => latestRef.current.onData?.(arg),
|
|
310
|
-
onFinish: (arg) => latestRef.current.onFinish?.(arg),
|
|
311
|
-
onError: (arg) => latestRef.current.onError?.(arg),
|
|
312
|
-
sendAutomaticallyWhen: (arg) => latestRef.current.sendAutomaticallyWhen?.(arg) ?? false
|
|
313
|
-
};
|
|
314
|
-
const chatKey = "chat" in options ? options.chat : options.id;
|
|
315
|
-
const { chat, isExternallyManaged } = useMemo(
|
|
316
|
-
() => ({
|
|
317
|
-
chat: "chat" in options ? options.chat : new Chat(chatOptions),
|
|
318
|
-
isExternallyManaged: "chat" in options
|
|
319
|
-
}),
|
|
320
|
-
[chatKey]
|
|
321
|
-
);
|
|
322
|
-
useEffect(() => {
|
|
323
|
-
if (isExternallyManaged) {
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
return () => {
|
|
327
|
-
void chat.stop();
|
|
328
|
-
};
|
|
329
|
-
}, [chat, isExternallyManaged]);
|
|
330
|
-
const messagesSnapshot = useMemo(() => ({ messages: chat.messages }), [chat]);
|
|
331
|
-
const subscribeToMessages = useCallback2(
|
|
332
|
-
(update) => {
|
|
333
|
-
let isSubscribed = true;
|
|
334
|
-
const updateMessages = () => {
|
|
335
|
-
if (!isSubscribed) {
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
messagesSnapshot.messages = chat.messages;
|
|
339
|
-
update();
|
|
340
|
-
};
|
|
341
|
-
const unsubscribe = chat["~registerMessagesCallback"](
|
|
342
|
-
updateMessages,
|
|
343
|
-
throttleWaitMs
|
|
344
|
-
);
|
|
345
|
-
messagesSnapshot.messages = chat.messages;
|
|
346
|
-
return () => {
|
|
347
|
-
isSubscribed = false;
|
|
348
|
-
unsubscribe();
|
|
349
|
-
};
|
|
350
|
-
},
|
|
351
|
-
[chat, messagesSnapshot, throttleWaitMs]
|
|
352
|
-
);
|
|
353
|
-
const getMessagesSnapshot = useCallback2(
|
|
354
|
-
() => messagesSnapshot.messages,
|
|
355
|
-
[messagesSnapshot]
|
|
356
|
-
);
|
|
357
|
-
const messages = useSyncExternalStore(
|
|
358
|
-
subscribeToMessages,
|
|
359
|
-
getMessagesSnapshot,
|
|
360
|
-
getMessagesSnapshot
|
|
361
|
-
);
|
|
362
|
-
const subscribeToStatus = useCallback2(
|
|
363
|
-
(update) => chat["~registerStatusCallback"](() => {
|
|
364
|
-
if (chat.status === "ready" || chat.status === "error") {
|
|
365
|
-
messagesSnapshot.messages = chat.messages;
|
|
366
|
-
}
|
|
367
|
-
update();
|
|
368
|
-
}),
|
|
369
|
-
[chat, messagesSnapshot]
|
|
370
|
-
);
|
|
371
|
-
const getStatusSnapshot = useCallback2(() => chat.status, [chat]);
|
|
372
|
-
const status = useSyncExternalStore(
|
|
373
|
-
subscribeToStatus,
|
|
374
|
-
getStatusSnapshot,
|
|
375
|
-
getStatusSnapshot
|
|
376
|
-
);
|
|
377
|
-
const error = useSyncExternalStore(
|
|
378
|
-
chat["~registerErrorCallback"],
|
|
379
|
-
() => chat.error,
|
|
380
|
-
() => chat.error
|
|
381
|
-
);
|
|
382
|
-
const setMessages = useCallback2(
|
|
383
|
-
(messagesParam) => {
|
|
384
|
-
if (typeof messagesParam === "function") {
|
|
385
|
-
messagesParam = messagesParam(chat.messages);
|
|
386
|
-
}
|
|
387
|
-
chat.messages = messagesParam;
|
|
388
|
-
},
|
|
389
|
-
[chat]
|
|
390
|
-
);
|
|
391
|
-
useEffect(() => {
|
|
392
|
-
if (resume) {
|
|
393
|
-
chat.resumeStream();
|
|
394
|
-
}
|
|
395
|
-
}, [resume, chat]);
|
|
396
|
-
return {
|
|
397
|
-
id: chat.id,
|
|
398
|
-
messages,
|
|
399
|
-
setMessages,
|
|
400
|
-
sendMessage: chat.sendMessage,
|
|
401
|
-
regenerate: chat.regenerate,
|
|
402
|
-
clearError: chat.clearError,
|
|
403
|
-
stop: chat.stop,
|
|
404
|
-
error,
|
|
405
|
-
resumeStream: chat.resumeStream,
|
|
406
|
-
status,
|
|
407
|
-
/**
|
|
408
|
-
* @deprecated Use `addToolOutput` instead.
|
|
409
|
-
*/
|
|
410
|
-
addToolResult: chat.addToolOutput,
|
|
411
|
-
addToolOutput: chat.addToolOutput,
|
|
412
|
-
addToolApprovalResponse: chat.addToolApprovalResponse
|
|
413
|
-
};
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/use-chat.ts
|
|
223
|
+
const automaticResumeRegistrations = /* @__PURE__ */ new WeakMap();
|
|
224
|
+
function registerAutomaticResume({ chat, registration }) {
|
|
225
|
+
let registrations = automaticResumeRegistrations.get(chat);
|
|
226
|
+
if (registrations == null) {
|
|
227
|
+
registrations = /* @__PURE__ */ new Set();
|
|
228
|
+
automaticResumeRegistrations.set(chat, registrations);
|
|
229
|
+
}
|
|
230
|
+
const shouldResume = registrations.size === 0;
|
|
231
|
+
registrations.add(registration);
|
|
232
|
+
if (shouldResume) chat.resumeStream();
|
|
233
|
+
return () => {
|
|
234
|
+
registrations.delete(registration);
|
|
235
|
+
if (registrations.size === 0) automaticResumeRegistrations.delete(chat);
|
|
236
|
+
};
|
|
414
237
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
]
|
|
509
|
-
);
|
|
510
|
-
const stop = useCallback3(() => {
|
|
511
|
-
abortControllerRef.current?.abort();
|
|
512
|
-
}, []);
|
|
513
|
-
const setCompletion = useCallback3(
|
|
514
|
-
(completion2) => {
|
|
515
|
-
mutate(completion2, false);
|
|
516
|
-
},
|
|
517
|
-
[mutate]
|
|
518
|
-
);
|
|
519
|
-
const complete = useCallback3(
|
|
520
|
-
async (prompt, options) => {
|
|
521
|
-
return triggerRequest(prompt, options);
|
|
522
|
-
},
|
|
523
|
-
[triggerRequest]
|
|
524
|
-
);
|
|
525
|
-
const [input, setInput] = useState2(initialInput);
|
|
526
|
-
const handleSubmit = useCallback3(
|
|
527
|
-
(event) => {
|
|
528
|
-
event?.preventDefault?.();
|
|
529
|
-
if (!input) {
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
const result = complete(input);
|
|
533
|
-
setInput("");
|
|
534
|
-
return result;
|
|
535
|
-
},
|
|
536
|
-
[input, complete, setInput]
|
|
537
|
-
);
|
|
538
|
-
const handleInputChange = useCallback3(
|
|
539
|
-
(e) => {
|
|
540
|
-
setInput(e.target.value);
|
|
541
|
-
},
|
|
542
|
-
[setInput]
|
|
543
|
-
);
|
|
544
|
-
return {
|
|
545
|
-
completion,
|
|
546
|
-
complete,
|
|
547
|
-
error,
|
|
548
|
-
setCompletion,
|
|
549
|
-
stop,
|
|
550
|
-
input,
|
|
551
|
-
setInput,
|
|
552
|
-
handleInputChange,
|
|
553
|
-
handleSubmit,
|
|
554
|
-
isLoading
|
|
555
|
-
};
|
|
238
|
+
function useChat({ throttle, experimental_throttle, resume = false, ...options } = {}) {
|
|
239
|
+
const throttleWaitMs = throttle ?? experimental_throttle;
|
|
240
|
+
const automaticResumeRegistration = useRef({});
|
|
241
|
+
const latestRef = useRef({});
|
|
242
|
+
if (!("chat" in options)) latestRef.current = {
|
|
243
|
+
onToolCall: options.onToolCall,
|
|
244
|
+
onData: options.onData,
|
|
245
|
+
onFinish: options.onFinish,
|
|
246
|
+
onError: options.onError,
|
|
247
|
+
sendAutomaticallyWhen: options.sendAutomaticallyWhen,
|
|
248
|
+
transport: options.transport
|
|
249
|
+
};
|
|
250
|
+
let defaultTransport;
|
|
251
|
+
const getTransport = () => latestRef.current.transport ?? (defaultTransport ??= new DefaultChatTransport());
|
|
252
|
+
const chatOptions = {
|
|
253
|
+
...options,
|
|
254
|
+
transport: {
|
|
255
|
+
sendMessages: (sendOptions) => getTransport().sendMessages(sendOptions),
|
|
256
|
+
reconnectToStream: (reconnectOptions) => getTransport().reconnectToStream(reconnectOptions)
|
|
257
|
+
},
|
|
258
|
+
onToolCall: (arg) => latestRef.current.onToolCall?.(arg),
|
|
259
|
+
onData: (arg) => latestRef.current.onData?.(arg),
|
|
260
|
+
onFinish: (arg) => latestRef.current.onFinish?.(arg),
|
|
261
|
+
onError: (arg) => latestRef.current.onError?.(arg),
|
|
262
|
+
sendAutomaticallyWhen: (arg) => latestRef.current.sendAutomaticallyWhen?.(arg) ?? false
|
|
263
|
+
};
|
|
264
|
+
const chatKey = "chat" in options ? options.chat : options.id;
|
|
265
|
+
const { chat, isExternallyManaged } = useMemo(() => ({
|
|
266
|
+
chat: "chat" in options ? options.chat : new Chat(chatOptions),
|
|
267
|
+
isExternallyManaged: "chat" in options
|
|
268
|
+
}), [chatKey]);
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (isExternallyManaged) return;
|
|
271
|
+
return () => {
|
|
272
|
+
chat.stop();
|
|
273
|
+
};
|
|
274
|
+
}, [chat, isExternallyManaged]);
|
|
275
|
+
const messagesSnapshot = useMemo(() => ({ messages: chat.messages }), [chat]);
|
|
276
|
+
const subscribeToMessages = useCallback((update) => {
|
|
277
|
+
let isSubscribed = true;
|
|
278
|
+
const updateMessages = () => {
|
|
279
|
+
if (!isSubscribed) return;
|
|
280
|
+
messagesSnapshot.messages = chat.messages;
|
|
281
|
+
update();
|
|
282
|
+
};
|
|
283
|
+
const unsubscribe = chat["~registerMessagesCallback"](updateMessages, throttleWaitMs);
|
|
284
|
+
messagesSnapshot.messages = chat.messages;
|
|
285
|
+
return () => {
|
|
286
|
+
isSubscribed = false;
|
|
287
|
+
unsubscribe();
|
|
288
|
+
};
|
|
289
|
+
}, [
|
|
290
|
+
chat,
|
|
291
|
+
messagesSnapshot,
|
|
292
|
+
throttleWaitMs
|
|
293
|
+
]);
|
|
294
|
+
const getMessagesSnapshot = useCallback(() => messagesSnapshot.messages, [messagesSnapshot]);
|
|
295
|
+
const messages = useSyncExternalStore(subscribeToMessages, getMessagesSnapshot, getMessagesSnapshot);
|
|
296
|
+
const subscribeToStatus = useCallback((update) => chat["~registerStatusCallback"](() => {
|
|
297
|
+
if (chat.status === "ready" || chat.status === "error") messagesSnapshot.messages = chat.messages;
|
|
298
|
+
update();
|
|
299
|
+
}), [chat, messagesSnapshot]);
|
|
300
|
+
const getStatusSnapshot = useCallback(() => chat.status, [chat]);
|
|
301
|
+
const status = useSyncExternalStore(subscribeToStatus, getStatusSnapshot, getStatusSnapshot);
|
|
302
|
+
const error = useSyncExternalStore(chat["~registerErrorCallback"], () => chat.error, () => chat.error);
|
|
303
|
+
const setMessages = useCallback((messagesParam) => {
|
|
304
|
+
if (typeof messagesParam === "function") messagesParam = messagesParam(chat.messages);
|
|
305
|
+
chat.messages = messagesParam;
|
|
306
|
+
}, [chat]);
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
if (resume) return registerAutomaticResume({
|
|
309
|
+
chat,
|
|
310
|
+
registration: automaticResumeRegistration.current
|
|
311
|
+
});
|
|
312
|
+
}, [resume, chat]);
|
|
313
|
+
return {
|
|
314
|
+
id: chat.id,
|
|
315
|
+
messages,
|
|
316
|
+
setMessages,
|
|
317
|
+
sendMessage: chat.sendMessage,
|
|
318
|
+
regenerate: chat.regenerate,
|
|
319
|
+
clearError: chat.clearError,
|
|
320
|
+
stop: chat.stop,
|
|
321
|
+
error,
|
|
322
|
+
resumeStream: chat.resumeStream,
|
|
323
|
+
status,
|
|
324
|
+
/**
|
|
325
|
+
* @deprecated Use `addToolOutput` instead.
|
|
326
|
+
*/
|
|
327
|
+
addToolResult: chat.addToolOutput,
|
|
328
|
+
addToolOutput: chat.addToolOutput,
|
|
329
|
+
addToolApprovalResponse: chat.addToolApprovalResponse
|
|
330
|
+
};
|
|
556
331
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/use-completion.ts
|
|
334
|
+
function useCompletion({ api = "/api/completion", id, initialCompletion = "", initialInput = "", credentials, headers, body, streamProtocol = "data", fetch, onFinish, onError, throttle: throttleWait, experimental_throttle } = {}) {
|
|
335
|
+
const throttleWaitMs = throttleWait ?? experimental_throttle;
|
|
336
|
+
const hookId = useId();
|
|
337
|
+
const completionId = id || hookId;
|
|
338
|
+
const { data, mutate } = useSWR([api, completionId], null, { fallbackData: initialCompletion });
|
|
339
|
+
const { data: isLoading = false, mutate: mutateLoading } = useSWR([completionId, "loading"], null);
|
|
340
|
+
const [error, setError] = useState(void 0);
|
|
341
|
+
const completion = data;
|
|
342
|
+
const abortControllerRef = useRef(null);
|
|
343
|
+
const requestIdRef = useRef(0);
|
|
344
|
+
const extraMetadataRef = useRef({
|
|
345
|
+
credentials,
|
|
346
|
+
headers,
|
|
347
|
+
body
|
|
348
|
+
});
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
extraMetadataRef.current = {
|
|
351
|
+
credentials,
|
|
352
|
+
headers,
|
|
353
|
+
body
|
|
354
|
+
};
|
|
355
|
+
}, [
|
|
356
|
+
credentials,
|
|
357
|
+
headers,
|
|
358
|
+
body
|
|
359
|
+
]);
|
|
360
|
+
const triggerRequest = useCallback(async (prompt, options) => {
|
|
361
|
+
const requestId = ++requestIdRef.current;
|
|
362
|
+
return callCompletionApi({
|
|
363
|
+
api,
|
|
364
|
+
prompt,
|
|
365
|
+
credentials: extraMetadataRef.current.credentials,
|
|
366
|
+
headers: {
|
|
367
|
+
...normalizeHeaders(extraMetadataRef.current.headers),
|
|
368
|
+
...normalizeHeaders(options?.headers)
|
|
369
|
+
},
|
|
370
|
+
body: {
|
|
371
|
+
...extraMetadataRef.current.body,
|
|
372
|
+
...options?.body
|
|
373
|
+
},
|
|
374
|
+
streamProtocol,
|
|
375
|
+
fetch,
|
|
376
|
+
setCompletion: throttle((completion) => {
|
|
377
|
+
if (requestIdRef.current === requestId) mutate(completion, false);
|
|
378
|
+
}, throttleWaitMs),
|
|
379
|
+
setLoading: mutateLoading,
|
|
380
|
+
setError,
|
|
381
|
+
setAbortController: (controller) => {
|
|
382
|
+
abortControllerRef.current = controller;
|
|
383
|
+
},
|
|
384
|
+
getAbortController: () => abortControllerRef.current,
|
|
385
|
+
onFinish,
|
|
386
|
+
onError
|
|
387
|
+
});
|
|
388
|
+
}, [
|
|
389
|
+
mutate,
|
|
390
|
+
mutateLoading,
|
|
391
|
+
api,
|
|
392
|
+
extraMetadataRef,
|
|
393
|
+
onFinish,
|
|
394
|
+
onError,
|
|
395
|
+
setError,
|
|
396
|
+
streamProtocol,
|
|
397
|
+
fetch,
|
|
398
|
+
throttleWaitMs
|
|
399
|
+
]);
|
|
400
|
+
const stop = useCallback(() => {
|
|
401
|
+
abortControllerRef.current?.abort();
|
|
402
|
+
}, []);
|
|
403
|
+
const setCompletion = useCallback((completion) => {
|
|
404
|
+
mutate(completion, false);
|
|
405
|
+
}, [mutate]);
|
|
406
|
+
const complete = useCallback(async (prompt, options) => {
|
|
407
|
+
return triggerRequest(prompt, options);
|
|
408
|
+
}, [triggerRequest]);
|
|
409
|
+
const [input, setInput] = useState(initialInput);
|
|
410
|
+
const handleSubmit = useCallback((event) => {
|
|
411
|
+
event?.preventDefault?.();
|
|
412
|
+
if (!input) return;
|
|
413
|
+
const result = complete(input);
|
|
414
|
+
setInput("");
|
|
415
|
+
return result;
|
|
416
|
+
}, [
|
|
417
|
+
input,
|
|
418
|
+
complete,
|
|
419
|
+
setInput
|
|
420
|
+
]);
|
|
421
|
+
return {
|
|
422
|
+
completion,
|
|
423
|
+
complete,
|
|
424
|
+
error,
|
|
425
|
+
setCompletion,
|
|
426
|
+
stop,
|
|
427
|
+
input,
|
|
428
|
+
setInput,
|
|
429
|
+
handleInputChange: useCallback((e) => {
|
|
430
|
+
setInput(e.target.value);
|
|
431
|
+
}, [setInput]),
|
|
432
|
+
handleSubmit,
|
|
433
|
+
isLoading
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/use-realtime.ts
|
|
438
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
439
|
+
const RealtimeStore = /* @__PURE__ */ (() => class RealtimeStore extends Experimental_AbstractRealtimeSession {
|
|
440
|
+
constructor(..._args) {
|
|
441
|
+
super(..._args);
|
|
442
|
+
this.state = {
|
|
443
|
+
status: "disconnected",
|
|
444
|
+
messages: [],
|
|
445
|
+
events: [],
|
|
446
|
+
isCapturing: false,
|
|
447
|
+
isPlaying: false
|
|
448
|
+
};
|
|
449
|
+
this.callbacks = {
|
|
450
|
+
status: /* @__PURE__ */ new Set(),
|
|
451
|
+
messages: /* @__PURE__ */ new Set(),
|
|
452
|
+
events: /* @__PURE__ */ new Set(),
|
|
453
|
+
isCapturing: /* @__PURE__ */ new Set(),
|
|
454
|
+
isPlaying: /* @__PURE__ */ new Set(),
|
|
455
|
+
session: /* @__PURE__ */ new Set()
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
get status() {
|
|
459
|
+
return this.state.status;
|
|
460
|
+
}
|
|
461
|
+
get messages() {
|
|
462
|
+
return this.state.messages;
|
|
463
|
+
}
|
|
464
|
+
get events() {
|
|
465
|
+
return this.state.events;
|
|
466
|
+
}
|
|
467
|
+
get isCapturing() {
|
|
468
|
+
return this.state.isCapturing;
|
|
469
|
+
}
|
|
470
|
+
get isPlaying() {
|
|
471
|
+
return this.state.isPlaying;
|
|
472
|
+
}
|
|
473
|
+
get session() {
|
|
474
|
+
return this.state.session;
|
|
475
|
+
}
|
|
476
|
+
subscribe(key, onChange) {
|
|
477
|
+
this.callbacks[key].add(onChange);
|
|
478
|
+
return () => {
|
|
479
|
+
this.callbacks[key].delete(onChange);
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
setState(key, value) {
|
|
483
|
+
this.state = {
|
|
484
|
+
...this.state,
|
|
485
|
+
[key]: value
|
|
486
|
+
};
|
|
487
|
+
this.callbacks[key].forEach((callback) => callback());
|
|
488
|
+
}
|
|
620
489
|
})();
|
|
621
490
|
function useRealtime(options) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const status = useSyncExternalStore2(
|
|
710
|
-
useCallback4((cb) => rt.subscribe("status", cb), [rt]),
|
|
711
|
-
() => rt.status,
|
|
712
|
-
() => rt.status
|
|
713
|
-
);
|
|
714
|
-
const messages = useSyncExternalStore2(
|
|
715
|
-
useCallback4((cb) => rt.subscribe("messages", cb), [rt]),
|
|
716
|
-
() => rt.messages,
|
|
717
|
-
() => rt.messages
|
|
718
|
-
);
|
|
719
|
-
const events = useSyncExternalStore2(
|
|
720
|
-
useCallback4((cb) => rt.subscribe("events", cb), [rt]),
|
|
721
|
-
() => rt.events,
|
|
722
|
-
() => rt.events
|
|
723
|
-
);
|
|
724
|
-
const isCapturing = useSyncExternalStore2(
|
|
725
|
-
useCallback4((cb) => rt.subscribe("isCapturing", cb), [rt]),
|
|
726
|
-
() => rt.isCapturing,
|
|
727
|
-
() => rt.isCapturing
|
|
728
|
-
);
|
|
729
|
-
const isPlaying = useSyncExternalStore2(
|
|
730
|
-
useCallback4((cb) => rt.subscribe("isPlaying", cb), [rt]),
|
|
731
|
-
() => rt.isPlaying,
|
|
732
|
-
() => rt.isPlaying
|
|
733
|
-
);
|
|
734
|
-
const session = useSyncExternalStore2(
|
|
735
|
-
useCallback4((cb) => rt.subscribe("session", cb), [rt]),
|
|
736
|
-
() => rt.session,
|
|
737
|
-
() => rt.session
|
|
738
|
-
);
|
|
739
|
-
return {
|
|
740
|
-
status,
|
|
741
|
-
messages,
|
|
742
|
-
events,
|
|
743
|
-
isCapturing,
|
|
744
|
-
isPlaying,
|
|
745
|
-
session,
|
|
746
|
-
...actions
|
|
747
|
-
};
|
|
491
|
+
const ownerRef = useRef(null);
|
|
492
|
+
const { model, api, startupTimeoutMs, closeTimeoutMs, rtcDisconnectTimeoutMs, sessionConfig, sampleRate, maxEvents, maxPlaybackBufferSeconds, onToolCall, onEvent, onError } = options;
|
|
493
|
+
const { token, websocket, session: sessionEndpoint } = api;
|
|
494
|
+
const protocols = JSON.stringify(api.protocols ?? []);
|
|
495
|
+
const rt = useMemo(() => {
|
|
496
|
+
const store = new RealtimeStore({
|
|
497
|
+
model,
|
|
498
|
+
api: token != null ? { token } : sessionEndpoint != null ? { session: sessionEndpoint } : {
|
|
499
|
+
websocket,
|
|
500
|
+
protocols: secureJsonParse(protocols)
|
|
501
|
+
},
|
|
502
|
+
startupTimeoutMs,
|
|
503
|
+
closeTimeoutMs,
|
|
504
|
+
rtcDisconnectTimeoutMs,
|
|
505
|
+
sessionConfig,
|
|
506
|
+
sampleRate,
|
|
507
|
+
maxEvents,
|
|
508
|
+
maxPlaybackBufferSeconds,
|
|
509
|
+
onEvent: (...args) => ownerRef.current?.store === store ? ownerRef.current.onEvent?.(...args) : void 0,
|
|
510
|
+
onError: (...args) => ownerRef.current?.store === store ? ownerRef.current.onError?.(...args) : void 0
|
|
511
|
+
});
|
|
512
|
+
return store;
|
|
513
|
+
}, [
|
|
514
|
+
model,
|
|
515
|
+
token,
|
|
516
|
+
sessionEndpoint,
|
|
517
|
+
websocket,
|
|
518
|
+
protocols,
|
|
519
|
+
startupTimeoutMs,
|
|
520
|
+
closeTimeoutMs,
|
|
521
|
+
rtcDisconnectTimeoutMs,
|
|
522
|
+
sessionConfig,
|
|
523
|
+
sampleRate,
|
|
524
|
+
maxEvents,
|
|
525
|
+
maxPlaybackBufferSeconds
|
|
526
|
+
]);
|
|
527
|
+
useInsertionEffect(() => {
|
|
528
|
+
ownerRef.current = {
|
|
529
|
+
store: rt,
|
|
530
|
+
onToolCall,
|
|
531
|
+
onEvent,
|
|
532
|
+
onError
|
|
533
|
+
};
|
|
534
|
+
rt.onToolCall = onToolCall == null ? void 0 : (...args) => ownerRef.current?.store === rt ? ownerRef.current.onToolCall?.(...args) : void 0;
|
|
535
|
+
return () => {
|
|
536
|
+
ownerRef.current = null;
|
|
537
|
+
};
|
|
538
|
+
});
|
|
539
|
+
useIsomorphicLayoutEffect(() => {
|
|
540
|
+
return () => rt.dispose();
|
|
541
|
+
}, [rt]);
|
|
542
|
+
const actions = useMemo(() => {
|
|
543
|
+
const current = () => {
|
|
544
|
+
if (ownerRef.current == null) throw new Error("Realtime controls require a mounted hook");
|
|
545
|
+
return ownerRef.current.store;
|
|
546
|
+
};
|
|
547
|
+
return {
|
|
548
|
+
connect: async (options) => {
|
|
549
|
+
const store = current();
|
|
550
|
+
return options == null ? store.connect() : store.connect(options);
|
|
551
|
+
},
|
|
552
|
+
close: async (options) => current().close(options),
|
|
553
|
+
resumePlayback: async () => current().resumePlayback(),
|
|
554
|
+
resumeAudioCapture: async () => current().resumeAudioCapture(),
|
|
555
|
+
disconnect: () => current().disconnect(),
|
|
556
|
+
addToolOutput: (callId, result) => current().addToolOutput(callId, result),
|
|
557
|
+
sendEvent: (event) => current().sendEvent(event),
|
|
558
|
+
sendTextMessage: (text) => current().sendTextMessage(text),
|
|
559
|
+
sendAudio: (audio) => current().sendAudio(audio),
|
|
560
|
+
commitAudio: () => current().commitAudio(),
|
|
561
|
+
clearAudioBuffer: () => current().clearAudioBuffer(),
|
|
562
|
+
requestResponse: (options) => current().requestResponse(options),
|
|
563
|
+
cancelResponse: () => current().cancelResponse(),
|
|
564
|
+
startAudioCapture: (stream) => current().startAudioCapture(stream),
|
|
565
|
+
stopAudioCapture: () => current().stopAudioCapture(),
|
|
566
|
+
stopPlayback: () => current().stopPlayback()
|
|
567
|
+
};
|
|
568
|
+
}, []);
|
|
569
|
+
return {
|
|
570
|
+
status: useSyncExternalStore(useCallback((cb) => rt.subscribe("status", cb), [rt]), () => rt.status, () => rt.status),
|
|
571
|
+
messages: useSyncExternalStore(useCallback((cb) => rt.subscribe("messages", cb), [rt]), () => rt.messages, () => rt.messages),
|
|
572
|
+
events: useSyncExternalStore(useCallback((cb) => rt.subscribe("events", cb), [rt]), () => rt.events, () => rt.events),
|
|
573
|
+
isCapturing: useSyncExternalStore(useCallback((cb) => rt.subscribe("isCapturing", cb), [rt]), () => rt.isCapturing, () => rt.isCapturing),
|
|
574
|
+
isPlaying: useSyncExternalStore(useCallback((cb) => rt.subscribe("isPlaying", cb), [rt]), () => rt.isPlaying, () => rt.isPlaying),
|
|
575
|
+
session: useSyncExternalStore(useCallback((cb) => rt.subscribe("session", cb), [rt]), () => rt.session, () => rt.session),
|
|
576
|
+
...actions
|
|
577
|
+
};
|
|
748
578
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
// src/mcp-apps/bridge.ts
|
|
758
|
-
import { isJSONObject } from "@ai-sdk/provider";
|
|
759
|
-
var MCP_APP_PROTOCOL_VERSION = "2026-01-26";
|
|
579
|
+
const experimental_useRealtime = useRealtime;
|
|
580
|
+
//#endregion
|
|
581
|
+
//#region src/mcp-apps/bridge.ts
|
|
582
|
+
const MCP_APP_PROTOCOL_VERSION = "2026-01-26";
|
|
583
|
+
/**
|
|
584
|
+
* Checks whether an iframe message looks like a JSON-RPC 2.0 message.
|
|
585
|
+
*/
|
|
760
586
|
function isJsonRpcMessage(value) {
|
|
761
|
-
|
|
587
|
+
return value != null && typeof value === "object" && !Array.isArray(value) && "jsonrpc" in value && value.jsonrpc === "2.0";
|
|
762
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* Checks whether a JSON-RPC message expects a response.
|
|
591
|
+
*/
|
|
763
592
|
function isRequest(message) {
|
|
764
|
-
|
|
593
|
+
return "method" in message && "id" in message;
|
|
765
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Checks whether a JSON-RPC message is a fire-and-forget notification.
|
|
597
|
+
*/
|
|
766
598
|
function isNotification(message) {
|
|
767
|
-
|
|
599
|
+
return "method" in message && !("id" in message);
|
|
768
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Normalizes unknown thrown values into an `Error`.
|
|
603
|
+
*/
|
|
769
604
|
function toError(error) {
|
|
770
|
-
|
|
605
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
771
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* Validates the params for app-initiated `tools/call` requests.
|
|
609
|
+
*/
|
|
772
610
|
function assertToolCallParams(params) {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
arguments: isJSONObject(params.arguments) ? params.arguments : void 0
|
|
779
|
-
};
|
|
611
|
+
if (!isJSONObject(params) || typeof params.name !== "string") throw new Error("Invalid tools/call params");
|
|
612
|
+
return {
|
|
613
|
+
name: params.name,
|
|
614
|
+
arguments: isJSONObject(params.arguments) ? params.arguments : void 0
|
|
615
|
+
};
|
|
780
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Validates `resources/read` params and limits reads to `ui://` app resources.
|
|
619
|
+
*/
|
|
781
620
|
function assertResourceReadParams(params) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (!params.uri.startsWith("ui://")) {
|
|
786
|
-
throw new Error(
|
|
787
|
-
`resources/read is limited to ui:// resources: ${params.uri}`
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
return { uri: params.uri };
|
|
621
|
+
if (!isJSONObject(params) || typeof params.uri !== "string") throw new Error("Invalid resources/read params");
|
|
622
|
+
if (!params.uri.startsWith("ui://")) throw new Error(`resources/read is limited to ui:// resources: ${params.uri}`);
|
|
623
|
+
return { uri: params.uri };
|
|
791
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* Validates `ui/open-link` params and allows only `https:`/`http:`/`mailto:`
|
|
627
|
+
* URLs.
|
|
628
|
+
*/
|
|
792
629
|
function assertOpenLinkParams(params) {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (scheme !== "https:" && scheme !== "http:" && scheme !== "mailto:") {
|
|
803
|
-
throw new Error(`Disallowed ui/open-link scheme: ${scheme}`);
|
|
804
|
-
}
|
|
805
|
-
return { url: params.url };
|
|
630
|
+
if (!isJSONObject(params) || typeof params.url !== "string") throw new Error("Invalid ui/open-link params");
|
|
631
|
+
let scheme;
|
|
632
|
+
try {
|
|
633
|
+
scheme = new URL(params.url).protocol;
|
|
634
|
+
} catch {
|
|
635
|
+
throw new Error(`Invalid ui/open-link url: ${params.url}`);
|
|
636
|
+
}
|
|
637
|
+
if (scheme !== "https:" && scheme !== "http:" && scheme !== "mailto:") throw new Error(`Disallowed ui/open-link scheme: ${scheme}`);
|
|
638
|
+
return { url: params.url };
|
|
806
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* Validates params for `ui/request-display-mode`.
|
|
642
|
+
*/
|
|
807
643
|
function assertDisplayModeParams(params) {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
return { mode: params.mode };
|
|
644
|
+
if (!isJSONObject(params) || params.mode !== "inline" && params.mode !== "fullscreen" && params.mode !== "pip") throw new Error("Invalid ui/request-display-mode params");
|
|
645
|
+
return { mode: params.mode };
|
|
812
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Host-side JSON-RPC bridge for one MCP App iframe.
|
|
649
|
+
*
|
|
650
|
+
* It handles the MCP Apps initialization handshake, sends tool input/result
|
|
651
|
+
* notifications to the iframe, and proxies allowed iframe requests through
|
|
652
|
+
* host-provided callbacks.
|
|
653
|
+
*
|
|
654
|
+
* @example
|
|
655
|
+
* ```ts
|
|
656
|
+
* const bridge = new MCPAppBridge({
|
|
657
|
+
* targetWindow: iframe.contentWindow!,
|
|
658
|
+
* handlers: {
|
|
659
|
+
* allowedTools: ['refreshDashboardData'],
|
|
660
|
+
* callTool: params => client.callTool(params),
|
|
661
|
+
* },
|
|
662
|
+
* });
|
|
663
|
+
*
|
|
664
|
+
* window.addEventListener('message', event => bridge.handleMessage(event));
|
|
665
|
+
* bridge.sendToolInput({ topic: 'usage' });
|
|
666
|
+
* ```
|
|
667
|
+
*/
|
|
813
668
|
var MCPAppBridge = class {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
this.pendingNotifications = [];
|
|
1060
|
-
for (const notification of notifications) {
|
|
1061
|
-
this.post(notification);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
/**
|
|
1065
|
-
* Sends a host-initiated JSON-RPC request to the iframe.
|
|
1066
|
-
*/
|
|
1067
|
-
request(method, params) {
|
|
1068
|
-
const id = this.nextRequestId++;
|
|
1069
|
-
this.post({ jsonrpc: "2.0", id, method, params });
|
|
1070
|
-
return new Promise((resolve2, reject) => {
|
|
1071
|
-
this.pendingResponses.set(id, { resolve: resolve2, reject });
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
/**
|
|
1075
|
-
* Posts a JSON-RPC message to the sandbox proxy iframe.
|
|
1076
|
-
*/
|
|
1077
|
-
post(message) {
|
|
1078
|
-
this.targetWindow.postMessage(message, this.targetOrigin);
|
|
1079
|
-
}
|
|
669
|
+
constructor({ targetWindow, targetOrigin = "*", handlers = {}, hostInfo = {
|
|
670
|
+
name: "ai-sdk-react",
|
|
671
|
+
version: "1.0.0"
|
|
672
|
+
}, hostContext = { displayMode: "inline" } }) {
|
|
673
|
+
this.initialized = false;
|
|
674
|
+
this.pendingNotifications = [];
|
|
675
|
+
this.nextRequestId = 0;
|
|
676
|
+
this.pendingResponses = /* @__PURE__ */ new Map();
|
|
677
|
+
this.targetWindow = targetWindow;
|
|
678
|
+
this.targetOrigin = targetOrigin;
|
|
679
|
+
this.handlers = handlers;
|
|
680
|
+
this.hostInfo = hostInfo;
|
|
681
|
+
this.hostContext = hostContext;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Replaces the callbacks used to serve iframe requests.
|
|
685
|
+
*/
|
|
686
|
+
setHandlers(handlers) {
|
|
687
|
+
this.handlers = handlers;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Updates host context and notifies the iframe after initialization.
|
|
691
|
+
*
|
|
692
|
+
* @example
|
|
693
|
+
* ```ts
|
|
694
|
+
* bridge.setHostContext({ theme: 'dark', displayMode: 'inline' });
|
|
695
|
+
* ```
|
|
696
|
+
*/
|
|
697
|
+
setHostContext(hostContext) {
|
|
698
|
+
this.hostContext = hostContext;
|
|
699
|
+
this.sendNotification({
|
|
700
|
+
method: "ui/notifications/host-context-changed",
|
|
701
|
+
params: hostContext
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Whether a `message` event came from the expected proxy window and origin.
|
|
706
|
+
* The origin check is skipped only when `targetOrigin` is the `'*'` default.
|
|
707
|
+
* Callers that intercept events before {@link handleMessage} share this check.
|
|
708
|
+
*/
|
|
709
|
+
acceptsEvent(event) {
|
|
710
|
+
return event.source === this.targetWindow && (this.targetOrigin === "*" || event.origin === this.targetOrigin);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Processes one `message` event from the sandbox proxy iframe.
|
|
714
|
+
*/
|
|
715
|
+
handleMessage(event) {
|
|
716
|
+
if (!this.acceptsEvent(event) || !isJsonRpcMessage(event.data)) return;
|
|
717
|
+
const message = event.data;
|
|
718
|
+
if ("result" in message || "error" in message) {
|
|
719
|
+
this.handleResponse(message);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (isRequest(message)) {
|
|
723
|
+
this.handleRequest(message);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (isNotification(message)) this.handleNotification(message);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Sends app HTML and sandbox settings to the sandbox proxy.
|
|
730
|
+
*/
|
|
731
|
+
sendSandboxResourceReady(params) {
|
|
732
|
+
this.post({
|
|
733
|
+
jsonrpc: "2.0",
|
|
734
|
+
method: "ui/notifications/sandbox-resource-ready",
|
|
735
|
+
params
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Sends final tool arguments to the MCP App.
|
|
740
|
+
*/
|
|
741
|
+
sendToolInput(input) {
|
|
742
|
+
this.sendNotification({
|
|
743
|
+
method: "ui/notifications/tool-input",
|
|
744
|
+
params: { arguments: input }
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Sends a completed MCP tool result to the MCP App.
|
|
749
|
+
*/
|
|
750
|
+
sendToolResult(result) {
|
|
751
|
+
this.sendNotification({
|
|
752
|
+
method: "ui/notifications/tool-result",
|
|
753
|
+
params: result
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Notifies the MCP App that the related tool call was cancelled.
|
|
758
|
+
*/
|
|
759
|
+
sendToolCancelled(reason) {
|
|
760
|
+
this.sendNotification({
|
|
761
|
+
method: "ui/notifications/tool-cancelled",
|
|
762
|
+
params: reason != null ? { reason } : {}
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Requests graceful teardown before the host removes the iframe.
|
|
767
|
+
*/
|
|
768
|
+
teardownResource() {
|
|
769
|
+
return this.request("ui/resource-teardown", {});
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Rejects pending bridge requests and clears queued notifications.
|
|
773
|
+
*/
|
|
774
|
+
close() {
|
|
775
|
+
for (const pending of this.pendingResponses.values()) pending.reject(/* @__PURE__ */ new Error("MCP App bridge closed"));
|
|
776
|
+
this.pendingResponses.clear();
|
|
777
|
+
this.pendingNotifications = [];
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Resolves or rejects a host-initiated request when the iframe responds.
|
|
781
|
+
*/
|
|
782
|
+
handleResponse(response) {
|
|
783
|
+
const pending = this.pendingResponses.get(response.id);
|
|
784
|
+
if (pending == null) return;
|
|
785
|
+
this.pendingResponses.delete(response.id);
|
|
786
|
+
if (response.error != null) pending.reject(new Error(response.error.message));
|
|
787
|
+
else pending.resolve(response.result);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Runs a handler for an iframe request and posts the JSON-RPC response.
|
|
791
|
+
*/
|
|
792
|
+
async handleRequest(request) {
|
|
793
|
+
try {
|
|
794
|
+
const result = await this.getRequestResult(request);
|
|
795
|
+
this.post({
|
|
796
|
+
jsonrpc: "2.0",
|
|
797
|
+
id: request.id,
|
|
798
|
+
result
|
|
799
|
+
});
|
|
800
|
+
} catch (error) {
|
|
801
|
+
const normalizedError = toError(error);
|
|
802
|
+
this.handlers.onError?.(normalizedError);
|
|
803
|
+
this.post({
|
|
804
|
+
jsonrpc: "2.0",
|
|
805
|
+
id: request.id,
|
|
806
|
+
error: {
|
|
807
|
+
code: -32603,
|
|
808
|
+
message: normalizedError.message
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Maps supported iframe request methods to host callbacks.
|
|
815
|
+
*/
|
|
816
|
+
async getRequestResult(request) {
|
|
817
|
+
switch (request.method) {
|
|
818
|
+
case "ui/initialize": return {
|
|
819
|
+
protocolVersion: MCP_APP_PROTOCOL_VERSION,
|
|
820
|
+
hostCapabilities: {
|
|
821
|
+
...this.handlers.callTool != null ? { serverTools: {} } : {},
|
|
822
|
+
...this.handlers.readResource != null ? { serverResources: {} } : {},
|
|
823
|
+
...this.handlers.onLog != null ? { logging: {} } : {}
|
|
824
|
+
},
|
|
825
|
+
hostInfo: this.hostInfo,
|
|
826
|
+
hostContext: this.hostContext
|
|
827
|
+
};
|
|
828
|
+
case "tools/call": {
|
|
829
|
+
if (this.handlers.callTool == null) throw new Error("No tools/call handler configured");
|
|
830
|
+
const params = assertToolCallParams(request.params);
|
|
831
|
+
if (this.handlers.allowedTools == null || !this.handlers.allowedTools.includes(params.name)) throw new Error(`Tool is not app-visible: ${params.name}`);
|
|
832
|
+
return this.handlers.callTool(params);
|
|
833
|
+
}
|
|
834
|
+
case "resources/read":
|
|
835
|
+
if (this.handlers.readResource == null) throw new Error("No resources/read handler configured");
|
|
836
|
+
return this.handlers.readResource(assertResourceReadParams(request.params));
|
|
837
|
+
case "resources/list":
|
|
838
|
+
if (this.handlers.listResources == null) throw new Error("No resources/list handler configured");
|
|
839
|
+
return this.handlers.listResources(request.params);
|
|
840
|
+
case "ui/open-link":
|
|
841
|
+
if (this.handlers.openLink == null) throw new Error("No ui/open-link handler configured");
|
|
842
|
+
return this.handlers.openLink(assertOpenLinkParams(request.params));
|
|
843
|
+
case "ui/message": return this.handlers.sendMessage?.(request.params) ?? {};
|
|
844
|
+
case "ui/update-model-context": return this.handlers.updateModelContext?.(request.params) ?? {};
|
|
845
|
+
case "ui/request-display-mode": return this.handlers.requestDisplayMode?.(assertDisplayModeParams(request.params)) ?? { mode: this.hostContext.displayMode ?? "inline" };
|
|
846
|
+
default: throw new Error(`Unsupported MCP App method: ${request.method}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Handles iframe lifecycle and telemetry notifications.
|
|
851
|
+
*/
|
|
852
|
+
handleNotification(notification) {
|
|
853
|
+
switch (notification.method) {
|
|
854
|
+
case "ui/notifications/initialized":
|
|
855
|
+
this.initialized = true;
|
|
856
|
+
this.flushNotifications();
|
|
857
|
+
this.handlers.onInitialized?.();
|
|
858
|
+
break;
|
|
859
|
+
case "ui/notifications/size-changed":
|
|
860
|
+
this.handlers.onSizeChange?.(notification.params);
|
|
861
|
+
break;
|
|
862
|
+
case "ui/notifications/request-teardown":
|
|
863
|
+
this.handlers.onRequestTeardown?.(notification.params);
|
|
864
|
+
break;
|
|
865
|
+
case "notifications/message": this.handlers.onLog?.(notification.params);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Sends a host-to-iframe notification, queueing it until app initialization.
|
|
870
|
+
*/
|
|
871
|
+
sendNotification(notification) {
|
|
872
|
+
const message = {
|
|
873
|
+
jsonrpc: "2.0",
|
|
874
|
+
...notification
|
|
875
|
+
};
|
|
876
|
+
if (!this.initialized && !notification.method.includes("sandbox")) {
|
|
877
|
+
this.pendingNotifications.push(message);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
this.post(message);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Sends notifications that were queued before `ui/notifications/initialized`.
|
|
884
|
+
*/
|
|
885
|
+
flushNotifications() {
|
|
886
|
+
const notifications = this.pendingNotifications;
|
|
887
|
+
this.pendingNotifications = [];
|
|
888
|
+
for (const notification of notifications) this.post(notification);
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Sends a host-initiated JSON-RPC request to the iframe.
|
|
892
|
+
*/
|
|
893
|
+
request(method, params) {
|
|
894
|
+
const id = this.nextRequestId++;
|
|
895
|
+
this.post({
|
|
896
|
+
jsonrpc: "2.0",
|
|
897
|
+
id,
|
|
898
|
+
method,
|
|
899
|
+
params
|
|
900
|
+
});
|
|
901
|
+
return new Promise((resolve, reject) => {
|
|
902
|
+
this.pendingResponses.set(id, {
|
|
903
|
+
resolve,
|
|
904
|
+
reject
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Posts a JSON-RPC message to the sandbox proxy iframe.
|
|
910
|
+
*/
|
|
911
|
+
post(message) {
|
|
912
|
+
this.targetWindow.postMessage(message, this.targetOrigin);
|
|
913
|
+
}
|
|
1080
914
|
};
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
915
|
+
//#endregion
|
|
916
|
+
//#region src/mcp-apps/sandbox.ts
|
|
917
|
+
const ALLOWED_CSP_SCHEMES = /* @__PURE__ */ new Set(["https:", "wss:"]);
|
|
1084
918
|
function sanitizeCSPSources(sources) {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
if (/["'`\s;,]/.test(origin)) {
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
1103
|
-
result.push(origin);
|
|
1104
|
-
}
|
|
1105
|
-
return result;
|
|
919
|
+
const result = [];
|
|
920
|
+
for (const source of sources ?? []) {
|
|
921
|
+
if (typeof source !== "string") continue;
|
|
922
|
+
let origin;
|
|
923
|
+
try {
|
|
924
|
+
const url = new URL(source);
|
|
925
|
+
if (!ALLOWED_CSP_SCHEMES.has(url.protocol) || url.host.length === 0 || url.host === "*") continue;
|
|
926
|
+
origin = url.origin;
|
|
927
|
+
} catch {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if (/["'`\s;,]/.test(origin)) continue;
|
|
931
|
+
result.push(origin);
|
|
932
|
+
}
|
|
933
|
+
return result;
|
|
1106
934
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
935
|
+
/**
|
|
936
|
+
* Converts MCP App CSP metadata into a Content-Security-Policy string.
|
|
937
|
+
*
|
|
938
|
+
* The returned value is meant to be passed to a sandbox proxy, which can apply
|
|
939
|
+
* it to the inner iframe document.
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* ```ts
|
|
943
|
+
* const csp = getMCPAppCSP({
|
|
944
|
+
* connectDomains: ['https://api.example.com'],
|
|
945
|
+
* resourceDomains: ['https://cdn.example.com'],
|
|
946
|
+
* });
|
|
947
|
+
* ```
|
|
948
|
+
*/
|
|
1109
949
|
function getMCPAppCSP(csp) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
`frame-src ${frameSrc.join(" ")}`
|
|
1130
|
-
].join("; ");
|
|
950
|
+
if (csp == null) return;
|
|
951
|
+
const connectSrc = ["'self'", ...sanitizeCSPSources(csp.connectDomains)];
|
|
952
|
+
const imgSrc = [
|
|
953
|
+
"'self'",
|
|
954
|
+
"data:",
|
|
955
|
+
...sanitizeCSPSources(csp.resourceDomains)
|
|
956
|
+
];
|
|
957
|
+
const frameSrc = ["'self'", ...sanitizeCSPSources(csp.frameDomains)];
|
|
958
|
+
return [
|
|
959
|
+
"default-src 'none'",
|
|
960
|
+
"base-uri 'none'",
|
|
961
|
+
"form-action 'none'",
|
|
962
|
+
"script-src 'unsafe-inline'",
|
|
963
|
+
"style-src 'unsafe-inline'",
|
|
964
|
+
`connect-src ${connectSrc.join(" ")}`,
|
|
965
|
+
`img-src ${imgSrc.join(" ")}`,
|
|
966
|
+
`font-src ${imgSrc.join(" ")}`,
|
|
967
|
+
`frame-src ${frameSrc.join(" ")}`
|
|
968
|
+
].join("; ");
|
|
1131
969
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
970
|
+
const MCP_APP_PERMISSION_FEATURES = {
|
|
971
|
+
camera: "camera",
|
|
972
|
+
microphone: "microphone",
|
|
973
|
+
geolocation: "geolocation",
|
|
974
|
+
clipboardWrite: "clipboard-write"
|
|
1137
975
|
};
|
|
976
|
+
/**
|
|
977
|
+
* Converts MCP App permission metadata into an iframe `allow` attribute.
|
|
978
|
+
*
|
|
979
|
+
* Deny-by-default: a capability is granted only when it is both requested in
|
|
980
|
+
* `permissions` and present in the host `allowedPermissions` allowlist.
|
|
981
|
+
* Omitting the allowlist grants nothing, mirroring `allowedTools` in the bridge.
|
|
982
|
+
*
|
|
983
|
+
* @example
|
|
984
|
+
* ```ts
|
|
985
|
+
* const allow = getMCPAppAllowAttribute(
|
|
986
|
+
* { microphone: {}, camera: {} },
|
|
987
|
+
* ['microphone'],
|
|
988
|
+
* );
|
|
989
|
+
* // "microphone" — camera requested by server but not host-allowed
|
|
990
|
+
* ```
|
|
991
|
+
*/
|
|
1138
992
|
function getMCPAppAllowAttribute(permissions, allowedPermissions) {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
const allow = allowedPermissions.filter((permission) => Boolean(permissions[permission])).map((permission) => MCP_APP_PERMISSION_FEATURES[permission]);
|
|
1143
|
-
return allow.length > 0 ? allow.join("; ") : void 0;
|
|
993
|
+
if (permissions == null || allowedPermissions == null) return;
|
|
994
|
+
const allow = allowedPermissions.filter((permission) => Boolean(permissions[permission])).map((permission) => MCP_APP_PERMISSION_FEATURES[permission]);
|
|
995
|
+
return allow.length > 0 ? allow.join("; ") : void 0;
|
|
1144
996
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/mcp-apps/utils.ts
|
|
999
|
+
/**
|
|
1000
|
+
* Extracts MCP App metadata from an AI SDK tool UI part.
|
|
1001
|
+
*
|
|
1002
|
+
* @example
|
|
1003
|
+
* ```ts
|
|
1004
|
+
* const app = getMCPAppFromToolPart({
|
|
1005
|
+
* type: 'dynamic-tool',
|
|
1006
|
+
* toolName: 'showDashboard',
|
|
1007
|
+
* toolCallId: 'call-1',
|
|
1008
|
+
* state: 'input-available',
|
|
1009
|
+
* input: { topic: 'usage' },
|
|
1010
|
+
* toolMetadata: {
|
|
1011
|
+
* app: {
|
|
1012
|
+
* resourceUri: 'ui://example/dashboard',
|
|
1013
|
+
* mimeType: 'text/html;profile=mcp-app',
|
|
1014
|
+
* },
|
|
1015
|
+
* },
|
|
1016
|
+
* });
|
|
1017
|
+
* // { resourceUri: 'ui://example/dashboard', mimeType: 'text/html;profile=mcp-app' }
|
|
1018
|
+
* ```
|
|
1019
|
+
*/
|
|
1148
1020
|
function getMCPAppFromToolPart(part) {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
))) {
|
|
1154
|
-
return void 0;
|
|
1155
|
-
}
|
|
1156
|
-
return appMetadata;
|
|
1021
|
+
const rawAppMetadata = part.toolMetadata?.app;
|
|
1022
|
+
const appMetadata = isJSONObject(rawAppMetadata) ? rawAppMetadata : void 0;
|
|
1023
|
+
if (appMetadata == null || appMetadata.mimeType !== "text/html;profile=mcp-app" || typeof appMetadata.resourceUri !== "string" || !appMetadata.resourceUri.startsWith("ui://") || appMetadata.visibility != null && (!Array.isArray(appMetadata.visibility) || appMetadata.visibility.some((value) => value !== "model" && value !== "app"))) return;
|
|
1024
|
+
return appMetadata;
|
|
1157
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Converts an AI SDK tool output into the MCP Apps tool-result shape.
|
|
1028
|
+
*
|
|
1029
|
+
* MCP Apps expect tool results to have `content` and optional
|
|
1030
|
+
* `structuredContent`. MCP tools already return that shape, but typed AI SDK
|
|
1031
|
+
* tools may return only structured data. This helper wraps structured-only
|
|
1032
|
+
* outputs so the iframe can receive a valid tool result notification.
|
|
1033
|
+
*
|
|
1034
|
+
* @example
|
|
1035
|
+
* ```ts
|
|
1036
|
+
* normalizeMCPAppToolResult({ cards: [] });
|
|
1037
|
+
* // { content: [], structuredContent: { cards: [] } }
|
|
1038
|
+
* ```
|
|
1039
|
+
*/
|
|
1158
1040
|
function normalizeMCPAppToolResult(output) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
structuredContent: output
|
|
1165
|
-
};
|
|
1041
|
+
if (output != null && typeof output === "object" && "content" in output) return output;
|
|
1042
|
+
return {
|
|
1043
|
+
content: [],
|
|
1044
|
+
structuredContent: output
|
|
1045
|
+
};
|
|
1166
1046
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region src/mcp-apps/app-frame.tsx
|
|
1049
|
+
/**
|
|
1050
|
+
* Derives the concrete origin of the sandbox proxy from its URL, so outbound
|
|
1051
|
+
* postMessage targets a specific origin instead of `'*'` and inbound messages
|
|
1052
|
+
* can be origin-checked. The proxy must be served from a stable, concrete
|
|
1053
|
+
* origin (the default outer sandbox keeps `allow-same-origin`). Falls back to
|
|
1054
|
+
* the host origin on a malformed URL, never `'*'`.
|
|
1055
|
+
*/
|
|
1170
1056
|
function deriveTargetOrigin(url) {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1057
|
+
const location = typeof window !== "undefined" ? window.location : void 0;
|
|
1058
|
+
try {
|
|
1059
|
+
return new URL(url, location?.href).origin;
|
|
1060
|
+
} catch {
|
|
1061
|
+
return location?.origin ?? "null";
|
|
1062
|
+
}
|
|
1177
1063
|
}
|
|
1178
|
-
function sendToolState({
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}) {
|
|
1183
|
-
if (bridge == null) {
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
if (input !== void 0) {
|
|
1187
|
-
bridge.sendToolInput(input);
|
|
1188
|
-
}
|
|
1189
|
-
if (output !== void 0) {
|
|
1190
|
-
bridge.sendToolResult(normalizeMCPAppToolResult(output));
|
|
1191
|
-
}
|
|
1064
|
+
function sendToolState({ bridge, input, output }) {
|
|
1065
|
+
if (bridge == null) return;
|
|
1066
|
+
if (input !== void 0) bridge.sendToolInput(input);
|
|
1067
|
+
if (output !== void 0) bridge.sendToolResult(normalizeMCPAppToolResult(output));
|
|
1192
1068
|
}
|
|
1193
|
-
function MCPAppFrame({
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
bridgeRef.current?.setHandlers(bridgeHandlers);
|
|
1287
|
-
}, [bridgeHandlers]);
|
|
1288
|
-
useEffect4(() => {
|
|
1289
|
-
if (hostContext != null) {
|
|
1290
|
-
bridgeRef.current?.setHostContext(hostContext);
|
|
1291
|
-
}
|
|
1292
|
-
}, [hostContext]);
|
|
1293
|
-
useEffect4(() => {
|
|
1294
|
-
if (initializedRef.current && input !== void 0) {
|
|
1295
|
-
bridgeRef.current?.sendToolInput(input);
|
|
1296
|
-
}
|
|
1297
|
-
}, [input]);
|
|
1298
|
-
useEffect4(() => {
|
|
1299
|
-
if (initializedRef.current && output !== void 0) {
|
|
1300
|
-
bridgeRef.current?.sendToolResult(normalizeMCPAppToolResult(output));
|
|
1301
|
-
}
|
|
1302
|
-
}, [output]);
|
|
1303
|
-
return /* @__PURE__ */ jsx(
|
|
1304
|
-
"iframe",
|
|
1305
|
-
{
|
|
1306
|
-
ref: iframeRef,
|
|
1307
|
-
title: "MCP App",
|
|
1308
|
-
"aria-label": sandbox.title ?? app.resourceUri,
|
|
1309
|
-
src: sandboxUrl,
|
|
1310
|
-
className: sandbox.className,
|
|
1311
|
-
style: sandbox.style,
|
|
1312
|
-
allow: resourceAllow,
|
|
1313
|
-
sandbox: sandbox.outerSandbox ?? MCP_APP_DEFAULT_OUTER_SANDBOX
|
|
1314
|
-
}
|
|
1315
|
-
);
|
|
1069
|
+
function MCPAppFrame({ app, resource, input, output, sandbox, handlers, hostInfo, hostContext }) {
|
|
1070
|
+
const iframeRef = useRef(null);
|
|
1071
|
+
const bridgeRef = useRef(void 0);
|
|
1072
|
+
const inputRef = useRef(input);
|
|
1073
|
+
const outputRef = useRef(output);
|
|
1074
|
+
const hostContextRef = useRef(hostContext);
|
|
1075
|
+
const initializedRef = useRef(false);
|
|
1076
|
+
inputRef.current = input;
|
|
1077
|
+
outputRef.current = output;
|
|
1078
|
+
hostContextRef.current = hostContext;
|
|
1079
|
+
const sandboxUrl = String(sandbox.url);
|
|
1080
|
+
const targetOrigin = sandbox.targetOrigin ?? deriveTargetOrigin(sandboxUrl);
|
|
1081
|
+
const resourceCSP = getMCPAppCSP(resource.meta?.csp);
|
|
1082
|
+
const resourceAllow = getMCPAppAllowAttribute(resource.meta?.permissions, sandbox.allowedPermissions);
|
|
1083
|
+
const innerSandbox = sandbox.innerSandbox ?? "allow-scripts allow-forms";
|
|
1084
|
+
const bridgeHandlers = useMemo(() => ({
|
|
1085
|
+
...handlers,
|
|
1086
|
+
onInitialized: () => {
|
|
1087
|
+
initializedRef.current = true;
|
|
1088
|
+
handlers?.onInitialized?.();
|
|
1089
|
+
sendToolState({
|
|
1090
|
+
bridge: bridgeRef.current,
|
|
1091
|
+
input: inputRef.current,
|
|
1092
|
+
output: outputRef.current
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
}), [handlers]);
|
|
1096
|
+
const bridgeHandlersRef = useRef(bridgeHandlers);
|
|
1097
|
+
bridgeHandlersRef.current = bridgeHandlers;
|
|
1098
|
+
useEffect(() => {
|
|
1099
|
+
const targetWindow = iframeRef.current?.contentWindow;
|
|
1100
|
+
if (targetWindow == null) return;
|
|
1101
|
+
initializedRef.current = false;
|
|
1102
|
+
const bridge = new MCPAppBridge({
|
|
1103
|
+
targetWindow,
|
|
1104
|
+
targetOrigin,
|
|
1105
|
+
handlers: bridgeHandlersRef.current,
|
|
1106
|
+
hostInfo,
|
|
1107
|
+
hostContext: hostContextRef.current
|
|
1108
|
+
});
|
|
1109
|
+
bridgeRef.current = bridge;
|
|
1110
|
+
const onMessage = (event) => {
|
|
1111
|
+
if (!bridge.acceptsEvent(event)) return;
|
|
1112
|
+
if (event.data?.jsonrpc === "2.0" && event.data.method === "ui/notifications/sandbox-proxy-ready") {
|
|
1113
|
+
bridge.sendSandboxResourceReady({
|
|
1114
|
+
html: resource.html,
|
|
1115
|
+
csp: resourceCSP,
|
|
1116
|
+
sandbox: innerSandbox,
|
|
1117
|
+
allow: resourceAllow
|
|
1118
|
+
});
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
bridge.handleMessage(event);
|
|
1122
|
+
};
|
|
1123
|
+
window.addEventListener("message", onMessage);
|
|
1124
|
+
return () => {
|
|
1125
|
+
initializedRef.current = false;
|
|
1126
|
+
window.removeEventListener("message", onMessage);
|
|
1127
|
+
bridge.teardownResource().catch(() => {});
|
|
1128
|
+
bridge.close();
|
|
1129
|
+
bridgeRef.current = void 0;
|
|
1130
|
+
};
|
|
1131
|
+
}, [
|
|
1132
|
+
hostInfo,
|
|
1133
|
+
innerSandbox,
|
|
1134
|
+
resource.html,
|
|
1135
|
+
resourceAllow,
|
|
1136
|
+
resourceCSP,
|
|
1137
|
+
sandboxUrl,
|
|
1138
|
+
targetOrigin
|
|
1139
|
+
]);
|
|
1140
|
+
useEffect(() => {
|
|
1141
|
+
bridgeRef.current?.setHandlers(bridgeHandlers);
|
|
1142
|
+
}, [bridgeHandlers]);
|
|
1143
|
+
useEffect(() => {
|
|
1144
|
+
if (hostContext != null) bridgeRef.current?.setHostContext(hostContext);
|
|
1145
|
+
}, [hostContext]);
|
|
1146
|
+
useEffect(() => {
|
|
1147
|
+
if (initializedRef.current && input !== void 0) bridgeRef.current?.sendToolInput(input);
|
|
1148
|
+
}, [input]);
|
|
1149
|
+
useEffect(() => {
|
|
1150
|
+
if (initializedRef.current && output !== void 0) bridgeRef.current?.sendToolResult(normalizeMCPAppToolResult(output));
|
|
1151
|
+
}, [output]);
|
|
1152
|
+
return /* @__PURE__ */ jsx("iframe", {
|
|
1153
|
+
ref: iframeRef,
|
|
1154
|
+
title: "MCP App",
|
|
1155
|
+
"aria-label": sandbox.title ?? app.resourceUri,
|
|
1156
|
+
src: sandboxUrl,
|
|
1157
|
+
className: sandbox.className,
|
|
1158
|
+
style: sandbox.style,
|
|
1159
|
+
allow: resourceAllow,
|
|
1160
|
+
sandbox: sandbox.outerSandbox ?? "allow-scripts allow-same-origin allow-forms"
|
|
1161
|
+
});
|
|
1316
1162
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1163
|
+
//#endregion
|
|
1164
|
+
//#region src/mcp-apps/app-renderer.tsx
|
|
1320
1165
|
function getToolPartOutput(part) {
|
|
1321
|
-
|
|
1166
|
+
return part.state === "output-available" ? part.output : void 0;
|
|
1322
1167
|
}
|
|
1323
1168
|
function getToolPartInput(part) {
|
|
1324
|
-
|
|
1169
|
+
return part.state === "input-available" || part.state === "output-available" ? part.input : void 0;
|
|
1325
1170
|
}
|
|
1326
|
-
function MCPAppRenderer({
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
const error = resourceProp == null ? loadedResourceForApp?.error : void 0;
|
|
1372
|
-
if (appForRender == null || error != null || resource == null) {
|
|
1373
|
-
return fallback;
|
|
1374
|
-
}
|
|
1375
|
-
return /* @__PURE__ */ jsx2(
|
|
1376
|
-
MCPAppFrame,
|
|
1377
|
-
{
|
|
1378
|
-
app: appForRender,
|
|
1379
|
-
resource,
|
|
1380
|
-
input: getToolPartInput(part),
|
|
1381
|
-
output: getToolPartOutput(part),
|
|
1382
|
-
sandbox,
|
|
1383
|
-
handlers,
|
|
1384
|
-
hostInfo,
|
|
1385
|
-
hostContext
|
|
1386
|
-
}
|
|
1387
|
-
);
|
|
1171
|
+
function MCPAppRenderer({ part, sandbox, resource: resourceProp, loadResource, handlers, hostInfo, hostContext, fallback = null }) {
|
|
1172
|
+
const app = getMCPAppFromToolPart(part);
|
|
1173
|
+
const [cachedApp, setCachedApp] = useState();
|
|
1174
|
+
const [loadedResource, setLoadedResource] = useState();
|
|
1175
|
+
useEffect(() => {
|
|
1176
|
+
if (app != null) setCachedApp((previous) => previous?.resourceUri === app.resourceUri ? previous : app);
|
|
1177
|
+
}, [app?.resourceUri]);
|
|
1178
|
+
const appForRender = app ?? cachedApp;
|
|
1179
|
+
useEffect(() => {
|
|
1180
|
+
if (appForRender == null || resourceProp != null || loadResource == null) return;
|
|
1181
|
+
let cancelled = false;
|
|
1182
|
+
const resourceUri = appForRender.resourceUri;
|
|
1183
|
+
loadResource(appForRender).then((resource) => {
|
|
1184
|
+
if (!cancelled) setLoadedResource({
|
|
1185
|
+
resourceUri,
|
|
1186
|
+
resource
|
|
1187
|
+
});
|
|
1188
|
+
}).catch((error) => {
|
|
1189
|
+
if (!cancelled) setLoadedResource({
|
|
1190
|
+
resourceUri,
|
|
1191
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
return () => {
|
|
1195
|
+
cancelled = true;
|
|
1196
|
+
};
|
|
1197
|
+
}, [
|
|
1198
|
+
appForRender?.resourceUri,
|
|
1199
|
+
loadResource,
|
|
1200
|
+
resourceProp
|
|
1201
|
+
]);
|
|
1202
|
+
const loadedResourceForApp = loadedResource?.resourceUri === appForRender?.resourceUri ? loadedResource : void 0;
|
|
1203
|
+
const resource = resourceProp ?? loadedResourceForApp?.resource;
|
|
1204
|
+
const error = resourceProp == null ? loadedResourceForApp?.error : void 0;
|
|
1205
|
+
if (appForRender == null || error != null || resource == null) return fallback;
|
|
1206
|
+
return /* @__PURE__ */ jsx(MCPAppFrame, {
|
|
1207
|
+
app: appForRender,
|
|
1208
|
+
resource,
|
|
1209
|
+
input: getToolPartInput(part),
|
|
1210
|
+
output: getToolPartOutput(part),
|
|
1211
|
+
sandbox,
|
|
1212
|
+
handlers,
|
|
1213
|
+
hostInfo,
|
|
1214
|
+
hostContext
|
|
1215
|
+
});
|
|
1388
1216
|
}
|
|
1217
|
+
//#endregion
|
|
1218
|
+
//#region src/index.ts
|
|
1219
|
+
/**
|
|
1220
|
+
* @deprecated Use `useObject` instead.
|
|
1221
|
+
*/
|
|
1222
|
+
const experimental_useObject = useObject;
|
|
1223
|
+
//#endregion
|
|
1224
|
+
export { Chat, MCPAppRenderer as experimental_MCPAppRenderer, experimental_useObject, experimental_useRealtime, useChat, useCompletion, useObject };
|
|
1389
1225
|
|
|
1390
|
-
// src/index.ts
|
|
1391
|
-
var experimental_useObject = useObject;
|
|
1392
|
-
export {
|
|
1393
|
-
Chat,
|
|
1394
|
-
MCPAppRenderer as experimental_MCPAppRenderer,
|
|
1395
|
-
experimental_useObject,
|
|
1396
|
-
experimental_useRealtime,
|
|
1397
|
-
useChat,
|
|
1398
|
-
useCompletion,
|
|
1399
|
-
useObject
|
|
1400
|
-
};
|
|
1401
1226
|
//# sourceMappingURL=index.js.map
|