@devicai/ui 0.43.0 → 0.45.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.
Files changed (87) hide show
  1. package/README.md +34 -2
  2. package/dist/cjs/api/client.js +26 -2
  3. package/dist/cjs/api/client.js.map +1 -1
  4. package/dist/cjs/api/types.js.map +1 -1
  5. package/dist/cjs/components/AICommandBar/AICommandBar.js +2 -1
  6. package/dist/cjs/components/AICommandBar/AICommandBar.js.map +1 -1
  7. package/dist/cjs/components/AICommandBar/useAICommandBar.js +3 -2
  8. package/dist/cjs/components/AICommandBar/useAICommandBar.js.map +1 -1
  9. package/dist/cjs/components/AIElementWrapper/AIElementWrapper.js +2 -1
  10. package/dist/cjs/components/AIElementWrapper/AIElementWrapper.js.map +1 -1
  11. package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js +3 -2
  12. package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
  13. package/dist/cjs/components/AIGenerationButton/AIGenerationButton.js +2 -1
  14. package/dist/cjs/components/AIGenerationButton/AIGenerationButton.js.map +1 -1
  15. package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js +3 -2
  16. package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
  17. package/dist/cjs/components/ChatDrawer/ChatDrawer.js +3 -2
  18. package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
  19. package/dist/cjs/components/ChatDrawer/ChatMessages.js +4 -4
  20. package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
  21. package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js +10 -4
  22. package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
  23. package/dist/cjs/components/IntegrationsModal/ConnectFieldsForm.js +60 -0
  24. package/dist/cjs/components/IntegrationsModal/ConnectFieldsForm.js.map +1 -0
  25. package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js +61 -7
  26. package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
  27. package/dist/cjs/hooks/useDevicChat.js +14 -5
  28. package/dist/cjs/hooks/useDevicChat.js.map +1 -1
  29. package/dist/cjs/hooks/usePolling.js +50 -1
  30. package/dist/cjs/hooks/usePolling.js.map +1 -1
  31. package/dist/cjs/index.js +3 -0
  32. package/dist/cjs/index.js.map +1 -1
  33. package/dist/cjs/provider/DevicProvider.js +3 -1
  34. package/dist/cjs/provider/DevicProvider.js.map +1 -1
  35. package/dist/cjs/styles.css +1 -1
  36. package/dist/esm/api/client.d.ts +31 -3
  37. package/dist/esm/api/client.js +26 -2
  38. package/dist/esm/api/client.js.map +1 -1
  39. package/dist/esm/api/types.d.ts +57 -0
  40. package/dist/esm/api/types.js.map +1 -1
  41. package/dist/esm/components/AICommandBar/AICommandBar.js +2 -1
  42. package/dist/esm/components/AICommandBar/AICommandBar.js.map +1 -1
  43. package/dist/esm/components/AICommandBar/AICommandBar.types.d.ts +7 -0
  44. package/dist/esm/components/AICommandBar/useAICommandBar.d.ts +2 -0
  45. package/dist/esm/components/AICommandBar/useAICommandBar.js +4 -3
  46. package/dist/esm/components/AICommandBar/useAICommandBar.js.map +1 -1
  47. package/dist/esm/components/AIElementWrapper/AIElementWrapper.js +2 -1
  48. package/dist/esm/components/AIElementWrapper/AIElementWrapper.js.map +1 -1
  49. package/dist/esm/components/AIElementWrapper/AIElementWrapper.types.d.ts +7 -0
  50. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.d.ts +2 -0
  51. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js +4 -3
  52. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
  53. package/dist/esm/components/AIGenerationButton/AIGenerationButton.js +2 -1
  54. package/dist/esm/components/AIGenerationButton/AIGenerationButton.js.map +1 -1
  55. package/dist/esm/components/AIGenerationButton/AIGenerationButton.types.d.ts +7 -0
  56. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.d.ts +2 -0
  57. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js +4 -3
  58. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
  59. package/dist/esm/components/ChatDrawer/ChatDrawer.js +3 -2
  60. package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
  61. package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +15 -0
  62. package/dist/esm/components/ChatDrawer/ChatMessages.d.ts +1 -1
  63. package/dist/esm/components/ChatDrawer/ChatMessages.js +4 -4
  64. package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
  65. package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.d.ts +7 -1
  66. package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js +10 -4
  67. package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
  68. package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.d.ts +29 -0
  69. package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.js +58 -0
  70. package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.js.map +1 -0
  71. package/dist/esm/components/IntegrationsModal/IntegrationsModal.js +61 -7
  72. package/dist/esm/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
  73. package/dist/esm/hooks/index.d.ts +1 -1
  74. package/dist/esm/hooks/useDevicChat.d.ts +3 -1
  75. package/dist/esm/hooks/useDevicChat.js +15 -6
  76. package/dist/esm/hooks/useDevicChat.js.map +1 -1
  77. package/dist/esm/hooks/usePolling.d.ts +17 -0
  78. package/dist/esm/hooks/usePolling.js +48 -2
  79. package/dist/esm/hooks/usePolling.js.map +1 -1
  80. package/dist/esm/index.d.ts +2 -2
  81. package/dist/esm/index.js +1 -1
  82. package/dist/esm/provider/DevicProvider.d.ts +1 -1
  83. package/dist/esm/provider/DevicProvider.js +3 -1
  84. package/dist/esm/provider/DevicProvider.js.map +1 -1
  85. package/dist/esm/provider/types.d.ts +22 -0
  86. package/dist/esm/styles.css +1 -1
  87. package/package.json +1 -1
@@ -4,6 +4,7 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var React = require('react');
5
5
  var reactDom = require('react-dom');
6
6
  var theme = require('../theme.js');
7
+ var ConnectFieldsForm = require('./ConnectFieldsForm.js');
7
8
  var IntegrationLogo = require('./IntegrationLogo.js');
8
9
  var useIntegrations = require('./useIntegrations.js');
9
10
 
@@ -80,6 +81,10 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
80
81
  const [busyApp, setBusyApp] = React.useState(null);
81
82
  /** Authorization URL surfaced as a link when the popup was blocked. */
82
83
  const [blockedUrl, setBlockedUrl] = React.useState(null);
84
+ /** The app whose credentials are being asked for, with what the server asked
85
+ * and every way it can be connected. Rendered inside that app's own card,
86
+ * where the user pressed Connect. */
87
+ const [setupPrompt, setSetupPrompt] = React.useState(null);
83
88
  const [query, setQuery] = React.useState("");
84
89
  const visible = React.useMemo(() => integrations.filter((i) => matches(i, query)), [integrations, query]);
85
90
  // Report the listing without making the caller's identity part of the
@@ -98,6 +103,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
98
103
  if (isOpen && !wasOpenRef.current) {
99
104
  setBlockedUrl(null);
100
105
  setActionError(null);
106
+ setSetupPrompt(null);
101
107
  setQuery("");
102
108
  // The very first open of an uncontrolled modal is already covered by the
103
109
  // hook switching on; asking again here would double every first open.
@@ -161,19 +167,37 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
161
167
  if (pollRef.current !== null)
162
168
  window.clearInterval(pollRef.current);
163
169
  }, []);
