@noodleseed/assistant 1.25.0 → 1.26.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 (45) hide show
  1. package/README.md +63 -24
  2. package/dist/{app-view-element-BM4lral-.d.ts → app-view-element-C_ATXseu.d.ts} +2 -3
  3. package/dist/{app-view-element-9ojWWYov.d.cts → app-view-element-CiFVNIke.d.cts} +2 -3
  4. package/dist/app-view.cjs +41 -213
  5. package/dist/app-view.cjs.map +1 -1
  6. package/dist/app-view.d.cts +3 -3
  7. package/dist/app-view.d.ts +3 -3
  8. package/dist/app-view.js +2 -2
  9. package/dist/{appearance-B6vSg8Ub.d.cts → appearance-C24W6BRj.d.cts} +7 -3
  10. package/dist/{appearance-B6vSg8Ub.d.ts → appearance-C24W6BRj.d.ts} +7 -3
  11. package/dist/{chunk-LNBUYHL7.js → chunk-5NBHHNI3.js} +2 -2
  12. package/dist/chunk-5NBHHNI3.js.map +1 -0
  13. package/dist/{chunk-SGMECNNU.js → chunk-5PW6PXCI.js} +702 -122
  14. package/dist/chunk-5PW6PXCI.js.map +1 -0
  15. package/dist/{chunk-7BQHW37I.js → chunk-FNXAVRL5.js} +7 -1
  16. package/dist/{chunk-7BQHW37I.js.map → chunk-FNXAVRL5.js.map} +1 -1
  17. package/dist/{chunk-P32MAR4K.js → chunk-XKTUXFGX.js} +42 -214
  18. package/dist/chunk-XKTUXFGX.js.map +1 -0
  19. package/dist/{client-BkogON2A.d.ts → client-BheCOERq.d.ts} +51 -2
  20. package/dist/{client-C07ppTU_.d.cts → client-BiMRVxjs.d.cts} +51 -2
  21. package/dist/client.cjs.map +1 -1
  22. package/dist/client.d.cts +2 -2
  23. package/dist/client.d.ts +2 -2
  24. package/dist/client.js +1 -1
  25. package/dist/embed.global.js +210 -161
  26. package/dist/index.cjs +767 -354
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +6 -53
  29. package/dist/index.d.ts +6 -53
  30. package/dist/index.js +3 -3
  31. package/dist/react/client.cjs.map +1 -1
  32. package/dist/react/client.d.cts +2 -2
  33. package/dist/react/client.d.ts +2 -2
  34. package/dist/react/client.js +1 -1
  35. package/dist/react.cjs +767 -354
  36. package/dist/react.cjs.map +1 -1
  37. package/dist/react.d.cts +4 -4
  38. package/dist/react.d.ts +4 -4
  39. package/dist/react.js +4 -4
  40. package/dist/server.d.cts +1 -1
  41. package/dist/server.d.ts +1 -1
  42. package/package.json +1 -1
  43. package/dist/chunk-LNBUYHL7.js.map +0 -1
  44. package/dist/chunk-P32MAR4K.js.map +0 -1
  45. package/dist/chunk-SGMECNNU.js.map +0 -1
package/README.md CHANGED
@@ -6,10 +6,10 @@ The package exports the canonical `<noodle-assistant>` managed Web Component, th
6
6
  `<noodle-app-view>` MCP App host from `@noodleseed/assistant/app-view`, a managed React wrapper and
7
7
  `NoodleAppView` adapter from `@noodleseed/assistant/react`, a renderer-free React hook from
8
8
  `@noodleseed/assistant/react/client`, a DOM-free client from `@noodleseed/assistant/client`, and the
9
- backend-only `createAssistantSession` helper from `@noodleseed/assistant/server`. Light, dark, and automatic
10
- themes work without configuration; the component inherits the deployed MCP server's brand kit while slots,
11
- methods, events, and semantic CSS variables let a SaaS developer integrate it without depending on internal
12
- DOM selectors.
9
+ backend-only `createAssistantSession` helper from `@noodleseed/assistant/server`. The component inherits the
10
+ deployed MCP server's brand kit while slots, methods, events, and semantic CSS variables let a SaaS developer
11
+ integrate it without depending on internal DOM selectors. Light, dark, automatic host-page matching, and
12
+ host-page inversion work without replacing conversation or App DOM.
13
13
 
14
14
  Never pass an embed client secret, model key, MCP token, or raw application session into the browser
15
15
  component. Exchange the already-authenticated user from the customer backend and return only the short-lived
@@ -22,7 +22,7 @@ The model configuration and SaaS integration credentials have different owners:
22
22
  | Noodle deployment | `ASSISTANT_MODEL_BASE_URL`, `ASSISTANT_MODEL`, `ASSISTANT_MODEL_API_KEY` | Managed with `noodle variables set` / `noodle secrets set` |
23
23
  | Customer backend | `NOODLE_SERVICE_URL`, `NOODLE_ASSISTANT_CLIENT_ID`, `NOODLE_ASSISTANT_CLIENT_SECRET` | Backend environment or secret manager only |
