@devicai/ui 0.47.0 → 0.49.0
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/cjs/api/client.js +68 -4
- package/dist/cjs/api/client.js.map +1 -1
- package/dist/cjs/api/types.js.map +1 -1
- package/dist/cjs/components/AICommandBar/useAICommandBar.js +2 -2
- package/dist/cjs/components/AICommandBar/useAICommandBar.js.map +1 -1
- package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js +2 -2
- package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
- package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js +2 -2
- package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js +8 -1
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js +2 -1
- package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js +36 -5
- package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/McpConnectForm.js +79 -0
- package/dist/cjs/components/IntegrationsModal/McpConnectForm.js.map +1 -0
- package/dist/cjs/components/IntegrationsModal/McpServersSection.js +234 -0
- package/dist/cjs/components/IntegrationsModal/McpServersSection.js.map +1 -0
- package/dist/cjs/components/IntegrationsModal/useTenantMcp.js +107 -0
- package/dist/cjs/components/IntegrationsModal/useTenantMcp.js.map +1 -0
- package/dist/cjs/hooks/useDevicChat.js +6 -7
- package/dist/cjs/hooks/useDevicChat.js.map +1 -1
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/styles.css +1 -1
- package/dist/cjs/utils/avatar.js +60 -0
- package/dist/cjs/utils/avatar.js.map +1 -0
- package/dist/esm/api/client.d.ts +48 -3
- package/dist/esm/api/client.js +68 -4
- package/dist/esm/api/client.js.map +1 -1
- package/dist/esm/api/types.d.ts +93 -0
- package/dist/esm/api/types.js.map +1 -1
- package/dist/esm/components/AICommandBar/useAICommandBar.js +2 -2
- package/dist/esm/components/AICommandBar/useAICommandBar.js.map +1 -1
- package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js +2 -2
- package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
- package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js +2 -2
- package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.js +8 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js +2 -1
- package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/IntegrationsModal.d.ts +8 -1
- package/dist/esm/components/IntegrationsModal/IntegrationsModal.js +36 -5
- package/dist/esm/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/McpConnectForm.d.ts +42 -0
- package/dist/esm/components/IntegrationsModal/McpConnectForm.js +77 -0
- package/dist/esm/components/IntegrationsModal/McpConnectForm.js.map +1 -0
- package/dist/esm/components/IntegrationsModal/McpServersSection.d.ts +25 -0
- package/dist/esm/components/IntegrationsModal/McpServersSection.js +232 -0
- package/dist/esm/components/IntegrationsModal/McpServersSection.js.map +1 -0
- package/dist/esm/components/IntegrationsModal/index.d.ts +6 -0
- package/dist/esm/components/IntegrationsModal/useTenantMcp.d.ts +40 -0
- package/dist/esm/components/IntegrationsModal/useTenantMcp.js +105 -0
- package/dist/esm/components/IntegrationsModal/useTenantMcp.js.map +1 -0
- package/dist/esm/hooks/useDevicChat.js +6 -7
- package/dist/esm/hooks/useDevicChat.js.map +1 -1
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/styles.css +1 -1
- package/dist/esm/utils/avatar.d.ts +18 -0
- package/dist/esm/utils/avatar.js +58 -0
- package/dist/esm/utils/avatar.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
|
+
import { McpConnectForm } from './McpConnectForm.js';
|
|
4
|
+
|
|
5
|
+
/** A random value tying an OAuth round trip to the window that started it. */
|
|
6
|
+
function newNonce() {
|
|
7
|
+
const c = typeof crypto !== "undefined" ? crypto : undefined;
|
|
8
|
+
if (c?.randomUUID)
|
|
9
|
+
return c.randomUUID();
|
|
10
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
11
|
+
}
|
|
12
|
+
function stateOf(server) {
|
|
13
|
+
const status = server.connection?.status;
|
|
14
|
+
if (status === "active")
|
|
15
|
+
return { key: "connected", label: "Connected" };
|
|
16
|
+
if (status === "error")
|
|
17
|
+
return { key: "reconnect", label: "Needs attention" };
|
|
18
|
+
if (status === "pending_auth")
|
|
19
|
+
return { key: "reconnect", label: "Waiting for sign-in" };
|
|
20
|
+
return { key: "disconnected", label: "Not connected" };
|
|
21
|
+
}
|
|
22
|
+
function matches(server, query) {
|
|
23
|
+
const q = query.trim().toLowerCase();
|
|
24
|
+
if (!q)
|
|
25
|
+
return true;
|
|
26
|
+
return (server.name.toLowerCase().includes(q) ||
|
|
27
|
+
server.url.toLowerCase().includes(q) ||
|
|
28
|
+
(server.description ?? "").toLowerCase().includes(q));
|
|
29
|
+
}
|
|
30
|
+
/** Identity of a row, stable whether or not it has been connected yet. */
|
|
31
|
+
function keyOf(server) {
|
|
32
|
+
return server.connection?.id ?? server.templateId ?? server.url;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The MCP servers section of the connected-apps panel.
|
|
36
|
+
*
|
|
37
|
+
* The end user sees one list: the servers their developer left ready to
|
|
38
|
+
* connect, the ones they added themselves, and — unless it was switched off —
|
|
39
|
+
* a button to add another. Both routes open the same form, because the only
|
|
40
|
+
* difference between them is who typed the url.
|
|
41
|
+
*
|
|
42
|
+
* Connecting opens a consent screen in a popup. The popup is opened empty
|
|
43
|
+
* *before* the request that produces its URL, because browsers only honour
|
|
44
|
+
* `window.open` inside the gesture that triggered it; opening it after the
|
|
45
|
+
* round trip is what gets it blocked. Two endings are watched, not one — a
|
|
46
|
+
* cancelled consent screen posts nothing back, so the close is watched too.
|
|
47
|
+
*/
|
|
48
|
+
function McpServersSection({ state, theme, query = "", }) {
|
|
49
|
+
const { servers, allowCustom, limits, loading, client, scope, refresh } = state;
|
|
50
|
+
const [actionError, setActionError] = useState(null);
|
|
51
|
+
const [busyKey, setBusyKey] = useState(null);
|
|
52
|
+
const [blockedUrl, setBlockedUrl] = useState(null);
|
|
53
|
+
/** The row whose form is open. `null` inside the object is "add your own". */
|
|
54
|
+
const [prompt, setPrompt] = useState(null);
|
|
55
|
+
const [formError, setFormError] = useState(null);
|
|
56
|
+
const pendingRef = useRef(null);
|
|
57
|
+
const popupRef = useRef(null);
|
|
58
|
+
const pollRef = useRef(null);
|
|
59
|
+
const finish = useCallback(() => {
|
|
60
|
+
if (pollRef.current !== null) {
|
|
61
|
+
window.clearInterval(pollRef.current);
|
|
62
|
+
pollRef.current = null;
|
|
63
|
+
}
|
|
64
|
+
pendingRef.current = null;
|
|
65
|
+
popupRef.current = null;
|
|
66
|
+
setBusyKey(null);
|
|
67
|
+
void refresh();
|
|
68
|
+
}, [refresh]);
|
|
69
|
+
// The callback page says it is done. A nudge, never the result: what is shown
|
|
70
|
+
// comes from re-reading the server, so a forged message costs one extra fetch
|
|
71
|
+
// and nothing else.
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const onMessage = (event) => {
|
|
74
|
+
const data = event.data;
|
|
75
|
+
if (!data || data.source !== "devic")
|
|
76
|
+
return;
|
|
77
|
+
if (data.type !== "mcp-connected")
|
|
78
|
+
return;
|
|
79
|
+
const pending = pendingRef.current;
|
|
80
|
+
if (!pending || data.returnTo !== pending.returnTo)
|
|
81
|
+
return;
|
|
82
|
+
popupRef.current?.close();
|
|
83
|
+
finish();
|
|
84
|
+
};
|
|
85
|
+
window.addEventListener("message", onMessage);
|
|
86
|
+
return () => window.removeEventListener("message", onMessage);
|
|
87
|
+
}, [finish]);
|
|
88
|
+
useEffect(() => () => {
|
|
89
|
+
if (pollRef.current !== null)
|
|
90
|
+
window.clearInterval(pollRef.current);
|
|
91
|
+
}, []);
|
|
92
|
+
const openPrompt = (server) => {
|
|
93
|
+
setFormError(null);
|
|
94
|
+
setActionError(null);
|
|
95
|
+
setBlockedUrl(null);
|
|
96
|
+
setPrompt({ server });
|
|
97
|
+
};
|
|
98
|
+
const submit = async (values) => {
|
|
99
|
+
if (!client || !prompt)
|
|
100
|
+
return;
|
|
101
|
+
const server = prompt.server;
|
|
102
|
+
const rowKey = server ? keyOf(server) : "new";
|
|
103
|
+
setBusyKey(rowKey);
|
|
104
|
+
setFormError(null);
|
|
105
|
+
const nonce = newNonce();
|
|
106
|
+
const returnTo = `${window.location.origin}/?devic_mcp=${nonce}`;
|
|
107
|
+
// Opened empty inside the submit gesture and navigated once the URL is
|
|
108
|
+
// known. Skipped when no browser is involved at all.
|
|
109
|
+
const popup = values.auth.mode === "oauth"
|
|
110
|
+
? window.open("", "devic-mcp-oauth", "width=520,height=680,menubar=no,toolbar=no")
|
|
111
|
+
: null;
|
|
112
|
+
try {
|
|
113
|
+
const existing = server?.connection;
|
|
114
|
+
const result = existing
|
|
115
|
+
? await client.reconnectMcpServer(existing.id, {
|
|
116
|
+
...scope,
|
|
117
|
+
returnTo,
|
|
118
|
+
auth: values.auth,
|
|
119
|
+
})
|
|
120
|
+
: await client.connectMcpServer({
|
|
121
|
+
...scope,
|
|
122
|
+
returnTo,
|
|
123
|
+
templateId: server?.templateId,
|
|
124
|
+
url: values.url,
|
|
125
|
+
name: values.name,
|
|
126
|
+
auth: values.auth,
|
|
127
|
+
});
|
|
128
|
+
if (result.requiresClientCredentials) {
|
|
129
|
+
// Not a failure the user can do nothing about: they own the server, so
|
|
130
|
+
// the form reopens with the fields and the redirect URI to authorise.
|
|
131
|
+
popup?.close();
|
|
132
|
+
setBusyKey(null);
|
|
133
|
+
setPrompt({
|
|
134
|
+
server,
|
|
135
|
+
callbackUrl: result.callbackUrl,
|
|
136
|
+
needsClientCredentials: true,
|
|
137
|
+
});
|
|
138
|
+
setFormError(result.error ?? null);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (result.status === "error") {
|
|
142
|
+
popup?.close();
|
|
143
|
+
setBusyKey(null);
|
|
144
|
+
setFormError(result.error ?? "The server could not be reached.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!result.authorizationUrl) {
|
|
148
|
+
// Connected outright: the key the user typed is the credential.
|
|
149
|
+
popup?.close();
|
|
150
|
+
setPrompt(null);
|
|
151
|
+
finish();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
pendingRef.current = { returnTo };
|
|
155
|
+
setPrompt(null);
|
|
156
|
+
if (popup && !popup.closed) {
|
|
157
|
+
popupRef.current = popup;
|
|
158
|
+
popup.location.href = result.authorizationUrl;
|
|
159
|
+
pollRef.current = window.setInterval(() => {
|
|
160
|
+
if (popup.closed)
|
|
161
|
+
finish();
|
|
162
|
+
}, 700);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Blocked (Safari, in-app browsers, extensions): hand over the URL.
|
|
166
|
+
setBlockedUrl({ url: result.authorizationUrl });
|
|
167
|
+
setBusyKey(null);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
popup?.close();
|
|
172
|
+
pendingRef.current = null;
|
|
173
|
+
setBusyKey(null);
|
|
174
|
+
setFormError(err instanceof Error ? err.message : String(err));
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const disconnect = async (server) => {
|
|
178
|
+
const connection = server.connection;
|
|
179
|
+
if (!client || !connection || busyKey)
|
|
180
|
+
return;
|
|
181
|
+
setBusyKey(keyOf(server));
|
|
182
|
+
setActionError(null);
|
|
183
|
+
try {
|
|
184
|
+
await client.disconnectMcpServer(connection.id, scope);
|
|
185
|
+
await refresh();
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
setActionError(err instanceof Error ? err.message : String(err));
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
setBusyKey(null);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const visible = servers.filter((s) => matches(s, query));
|
|
195
|
+
const atLimit = limits.used >= limits.maxServers;
|
|
196
|
+
// Nothing offered and nothing addable: the whole section would be a heading
|
|
197
|
+
// over an empty box, which says less than saying nothing.
|
|
198
|
+
if (!servers.length && !allowCustom)
|
|
199
|
+
return null;
|
|
200
|
+
return (jsxs("div", { className: "devic-int-mcp-section", children: [jsxs("div", { className: "devic-int-mcp-heading", children: [jsx("h4", { children: "MCP servers" }), jsx("span", { children: "Connect a server and its tools become available in this chat." })] }), actionError && jsx("div", { className: "devic-int-error", children: actionError }), blockedUrl && (jsxs("div", { className: "devic-int-notice", children: ["Your browser blocked the pop-up.", " ", jsx("a", { href: blockedUrl.url, target: "_blank", rel: "noopener noreferrer", onClick: () => {
|
|
201
|
+
pendingRef.current = null;
|
|
202
|
+
setBlockedUrl(null);
|
|
203
|
+
}, children: "Open the authorisation page" }), " ", "and come back \u2014 then use Refresh."] })), visible.length > 0 && (jsx("div", { className: "devic-int-grid", children: visible.map((server) => {
|
|
204
|
+
const cardState = stateOf(server);
|
|
205
|
+
const rowKey = keyOf(server);
|
|
206
|
+
const busy = busyKey === rowKey;
|
|
207
|
+
const connection = server.connection;
|
|
208
|
+
return (jsxs("div", { className: "devic-int-card", "data-state": cardState.key, children: [jsxs("div", { className: "devic-int-card-head", children: [server.logoUrl ? (jsx("img", { className: "devic-int-mcp-logo", src: server.logoUrl, alt: "", "aria-hidden": "true" })) : (jsx("span", { className: "devic-int-mcp-logo devic-int-mcp-logo-fallback", children: server.name.slice(0, 1).toUpperCase() })), jsxs("span", { className: "devic-int-state", children: [jsx("span", { className: "devic-int-dot", "data-ok": cardState.key === "connected", "data-off": cardState.key === "disconnected", "aria-hidden": "true" }), cardState.label] })] }), jsx("div", { className: "devic-int-name", title: server.name, children: server.name }), jsx("div", { className: "devic-int-description", title: server.description ?? server.url, children: server.description ?? server.url }), connection?.shared && (
|
|
209
|
+
// Worth saying before they wonder why they cannot remove it.
|
|
210
|
+
jsx("div", { className: "devic-int-mcp-note", children: "Connected for everyone on this account" })), connection?.status === "active" &&
|
|
211
|
+
typeof connection.toolCount === "number" && (jsxs("div", { className: "devic-int-mcp-note", children: [connection.toolCount, " tool", connection.toolCount === 1 ? "" : "s", " available"] })), connection?.lastProbeError && (jsx("div", { className: "devic-int-mcp-note devic-int-mcp-note-warn", title: connection.lastProbeError, children: connection.lastProbeError })), jsx("button", { type: "button", className: `devic-int-btn devic-int-btn-block${cardState.key === "connected" ? "" : " devic-int-btn-primary"}`, onClick: () => openPrompt(server), disabled: busy ||
|
|
212
|
+
!!busyKey ||
|
|
213
|
+
connection?.readOnly ||
|
|
214
|
+
(!connection && atLimit), title: connection?.readOnly
|
|
215
|
+
? "This server was connected for the whole account."
|
|
216
|
+
: !connection && atLimit
|
|
217
|
+
? `You can have ${limits.maxServers} connected at a time.`
|
|
218
|
+
: undefined, children: busy
|
|
219
|
+
? "Waiting…"
|
|
220
|
+
: cardState.key === "disconnected"
|
|
221
|
+
? "Connect"
|
|
222
|
+
: "Reconnect" }), connection && !connection.readOnly && (jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-block devic-int-mcp-remove", onClick: () => void disconnect(server), disabled: !!busyKey, children: "Disconnect" }))] }, rowKey));
|
|
223
|
+
}) })), allowCustom && (jsx("button", { type: "button", className: "devic-int-btn devic-int-mcp-add", onClick: () => openPrompt(null), disabled: loading || !!busyKey || atLimit, title: atLimit
|
|
224
|
+
? `You can have ${limits.maxServers} MCP server${limits.maxServers === 1 ? "" : "s"} connected at a time.`
|
|
225
|
+
: undefined, children: "+ Add your own MCP" })), prompt && (jsx(McpConnectForm, { server: prompt.server, callbackUrl: prompt.callbackUrl, needsClientCredentials: prompt.needsClientCredentials, submitting: busyKey === (prompt.server ? keyOf(prompt.server) : "new"), error: formError, theme: theme, onCancel: () => {
|
|
226
|
+
setPrompt(null);
|
|
227
|
+
setFormError(null);
|
|
228
|
+
}, onSubmit: submit }))] }));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export { McpServersSection };
|
|
232
|
+
//# sourceMappingURL=McpServersSection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpServersSection.js","sources":["../../../../src/components/IntegrationsModal/McpServersSection.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, type JSX } from \"react\";\nimport type {\n TenantMcpAuthInput,\n TenantMcpConnectResult,\n TenantMcpServer,\n} from \"../../api/types\";\nimport type { DevicTheme } from \"../theme\";\nimport { McpConnectForm } from \"./McpConnectForm\";\nimport type { TenantMcpState } from \"./useTenantMcp\";\n\nexport interface McpServersSectionProps {\n state: TenantMcpState;\n theme?: DevicTheme;\n /** Filter typed in the modal's search box. */\n query?: string;\n}\n\n/** Message the OAuth callback page posts back when an MCP connection is done. */\ninterface McpCallbackMessage {\n source?: string;\n type?: string;\n status?: string;\n returnTo?: string;\n}\n\n/** A random value tying an OAuth round trip to the window that started it. */\nfunction newNonce(): string {\n const c = typeof crypto !== \"undefined\" ? crypto : undefined;\n if (c?.randomUUID) return c.randomUUID();\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\nfunction stateOf(server: TenantMcpServer): {\n key: \"connected\" | \"reconnect\" | \"disconnected\";\n label: string;\n} {\n const status = server.connection?.status;\n if (status === \"active\") return { key: \"connected\", label: \"Connected\" };\n if (status === \"error\") return { key: \"reconnect\", label: \"Needs attention\" };\n if (status === \"pending_auth\")\n return { key: \"reconnect\", label: \"Waiting for sign-in\" };\n return { key: \"disconnected\", label: \"Not connected\" };\n}\n\nfunction matches(server: TenantMcpServer, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (\n server.name.toLowerCase().includes(q) ||\n server.url.toLowerCase().includes(q) ||\n (server.description ?? \"\").toLowerCase().includes(q)\n );\n}\n\n/** Identity of a row, stable whether or not it has been connected yet. */\nfunction keyOf(server: TenantMcpServer): string {\n return server.connection?.id ?? server.templateId ?? server.url;\n}\n\n/**\n * The MCP servers section of the connected-apps panel.\n *\n * The end user sees one list: the servers their developer left ready to\n * connect, the ones they added themselves, and — unless it was switched off —\n * a button to add another. Both routes open the same form, because the only\n * difference between them is who typed the url.\n *\n * Connecting opens a consent screen in a popup. The popup is opened empty\n * *before* the request that produces its URL, because browsers only honour\n * `window.open` inside the gesture that triggered it; opening it after the\n * round trip is what gets it blocked. Two endings are watched, not one — a\n * cancelled consent screen posts nothing back, so the close is watched too.\n */\nexport function McpServersSection({\n state,\n theme,\n query = \"\",\n}: McpServersSectionProps): JSX.Element | null {\n const { servers, allowCustom, limits, loading, client, scope, refresh } = state;\n\n const [actionError, setActionError] = useState<string | null>(null);\n const [busyKey, setBusyKey] = useState<string | null>(null);\n const [blockedUrl, setBlockedUrl] = useState<{ url: string } | null>(null);\n /** The row whose form is open. `null` inside the object is \"add your own\". */\n const [prompt, setPrompt] = useState<{\n server: TenantMcpServer | null;\n callbackUrl?: string;\n needsClientCredentials?: boolean;\n } | null>(null);\n const [formError, setFormError] = useState<string | null>(null);\n\n const pendingRef = useRef<{ returnTo: string } | null>(null);\n const popupRef = useRef<Window | null>(null);\n const pollRef = useRef<number | null>(null);\n\n const finish = useCallback(() => {\n if (pollRef.current !== null) {\n window.clearInterval(pollRef.current);\n pollRef.current = null;\n }\n pendingRef.current = null;\n popupRef.current = null;\n setBusyKey(null);\n void refresh();\n }, [refresh]);\n\n // The callback page says it is done. A nudge, never the result: what is shown\n // comes from re-reading the server, so a forged message costs one extra fetch\n // and nothing else.\n useEffect(() => {\n const onMessage = (event: MessageEvent) => {\n const data = event.data as McpCallbackMessage | undefined;\n if (!data || data.source !== \"devic\") return;\n if (data.type !== \"mcp-connected\") return;\n const pending = pendingRef.current;\n if (!pending || data.returnTo !== pending.returnTo) return;\n popupRef.current?.close();\n finish();\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [finish]);\n\n useEffect(\n () => () => {\n if (pollRef.current !== null) window.clearInterval(pollRef.current);\n },\n []\n );\n\n const openPrompt = (server: TenantMcpServer | null) => {\n setFormError(null);\n setActionError(null);\n setBlockedUrl(null);\n setPrompt({ server });\n };\n\n const submit = async (values: {\n url?: string;\n name?: string;\n auth: TenantMcpAuthInput;\n }) => {\n if (!client || !prompt) return;\n const server = prompt.server;\n const rowKey = server ? keyOf(server) : \"new\";\n setBusyKey(rowKey);\n setFormError(null);\n\n const nonce = newNonce();\n const returnTo = `${window.location.origin}/?devic_mcp=${nonce}`;\n // Opened empty inside the submit gesture and navigated once the URL is\n // known. Skipped when no browser is involved at all.\n const popup =\n values.auth.mode === \"oauth\"\n ? window.open(\n \"\",\n \"devic-mcp-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n )\n : null;\n\n try {\n const existing = server?.connection;\n const result: TenantMcpConnectResult = existing\n ? await client.reconnectMcpServer(existing.id, {\n ...scope,\n returnTo,\n auth: values.auth,\n })\n : await client.connectMcpServer({\n ...scope,\n returnTo,\n templateId: server?.templateId,\n url: values.url,\n name: values.name,\n auth: values.auth,\n });\n\n if (result.requiresClientCredentials) {\n // Not a failure the user can do nothing about: they own the server, so\n // the form reopens with the fields and the redirect URI to authorise.\n popup?.close();\n setBusyKey(null);\n setPrompt({\n server,\n callbackUrl: result.callbackUrl,\n needsClientCredentials: true,\n });\n setFormError(result.error ?? null);\n return;\n }\n\n if (result.status === \"error\") {\n popup?.close();\n setBusyKey(null);\n setFormError(result.error ?? \"The server could not be reached.\");\n return;\n }\n\n if (!result.authorizationUrl) {\n // Connected outright: the key the user typed is the credential.\n popup?.close();\n setPrompt(null);\n finish();\n return;\n }\n\n pendingRef.current = { returnTo };\n setPrompt(null);\n if (popup && !popup.closed) {\n popupRef.current = popup;\n popup.location.href = result.authorizationUrl;\n pollRef.current = window.setInterval(() => {\n if (popup.closed) finish();\n }, 700);\n } else {\n // Blocked (Safari, in-app browsers, extensions): hand over the URL.\n setBlockedUrl({ url: result.authorizationUrl });\n setBusyKey(null);\n }\n } catch (err) {\n popup?.close();\n pendingRef.current = null;\n setBusyKey(null);\n setFormError(err instanceof Error ? err.message : String(err));\n }\n };\n\n const disconnect = async (server: TenantMcpServer) => {\n const connection = server.connection;\n if (!client || !connection || busyKey) return;\n setBusyKey(keyOf(server));\n setActionError(null);\n try {\n await client.disconnectMcpServer(connection.id, scope);\n await refresh();\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyKey(null);\n }\n };\n\n const visible = servers.filter((s) => matches(s, query));\n const atLimit = limits.used >= limits.maxServers;\n\n // Nothing offered and nothing addable: the whole section would be a heading\n // over an empty box, which says less than saying nothing.\n if (!servers.length && !allowCustom) return null;\n\n return (\n <div className=\"devic-int-mcp-section\">\n <div className=\"devic-int-mcp-heading\">\n <h4>MCP servers</h4>\n <span>Connect a server and its tools become available in this chat.</span>\n </div>\n\n {actionError && <div className=\"devic-int-error\">{actionError}</div>}\n\n {blockedUrl && (\n <div className=\"devic-int-notice\">\n Your browser blocked the pop-up.{\" \"}\n <a\n href={blockedUrl.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => {\n pendingRef.current = null;\n setBlockedUrl(null);\n }}\n >\n Open the authorisation page\n </a>{\" \"}\n and come back — then use Refresh.\n </div>\n )}\n\n {visible.length > 0 && (\n <div className=\"devic-int-grid\">\n {visible.map((server) => {\n const cardState = stateOf(server);\n const rowKey = keyOf(server);\n const busy = busyKey === rowKey;\n const connection = server.connection;\n return (\n <div\n key={rowKey}\n className=\"devic-int-card\"\n data-state={cardState.key}\n >\n <div className=\"devic-int-card-head\">\n {server.logoUrl ? (\n <img\n className=\"devic-int-mcp-logo\"\n src={server.logoUrl}\n alt=\"\"\n aria-hidden=\"true\"\n />\n ) : (\n <span className=\"devic-int-mcp-logo devic-int-mcp-logo-fallback\">\n {server.name.slice(0, 1).toUpperCase()}\n </span>\n )}\n <span className=\"devic-int-state\">\n <span\n className=\"devic-int-dot\"\n data-ok={cardState.key === \"connected\"}\n data-off={cardState.key === \"disconnected\"}\n aria-hidden=\"true\"\n />\n {cardState.label}\n </span>\n </div>\n\n <div className=\"devic-int-name\" title={server.name}>\n {server.name}\n </div>\n\n <div\n className=\"devic-int-description\"\n title={server.description ?? server.url}\n >\n {server.description ?? server.url}\n </div>\n\n {connection?.shared && (\n // Worth saying before they wonder why they cannot remove it.\n <div className=\"devic-int-mcp-note\">\n Connected for everyone on this account\n </div>\n )}\n\n {connection?.status === \"active\" &&\n typeof connection.toolCount === \"number\" && (\n <div className=\"devic-int-mcp-note\">\n {connection.toolCount} tool\n {connection.toolCount === 1 ? \"\" : \"s\"} available\n </div>\n )}\n\n {connection?.lastProbeError && (\n <div\n className=\"devic-int-mcp-note devic-int-mcp-note-warn\"\n title={connection.lastProbeError}\n >\n {connection.lastProbeError}\n </div>\n )}\n\n <button\n type=\"button\"\n className={`devic-int-btn devic-int-btn-block${\n cardState.key === \"connected\" ? \"\" : \" devic-int-btn-primary\"\n }`}\n onClick={() => openPrompt(server)}\n disabled={\n busy ||\n !!busyKey ||\n connection?.readOnly ||\n (!connection && atLimit)\n }\n title={\n connection?.readOnly\n ? \"This server was connected for the whole account.\"\n : !connection && atLimit\n ? `You can have ${limits.maxServers} connected at a time.`\n : undefined\n }\n >\n {busy\n ? \"Waiting…\"\n : cardState.key === \"disconnected\"\n ? \"Connect\"\n : \"Reconnect\"}\n </button>\n\n {connection && !connection.readOnly && (\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-block devic-int-mcp-remove\"\n onClick={() => void disconnect(server)}\n disabled={!!busyKey}\n >\n Disconnect\n </button>\n )}\n </div>\n );\n })}\n </div>\n )}\n\n {allowCustom && (\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-mcp-add\"\n onClick={() => openPrompt(null)}\n disabled={loading || !!busyKey || atLimit}\n title={\n atLimit\n ? `You can have ${limits.maxServers} MCP server${\n limits.maxServers === 1 ? \"\" : \"s\"\n } connected at a time.`\n : undefined\n }\n >\n + Add your own MCP\n </button>\n )}\n\n {prompt && (\n <McpConnectForm\n server={prompt.server}\n callbackUrl={prompt.callbackUrl}\n needsClientCredentials={prompt.needsClientCredentials}\n submitting={\n busyKey === (prompt.server ? keyOf(prompt.server) : \"new\")\n }\n error={formError}\n theme={theme}\n onCancel={() => {\n setPrompt(null);\n setFormError(null);\n }}\n onSubmit={submit}\n />\n )}\n </div>\n );\n}\n\nexport default McpServersSection;\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
|
|
@@ -10,3 +10,9 @@ export { IntegrationLogo } from "./IntegrationLogo";
|
|
|
10
10
|
export type { IntegrationLogoProps } from "./IntegrationLogo";
|
|
11
11
|
export { useIntegrations } from "./useIntegrations";
|
|
12
12
|
export type { IntegrationsState, IntegrationsScope, UseIntegrationsOptions, } from "./useIntegrations";
|
|
13
|
+
export { McpServersSection } from "./McpServersSection";
|
|
14
|
+
export type { McpServersSectionProps } from "./McpServersSection";
|
|
15
|
+
export { McpConnectForm } from "./McpConnectForm";
|
|
16
|
+
export type { McpConnectFormProps } from "./McpConnectForm";
|
|
17
|
+
export { useTenantMcp } from "./useTenantMcp";
|
|
18
|
+
export type { TenantMcpState, UseTenantMcpOptions } from "./useTenantMcp";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DevicApiClient } from "../../api/client";
|
|
2
|
+
import type { TenantMcpServer } from "../../api/types";
|
|
3
|
+
import type { IntegrationsScope } from "./useIntegrations";
|
|
4
|
+
export interface UseTenantMcpOptions extends IntegrationsScope {
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Fetch when this turns true, and only then. The modal passes its own open
|
|
9
|
+
* state, so a panel nobody opens costs no request at all.
|
|
10
|
+
*/
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface TenantMcpState {
|
|
14
|
+
servers: TenantMcpServer[];
|
|
15
|
+
loading: boolean;
|
|
16
|
+
error: string | null;
|
|
17
|
+
/** Whether this assistant offers MCP servers to its tenants at all. */
|
|
18
|
+
offered: boolean;
|
|
19
|
+
/** Whether the end user may add a server of their own. */
|
|
20
|
+
allowCustom: boolean;
|
|
21
|
+
limits: {
|
|
22
|
+
maxServers: number;
|
|
23
|
+
maxToolsPerServer: number;
|
|
24
|
+
used: number;
|
|
25
|
+
};
|
|
26
|
+
/** True once a first answer (or refusal) has arrived. */
|
|
27
|
+
settled: boolean;
|
|
28
|
+
refresh: () => Promise<void>;
|
|
29
|
+
client: DevicApiClient | null;
|
|
30
|
+
scope: IntegrationsScope;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The MCP servers an assistant offers its tenants, together with the ones this
|
|
34
|
+
* tenant has connected — loaded once and shared.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately a sibling of `useIntegrations` rather than part of it: the two
|
|
37
|
+
* answer different endpoints, and folding them together would mean an assistant
|
|
38
|
+
* that offers only one of them pays for both on every open.
|
|
39
|
+
*/
|
|
40
|
+
export declare function useTenantMcp(options: UseTenantMcpOptions): TenantMcpState;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useMemo, useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { DevicApiClient } from '../../api/client.js';
|
|
3
|
+
import 'react/jsx-runtime';
|
|
4
|
+
import { useOptionalDevicContext } from '../../provider/DevicContext.js';
|
|
5
|
+
|
|
6
|
+
/** A 4xx here means "not for you", not "something broke". */
|
|
7
|
+
function isRefusal(err) {
|
|
8
|
+
const status = err?.statusCode;
|
|
9
|
+
return typeof status === "number" && status >= 400 && status < 500;
|
|
10
|
+
}
|
|
11
|
+
const NO_LIMITS = { maxServers: 0, maxToolsPerServer: 0, used: 0 };
|
|
12
|
+
/**
|
|
13
|
+
* The MCP servers an assistant offers its tenants, together with the ones this
|
|
14
|
+
* tenant has connected — loaded once and shared.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately a sibling of `useIntegrations` rather than part of it: the two
|
|
17
|
+
* answer different endpoints, and folding them together would mean an assistant
|
|
18
|
+
* that offers only one of them pays for both on every open.
|
|
19
|
+
*/
|
|
20
|
+
function useTenantMcp(options) {
|
|
21
|
+
const { assistantId, tenantId, subtenantId, apiKey, baseUrl, enabled = true } = options;
|
|
22
|
+
const context = useOptionalDevicContext();
|
|
23
|
+
const resolvedBaseUrl = baseUrl || context?.baseUrl || "https://api.devic.ai";
|
|
24
|
+
const resolvedTenantId = tenantId || context?.tenantId;
|
|
25
|
+
const resolvedSubtenantId = subtenantId || context?.subtenantId;
|
|
26
|
+
const resolvedApiKey = apiKey || context?.apiKey;
|
|
27
|
+
const resolvedTenantSession = context?.getTenantSession;
|
|
28
|
+
const onSessionExpired = context?.onSessionExpired;
|
|
29
|
+
const client = useMemo(() => resolvedApiKey || resolvedTenantSession
|
|
30
|
+
? new DevicApiClient({
|
|
31
|
+
apiKey: resolvedApiKey,
|
|
32
|
+
baseUrl: resolvedBaseUrl,
|
|
33
|
+
getTenantSession: resolvedTenantSession,
|
|
34
|
+
onSessionExpired,
|
|
35
|
+
})
|
|
36
|
+
: null, [resolvedApiKey, resolvedTenantSession, resolvedBaseUrl]);
|
|
37
|
+
const scope = useMemo(() => ({
|
|
38
|
+
assistantId,
|
|
39
|
+
tenantId: resolvedTenantId || undefined,
|
|
40
|
+
subtenantId: resolvedSubtenantId || undefined,
|
|
41
|
+
}), [assistantId, resolvedTenantId, resolvedSubtenantId]);
|
|
42
|
+
const [listing, setListing] = useState(null);
|
|
43
|
+
const [loading, setLoading] = useState(false);
|
|
44
|
+
const [error, setError] = useState(null);
|
|
45
|
+
const [settled, setSettled] = useState(false);
|
|
46
|
+
const refresh = useCallback(async () => {
|
|
47
|
+
if (!client) {
|
|
48
|
+
setError("No credentials configured");
|
|
49
|
+
setSettled(true);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
setLoading(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
setListing(await client.getMcpServers(scope));
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
setListing(null);
|
|
59
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
60
|
+
if (!isRefusal(err)) {
|
|
61
|
+
// A refusal is an ordinary answer — a disabled feature, a missing
|
|
62
|
+
// tenant — and stays quiet. A 500 is not, and swallowing it would
|
|
63
|
+
// leave an integrator with a panel that never appears and no reason.
|
|
64
|
+
console.warn("[devic-ui] could not load MCP servers", err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
setLoading(false);
|
|
69
|
+
setSettled(true);
|
|
70
|
+
}
|
|
71
|
+
}, [client, scope]);
|
|
72
|
+
// Once per scope, not once per render of whatever switched it on. A different
|
|
73
|
+
// tenant is a different listing, and the previous one is dropped first:
|
|
74
|
+
// showing it while the new one arrives would name servers that are not this
|
|
75
|
+
// user's.
|
|
76
|
+
const key = `${client ? "k" : "-"}|${scope.assistantId}|${scope.tenantId ?? ""}|${scope.subtenantId ?? ""}`;
|
|
77
|
+
const loadedKeyRef = useRef(null);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!enabled || loadedKeyRef.current === key)
|
|
80
|
+
return;
|
|
81
|
+
if (loadedKeyRef.current !== null) {
|
|
82
|
+
setListing(null);
|
|
83
|
+
setSettled(false);
|
|
84
|
+
}
|
|
85
|
+
loadedKeyRef.current = key;
|
|
86
|
+
void refresh();
|
|
87
|
+
}, [enabled, key, refresh]);
|
|
88
|
+
return {
|
|
89
|
+
servers: listing?.servers ?? [],
|
|
90
|
+
loading,
|
|
91
|
+
error,
|
|
92
|
+
// Offered only once the server has said so. Anything hanging off this stays
|
|
93
|
+
// hidden until then, so nothing appears and then disappears.
|
|
94
|
+
offered: !!listing?.offered,
|
|
95
|
+
allowCustom: !!listing?.allowCustom,
|
|
96
|
+
limits: listing?.limits ?? NO_LIMITS,
|
|
97
|
+
settled,
|
|
98
|
+
refresh,
|
|
99
|
+
client,
|
|
100
|
+
scope,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { useTenantMcp };
|
|
105
|
+
//# sourceMappingURL=useTenantMcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTenantMcp.js","sources":["../../../../src/components/IntegrationsModal/useTenantMcp.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { DevicApiError } from \"../../api/client\";\nimport { DevicApiClient } from \"../../api/client\";\nimport type { TenantMcpListing, TenantMcpServer } from \"../../api/types\";\nimport { useOptionalDevicContext } from \"../../provider\";\nimport type { IntegrationsScope } from \"./useIntegrations\";\n\nexport interface UseTenantMcpOptions extends IntegrationsScope {\n apiKey?: string;\n baseUrl?: string;\n /**\n * Fetch when this turns true, and only then. The modal passes its own open\n * state, so a panel nobody opens costs no request at all.\n */\n enabled?: boolean;\n}\n\nexport interface TenantMcpState {\n servers: TenantMcpServer[];\n loading: boolean;\n error: string | null;\n /** Whether this assistant offers MCP servers to its tenants at all. */\n offered: boolean;\n /** Whether the end user may add a server of their own. */\n allowCustom: boolean;\n limits: { maxServers: number; maxToolsPerServer: number; used: number };\n /** True once a first answer (or refusal) has arrived. */\n settled: boolean;\n refresh: () => Promise<void>;\n client: DevicApiClient | null;\n scope: IntegrationsScope;\n}\n\n/** A 4xx here means \"not for you\", not \"something broke\". */\nfunction isRefusal(err: unknown): boolean {\n const status = (err as DevicApiError)?.statusCode;\n return typeof status === \"number\" && status >= 400 && status < 500;\n}\n\nconst NO_LIMITS = { maxServers: 0, maxToolsPerServer: 0, used: 0 };\n\n/**\n * The MCP servers an assistant offers its tenants, together with the ones this\n * tenant has connected — loaded once and shared.\n *\n * Deliberately a sibling of `useIntegrations` rather than part of it: the two\n * answer different endpoints, and folding them together would mean an assistant\n * that offers only one of them pays for both on every open.\n */\nexport function useTenantMcp(options: UseTenantMcpOptions): TenantMcpState {\n const { assistantId, tenantId, subtenantId, apiKey, baseUrl, enabled = true } =\n options;\n\n const context = useOptionalDevicContext();\n const resolvedBaseUrl = baseUrl || context?.baseUrl || \"https://api.devic.ai\";\n const resolvedTenantId = tenantId || context?.tenantId;\n const resolvedSubtenantId = subtenantId || context?.subtenantId;\n const resolvedApiKey = apiKey || context?.apiKey;\n const resolvedTenantSession = context?.getTenantSession;\n const onSessionExpired = context?.onSessionExpired;\n\n const client = useMemo(\n () =>\n resolvedApiKey || resolvedTenantSession\n ? new DevicApiClient({\n apiKey: resolvedApiKey,\n baseUrl: resolvedBaseUrl,\n getTenantSession: resolvedTenantSession,\n onSessionExpired,\n })\n : null,\n [resolvedApiKey, resolvedTenantSession, resolvedBaseUrl]\n );\n\n const scope = useMemo(\n () => ({\n assistantId,\n tenantId: resolvedTenantId || undefined,\n subtenantId: resolvedSubtenantId || undefined,\n }),\n [assistantId, resolvedTenantId, resolvedSubtenantId]\n );\n\n const [listing, setListing] = useState<TenantMcpListing | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [settled, setSettled] = useState(false);\n\n const refresh = useCallback(async () => {\n if (!client) {\n setError(\"No credentials configured\");\n setSettled(true);\n return;\n }\n setLoading(true);\n setError(null);\n try {\n setListing(await client.getMcpServers(scope));\n } catch (err) {\n setListing(null);\n setError(err instanceof Error ? err.message : String(err));\n if (!isRefusal(err)) {\n // A refusal is an ordinary answer — a disabled feature, a missing\n // tenant — and stays quiet. A 500 is not, and swallowing it would\n // leave an integrator with a panel that never appears and no reason.\n console.warn(\"[devic-ui] could not load MCP servers\", err);\n }\n } finally {\n setLoading(false);\n setSettled(true);\n }\n }, [client, scope]);\n\n // Once per scope, not once per render of whatever switched it on. A different\n // tenant is a different listing, and the previous one is dropped first:\n // showing it while the new one arrives would name servers that are not this\n // user's.\n const key = `${client ? \"k\" : \"-\"}|${scope.assistantId}|${scope.tenantId ?? \"\"}|${scope.subtenantId ?? \"\"}`;\n const loadedKeyRef = useRef<string | null>(null);\n useEffect(() => {\n if (!enabled || loadedKeyRef.current === key) return;\n if (loadedKeyRef.current !== null) {\n setListing(null);\n setSettled(false);\n }\n loadedKeyRef.current = key;\n void refresh();\n }, [enabled, key, refresh]);\n\n return {\n servers: listing?.servers ?? [],\n loading,\n error,\n // Offered only once the server has said so. Anything hanging off this stays\n // hidden until then, so nothing appears and then disappears.\n offered: !!listing?.offered,\n allowCustom: !!listing?.allowCustom,\n limits: listing?.limits ?? NO_LIMITS,\n settled,\n refresh,\n client,\n scope,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -321,8 +321,9 @@ function useDevicChat(options) {
|
|
|
321
321
|
setIsLoading(false);
|
|
322
322
|
}
|
|
323
323
|
if (responses.length > 0) {
|
|
324
|
-
// Send tool responses back to the API
|
|
325
|
-
|
|
324
|
+
// Send tool responses back to the API, restating the tools so the
|
|
325
|
+
// continuation keeps them on offer
|
|
326
|
+
await clientRef.current.sendToolResponses(assistantId, chatUid, responses, toolSchemas);
|
|
326
327
|
// Only resume polling if no widgets are blocking
|
|
327
328
|
if (widgetCalls.length === 0) {
|
|
328
329
|
setShouldPoll(true);
|
|
@@ -335,7 +336,7 @@ function useDevicChat(options) {
|
|
|
335
336
|
setError(error);
|
|
336
337
|
onErrorRef.current?.(error);
|
|
337
338
|
}
|
|
338
|
-
}, [chatUid, assistantId, handleToolCalls, extractPendingToolCalls]);
|
|
339
|
+
}, [chatUid, assistantId, handleToolCalls, extractPendingToolCalls, toolSchemas]);
|
|
339
340
|
// Send a message
|
|
340
341
|
const sendMessage = useCallback(async (message, sendOptions) => {
|
|
341
342
|
if (!clientRef.current) {
|
|
@@ -601,9 +602,7 @@ function useDevicChat(options) {
|
|
|
601
602
|
setPendingWidgetCalls(remaining);
|
|
602
603
|
try {
|
|
603
604
|
logRef.current.log('[useDevicChat] sending widget tool response', toolCallId);
|
|
604
|
-
await clientRef.current.sendToolResponses(assistantId, uid, [
|
|
605
|
-
{ tool_call_id: toolCallId, content: response, role: 'tool' },
|
|
606
|
-
]);
|
|
605
|
+
await clientRef.current.sendToolResponses(assistantId, uid, [{ tool_call_id: toolCallId, content: response, role: 'tool' }], toolSchemas);
|
|
607
606
|
// Resume polling only if no more widget calls are blocking
|
|
608
607
|
if (remaining.length === 0) {
|
|
609
608
|
setShouldPoll(true);
|
|
@@ -616,7 +615,7 @@ function useDevicChat(options) {
|
|
|
616
615
|
setError(error);
|
|
617
616
|
onErrorRef.current?.(error);
|
|
618
617
|
}
|
|
619
|
-
}, [assistantId]);
|
|
618
|
+
}, [assistantId, toolSchemas]);
|
|
620
619
|
// Cancel a pending widget tool call (sends an error response to the model)
|
|
621
620
|
const cancelWidgetCall = useCallback(async (toolCallId, reason) => {
|
|
622
621
|
await submitWidgetResponse(toolCallId, {
|