164
- const handleConnect = async (integration) => {
165
- if (!client || busyApp)
170
+ /**
171
+ * Connects an app, asking for credentials only if it needs them.
172
+ *
173
+ * Optimistic: the request goes out first, and the form appears only when the
174
+ * server answers that something is missing. Most apps take an API key rather
175
+ * than an OAuth round trip, so this is the path that used to fail outright.
176
+ */
177
+ const handleConnect = async (integration, values) => {
178
+ if (!client || (busyApp && busyApp !== integration.app))
166
179
  return;
167
180
  setActionError(null);
168
181
  setBlockedUrl(null);
169
182
  setBusyApp(integration.app);
170
183
  const nonce = newNonce();
171
184
  const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;
172
- // Opened empty inside the click, navigated once the URL is known.
185
+ // Opened empty inside the click, navigated once the URL is known. Both
186
+ // entry points are gestures — the card's button and the form's submit —
187
+ // so this holds on the retry too.
173
188
  const popup = window.open("", "devic-oauth", "width=520,height=680,menubar=no,toolbar=no");
174
189
  try {
175
- const { authorizationUrl } = await client.connectIntegration(integration.app, { ...scope, returnTo });
190
+ const { connected, authorizationUrl } = await client.connectIntegration(integration.app, { ...scope, returnTo, ...values });
191
+ // Nothing to authorise: the key the user typed is the account. Close the
192
+ // window that was opened in case it was needed and re-read the listing.
193
+ if (connected || !authorizationUrl) {
194
+ popup?.close();
195
+ setSetupPrompt(null);
196
+ finishConnect();
197
+ return;
198
+ }
176
199
  pendingRef.current = { app: integration.app, returnTo };
200
+ setSetupPrompt(null);
177
201
  if (popup && !popup.closed) {
178
202
  popupRef.current = popup;
179
203
  popup.location.href = authorizationUrl;
@@ -195,8 +219,38 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
195
219
  catch (err) {
196
220
  popup?.close();
197
221
  pendingRef.current = null;
198
- setActionError(err instanceof Error ? err.message : String(err));
199
222
  setBusyApp(null);
223
+ const setup = err?.setupRequired;
224
+ if (setup) {
225
+ await openSetupForm(integration, setup);
226
+ return;
227
+ }
228
+ setActionError(err instanceof Error ? err.message : String(err));
229
+ }
230
+ };
231
+ /**
232
+ * Opens the credentials form for an app that needs one.
233
+ *
234
+ * `stage: "app"` is not the end user's to fix — it means the developer who
235
+ * embedded this widget has not registered an application with the provider
236
+ * yet. Asking a stranger for someone else's client secret would be both
237
+ * useless and a good way to teach them to hand credentials to a form, so it
238
+ * is reported as unavailable instead.
239
+ */
240
+ const openSetupForm = async (integration, setup) => {
241
+ if (setup.stage === "app") {
242
+ setActionError(`${integration.name} is not available yet — it still needs to be set up ` +
243
+ `by the app's provider.`);
244
+ return;
245
+ }
246
+ try {
247
+ const { schemes } = await client.getIntegrationAuth(integration.app, scope);
248
+ if (!schemes.length)
249
+ throw new Error(setup.message);
250
+ setSetupPrompt({ app: integration.app, setup, schemes });
251
+ }
252
+ catch (err) {
253
+ setActionError(err instanceof Error ? err.message : String(err));
200
254
  }
201
255
  };
202
256
  const handleDisconnect = async (app, account) => {
@@ -227,7 +281,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
227
281
  }, children: "Open the authorisation page" }), " ", "and come back \u2014 then use Refresh."] })), loading && integrations.length === 0 ? (jsxRuntime.jsx("div", { className: "devic-int-loading", children: "Loading apps\u2026" })) : integrations.length === 0 ? (jsxRuntime.jsx("div", { className: "devic-int-empty", children: "No apps available here yet." })) : visible.length === 0 ? (jsxRuntime.jsxs("div", { className: "devic-int-empty", children: ["No apps match \u201C", query.trim(), "\u201D."] })) : (jsxRuntime.jsx("div", { className: "devic-int-grid", children: visible.map((integration) => {
228
282
  const cardState = stateOf(integration);
229
283
  const busy = busyApp === integration.app;
230
- return (jsxRuntime.jsxs("div", { className: "devic-int-card", "data-state": cardState.key, children: [jsxRuntime.jsxs("div", { className: "devic-int-card-head", children: [jsxRuntime.jsx(IntegrationLogo.IntegrationLogo, { integration: integration }), jsxRuntime.jsxs("span", { className: "devic-int-state", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": cardState.key === "connected", "data-off": cardState.key === "disconnected", "aria-hidden": "true" }), cardState.label] })] }), jsxRuntime.jsx("div", { className: "devic-int-name", title: integration.name, children: integration.name }), integration.description && (jsxRuntime.jsx("div", { className: "devic-int-description", title: integration.description, children: integration.description })), jsxRuntime.jsx("button", { type: "button", className: `devic-int-btn devic-int-btn-block${cardState.key === "connected"
284
+ return (jsxRuntime.jsxs("div", { className: "devic-int-card", "data-state": cardState.key, children: [jsxRuntime.jsxs("div", { className: "devic-int-card-head", children: [jsxRuntime.jsx(IntegrationLogo.IntegrationLogo, { integration: integration }), jsxRuntime.jsxs("span", { className: "devic-int-state", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": cardState.key === "connected", "data-off": cardState.key === "disconnected", "aria-hidden": "true" }), cardState.label] })] }), jsxRuntime.jsx("div", { className: "devic-int-name", title: integration.name, children: integration.name }), integration.description && (jsxRuntime.jsx("div", { className: "devic-int-description", title: integration.description, children: integration.description })), setupPrompt?.app === integration.app ? (jsxRuntime.jsx(ConnectFieldsForm.ConnectFieldsForm, { integration: integration, schemes: setupPrompt.schemes, initialScheme: setupPrompt.setup.authScheme, onlyFields: setupPrompt.setup.fields, submitting: busy, onCancel: () => setSetupPrompt(null), onSubmit: (values) => handleConnect(integration, values) })) : (jsxRuntime.jsx("button", { type: "button", className: `devic-int-btn devic-int-btn-block${cardState.key === "connected"
231
285
  ? ""
232
286
  : " devic-int-btn-primary"}`, onClick: () => handleConnect(integration), disabled: busy || !!busyApp, title: cardState.key === "connected"
233
287
  ? "Sign in with a different account. The one connected now is replaced."
@@ -240,7 +294,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
240
294
  : // Not "Add account": one account per app is all
241
295
  // the assistant can use, and connecting again
242
296
  // retires the previous one.
243
- "Switch account" }), integration.accounts.length > 0 && (jsxRuntime.jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxRuntime.jsxs("li", { className: "devic-int-account", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxRuntime.jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxRuntime.jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-unlink", onClick: () => handleDisconnect(integration.app, account), disabled: !!busyApp, title: "Disconnect this account", "aria-label": `Disconnect ${integration.name}`, children: "\u00D7" })] }, account.id))) }))] }, integration.app));
297
+ "Switch account" })), integration.accounts.length > 0 && (jsxRuntime.jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxRuntime.jsxs("li", { className: "devic-int-account", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxRuntime.jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxRuntime.jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-unlink", onClick: () => handleDisconnect(integration.app, account), disabled: !!busyApp, title: "Disconnect this account", "aria-label": `Disconnect ${integration.name}`, children: "\u00D7" })] }, account.id))) }))] }, integration.app));
244
298
  }) }))] }), jsxRuntime.jsxs("div", { className: "devic-int-footer", children: [jsxRuntime.jsx("span", { children: "Only you can see and use the accounts you connect here." }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small", onClick: () => void refresh(true), disabled: loading || !!busyApp, children: "Refresh" })] })] }) }), document.body);
245
299
  }
246
300
 
@@ -1 +1 @@
1
- {"version":3,"file":"IntegrationsModal.js","sources":["../../../../../src/components/IntegrationsModal/IntegrationsModal.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type JSX,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { Integration, IntegrationAccount } from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\nimport { IntegrationLogo } from \"./IntegrationLogo\";\nimport { useIntegrations, type IntegrationsState } from \"./useIntegrations\";\nimport \"./IntegrationsModal.css\";\n\n/** Message the OAuth callback page posts back to this window when it is done. */\ninterface CallbackMessage {\n source?: string;\n type?: string;\n app?: string;\n status?: string;\n returnTo?: string;\n}\n\nexport interface IntegrationsModalProps {\n /** Whether the modal is visible. */\n isOpen: boolean;\n onClose: () => void;\n /** Assistant whose offered apps are shown. */\n assistantId: string;\n /** Tenant of the end user (falls back to the provider's tenantId). */\n tenantId?: string;\n /** Subtenant of the end user (falls back to the provider's subtenantId). */\n subtenantId?: string;\n /** API key override (falls back to the provider's). */\n apiKey?: string;\n /** Base URL override (falls back to the provider's). */\n baseUrl?: string;\n /** Modal title. @default \"Connected apps\" */\n title?: string;\n /** Search field placeholder. @default \"Search connected apps\" */\n searchPlaceholder?: string;\n /** Called after an account is connected or disconnected. */\n onChange?: (integrations: Integration[]) => void;\n /**\n * Colours and font. Same names as the drawer's style options, and the drawer\n * passes its own down — a dialog opening in the default light palette over a\n * themed application is the one thing this must not do.\n */\n theme?: DevicTheme;\n /**\n * Listing loaded elsewhere (see `useIntegrations`). The drawer already has to\n * load it to decide whether its button exists, and passing it down is what\n * keeps opening the modal from asking for the very same thing again.\n */\n state?: IntegrationsState;\n}\n\nfunction PlugIcon(): JSX.Element {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22v-5\" />\n <path d=\"M9 8V2\" />\n <path d=\"M15 8V2\" />\n <path d=\"M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z\" />\n </svg>\n );\n}\n\nfunction SearchIcon(): JSX.Element {\n return (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </svg>\n );\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\n/** How the card describes an app at a glance. */\nfunction stateOf(integration: Integration): {\n key: \"connected\" | \"reconnect\" | \"disconnected\";\n label: string;\n} {\n if (integration.connected) return { key: \"connected\", label: \"Connected\" };\n if (integration.accounts.some((a) => a.needsReconnect)) {\n return { key: \"reconnect\", label: \"Needs reconnection\" };\n }\n return { key: \"disconnected\", label: \"Not connected\" };\n}\n\nfunction accountLabel(account: IntegrationAccount): string {\n if (!account.connectedAt) return account.status.toLowerCase();\n const when = new Date(account.connectedAt);\n if (Number.isNaN(when.getTime())) return account.status.toLowerCase();\n return `connected ${when.toLocaleDateString()}`;\n}\n\nfunction matches(integration: Integration, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (\n integration.name.toLowerCase().includes(q) ||\n integration.app.toLowerCase().includes(q) ||\n (integration.description ?? \"\").toLowerCase().includes(q)\n );\n}\n\n/**\n * Modal where the END USER of an application manages their *own* third-party\n * accounts: the apps the developer offered to tenants of this assistant, each\n * with the accounts this tenant has connected, and the buttons to add or\n * remove one.\n *\n * Backed by `/api/v1/tenant-integrations`, which resolves the tenant\n * server-side, so what is listed here is only ever this tenant's — never the\n * workspace-wide accounts an admin connected, and never another tenant's.\n *\n * Connecting opens the provider's consent screen in a popup. The popup is\n * opened empty *before* the request that produces its URL, because browsers\n * only honour `window.open` inside the gesture that triggered it: opening it\n * after the round trip is what gets it blocked. When it is blocked anyway, the\n * URL is offered as a link instead.\n */\nexport function IntegrationsModal({\n isOpen,\n onClose,\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n title = \"Connected apps\",\n searchPlaceholder = \"Search connected apps\",\n onChange,\n theme,\n state,\n}: IntegrationsModalProps): JSX.Element | null {\n // Hooks cannot be skipped, so the fallback is always built and only fetches\n // when nobody handed a listing down.\n const own = useIntegrations({\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n enabled: isOpen && !state,\n });\n const { integrations, loading, error: loadError, refresh, client, scope } =\n state ?? own;\n\n /** Errors from connecting or disconnecting, kept apart from load failures. */\n const [actionError, setActionError] = useState<string | null>(null);\n const error = actionError ?? loadError;\n /** App slug with a connect/disconnect in flight, so only its card is busy. */\n const [busyApp, setBusyApp] = useState<string | null>(null);\n /** Authorization URL surfaced as a link when the popup was blocked. */\n const [blockedUrl, setBlockedUrl] = useState<{ app: string; url: string } | null>(\n null\n );\n const [query, setQuery] = useState(\"\");\n\n const visible = useMemo(\n () => integrations.filter((i) => matches(i, query)),\n [integrations, query]\n );\n\n // Report the listing without making the caller's identity part of the\n // dependency: an inline arrow would fire this on every render.\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n useEffect(() => {\n if (integrations.length) onChangeRef.current?.(integrations);\n }, [integrations]);\n\n // Reopening starts clean, and re-reads: accounts may have been connected or\n // revoked elsewhere since the last look.\n const wasOpenRef = useRef(false);\n const openedBeforeRef = useRef(false);\n useEffect(() => {\n if (isOpen && !wasOpenRef.current) {\n setBlockedUrl(null);\n setActionError(null);\n setQuery(\"\");\n // The very first open of an uncontrolled modal is already covered by the\n // hook switching on; asking again here would double every first open.\n if (state || openedBeforeRef.current) void refresh();\n openedBeforeRef.current = true;\n }\n wasOpenRef.current = isOpen;\n // `state.refresh` is stable per scope; re-running on every render of the\n // owner is exactly what this must not do.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen]);\n\n // Escape closes\n useEffect(() => {\n if (!isOpen) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [isOpen, onClose]);\n\n /** The round trip currently in flight, if any. */\n const pendingRef = useRef<{ app: string; returnTo: string } | null>(null);\n const popupRef = useRef<Window | null>(null);\n const pollRef = useRef<number | null>(null);\n\n const finishConnect = 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 setBusyApp(null);\n void refresh(true);\n }, [refresh]);\n\n // The callback page tells us it is done. Treat the message as a nudge, never\n // as the result: what is displayed comes from re-reading the server, so a\n // forged message can at worst cause one redundant fetch.\n useEffect(() => {\n if (!isOpen) return;\n const onMessage = (event: MessageEvent) => {\n const data = event.data as CallbackMessage | undefined;\n if (!data || data.source !== \"devic\") return;\n if (data.type !== \"integration-connected\") return;\n const pending = pendingRef.current;\n if (!pending || data.returnTo !== pending.returnTo) return;\n popupRef.current?.close();\n finishConnect();\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [isOpen, finishConnect]);\n\n // Stop polling if the modal goes away mid-flow.\n useEffect(\n () => () => {\n if (pollRef.current !== null) window.clearInterval(pollRef.current);\n },\n []\n );\n\n const handleConnect = async (integration: Integration) => {\n if (!client || busyApp) return;\n setActionError(null);\n setBlockedUrl(null);\n setBusyApp(integration.app);\n\n const nonce = newNonce();\n const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;\n // Opened empty inside the click, navigated once the URL is known.\n const popup = window.open(\n \"\",\n \"devic-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n );\n\n try {\n const { authorizationUrl } = await client.connectIntegration(\n integration.app,\n { ...scope, returnTo }\n );\n pendingRef.current = { app: integration.app, returnTo };\n if (popup && !popup.closed) {\n popupRef.current = popup;\n popup.location.href = authorizationUrl;\n // The user may close the popup without the callback ever posting back\n // — a cancelled consent screen, or a provider that lands somewhere\n // else. Watching for the close is what keeps the card from staying\n // busy forever.\n pollRef.current = window.setInterval(() => {\n if (popup.closed) finishConnect();\n }, 700);\n } else {\n // Blocked (Safari, in-app browsers, extensions): hand over the URL.\n setBlockedUrl({ app: integration.app, url: authorizationUrl });\n setBusyApp(null);\n }\n } catch (err) {\n popup?.close();\n pendingRef.current = null;\n setActionError(err instanceof Error ? err.message : String(err));\n setBusyApp(null);\n }\n };\n\n const handleDisconnect = async (app: string, account: IntegrationAccount) => {\n if (!client || busyApp) return;\n setBusyApp(app);\n setActionError(null);\n try {\n await client.disconnectIntegration(account.id, scope);\n await refresh(true);\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyApp(null);\n }\n };\n\n if (!isOpen) return null;\n\n return createPortal(\n // The variables go on the overlay, not on the modal: the backdrop is part\n // of the dialog, and a portal inherits nothing from the drawer that opened\n // it.\n <div\n className=\"devic-int-overlay\"\n style={themeVars(theme)}\n data-dark={isDarkTheme(theme)}\n onClick={onClose}\n >\n <div\n className=\"devic-int-modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"devic-int-header\">\n <h3 className=\"devic-int-title\">\n <PlugIcon />\n {title}\n </h3>\n <button\n className=\"devic-int-close\"\n onClick={onClose}\n type=\"button\"\n aria-label=\"Close\"\n >\n ×\n </button>\n </div>\n\n <div className=\"devic-int-search\">\n <SearchIcon />\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={searchPlaceholder}\n aria-label={searchPlaceholder}\n autoComplete=\"off\"\n />\n </div>\n\n <div className=\"devic-int-body\">\n {error && <div className=\"devic-int-error\">{error}</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 {loading && integrations.length === 0 ? (\n <div className=\"devic-int-loading\">Loading apps…</div>\n ) : integrations.length === 0 ? (\n <div className=\"devic-int-empty\">No apps available here yet.</div>\n ) : visible.length === 0 ? (\n <div className=\"devic-int-empty\">\n No apps match “{query.trim()}”.\n </div>\n ) : (\n <div className=\"devic-int-grid\">\n {visible.map((integration) => {\n const cardState = stateOf(integration);\n const busy = busyApp === integration.app;\n return (\n <div\n key={integration.app}\n className=\"devic-int-card\"\n data-state={cardState.key}\n >\n <div className=\"devic-int-card-head\">\n <IntegrationLogo integration={integration} />\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={integration.name}>\n {integration.name}\n </div>\n\n {integration.description && (\n <div\n className=\"devic-int-description\"\n title={integration.description}\n >\n {integration.description}\n </div>\n )}\n\n <button\n type=\"button\"\n className={`devic-int-btn devic-int-btn-block${\n cardState.key === \"connected\"\n ? \"\"\n : \" devic-int-btn-primary\"\n }`}\n onClick={() => handleConnect(integration)}\n disabled={busy || !!busyApp}\n title={\n cardState.key === \"connected\"\n ? \"Sign in with a different account. The one connected now is replaced.\"\n : undefined\n }\n >\n {busy\n ? \"Waiting…\"\n : cardState.key === \"disconnected\"\n ? \"Connect\"\n : cardState.key === \"reconnect\"\n ? \"Reconnect\"\n : // Not \"Add account\": one account per app is all\n // the assistant can use, and connecting again\n // retires the previous one.\n \"Switch account\"}\n </button>\n\n {integration.accounts.length > 0 && (\n <ul className=\"devic-int-accounts\">\n {integration.accounts.map((account) => (\n <li key={account.id} className=\"devic-int-account\">\n <span\n className=\"devic-int-dot\"\n data-ok={!account.needsReconnect}\n aria-hidden=\"true\"\n />\n <span className=\"devic-int-account-label\">\n {accountLabel(account)}\n {account.needsReconnect && (\n <span className=\"devic-int-account-warn\">\n {\" \"}\n · reconnect required\n </span>\n )}\n </span>\n <button\n type=\"button\"\n className=\"devic-int-unlink\"\n onClick={() =>\n handleDisconnect(integration.app, account)\n }\n disabled={!!busyApp}\n title=\"Disconnect this account\"\n aria-label={`Disconnect ${integration.name}`}\n >\n ×\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n <div className=\"devic-int-footer\">\n <span>Only you can see and use the accounts you connect here.</span>\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-small\"\n onClick={() => void refresh(true)}\n disabled={loading || !!busyApp}\n >\n Refresh\n </button>\n </div>\n </div>\n </div>,\n document.body\n );\n}\n\nexport default IntegrationsModal;\n"],"names":["_jsxs","_jsx","theme","useIntegrations","useState","useMemo","useRef","useEffect","useCallback","createPortal","themeVars","isDarkTheme","IntegrationLogo"],"mappings":";;;;;;;;;AA0DA,SAAS,QAAQ,GAAA;AACf,IAAA,QACEA,eAAA,CAAA,KAAA,EAAA,EACE,KAAK,EAAC,IAAI,EACV,MAAM,EAAC,IAAI,EACX,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EAAA,aAAA,EACV,MAAM,EAAA,QAAA,EAAA,CAElBC,cAAA,CAAA,MAAA,EAAA,EAAM,CAAC,EAAC,WAAW,EAAA,CAAG,EACtBA,cAAA,CAAA,MAAA,EAAA,EAAM,CAAC,EAAC,QAAQ,EAAA,CAAG,EACnBA,cAAA,CAAA,MAAA,EAAA,EAAM,CAAC,EAAC,SAAS,EAAA,CAAG,EACpBA,cAAA,CAAA,MAAA,EAAA,EAAM,CAAC,EAAC,2CAA2C,EAAA,CAAG,CAAA,EAAA,CAClD;AAEV;AAEA,SAAS,UAAU,GAAA;IACjB,QACED,eAAA,CAAA,KAAA,EAAA,EACE,KAAK,EAAC,IAAI,EACV,MAAM,EAAC,IAAI,EACX,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EAAA,aAAA,EACV,MAAM,EAAA,QAAA,EAAA,CAElBC,cAAA,CAAA,QAAA,EAAA,EAAQ,EAAE,EAAC,IAAI,EAAC,EAAE,EAAC,IAAI,EAAC,CAAC,EAAC,GAAG,EAAA,CAAG,EAChCA,cAAA,CAAA,MAAA,EAAA,EAAM,CAAC,EAAC,gBAAgB,EAAA,CAAG,CAAA,EAAA,CACvB;AAEV;AAEA;AACA,SAAS,QAAQ,GAAA;AACf,IAAA,MAAM,CAAC,GAAG,OAAO,MAAM,KAAK,WAAW,GAAG,MAAM,GAAG,SAAS;IAC5D,IAAI,CAAC,EAAE,UAAU;AAAE,QAAA,OAAO,CAAC,CAAC,UAAU,EAAE;IACxC,OAAO,CAAA,EAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA,CAAA,EAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA,CAAE;AAC5E;AAEA;AACA,SAAS,OAAO,CAAC,WAAwB,EAAA;IAIvC,IAAI,WAAW,CAAC,SAAS;QAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE;AAC1E,IAAA,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,EAAE;QACtD,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,oBAAoB,EAAE;IAC1D;IACA,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,eAAe,EAAE;AACxD;AAEA,SAAS,YAAY,CAAC,OAA2B,EAAA;IAC/C,IAAI,CAAC,OAAO,CAAC,WAAW;AAAE,QAAA,OAAO,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE;IAC7D,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC;IAC1C,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;AAAE,QAAA,OAAO,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE;AACrE,IAAA,OAAO,aAAa,IAAI,CAAC,kBAAkB,EAAE,EAAE;AACjD;AAEA,SAAS,OAAO,CAAC,WAAwB,EAAE,KAAa,EAAA;IACtD,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE;AACpC,IAAA,IAAI,CAAC,CAAC;AAAE,QAAA,OAAO,IAAI;IACnB,QACE,WAAW,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC1C,WAAW,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AACzC,QAAA,CAAC,WAAW,CAAC,WAAW,IAAI,EAAE,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE7D;AAEA;;;;;;;;;;;;;;;AAeG;AACG,SAAU,iBAAiB,CAAC,EAChC,MAAM,EACN,OAAO,EACP,WAAW,EACX,QAAQ,EACR,WAAW,EACX,MAAM,EACN,OAAO,EACP,KAAK,GAAG,gBAAgB,EACxB,iBAAiB,GAAG,uBAAuB,EAC3C,QAAQ,SACRC,OAAK,EACL,KAAK,GACkB,EAAA;;;IAGvB,MAAM,GAAG,GAAGC,+BAAe,CAAC;QAC1B,WAAW;QACX,QAAQ;QACR,WAAW;QACX,MAAM;QACN,OAAO;AACP,QAAA,OAAO,EAAE,MAAM,IAAI,CAAC,KAAK;AAC1B,KAAA,CAAC;AACF,IAAA,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GACvE,KAAK,IAAI,GAAG;;IAGd,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAGC,cAAQ,CAAgB,IAAI,CAAC;AACnE,IAAA,MAAM,KAAK,GAAG,WAAW,IAAI,SAAS;;IAEtC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAGA,cAAQ,CAAgB,IAAI,CAAC;;IAE3D,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAGA,cAAQ,CAC1C,IAAI,CACL;IACD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAGA,cAAQ,CAAC,EAAE,CAAC;AAEtC,IAAA,MAAM,OAAO,GAAGC,aAAO,CACrB,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EACnD,CAAC,YAAY,EAAE,KAAK,CAAC,CACtB;;;AAID,IAAA,MAAM,WAAW,GAAGC,YAAM,CAAC,QAAQ,CAAC;AACpC,IAAA,WAAW,CAAC,OAAO,GAAG,QAAQ;IAC9BC,eAAS,CAAC,MAAK;QACb,IAAI,YAAY,CAAC,MAAM;AAAE,YAAA,WAAW,CAAC,OAAO,GAAG,YAAY,CAAC;AAC9D,IAAA,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC;;;AAIlB,IAAA,MAAM,UAAU,GAAGD,YAAM,CAAC,KAAK,CAAC;AAChC,IAAA,MAAM,eAAe,GAAGA,YAAM,CAAC,KAAK,CAAC;IACrCC,eAAS,CAAC,MAAK;AACb,QAAA,IAAI,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE;YACjC,aAAa,CAAC,IAAI,CAAC;YACnB,cAAc,CAAC,IAAI,CAAC;YACpB,QAAQ,CAAC,EAAE,CAAC;;;AAGZ,YAAA,IAAI,KAAK,IAAI,eAAe,CAAC,OAAO;gBAAE,KAAK,OAAO,EAAE;AACpD,YAAA,eAAe,CAAC,OAAO,GAAG,IAAI;QAChC;AACA,QAAA,UAAU,CAAC,OAAO,GAAG,MAAM;;;;AAI7B,IAAA,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;;IAGZA,eAAS,CAAC,MAAK;AACb,QAAA,IAAI,CAAC,MAAM;YAAE;AACb,QAAA,MAAM,KAAK,GAAG,CAAC,CAAgB,KAAI;AACjC,YAAA,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ;AAAE,gBAAA,OAAO,EAAE;AACnC,QAAA,CAAC;AACD,QAAA,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,KAAK,CAAC;QAC3C,OAAO,MAAM,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,KAAK,CAAC;AAC7D,IAAA,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;;AAGrB,IAAA,MAAM,UAAU,GAAGD,YAAM,CAA2C,IAAI,CAAC;AACzE,IAAA,MAAM,QAAQ,GAAGA,YAAM,CAAgB,IAAI,CAAC;AAC5C,IAAA,MAAM,OAAO,GAAGA,YAAM,CAAgB,IAAI,CAAC;AAE3C,IAAA,MAAM,aAAa,GAAGE,iBAAW,CAAC,MAAK;AACrC,QAAA,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,EAAE;AAC5B,YAAA,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC;AACrC,YAAA,OAAO,CAAC,OAAO,GAAG,IAAI;QACxB;AACA,QAAA,UAAU,CAAC,OAAO,GAAG,IAAI;AACzB,QAAA,QAAQ,CAAC,OAAO,GAAG,IAAI;QACvB,UAAU,CAAC,IAAI,CAAC;AAChB,QAAA,KAAK,OAAO,CAAC,IAAI,CAAC;AACpB,IAAA,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;;;;IAKbD,eAAS,CAAC,MAAK;AACb,QAAA,IAAI,CAAC,MAAM;YAAE;AACb,QAAA,MAAM,SAAS,GAAG,CAAC,KAAmB,KAAI;AACxC,YAAA,MAAM,IAAI,GAAG,KAAK,CAAC,IAAmC;AACtD,YAAA,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,OAAO;gBAAE;AACtC,YAAA,IAAI,IAAI,CAAC,IAAI,KAAK,uBAAuB;gBAAE;AAC3C,YAAA,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO;YAClC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ;gBAAE;AACpD,YAAA,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE;AACzB,YAAA,aAAa,EAAE;AACjB,QAAA,CAAC;AACD,QAAA,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC;QAC7C,OAAO,MAAM,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC;AAC/D,IAAA,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;;AAG3B,IAAAA,eAAS,CACP,MAAM,MAAK;AACT,QAAA,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI;AAAE,YAAA,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC;IACrE,CAAC,EACD,EAAE,CACH;AAED,IAAA,MAAM,aAAa,GAAG,OAAO,WAAwB,KAAI;QACvD,IAAI,CAAC,MAAM,IAAI,OAAO;YAAE;QACxB,cAAc,CAAC,IAAI,CAAC;QACpB,aAAa,CAAC,IAAI,CAAC;AACnB,QAAA,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC;AAE3B,QAAA,MAAM,KAAK,GAAG,QAAQ,EAAE;QACxB,MAAM,QAAQ,GAAG,CAAA,EAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAA,cAAA,EAAiB,KAAK,CAAA,CAAE;;AAElE,QAAA,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CACvB,EAAE,EACF,aAAa,EACb,4CAA4C,CAC7C;AAED,QAAA,IAAI;YACF,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAC1D,WAAW,CAAC,GAAG,EACf,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,CACvB;AACD,YAAA,UAAU,CAAC,OAAO,GAAG,EAAE,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE;AACvD,YAAA,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;AAC1B,gBAAA,QAAQ,CAAC,OAAO,GAAG,KAAK;AACxB,gBAAA,KAAK,CAAC,QAAQ,CAAC,IAAI,GAAG,gBAAgB;;;;;gBAKtC,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,WAAW,CAAC,MAAK;oBACxC,IAAI,KAAK,CAAC,MAAM;AAAE,wBAAA,aAAa,EAAE;gBACnC,CAAC,EAAE,GAAG,CAAC;YACT;iBAAO;;AAEL,gBAAA,aAAa,CAAC,EAAE,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,gBAAgB,EAAE,CAAC;gBAC9D,UAAU,CAAC,IAAI,CAAC;YAClB;QACF;QAAE,OAAO,GAAG,EAAE;YACZ,KAAK,EAAE,KAAK,EAAE;AACd,YAAA,UAAU,CAAC,OAAO,GAAG,IAAI;AACzB,YAAA,cAAc,CAAC,GAAG,YAAY,KAAK,GAAG,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAChE,UAAU,CAAC,IAAI,CAAC;QAClB;AACF,IAAA,CAAC;IAED,MAAM,gBAAgB,GAAG,OAAO,GAAW,EAAE,OAA2B,KAAI;QAC1E,IAAI,CAAC,MAAM,IAAI,OAAO;YAAE;QACxB,UAAU,CAAC,GAAG,CAAC;QACf,cAAc,CAAC,IAAI,CAAC;AACpB,QAAA,IAAI;YACF,MAAM,MAAM,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,CAAC;AACrD,YAAA,MAAM,OAAO,CAAC,IAAI,CAAC;QACrB;QAAE,OAAO,GAAG,EAAE;AACZ,YAAA,cAAc,CAAC,GAAG,YAAY,KAAK,GAAG,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAClE;gBAAU;YACR,UAAU,CAAC,IAAI,CAAC;QAClB;AACF,IAAA,CAAC;AAED,IAAA,IAAI,CAAC,MAAM;AAAE,QAAA,OAAO,IAAI;AAExB,IAAA,OAAOE,qBAAY;;;;AAIjB,IAAAR,cAAA,CAAA,KAAA,EAAA,EACE,SAAS,EAAC,mBAAmB,EAC7B,KAAK,EAAES,eAAS,CAACR,OAAK,CAAC,eACZS,iBAAW,CAACT,OAAK,CAAC,EAC7B,OAAO,EAAE,OAAO,EAAA,QAAA,EAEhBF,eAAA,CAAA,KAAA,EAAA,EACE,SAAS,EAAC,iBAAiB,EAC3B,IAAI,EAAC,QAAQ,EAAA,YAAA,EACF,MAAM,EAAA,YAAA,EACL,KAAK,EACjB,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,eAAe,EAAE,EAAA,QAAA,EAAA,CAEnCA,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,kBAAkB,EAAA,QAAA,EAAA,CAC/BA,eAAA,CAAA,IAAA,EAAA,EAAI,SAAS,EAAC,iBAAiB,EAAA,QAAA,EAAA,CAC7BC,cAAA,CAAC,QAAQ,EAAA,EAAA,CAAG,EACX,KAAK,CAAA,EAAA,CACH,EACLA,cAAA,CAAA,QAAA,EAAA,EACE,SAAS,EAAC,iBAAiB,EAC3B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAC,QAAQ,EAAA,YAAA,EACF,OAAO,EAAA,QAAA,EAAA,QAAA,EAAA,CAGX,CAAA,EAAA,CACL,EAEND,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,kBAAkB,EAAA,QAAA,EAAA,CAC/BC,cAAA,CAAC,UAAU,KAAG,EACdA,cAAA,CAAA,OAAA,EAAA,EACE,IAAI,EAAC,QAAQ,EACb,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACzC,WAAW,EAAE,iBAAiB,EAAA,YAAA,EAClB,iBAAiB,EAC7B,YAAY,EAAC,KAAK,EAAA,CAClB,CAAA,EAAA,CACE,EAEND,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,gBAAgB,aAC5B,KAAK,IAAIC,cAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,iBAAiB,EAAA,QAAA,EAAE,KAAK,EAAA,CAAO,EAEvD,UAAU,KACTD,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,kBAAkB,EAAA,QAAA,EAAA,CAAA,kCAAA,EACE,GAAG,EACpCC,sBACE,IAAI,EAAE,UAAU,CAAC,GAAG,EACpB,MAAM,EAAC,QAAQ,EACf,GAAG,EAAC,qBAAqB,EACzB,OAAO,EAAE,MAAK;AACZ,wCAAA,UAAU,CAAC,OAAO,GAAG,IAAI;wCACzB,aAAa,CAAC,IAAI,CAAC;AACrB,oCAAA,CAAC,4CAGC,EAAC,GAAG,EAAA,wCAAA,CAAA,EAAA,CAEJ,CACP,EAEA,OAAO,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IACnCA,cAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,mBAAmB,mCAAoB,IACpD,YAAY,CAAC,MAAM,KAAK,CAAC,IAC3BA,cAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,iBAAiB,EAAA,QAAA,EAAA,6BAAA,EAAA,CAAkC,IAChE,OAAO,CAAC,MAAM,KAAK,CAAC,IACtBD,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,iBAAiB,EAAA,QAAA,EAAA,CAAA,sBAAA,EACd,KAAK,CAAC,IAAI,EAAE,EAAA,SAAA,CAAA,EAAA,CACxB,KAENC,wBAAK,SAAS,EAAC,gBAAgB,EAAA,QAAA,EAC5B,OAAO,CAAC,GAAG,CAAC,CAAC,WAAW,KAAI;AAC3B,gCAAA,MAAM,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;AACtC,gCAAA,MAAM,IAAI,GAAG,OAAO,KAAK,WAAW,CAAC,GAAG;gCACxC,QACED,yBAEE,SAAS,EAAC,gBAAgB,EAAA,YAAA,EACd,SAAS,CAAC,GAAG,EAAA,QAAA,EAAA,CAEzBA,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,qBAAqB,EAAA,QAAA,EAAA,CAClCC,cAAA,CAACW,+BAAe,EAAA,EAAC,WAAW,EAAE,WAAW,EAAA,CAAI,EAC7CZ,eAAA,CAAA,MAAA,EAAA,EAAM,SAAS,EAAC,iBAAiB,EAAA,QAAA,EAAA,CAC/BC,yBACE,SAAS,EAAC,eAAe,EAAA,SAAA,EAChB,SAAS,CAAC,GAAG,KAAK,WAAW,cAC5B,SAAS,CAAC,GAAG,KAAK,cAAc,EAAA,aAAA,EAC9B,MAAM,EAAA,CAClB,EACD,SAAS,CAAC,KAAK,CAAA,EAAA,CACX,IACH,EAENA,cAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,gBAAgB,EAAC,KAAK,EAAE,WAAW,CAAC,IAAI,EAAA,QAAA,EACpD,WAAW,CAAC,IAAI,EAAA,CACb,EAEL,WAAW,CAAC,WAAW,KACtBA,cAAA,CAAA,KAAA,EAAA,EACE,SAAS,EAAC,uBAAuB,EACjC,KAAK,EAAE,WAAW,CAAC,WAAW,YAE7B,WAAW,CAAC,WAAW,EAAA,CACpB,CACP,EAEDA,2BACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,oCACT,SAAS,CAAC,GAAG,KAAK;AAChB,kDAAE;AACF,kDAAE,wBACN,CAAA,CAAE,EACF,OAAO,EAAE,MAAM,aAAa,CAAC,WAAW,CAAC,EACzC,QAAQ,EAAE,IAAI,IAAI,CAAC,CAAC,OAAO,EAC3B,KAAK,EACH,SAAS,CAAC,GAAG,KAAK;AAChB,kDAAE;kDACA,SAAS,EAAA,QAAA,EAGd;AACC,kDAAE;AACF,kDAAE,SAAS,CAAC,GAAG,KAAK;AAClB,sDAAE;AACF,sDAAE,SAAS,CAAC,GAAG,KAAK;AAClB,0DAAE;AACF;;;AAGE,4DAAA,gBAAgB,EAAA,CACjB,EAER,WAAW,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,KAC9BA,cAAA,CAAA,IAAA,EAAA,EAAI,SAAS,EAAC,oBAAoB,EAAA,QAAA,EAC/B,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,MAChCD,eAAA,CAAA,IAAA,EAAA,EAAqB,SAAS,EAAC,mBAAmB,EAAA,QAAA,EAAA,CAChDC,cAAA,CAAA,MAAA,EAAA,EACE,SAAS,EAAC,eAAe,EAAA,SAAA,EAChB,CAAC,OAAO,CAAC,cAAc,EAAA,aAAA,EACpB,MAAM,EAAA,CAClB,EACFD,eAAA,CAAA,MAAA,EAAA,EAAM,SAAS,EAAC,yBAAyB,EAAA,QAAA,EAAA,CACtC,YAAY,CAAC,OAAO,CAAC,EACrB,OAAO,CAAC,cAAc,KACrBA,eAAA,CAAA,MAAA,EAAA,EAAM,SAAS,EAAC,wBAAwB,EAAA,QAAA,EAAA,CACrC,GAAG,EAAA,2BAAA,CAAA,EAAA,CAEC,CACR,CAAA,EAAA,CACI,EACPC,cAAA,CAAA,QAAA,EAAA,EACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,kBAAkB,EAC5B,OAAO,EAAE,MACP,gBAAgB,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,EAE5C,QAAQ,EAAE,CAAC,CAAC,OAAO,EACnB,KAAK,EAAC,yBAAyB,EAAA,YAAA,EACnB,CAAA,WAAA,EAAc,WAAW,CAAC,IAAI,CAAA,CAAE,EAAA,QAAA,EAAA,QAAA,EAAA,CAGrC,CAAA,EAAA,EA1BF,OAAO,CAAC,EAAE,CA2Bd,CACN,CAAC,EAAA,CACC,CACN,CAAA,EAAA,EA1FI,WAAW,CAAC,GAAG,CA2FhB;4BAEV,CAAC,CAAC,GACE,CACP,CAAA,EAAA,CACG,EAEND,eAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,kBAAkB,EAAA,QAAA,EAAA,CAC/BC,cAAA,CAAA,MAAA,EAAA,EAAA,QAAA,EAAA,yDAAA,EAAA,CAAoE,EACpEA,cAAA,CAAA,QAAA,EAAA,EACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,mCAAmC,EAC7C,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,EACjC,QAAQ,EAAE,OAAO,IAAI,CAAC,CAAC,OAAO,EAAA,QAAA,EAAA,SAAA,EAAA,CAGvB,IACL,CAAA,EAAA,CACF,EAAA,CACF,EACN,QAAQ,CAAC,IAAI,CACd;AACH;;;;"}
1
+ {"version":3,"file":"IntegrationsModal.js","sources":["../../../../../src/components/IntegrationsModal/IntegrationsModal.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type JSX,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { DevicApiError } from \"../../api/client\";\nimport type {\n Integration,\n IntegrationAccount,\n IntegrationAuthScheme,\n IntegrationSetupRequired,\n} from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\nimport { ConnectFieldsForm } from \"./ConnectFieldsForm\";\nimport { IntegrationLogo } from \"./IntegrationLogo\";\nimport { useIntegrations, type IntegrationsState } from \"./useIntegrations\";\nimport \"./IntegrationsModal.css\";\n\n/** Message the OAuth callback page posts back to this window when it is done. */\ninterface CallbackMessage {\n source?: string;\n type?: string;\n app?: string;\n status?: string;\n returnTo?: string;\n}\n\nexport interface IntegrationsModalProps {\n /** Whether the modal is visible. */\n isOpen: boolean;\n onClose: () => void;\n /** Assistant whose offered apps are shown. */\n assistantId: string;\n /** Tenant of the end user (falls back to the provider's tenantId). */\n tenantId?: string;\n /** Subtenant of the end user (falls back to the provider's subtenantId). */\n subtenantId?: string;\n /** API key override (falls back to the provider's). */\n apiKey?: string;\n /** Base URL override (falls back to the provider's). */\n baseUrl?: string;\n /** Modal title. @default \"Connected apps\" */\n title?: string;\n /** Search field placeholder. @default \"Search connected apps\" */\n searchPlaceholder?: string;\n /** Called after an account is connected or disconnected. */\n onChange?: (integrations: Integration[]) => void;\n /**\n * Colours and font. Same names as the drawer's style options, and the drawer\n * passes its own down — a dialog opening in the default light palette over a\n * themed application is the one thing this must not do.\n */\n theme?: DevicTheme;\n /**\n * Listing loaded elsewhere (see `useIntegrations`). The drawer already has to\n * load it to decide whether its button exists, and passing it down is what\n * keeps opening the modal from asking for the very same thing again.\n */\n state?: IntegrationsState;\n}\n\nfunction PlugIcon(): JSX.Element {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22v-5\" />\n <path d=\"M9 8V2\" />\n <path d=\"M15 8V2\" />\n <path d=\"M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z\" />\n </svg>\n );\n}\n\nfunction SearchIcon(): JSX.Element {\n return (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </svg>\n );\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\n/** How the card describes an app at a glance. */\nfunction stateOf(integration: Integration): {\n key: \"connected\" | \"reconnect\" | \"disconnected\";\n label: string;\n} {\n if (integration.connected) return { key: \"connected\", label: \"Connected\" };\n if (integration.accounts.some((a) => a.needsReconnect)) {\n return { key: \"reconnect\", label: \"Needs reconnection\" };\n }\n return { key: \"disconnected\", label: \"Not connected\" };\n}\n\nfunction accountLabel(account: IntegrationAccount): string {\n if (!account.connectedAt) return account.status.toLowerCase();\n const when = new Date(account.connectedAt);\n if (Number.isNaN(when.getTime())) return account.status.toLowerCase();\n return `connected ${when.toLocaleDateString()}`;\n}\n\nfunction matches(integration: Integration, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (\n integration.name.toLowerCase().includes(q) ||\n integration.app.toLowerCase().includes(q) ||\n (integration.description ?? \"\").toLowerCase().includes(q)\n );\n}\n\n/**\n * Modal where the END USER of an application manages their *own* third-party\n * accounts: the apps the developer offered to tenants of this assistant, each\n * with the accounts this tenant has connected, and the buttons to add or\n * remove one.\n *\n * Backed by `/api/v1/tenant-integrations`, which resolves the tenant\n * server-side, so what is listed here is only ever this tenant's — never the\n * workspace-wide accounts an admin connected, and never another tenant's.\n *\n * Connecting opens the provider's consent screen in a popup. The popup is\n * opened empty *before* the request that produces its URL, because browsers\n * only honour `window.open` inside the gesture that triggered it: opening it\n * after the round trip is what gets it blocked. When it is blocked anyway, the\n * URL is offered as a link instead.\n */\nexport function IntegrationsModal({\n isOpen,\n onClose,\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n title = \"Connected apps\",\n searchPlaceholder = \"Search connected apps\",\n onChange,\n theme,\n state,\n}: IntegrationsModalProps): JSX.Element | null {\n // Hooks cannot be skipped, so the fallback is always built and only fetches\n // when nobody handed a listing down.\n const own = useIntegrations({\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n enabled: isOpen && !state,\n });\n const { integrations, loading, error: loadError, refresh, client, scope } =\n state ?? own;\n\n /** Errors from connecting or disconnecting, kept apart from load failures. */\n const [actionError, setActionError] = useState<string | null>(null);\n const error = actionError ?? loadError;\n /** App slug with a connect/disconnect in flight, so only its card is busy. */\n const [busyApp, setBusyApp] = useState<string | null>(null);\n /** Authorization URL surfaced as a link when the popup was blocked. */\n const [blockedUrl, setBlockedUrl] = useState<{ app: string; url: string } | null>(\n null\n );\n /** The app whose credentials are being asked for, with what the server asked\n * and every way it can be connected. Rendered inside that app's own card,\n * where the user pressed Connect. */\n const [setupPrompt, setSetupPrompt] = useState<{\n app: string;\n setup: IntegrationSetupRequired;\n schemes: IntegrationAuthScheme[];\n } | null>(null);\n const [query, setQuery] = useState(\"\");\n\n const visible = useMemo(\n () => integrations.filter((i) => matches(i, query)),\n [integrations, query]\n );\n\n // Report the listing without making the caller's identity part of the\n // dependency: an inline arrow would fire this on every render.\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n useEffect(() => {\n if (integrations.length) onChangeRef.current?.(integrations);\n }, [integrations]);\n\n // Reopening starts clean, and re-reads: accounts may have been connected or\n // revoked elsewhere since the last look.\n const wasOpenRef = useRef(false);\n const openedBeforeRef = useRef(false);\n useEffect(() => {\n if (isOpen && !wasOpenRef.current) {\n setBlockedUrl(null);\n setActionError(null);\n setSetupPrompt(null);\n setQuery(\"\");\n // The very first open of an uncontrolled modal is already covered by the\n // hook switching on; asking again here would double every first open.\n if (state || openedBeforeRef.current) void refresh();\n openedBeforeRef.current = true;\n }\n wasOpenRef.current = isOpen;\n // `state.refresh` is stable per scope; re-running on every render of the\n // owner is exactly what this must not do.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen]);\n\n // Escape closes\n useEffect(() => {\n if (!isOpen) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [isOpen, onClose]);\n\n /** The round trip currently in flight, if any. */\n const pendingRef = useRef<{ app: string; returnTo: string } | null>(null);\n const popupRef = useRef<Window | null>(null);\n const pollRef = useRef<number | null>(null);\n\n const finishConnect = 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 setBusyApp(null);\n void refresh(true);\n }, [refresh]);\n\n // The callback page tells us it is done. Treat the message as a nudge, never\n // as the result: what is displayed comes from re-reading the server, so a\n // forged message can at worst cause one redundant fetch.\n useEffect(() => {\n if (!isOpen) return;\n const onMessage = (event: MessageEvent) => {\n const data = event.data as CallbackMessage | undefined;\n if (!data || data.source !== \"devic\") return;\n if (data.type !== \"integration-connected\") return;\n const pending = pendingRef.current;\n if (!pending || data.returnTo !== pending.returnTo) return;\n popupRef.current?.close();\n finishConnect();\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [isOpen, finishConnect]);\n\n // Stop polling if the modal goes away mid-flow.\n useEffect(\n () => () => {\n if (pollRef.current !== null) window.clearInterval(pollRef.current);\n },\n []\n );\n\n /**\n * Connects an app, asking for credentials only if it needs them.\n *\n * Optimistic: the request goes out first, and the form appears only when the\n * server answers that something is missing. Most apps take an API key rather\n * than an OAuth round trip, so this is the path that used to fail outright.\n */\n const handleConnect = async (\n integration: Integration,\n values?: { authScheme: string; accountFields: Record<string, string> }\n ) => {\n if (!client || (busyApp && busyApp !== integration.app)) return;\n setActionError(null);\n setBlockedUrl(null);\n setBusyApp(integration.app);\n\n const nonce = newNonce();\n const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;\n // Opened empty inside the click, navigated once the URL is known. Both\n // entry points are gestures — the card's button and the form's submit —\n // so this holds on the retry too.\n const popup = window.open(\n \"\",\n \"devic-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n );\n\n try {\n const { connected, authorizationUrl } = await client.connectIntegration(\n integration.app,\n { ...scope, returnTo, ...values }\n );\n\n // Nothing to authorise: the key the user typed is the account. Close the\n // window that was opened in case it was needed and re-read the listing.\n if (connected || !authorizationUrl) {\n popup?.close();\n setSetupPrompt(null);\n finishConnect();\n return;\n }\n\n pendingRef.current = { app: integration.app, returnTo };\n setSetupPrompt(null);\n if (popup && !popup.closed) {\n popupRef.current = popup;\n popup.location.href = authorizationUrl;\n // The user may close the popup without the callback ever posting back\n // — a cancelled consent screen, or a provider that lands somewhere\n // else. Watching for the close is what keeps the card from staying\n // busy forever.\n pollRef.current = window.setInterval(() => {\n if (popup.closed) finishConnect();\n }, 700);\n } else {\n // Blocked (Safari, in-app browsers, extensions): hand over the URL.\n setBlockedUrl({ app: integration.app, url: authorizationUrl });\n setBusyApp(null);\n }\n } catch (err) {\n popup?.close();\n pendingRef.current = null;\n setBusyApp(null);\n const setup = (err as DevicApiError)?.setupRequired;\n if (setup) {\n await openSetupForm(integration, setup);\n return;\n }\n setActionError(err instanceof Error ? err.message : String(err));\n }\n };\n\n /**\n * Opens the credentials form for an app that needs one.\n *\n * `stage: \"app\"` is not the end user's to fix — it means the developer who\n * embedded this widget has not registered an application with the provider\n * yet. Asking a stranger for someone else's client secret would be both\n * useless and a good way to teach them to hand credentials to a form, so it\n * is reported as unavailable instead.\n */\n const openSetupForm = async (\n integration: Integration,\n setup: IntegrationSetupRequired\n ) => {\n if (setup.stage === \"app\") {\n setActionError(\n `${integration.name} is not available yet — it still needs to be set up ` +\n `by the app's provider.`\n );\n return;\n }\n try {\n const { schemes } = await client!.getIntegrationAuth(\n integration.app,\n scope\n );\n if (!schemes.length) throw new Error(setup.message);\n setSetupPrompt({ app: integration.app, setup, schemes });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\n }\n };\n\n const handleDisconnect = async (app: string, account: IntegrationAccount) => {\n if (!client || busyApp) return;\n setBusyApp(app);\n setActionError(null);\n try {\n await client.disconnectIntegration(account.id, scope);\n await refresh(true);\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyApp(null);\n }\n };\n\n if (!isOpen) return null;\n\n return createPortal(\n // The variables go on the overlay, not on the modal: the backdrop is part\n // of the dialog, and a portal inherits nothing from the drawer that opened\n // it.\n <div\n className=\"devic-int-overlay\"\n style={themeVars(theme)}\n data-dark={isDarkTheme(theme)}\n onClick={onClose}\n >\n <div\n className=\"devic-int-modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"devic-int-header\">\n <h3 className=\"devic-int-title\">\n <PlugIcon />\n {title}\n </h3>\n <button\n className=\"devic-int-close\"\n onClick={onClose}\n type=\"button\"\n aria-label=\"Close\"\n >\n ×\n </button>\n </div>\n\n <div className=\"devic-int-search\">\n <SearchIcon />\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={searchPlaceholder}\n aria-label={searchPlaceholder}\n autoComplete=\"off\"\n />\n </div>\n\n <div className=\"devic-int-body\">\n {error && <div className=\"devic-int-error\">{error}</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 {loading && integrations.length === 0 ? (\n <div className=\"devic-int-loading\">Loading apps…</div>\n ) : integrations.length === 0 ? (\n <div className=\"devic-int-empty\">No apps available here yet.</div>\n ) : visible.length === 0 ? (\n <div className=\"devic-int-empty\">\n No apps match “{query.trim()}”.\n </div>\n ) : (\n <div className=\"devic-int-grid\">\n {visible.map((integration) => {\n const cardState = stateOf(integration);\n const busy = busyApp === integration.app;\n return (\n <div\n key={integration.app}\n className=\"devic-int-card\"\n data-state={cardState.key}\n >\n <div className=\"devic-int-card-head\">\n <IntegrationLogo integration={integration} />\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={integration.name}>\n {integration.name}\n </div>\n\n {integration.description && (\n <div\n className=\"devic-int-description\"\n title={integration.description}\n >\n {integration.description}\n </div>\n )}\n\n {/* The form replaces this app's button rather than opening\n a dialog of its own: it belongs to the card the user\n pressed, and the modal is already a dialog. */}\n {setupPrompt?.app === integration.app ? (\n <ConnectFieldsForm\n integration={integration}\n schemes={setupPrompt.schemes}\n initialScheme={setupPrompt.setup.authScheme}\n onlyFields={setupPrompt.setup.fields}\n submitting={busy}\n onCancel={() => setSetupPrompt(null)}\n onSubmit={(values) => handleConnect(integration, values)}\n />\n ) : (\n <button\n type=\"button\"\n className={`devic-int-btn devic-int-btn-block${\n cardState.key === \"connected\"\n ? \"\"\n : \" devic-int-btn-primary\"\n }`}\n onClick={() => handleConnect(integration)}\n disabled={busy || !!busyApp}\n title={\n cardState.key === \"connected\"\n ? \"Sign in with a different account. The one connected now is replaced.\"\n : undefined\n }\n >\n {busy\n ? \"Waiting…\"\n : cardState.key === \"disconnected\"\n ? \"Connect\"\n : cardState.key === \"reconnect\"\n ? \"Reconnect\"\n : // Not \"Add account\": one account per app is all\n // the assistant can use, and connecting again\n // retires the previous one.\n \"Switch account\"}\n </button>\n )}\n\n {integration.accounts.length > 0 && (\n <ul className=\"devic-int-accounts\">\n {integration.accounts.map((account) => (\n <li key={account.id} className=\"devic-int-account\">\n <span\n className=\"devic-int-dot\"\n data-ok={!account.needsReconnect}\n aria-hidden=\"true\"\n />\n <span className=\"devic-int-account-label\">\n {accountLabel(account)}\n {account.needsReconnect && (\n <span className=\"devic-int-account-warn\">\n {\" \"}\n · reconnect required\n </span>\n )}\n </span>\n <button\n type=\"button\"\n className=\"devic-int-unlink\"\n onClick={() =>\n handleDisconnect(integration.app, account)\n }\n disabled={!!busyApp}\n title=\"Disconnect this account\"\n aria-label={`Disconnect ${integration.name}`}\n >\n ×\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n <div className=\"devic-int-footer\">\n <span>Only you can see and use the accounts you connect here.</span>\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-small\"\n onClick={() => void refresh(true)}\n disabled={loading || !!busyApp}\n >\n Refresh\n </button>\n </div>\n </div>\n </div>,\n document.body\n );\n}\n\nexport default IntegrationsModal;\n"],"names":["_jsxs","_jsx","theme","useIntegrations","useState","useMemo","useRef","useEffect","useCallback","createPortal","themeVars","isDarkTheme","IntegrationLogo","ConnectFieldsForm"],"mappings":"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"}
@@ -8,6 +8,12 @@ var logger = require('../utils/logger.js');
8
8
  var usePolling = require('./usePolling.js');
9
9
  var useModelInterface = require('./useModelInterface.js');
10
10
 
11
+ /**
12
+ * Cadence for the handoff watch, which only waits for the parent thread to
13
+ * leave `handed_off` and so does not need the main conversation's rhythm.
14
+ * A configured `pollingInterval` still wins over it.
15
+ */
16
+ const DEFAULT_HANDOFF_POLL_INTERVAL_MS = 5000;
11
17
  /**
12
18
  * Main hook for managing chat with a Devic assistant
13
19
  *
@@ -31,7 +37,7 @@ var useModelInterface = require('./useModelInterface.js');
31
37
  * ```
32
38
  */
33
39
  function useDevicChat(options) {
34
- const { assistantId, chatUid: initialChatUid, apiKey: propsApiKey, baseUrl: propsBaseUrl, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, enabledTools, modelInterfaceTools = [], pollingInterval = 1000, onMessageSent, onMessageReceived, onToolCall, onError, onChatCreated, onFileUpload, debug: propsDebug, } = options;
40
+ const { assistantId, chatUid: initialChatUid, apiKey: propsApiKey, baseUrl: propsBaseUrl, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, enabledTools, modelInterfaceTools = [], pollingInterval: propsPollingInterval, onMessageSent, onMessageReceived, onToolCall, onError, onChatCreated, onFileUpload, debug: propsDebug, } = options;
35
41
  // Get context (may be null if not wrapped in provider)
36
42
  const context = DevicContext.useOptionalDevicContext();
37
43
  // Resolve configuration
@@ -48,6 +54,8 @@ function useDevicChat(options) {
48
54
  };
49
55
  // Provider tags + hook tags, deduped. Per-message tags are merged at send time.
50
56
  const resolvedTags = React.useMemo(() => Array.from(new Set([...(context?.tags ?? []), ...(tags ?? [])])), [context?.tags, tags]);
57
+ const pollingInterval = usePolling.resolvePollingInterval(propsPollingInterval, context?.pollingInterval);
58
+ const handoffPollingInterval = usePolling.resolvePollingInterval(propsPollingInterval, context?.pollingInterval, DEFAULT_HANDOFF_POLL_INTERVAL_MS);
51
59
  const debug = propsDebug ?? context?.debug ?? false;
52
60
  const log = React.useMemo(() => logger.createLogger(debug), [debug]);
53
61
  const logRef = React.useRef(log);
@@ -523,8 +531,9 @@ function useDevicChat(options) {
523
531
  setIsLoading(false);
524
532
  }
525
533
  }, [assistantId, resolvedTenantId, resumeFromRealtimeStatus, mergeRecalledMemories]);
526
- // Handoff polling: while handedOff is true, poll the realtime endpoint every 5s
527
- // to detect when the parent thread is no longer in handed_off state.
534
+ // Handoff polling: while handedOff is true, poll the realtime endpoint every
535
+ // 5s (or the configured cadence) to detect when the parent thread is no
536
+ // longer in handed_off state.
528
537
  React.useEffect(() => {
529
538
  if (!handedOff || !chatUid || !clientRef.current)
530
539
  return;
@@ -546,14 +555,14 @@ function useDevicChat(options) {
546
555
  }
547
556
  catch { }
548
557
  };
549
- handoffPollRef.current = setInterval(pollHandoff, 5000);
558
+ handoffPollRef.current = setInterval(pollHandoff, handoffPollingInterval);
550
559
  return () => {
551
560
  if (handoffPollRef.current) {
552
561
  clearInterval(handoffPollRef.current);
553
562
  handoffPollRef.current = null;
554
563
  }
555
564
  };
556
- }, [handedOff, chatUid, assistantId]);
565
+ }, [handedOff, chatUid, assistantId, handoffPollingInterval]);
557
566
  // Called by HandoffSubagentWidget when the subthread reaches a terminal state
558
567
  const onHandoffCompleted = React.useCallback(() => {
559
568
  logRef.current.log('[useDevicChat] onHandoffCompleted called');