24
24
 
25
- `allowedOrigins` accepts exact origins. Production origins must be HTTPS; plain HTTP is accepted only for
25
+ `access.origins` accepts exact origins. Production origins must be HTTPS; plain HTTP is accepted only for
26
26
  loopback development origins such as `http://localhost:3000`. Local MCP authoring remains available without
27
27
  login, but an external browser embed needs an active deployment before its deployment-bound client can be
28
28
  created.
@@ -35,8 +35,8 @@ troubleshooting) lives at <https://docs.noodleseed.dev/guides/embedded-assistant
35
35
  the server itself is `@noodleseed/one` (`npm install -g @noodleseed/one`).
36
36
 
37
37
  If your page sends a `Content-Security-Policy`, allow the Noodle service origin in `connect-src` (turns and
38
- event streams) and the exact session-advertised `endpoints.sandbox` origin in `frame-src` (it may be a
39
- dedicated sandbox origin, so your `script-src` can stay strict). Details are in the guide's
38
+ event streams) **and** `frame-src` (app widgets render inside a hosted sandbox document served from the
39
+ service origin, `endpoints.sandbox`, so your `script-src` can stay strict). Details are in the guide's
40
40
  "Content-Security-Policy on your page" section.
41
41
 
42
42
  ## Quick start
@@ -55,6 +55,7 @@ structure and treatment in `embeddedAssistant({ presentation })`:
55
55
 
56
56
  ```ts
57
57
  import {
58
+ authenticatedWebsite,
58
59
  embeddedAssistant,
59
60
  openAICompatible,
60
61
  secret,
@@ -83,19 +84,23 @@ export default server(
83
84
  model: variable("ASSISTANT_MODEL"),
84
85
  apiKey: secret("ASSISTANT_MODEL_API_KEY"),
85
86
  }),
86
- allowedOrigins: ["http://localhost:3000", "https://app.acme.example"],
87
+ access: authenticatedWebsite({
88
+ origins: ["http://localhost:3000", "https://app.acme.example"],
89
+ }),
87
90
  layout: {
88
91
  mode: "floating",
89
- position: "bottom-right",
90
- panelWidth: 520,
92
+ position: "bottom-center",
93
+ panelWidth: 970,
91
94
  panelMinHeight: 540,
92
- panelMaxHeight: 740,
93
- edgeOffset: 24,
95
+ panelMaxHeight: 1025,
96
+ edgeOffset: 20,
94
97
  },
95
- behavior: { showTimestamps: true },
98
+ theme: "invert",
99
+ behavior: { showTimestamps: true, showPoweredBy: false },
96
100
  labels: {
97
101
  welcomeHeading: "How can Acme help?",
98
102
  welcomeMessage: "Fast answers from the tools your team already uses.",
103
+ launcherPlaceholder: "Ask Acme anything",
99
104
  composerPlaceholder: "Message Acme Support…",
100
105
  sessionReady: "Acme support is online",
101
106
  },
@@ -107,6 +112,7 @@ export default server(
107
112
  radius: 20,
108
113
  },
109
114
  launcher: {
115
+ style: "bubble",
110
116
  icon: "chat",
111
117
  size: "lg",
112
118
  status: "session",
@@ -144,10 +150,15 @@ markup-looking text stays text. `presentation.panel.radius` is a bounded panel-s
144
150
  not a second color or identity source. An HTTPS or packaged SVG referenced by `branding.logo`, `branding.mark`, or
145
151
  `branding.avatar` is a bounded asset, not inline renderer markup.
146
152
 
147
- If `presentation` is omitted, the rounded Halo-inspired baseline remains: a frosted glass panel with a 24px
148
- radius, soft elevation, subtle border/motion, medium brand-mark launcher without status or effect,
149
- undecorated header, pill composer and action controls, and rounded user bubbles with plain assistant messages
150
- at comfortable width. Partial objects merge with those defaults.
153
+ With UI overrides omitted, the complete managed baseline remains: a bottom-center frosted launcher pill that
154
+ morphs into a prompt input before opening; a 970px outer desktop shell with 20px side padding, 85vh height,
155
+ 1025px maximum height, and a 24px panel using the built-in `#F8F8F8` light and `#0C0A09` dark surfaces;
156
+ bottom prompt chips and pill composer; plain assistant messages and 85%-wide user bubbles; a Noodle Seed
157
+ attribution row; and safe-area-aware mobile fullscreen.
158
+ The generic prompt chips ship as defaults, while an authored list—including `[]`—replaces them. Set
159
+ `presentation.launcher.style` to `bubble` for a direct-open 44px launcher. The baseline does not include the
160
+ “Available on ChatGPT” promotion. Partial configuration objects merge with the remaining defaults;
161
+ `presentation.panel.surface: "glass"` remains available when a translucent panel is intentional.
151
162
 
152
163
  ### Configure and deploy
153
164
 
@@ -294,6 +305,22 @@ import "@noodleseed/assistant";
294
305
  ></noodle-assistant>
295
306
  ```
296
307
 
308
+ That single element is the complete managed assistant in React, Vue, Angular, or plain DOM. The
309
+ default pill expands into a prompt first; set `presentation.launcher.style: "bubble"` in `server.ts` when the
310
+ launcher should open the panel directly. Frameworks must treat `noodle-assistant` as a custom element.
311
+
312
+ When the host needs an authenticated fetch wrapper, assign complex values as properties before connecting
313
+ the element so the first session exchange already carries the application's credentials:
314
+
315
+ ```ts
316
+ import type { NoodleAssistantElement } from "@noodleseed/assistant";
317
+
318
+ const assistant = document.createElement("noodle-assistant") as NoodleAssistantElement;
319
+ assistant.fetch = authenticatedFetch;
320
+ assistant.sessionEndpoint = "/api/assistant/session";
321
+ document.body.append(assistant);
322
+ ```
323
+
297
324
  Or use React:
298
325
 
299
326
  ```tsx
@@ -410,8 +437,13 @@ import "@noodleseed/assistant/app-view";
410
437
  </template>
411
438
  ```
412
439
 
413
- Framework setup, lifecycle ownership, sandbox/CSP behavior, and staged diagnostics are owned by the
414
- [Bring your own UI guide](https://docs.noodleseed.dev/docs/guides/bring-your-own-ui).
440
+ Configure Vue's `isCustomElement` for `noodle-app-view`. Angular uses the same element with `[client]`,
441
+ `[view]`, and `[theme]` property bindings. Do not serialize `client` or `view` into attributes.
442
+
443
+ `<noodle-app-view>` is the canonical host; `NoodleAppView` delegates to it. Both use the
444
+ service-advertised sandbox URL, route App calls through the supplied client, publish theme changes without
445
+ replacing the iframe, and request standard teardown on semantic replacement, disconnect, or an App teardown
446
+ request.
415
447
 
416
448
  The App document owns its action intent: it calls standard `tools/call` after connecting and never relies on
417
449
  native form navigation. If that call needs input or confirmation, the same `client` publishes the normal
@@ -636,24 +668,31 @@ noodle-assistant {
636
668
  ```
637
669
 
638
670
  For each matching region or token, precedence is the typed host appearance object, then host-provided slot
639
- content or a public CSS custom property, then compiled server `presentation`/`branding`, then built-in
640
- defaults. The public slots are `launcher-icon`,
671
+ content or a public CSS custom property, then the saved environment operator override, compiled server
672
+ `presentation`/`branding`, and built-in defaults. The public slots are `launcher-icon`,
641
673
  `header-leading`, `header-actions`, `empty-state`, `composer-leading`, `composer-trailing`, and
642
674
  `conversation-footer`; slotted host DOM replaces the renderer fallback for that region. Slots are a trusted
643
675
  embedding-page integration API, not a way to put HTML or callbacks in deployment configuration. Internal
644
676
  shadow-DOM selectors and classes are not public API.
645
677
 
646
- `theme="auto"` follows the browser's operating-system color preference. When the embedding application has
678
+ The Console **Assistant** tab and `noodle assistant appearance show|apply|reset` manage that environment
679
+ override. Existing embed code does not change: public elements load its non-secret configuration before
680
+ first paint without spending a session mint, while authenticated elements receive it from session exchange.
681
+ Each new session pins one resolved revision; an open conversation remains visually stable.
682
+
683
+ `theme="auto"` follows an explicit `light`/`dark` host-page class or data attribute, then the browser's
684
+ operating-system preference; `theme="invert"` selects the opposite host mode. When the embedding application has
647
685
  its own theme toggle, pass its resolved `"light"` or `"dark"` value to `NoodleAssistant` and every
648
686
  `<noodle-app-view>`/`NoodleAppView`. Updates change the assistant in place and notify mounted MCP Apps through standard host
649
687
  context.
650
688
 
651
689
  Server branding controls customer name, themed logo/mark/avatar assets, semantic light/dark colors, density,
652
- radius, typography, and automatic theme. `embeddedAssistant(...)` controls floating/inline/drawer layout,
653
- position and dimensions, mobile and launcher/header/avatar/timestamp behavior, visible labels, suggested
690
+ radius, typography, and automatic theme. `embeddedAssistant(...)` controls theme selection,
691
+ floating/inline/drawer layout, position and dimensions, mobile and launcher/header/avatar/timestamp/attribution behavior, visible labels, suggested
654
692
  prompts, privacy/terms links, locale, and text direction.
655
693
  Named slots cover launcher/header/composer/footer extensions. Public methods include `open`, `close`,
656
694
  `toggle`, `focusComposer`, `sendMessage`, `stop`, `reconnect`, `updateContext`, `updateModelContext`, `respond`, `confirmTool`, and `resetSession`;
695
+ lifecycle properties include `sessionEndpoint`, `embedId`, `serviceUrl`, `fetch`, and `appearance`;
657
696
  lifecycle, message, tool-proposal, interaction-resolution, view-availability, context, and error events
658
697
  support application integration. `confirmTool(id)` remains the compatibility shorthand for
659
698
  `respond(id, { action: 'accept' })`.
@@ -1,10 +1,9 @@
1
- import { A as AssistantClient, a as AssistantViewAvailableDetail, e as AssistantErrorDetail } from './client-BkogON2A.js';
1
+ import { A as AssistantClient, a as AssistantViewAvailableDetail, e as AssistantErrorDetail } from './client-BheCOERq.js';
2
2
 
3
3
  declare const APP_VIEW_TAG_NAME = "noodle-app-view";
4
4
  interface AssistantAppViewErrorDetail {
5
- readonly code: 'view_render_timeout' | 'view_bridge_error';
5
+ readonly code: 'view_render_timeout';
6
6
  readonly retryable: false;
7
- readonly stage: 'sandbox_load' | 'sandbox_proxy' | 'app_initialize';
8
7
  }
9
8
  declare global {
10
9
  interface HTMLElementTagNameMap {
@@ -1,10 +1,9 @@
1
- import { A as AssistantClient, a as AssistantViewAvailableDetail, e as AssistantErrorDetail } from './client-C07ppTU_.cjs';
1
+ import { A as AssistantClient, a as AssistantViewAvailableDetail, e as AssistantErrorDetail } from './client-BiMRVxjs.cjs';
2
2
 
3
3
  declare const APP_VIEW_TAG_NAME = "noodle-app-view";
4
4
  interface AssistantAppViewErrorDetail {
5
- readonly code: 'view_render_timeout' | 'view_bridge_error';
5
+ readonly code: 'view_render_timeout';
6
6
  readonly retryable: false;
7
- readonly stage: 'sandbox_load' | 'sandbox_proxy' | 'app_initialize';
8
7
  }
9
8
  declare global {
10
9
  interface HTMLElementTagNameMap {
package/dist/app-view.cjs CHANGED
@@ -15197,7 +15197,7 @@ __export(app_view_exports, {
15197
15197
  });
15198
15198
  module.exports = __toCommonJS(app_view_exports);
15199
15199
 
15200
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_supports-color@7.2.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
15200
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
15201
15201
  init_v4();
15202
15202
  var RELATED_TASK_META_KEY = "io.modelcontextprotocol/related-task";
15203
15203
  var JSONRPC_VERSION = "2.0";
@@ -16718,7 +16718,7 @@ var UrlElicitationRequiredError = class extends McpError {
16718
16718
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/mini/parse.js
16719
16719
  init_core2();
16720
16720
 
16721
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_supports-color@7.2.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
16721
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
16722
16722
  function isZ4Schema(s2) {
16723
16723
  const schema = s2;
16724
16724
  return !!schema._zod;
@@ -16781,7 +16781,7 @@ function getLiteralValue(schema) {
16781
16781
  return void 0;
16782
16782
  }
16783
16783
 
16784
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_supports-color@7.2.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
16784
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
16785
16785
  function isTerminal(status) {
16786
16786
  return status === "completed" || status === "failed" || status === "cancelled";
16787
16787
  }
@@ -16789,7 +16789,7 @@ function isTerminal(status) {
16789
16789
  // ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@4.4.3/node_modules/zod-to-json-schema/dist/esm/parsers/string.js
16790
16790
  var ALPHA_NUMERIC = new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
16791
16791
 
16792
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_supports-color@7.2.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js
16792
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js
16793
16793
  function getMethodLiteral(schema) {
16794
16794
  const shape = getObjectShape(schema);
16795
16795
  const methodSchema = shape?.method;
@@ -16810,7 +16810,7 @@ function parseWithCompat(schema, data) {
16810
16810
  return result.data;
16811
16811
  }
16812
16812
 
16813
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_supports-color@7.2.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
16813
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
16814
16814
  var DEFAULT_REQUEST_TIMEOUT_MSEC = 6e4;
16815
16815
  var Protocol = class {
16816
16816
  constructor(_options) {
@@ -17922,15 +17922,6 @@ var E = class {
17922
17922
  sessionId;
17923
17923
  setProtocolVersion;
17924
17924
  };
17925
- function AZ(X) {
17926
- if (!X) return "";
17927
- let Y = [];
17928
- if (X.camera) Y.push("camera");
17929
- if (X.microphone) Y.push("microphone");
17930
- if (X.geolocation) Y.push("geolocation");
17931
- if (X.clipboardWrite) Y.push("clipboard-write");
17932
- return Y.join("; ");
17933
- }
17934
17925
  var KX = [j];
17935
17926
  var DX = class extends _ {
17936
17927
  _client;
@@ -18201,131 +18192,34 @@ function GX(X, Y) {
18201
18192
  return JSON.stringify(X) === JSON.stringify(Y);
18202
18193
  }
18203
18194
 
18204
- // src/app-sandbox-policy.ts
18205
- var MAX_DOMAINS_PER_DIRECTIVE = 32;
18206
- function isRecord(value) {
18207
- return typeof value === "object" && value !== null && !Array.isArray(value);
18208
- }
18209
- function isLoopback(hostname3) {
18210
- return hostname3 === "localhost" || hostname3 === "127.0.0.1" || hostname3 === "[::1]";
18211
- }
18212
- function normalizeDomain(value, connect) {
18213
- if (value.length > 512 || /[;\r\n'"\s]/u.test(value)) return void 0;
18214
- try {
18215
- const parsed = new URL(value);
18216
- const secure = parsed.protocol === "https:" || connect && parsed.protocol === "wss:";
18217
- const local = isLoopback(parsed.hostname) && (parsed.protocol === "http:" || connect && parsed.protocol === "ws:");
18218
- if (!secure && !local) return void 0;
18219
- if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
18220
- return void 0;
18221
- }
18222
- return parsed.origin;
18223
- } catch {
18224
- return void 0;
18225
- }
18226
- }
18227
- function normalizeDomains(values, connect) {
18228
- const normalized = [
18229
- ...new Set(
18230
- values.slice(0, MAX_DOMAINS_PER_DIRECTIVE).map((value) => typeof value === "string" ? normalizeDomain(value, connect) : void 0).filter((value) => value !== void 0)
18231
- )
18232
- ];
18233
- return normalized;
18234
- }
18235
- function resolveAssistantAppSandboxPolicy(resourceMeta) {
18236
- if (!isRecord(resourceMeta)) return {};
18237
- const rawCsp = resourceMeta.csp;
18238
- const csp = isRecord(rawCsp) ? {
18239
- ...Array.isArray(rawCsp.connectDomains) ? { connectDomains: normalizeDomains(rawCsp.connectDomains, true) } : {},
18240
- ...Array.isArray(rawCsp.resourceDomains) ? { resourceDomains: normalizeDomains(rawCsp.resourceDomains, false) } : {},
18241
- ...Array.isArray(rawCsp.frameDomains) ? { frameDomains: normalizeDomains(rawCsp.frameDomains, false) } : {},
18242
- ...Array.isArray(rawCsp.baseUriDomains) ? { baseUriDomains: normalizeDomains(rawCsp.baseUriDomains, false) } : {}
18243
- } : void 0;
18244
- const rawPermissions = resourceMeta.permissions;
18245
- const permissions = isRecord(rawPermissions) ? {
18246
- ...isRecord(rawPermissions.camera) ? { camera: {} } : {},
18247
- ...isRecord(rawPermissions.microphone) ? { microphone: {} } : {},
18248
- ...isRecord(rawPermissions.geolocation) ? { geolocation: {} } : {},
18249
- ...isRecord(rawPermissions.clipboardWrite) ? { clipboardWrite: {} } : {}
18250
- } : void 0;
18251
- const allowAttribute = AZ(permissions);
18252
- return {
18253
- ...csp === void 0 ? {} : { csp },
18254
- ...permissions === void 0 ? {} : { permissions },
18255
- ...allowAttribute ? { allowAttribute } : {}
18256
- };
18257
- }
18258
- function assistantAppSandboxUrl(sandboxUrl, policy) {
18259
- const parsed = new URL(sandboxUrl);
18260
- if (policy.csp) parsed.searchParams.set("csp", JSON.stringify(policy.csp));
18261
- const parentOrigin = globalThis.location?.origin;
18262
- if (parentOrigin && parentOrigin !== "null") {
18263
- parsed.hash = new URLSearchParams({ parentOrigin }).toString();
18264
- }
18265
- return parsed.href;
18266
- }
18267
-
18268
18195
  // src/app-host.ts
18269
- var PROXY_DOCUMENT = `<!doctype html><meta charset="utf-8"><style>html,body,iframe{border:0;margin:0;width:100%;height:100%;overflow:hidden}body{background:transparent}</style><body><script>
18270
- let inner,loaded=false;
18271
- if(self===top)throw new Error('The assistant sandbox relay must be embedded');
18272
- const standardSandbox='allow-scripts allow-same-origin allow-forms';
18273
- const rawParentOrigin=new URLSearchParams(location.hash.slice(1)).get('parentOrigin');
18274
- let expectedParentOrigin;
18275
- try{const parsed=new URL(rawParentOrigin||'');if(parsed.origin===rawParentOrigin&&/^https?:$/.test(parsed.protocol))expectedParentOrigin=parsed.origin}catch{}
18276
- const parentTarget=expectedParentOrigin||'*';
18277
- const ownOrigin=self.origin;
18196
+ var PROXY_DOCUMENT = `<!doctype html><meta charset="utf-8"><style>html,body,iframe{border:0;margin:0;width:100%;height:100%;overflow:hidden}body{background:transparent}</style><script>
18197
+ let inner;
18278
18198
  const ready={jsonrpc:'2.0',method:'ui/notifications/sandbox-proxy-ready',params:{}};
18279
18199
  // Re-announce until the host acknowledges with the resource: when this document is hosted
18280
18200
  // cross-origin, the first announcement can arrive before the host's load handler is listening.
18281
- const announce=setInterval(()=>parent.postMessage(ready,parentTarget),120);
18201
+ const announce=setInterval(()=>parent.postMessage(ready,'*'),120);
18282
18202
  setTimeout(()=>clearInterval(announce),15000);
18283
18203
  addEventListener('message',(event)=>{
18284
18204
  if(event.source===parent){
18285
- if(expectedParentOrigin&&event.origin!==expectedParentOrigin)return;
18286
18205
  const message=event.data;
18287
18206
  if(message?.method==='ui/notifications/sandbox-resource-ready'){
18288
18207
  clearInterval(announce);
18289
- if(loaded)return;
18290
- loaded=true;
18291
- const requestedSandbox=message.params?.sandbox;
18208
+ if(inner)return;
18292
18209
  inner=document.createElement('iframe');
18293
- inner.setAttribute('sandbox',requestedSandbox===standardSandbox?standardSandbox:'allow-scripts');
18210
+ inner.setAttribute('sandbox',message.params?.sandbox||'allow-scripts');
18294
18211
  inner.setAttribute('referrerpolicy','no-referrer');
18295
- const permissions=message.params?.permissions;
18296
- const allow=[];
18297
- if(permissions?.camera)allow.push('camera');
18298
- if(permissions?.microphone)allow.push('microphone');
18299
- if(permissions?.geolocation)allow.push('geolocation');
18300
- if(permissions?.clipboardWrite)allow.push('clipboard-write');
18301
- if(allow.length)inner.setAttribute('allow',allow.join('; '));
18302
- document.body.append(inner);
18303
- let html=String(message.params?.html||'');
18304
- const csp=message.params?.csp;
18305
- if(csp&&typeof csp==='object'){
18306
- const list=(value,fallback)=>Array.isArray(value)&&value.length?value.join(' '):fallback;
18307
- const resources=Array.isArray(csp.resourceDomains)?csp.resourceDomains:[];
18308
- const suffix=resources.length?' '+resources.join(' '):'';
18309
- const policy=["default-src 'none'","script-src 'unsafe-inline'"+suffix,"style-src 'unsafe-inline'"+suffix,'img-src data: blob:'+suffix,'font-src data: blob:'+suffix,'media-src data: blob:'+suffix,'worker-src blob:'+suffix,'connect-src '+list(csp.connectDomains,"'none'"),'frame-src '+list(csp.frameDomains,"'none'"),"object-src 'none'","form-action 'none'",'base-uri '+list(csp.baseUriDomains,"'none'")].join('; ');
18310
- const meta='<meta http-equiv="Content-Security-Policy" content="'+policy.replace(/&/g,'&amp;').replace(/"/g,'&quot;')+'">';
18311
- const doctype=html.match(/^\\s*<!doctype[^>]*>/i);
18312
- html=doctype?html.slice(0,doctype[0].length)+meta+html.slice(doctype[0].length):meta+html;
18313
- }
18314
- try{
18315
- const doc=inner.contentDocument||inner.contentWindow?.document;
18316
- if(!doc)throw new Error('document unavailable');
18317
- doc.open();doc.write(html);doc.close();
18318
- }catch{inner.srcdoc=html}
18212
+ inner.srcdoc=String(message.params?.html||'');
18213
+ document.body.replaceChildren(inner);
18319
18214
  return;
18320
18215
  }
18321
18216
  inner?.contentWindow?.postMessage(message,'*');
18322
18217
  } else if(inner && event.source===inner.contentWindow) {
18323
- if(event.origin!==ownOrigin)return;
18324
- parent.postMessage(event.data,parentTarget);
18218
+ parent.postMessage(event.data,'*');
18325
18219
  }
18326
18220
  });
18327
- parent.postMessage(ready,parentTarget);
18328
- </script></body>`;
18221
+ parent.postMessage(ready,'*');
18222
+ </script>`;
18329
18223
  function isAllowedAppLink(value, allowedDomains, baseUrl) {
18330
18224
  try {
18331
18225
  const parsed = new URL(value, baseUrl);
@@ -18352,42 +18246,16 @@ function createAssistantAppHostContext(theme, allowFullscreen = false, displayMo
18352
18246
  }
18353
18247
  var APP_RENDER_TIMEOUT_MS = 1e4;
18354
18248
  var APP_TEARDOWN_TIMEOUT_MS = 1e3;
18355
- var STANDARD_SANDBOX = "allow-scripts allow-same-origin allow-forms";
18356
18249
  var mountedApps = /* @__PURE__ */ new WeakMap();
18357
18250
  function resolveSandboxUrl(value) {
18358
18251
  if (!value) return void 0;
18359
18252
  try {
18360
18253
  const parsed = new URL(value);
18361
- const loopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
18362
- const permittedProtocol = parsed.protocol === "https:" || parsed.protocol === "http:" && loopback;
18363
- const embedderOrigin = globalThis.location?.origin;
18364
- if (!permittedProtocol || embedderOrigin && embedderOrigin !== "null" && parsed.origin === embedderOrigin) {
18365
- return void 0;
18366
- }
18367
- return parsed.href;
18254
+ return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.href : void 0;
18368
18255
  } catch {
18369
18256
  return void 0;
18370
18257
  }
18371
18258
  }
18372
- function copyAppJson(value, depth = 0) {
18373
- if (depth > 64) throw new Error("App payload nesting exceeds the host limit");
18374
- if (value === null || typeof value === "string" || typeof value === "boolean") return value;
18375
- if (typeof value === "number") {
18376
- if (!Number.isFinite(value)) throw new Error("App payload numbers must be finite");
18377
- return value;
18378
- }
18379
- if (Array.isArray(value)) return value.map((entry) => copyAppJson(entry, depth + 1));
18380
- const copied = {};
18381
- for (const [key, entry] of Object.entries(value)) {
18382
- Object.defineProperty(copied, key, {
18383
- value: copyAppJson(entry, depth + 1),
18384
- enumerable: true,
18385
- configurable: true,
18386
- writable: true
18387
- });
18388
- }
18389
- return copied;
18390
- }
18391
18259
  function mountAssistantApp(detail, actions, options = {}) {
18392
18260
  if (!detail.html) return void 0;
18393
18261
  const card = document.createElement("section");
@@ -18408,25 +18276,16 @@ function mountAssistantApp(detail, actions, options = {}) {
18408
18276
  const frame = document.createElement("iframe");
18409
18277
  frame.className = "noodle-app-frame";
18410
18278
  frame.title = detail.title ?? detail.tool;
18411
- const policy = resolveAssistantAppSandboxPolicy(detail.resourceMeta);
18279
+ frame.setAttribute("sandbox", "allow-scripts");
18412
18280
  frame.setAttribute("referrerpolicy", "no-referrer");
18413
18281
  const sandboxUrl = resolveSandboxUrl(options.sandboxUrl);
18414
- const appliedPolicy = sandboxUrl ? policy : { csp: policy.csp ?? {} };
18415
- if (sandboxUrl) {
18416
- frame.setAttribute("sandbox", STANDARD_SANDBOX);
18417
- if (policy.allowAttribute) frame.setAttribute("allow", policy.allowAttribute);
18418
- frame.src = assistantAppSandboxUrl(sandboxUrl, policy);
18419
- } else {
18420
- frame.setAttribute("sandbox", "allow-scripts");
18421
- frame.srcdoc = PROXY_DOCUMENT;
18422
- }
18282
+ if (sandboxUrl) frame.src = sandboxUrl;
18283
+ else frame.srcdoc = PROXY_DOCUMENT;
18423
18284
  card.append(frame);
18424
18285
  let mountedBridge;
18425
18286
  let mountedTransport;
18426
18287
  let destroyed = false;
18427
18288
  let initialized = false;
18428
- let renderFailed = false;
18429
- let renderStage = "sandbox_load";
18430
18289
  let currentTheme = actions.theme;
18431
18290
  let displayMode = "inline";
18432
18291
  let hostContext = createAssistantAppHostContext(
@@ -18435,15 +18294,6 @@ function mountAssistantApp(detail, actions, options = {}) {
18435
18294
  displayMode
18436
18295
  );
18437
18296
  let destroyPromise;
18438
- let transportClosePromise;
18439
- let renderTimer;
18440
- const closeTransport = () => {
18441
- if (transportClosePromise) return transportClosePromise;
18442
- const closing = mountedTransport?.close().catch(() => {
18443
- }) ?? Promise.resolve();
18444
- transportClosePromise = closing;
18445
- return closing;
18446
- };
18447
18297
  const applyDisplayMode = (requestedMode) => {
18448
18298
  displayMode = requestedMode === "fullscreen" && options.allowFullscreen === true ? "fullscreen" : "inline";
18449
18299
  card.toggleAttribute("data-fullscreen", displayMode === "fullscreen");
@@ -18461,26 +18311,18 @@ function mountAssistantApp(detail, actions, options = {}) {
18461
18311
  applyDisplayMode("inline");
18462
18312
  frame.focus();
18463
18313
  });
18464
- const showRenderFailure = (code) => {
18465
- if (code === "view_render_timeout" && initialized || destroyed || renderFailed || !frame.isConnected)
18466
- return;
18467
- renderFailed = true;
18468
- clearTimeout(renderTimer);
18314
+ const renderTimer = setTimeout(() => {
18315
+ if (initialized || !frame.isConnected) return;
18469
18316
  const note = document.createElement("div");
18470
18317
  note.className = "noodle-app-fallback";
18471
18318
  note.textContent = "This app view could not be displayed.";
18472
18319
  frame.replaceWith(note);
18473
- options.onRenderFailure?.({ code, retryable: false, stage: renderStage });
18474
- void closeTransport();
18475
- };
18476
- renderTimer = setTimeout(() => {
18477
- showRenderFailure("view_render_timeout");
18320
+ options.onRenderFailure?.({ code: "view_render_timeout", retryable: false });
18478
18321
  }, APP_RENDER_TIMEOUT_MS);
18479
18322
  frame.addEventListener(
18480
18323
  "load",
18481
18324
  () => {
18482
- if (destroyed || renderFailed) return;
18483
- renderStage = "sandbox_proxy";
18325
+ if (destroyed) return;
18484
18326
  const target = frame.contentWindow;
18485
18327
  if (!target) return;
18486
18328
  const bridge = new DX(
@@ -18491,11 +18333,7 @@ function mountAssistantApp(detail, actions, options = {}) {
18491
18333
  serverTools: {},
18492
18334
  serverResources: {},
18493
18335
  updateModelContext: { text: {}, structuredContent: {} },
18494
- message: { text: {} },
18495
- sandbox: {
18496
- ...appliedPolicy.csp === void 0 ? {} : { csp: appliedPolicy.csp },
18497
- ...appliedPolicy.permissions === void 0 ? {} : { permissions: appliedPolicy.permissions }
18498
- }
18336
+ message: { text: {} }
18499
18337
  },
18500
18338
  {
18501
18339
  hostContext
@@ -18503,15 +18341,13 @@ function mountAssistantApp(detail, actions, options = {}) {
18503
18341
  );
18504
18342
  mountedBridge = bridge;
18505
18343
  bridge.onsandboxready = () => {
18506
- if (destroyed || renderFailed) return;
18507
- renderStage = "app_initialize";
18344
+ if (destroyed) return;
18508
18345
  const current = options.getDetail?.() ?? detail;
18509
18346
  void bridge.sendSandboxResourceReady({
18510
18347
  html: current.html,
18511
- sandbox: sandboxUrl ? STANDARD_SANDBOX : "allow-scripts",
18512
- ...appliedPolicy.csp === void 0 ? {} : { csp: appliedPolicy.csp },
18513
- ...appliedPolicy.permissions === void 0 ? {} : { permissions: appliedPolicy.permissions }
18514
- }).catch(() => showRenderFailure("view_bridge_error"));
18348
+ sandbox: "allow-scripts",
18349
+ ...current.resourceMeta ?? {}
18350
+ });
18515
18351
  };
18516
18352
  bridge.onsizechange = ({ height }) => {
18517
18353
  if (destroyed) return;
@@ -18575,30 +18411,21 @@ function mountAssistantApp(detail, actions, options = {}) {
18575
18411
  return await actions.client.requestApp("resources/read", params);
18576
18412
  };
18577
18413
  bridge.oninitialized = () => {
18578
- if (destroyed || renderFailed) return;
18579
- try {
18580
- const current = options.getDetail?.() ?? detail;
18581
- const copiedArguments = copyAppJson(current.arguments ?? {});
18582
- const copiedResult = copyAppJson(current.result);
18583
- const argumentsRecord = typeof copiedArguments === "object" && copiedArguments !== null && !Array.isArray(copiedArguments) ? copiedArguments : {};
18584
- initialized = true;
18585
- clearTimeout(renderTimer);
18586
- bridge.setHostContext(hostContext);
18587
- void Promise.all([
18588
- bridge.sendToolInput({ arguments: argumentsRecord }),
18589
- bridge.sendToolResult({
18590
- content: [{ type: "text", text: JSON.stringify(copiedResult) }],
18591
- ...typeof copiedResult === "object" && copiedResult !== null && !Array.isArray(copiedResult) ? { structuredContent: copiedResult } : {}
18592
- })
18593
- ]).catch(() => showRenderFailure("view_bridge_error"));
18594
- } catch {
18595
- showRenderFailure("view_bridge_error");
18596
- }
18414
+ if (destroyed) return;
18415
+ initialized = true;
18416
+ clearTimeout(renderTimer);
18417
+ bridge.setHostContext(hostContext);
18418
+ const current = options.getDetail?.() ?? detail;
18419
+ void bridge.sendToolInput({ arguments: current.arguments ?? {} });
18420
+ void bridge.sendToolResult({
18421
+ content: [{ type: "text", text: JSON.stringify(current.result) }],
18422
+ ...typeof current.result === "object" && current.result !== null && !Array.isArray(current.result) ? { structuredContent: current.result } : {}
18423
+ });
18597
18424
  };
18598
18425
  const transport = new E(target, target);
18599
18426
  mountedTransport = transport;
18600
18427
  void bridge.connect(transport).catch(() => {
18601
- showRenderFailure("view_bridge_error");
18428
+ void destroy();
18602
18429
  });
18603
18430
  },
18604
18431
  { once: true }
@@ -18624,7 +18451,8 @@ function mountAssistantApp(detail, actions, options = {}) {
18624
18451
  }
18625
18452
  } catch {
18626
18453
  }
18627
- await closeTransport();
18454
+ await mountedTransport?.close().catch(() => {
18455
+ });
18628
18456
  card.remove();
18629
18457
  mountedApps.delete(card);
18630
18458
  })();