sunpeak 0.20.22 → 0.20.28

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 (66) hide show
  1. package/README.md +29 -4
  2. package/bin/commands/inspect.mjs +18 -0
  3. package/dist/chatgpt/index.cjs +1 -1
  4. package/dist/chatgpt/index.js +1 -1
  5. package/dist/claude/index.cjs +1 -1
  6. package/dist/claude/index.js +1 -1
  7. package/dist/embed.css +2 -0
  8. package/dist/host/chatgpt/index.cjs +1 -1
  9. package/dist/host/chatgpt/index.js +1 -1
  10. package/dist/index.cjs +3 -3
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.js +3 -3
  13. package/dist/index.js.map +1 -1
  14. package/dist/inspector/app-flatten.d.ts +7 -0
  15. package/dist/inspector/app-types.d.ts +83 -0
  16. package/dist/inspector/iframe-resource.d.ts +19 -1
  17. package/dist/inspector/index.cjs +3 -1
  18. package/dist/inspector/index.cjs.map +1 -1
  19. package/dist/inspector/index.d.ts +2 -0
  20. package/dist/inspector/index.js +3 -2
  21. package/dist/inspector/index.js.map +1 -1
  22. package/dist/inspector/inline-helper-script.d.ts +22 -0
  23. package/dist/inspector/inspector.d.ts +14 -1
  24. package/dist/inspector/simple-sidebar.d.ts +13 -1
  25. package/dist/inspector/use-inspector-state.d.ts +1 -0
  26. package/dist/{inspector-DtEighD9.cjs → inspector-DSX76Z-W.cjs} +340 -28
  27. package/dist/inspector-DSX76Z-W.cjs.map +1 -0
  28. package/dist/{inspector-CJNvLoHo.js → inspector-tIphAHtK.js} +336 -30
  29. package/dist/inspector-tIphAHtK.js.map +1 -0
  30. package/dist/mcp/index.cjs +1 -1
  31. package/dist/mcp/index.cjs.map +1 -1
  32. package/dist/mcp/index.js +1 -1
  33. package/dist/mcp/index.js.map +1 -1
  34. package/dist/sandbox-proxy.html +173 -0
  35. package/dist/style.css +9 -1
  36. package/dist/types/simulation.d.ts +3 -8
  37. package/dist/{use-app-cSBm5Pjl.js → use-app-C2pGHlnF.js} +2 -2
  38. package/dist/{use-app-cSBm5Pjl.js.map → use-app-C2pGHlnF.js.map} +1 -1
  39. package/dist/{use-app-CxtSfkSF.cjs → use-app-DIWh7-3f.cjs} +2 -2
  40. package/dist/{use-app-CxtSfkSF.cjs.map → use-app-DIWh7-3f.cjs.map} +1 -1
  41. package/package.json +14 -11
  42. package/template/dist/albums/albums.html +1 -1
  43. package/template/dist/albums/albums.json +1 -1
  44. package/template/dist/carousel/carousel.html +1 -1
  45. package/template/dist/carousel/carousel.json +1 -1
  46. package/template/dist/map/map.html +1 -1
  47. package/template/dist/map/map.json +1 -1
  48. package/template/dist/review/review.html +1 -1
  49. package/template/dist/review/review.json +1 -1
  50. package/template/node_modules/.bin/playwright +2 -2
  51. package/template/node_modules/.bin/vite +2 -2
  52. package/template/node_modules/.bin/vitest +2 -2
  53. package/template/node_modules/.vite/deps/_metadata.json +4 -4
  54. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js +1 -1
  55. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js.map +1 -1
  56. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js +1 -1
  57. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js.map +1 -1
  58. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js +1 -1
  59. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js.map +1 -1
  60. package/template/node_modules/.vite-mcp/deps/_metadata.json +24 -24
  61. package/template/node_modules/.vite-mcp/deps/vitest.js +8 -8
  62. package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -1
  63. package/template/package.json +4 -4
  64. package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-claude-linux.png +0 -0
  65. package/dist/inspector-CJNvLoHo.js.map +0 -1
  66. package/dist/inspector-DtEighD9.cjs.map +0 -1
@@ -4224,7 +4224,7 @@ registerHostShell({
4224
4224
  }`
4225
4225
  });
4226
4226
  //#endregion
4227
- //#region ../../node_modules/.pnpm/@modelcontextprotocol+ext-apps@1.7.1_@modelcontextprotocol+sdk@1.29.0_zod@4.4.3__react-_3f5c1a6a4033ae62a19a2bb834f0e050/node_modules/@modelcontextprotocol/ext-apps/dist/src/app-bridge.js
4227
+ //#region ../../node_modules/.pnpm/@modelcontextprotocol+ext-apps@1.7.2_@modelcontextprotocol+sdk@1.29.0_zod@4.4.3__react-_f5b843da9146ebea748e10ad8dfce46a/node_modules/@modelcontextprotocol/ext-apps/dist/src/app-bridge.js
4228
4228
  ((X) => typeof require < "u" ? require : typeof Proxy < "u" ? new Proxy(X, { get: (Y, Z) => (typeof require < "u" ? require : Y)[Z] }) : X)(function(X) {
4229
4229
  if (typeof require < "u") return require.apply(this, arguments);
4230
4230
  throw Error("Dynamic require of \"" + X + "\" is not supported");
@@ -5546,6 +5546,145 @@ iframe { border: none; width: 100%; height: 100%; display: block; }
5546
5546
  </body>
5547
5547
  </html>`;
5548
5548
  }
5549
+ var SUNPEAK_INLINE_HELPER_SCRIPT = `
5550
+ (function() {
5551
+ if (window.sunpeak) return; // already installed (e.g. parent hot-reload)
5552
+ // Opt-out: resource HTML using the real MCP Apps SDK can suppress the
5553
+ // helper to avoid sending two ui/initialize requests. Manual lowercase
5554
+ // comparison so content="off" / "OFF" / "Off" all match (the Selectors
5555
+ // L4 case-insensitive flag isn't supported in all test environments).
5556
+ try {
5557
+ var metas = document.querySelectorAll('meta[name="sunpeak-helper"]');
5558
+ for (var mi = 0; mi < metas.length; mi++) {
5559
+ var contentAttr = metas[mi].getAttribute('content');
5560
+ if (contentAttr && contentAttr.toLowerCase() === 'off') return;
5561
+ }
5562
+ } catch (e) { /* document not ready or querySelector missing */ }
5563
+
5564
+ var listeners = {
5565
+ toolInput: [],
5566
+ toolInputPartial: [],
5567
+ toolResult: [],
5568
+ toolCancelled: [],
5569
+ hostContext: []
5570
+ };
5571
+ var last = {
5572
+ toolInput: undefined,
5573
+ toolInputPartial: undefined,
5574
+ toolResult: undefined,
5575
+ toolCancelled: undefined,
5576
+ hostContext: undefined
5577
+ };
5578
+ function dispatch(channel, value) {
5579
+ last[channel] = value;
5580
+ // Iterate over a SNAPSHOT — callbacks that unsubscribe themselves
5581
+ // mutate the live list, which would skip subsequent callbacks under a
5582
+ // for-by-index loop on the original array.
5583
+ var list = listeners[channel].slice();
5584
+ for (var i = 0; i < list.length; i++) {
5585
+ try { list[i](value); } catch (e) { console.error('[sunpeak] callback error:', e); }
5586
+ }
5587
+ }
5588
+ // Map channel names to the public method name for error messages.
5589
+ var channelMethodName = {
5590
+ toolInput: 'onToolInput',
5591
+ toolInputPartial: 'onToolInputPartial',
5592
+ toolResult: 'onToolResult',
5593
+ toolCancelled: 'onToolCancelled',
5594
+ hostContext: 'onHostContextChange'
5595
+ };
5596
+ function subscribe(channel) {
5597
+ return function(cb) {
5598
+ if (typeof cb !== 'function') {
5599
+ throw new TypeError('window.sunpeak.' + channelMethodName[channel] + ' expects a function');
5600
+ }
5601
+ listeners[channel].push(cb);
5602
+ if (last[channel] !== undefined) {
5603
+ try { cb(last[channel]); } catch (e) { console.error('[sunpeak] callback error:', e); }
5604
+ }
5605
+ return function unsubscribe() {
5606
+ var idx = listeners[channel].indexOf(cb);
5607
+ if (idx >= 0) listeners[channel].splice(idx, 1);
5608
+ };
5609
+ };
5610
+ }
5611
+
5612
+ window.sunpeak = {
5613
+ onToolInput: subscribe('toolInput'),
5614
+ onToolInputPartial: subscribe('toolInputPartial'),
5615
+ onToolResult: subscribe('toolResult'),
5616
+ onToolCancelled: subscribe('toolCancelled'),
5617
+ onHostContextChange: subscribe('hostContext')
5618
+ };
5619
+
5620
+ var nextId = 1;
5621
+ var pending = {};
5622
+
5623
+ function sendRequest(method, params) {
5624
+ var id = nextId++;
5625
+ return new Promise(function(resolve, reject) {
5626
+ pending[id] = { resolve: resolve, reject: reject };
5627
+ try {
5628
+ window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, '*');
5629
+ } catch (e) {
5630
+ delete pending[id];
5631
+ reject(e);
5632
+ }
5633
+ });
5634
+ }
5635
+ function sendNotification(method, params) {
5636
+ try {
5637
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params || {} }, '*');
5638
+ } catch (e) { /* parent detached */ }
5639
+ }
5640
+
5641
+ window.addEventListener('message', function(ev) {
5642
+ // Only trust messages from the actual parent (the sandbox proxy).
5643
+ // Without this guard, a sibling iframe in the same browsing context or
5644
+ // a browser extension content script could forge JSON-RPC notifications
5645
+ // and drive the embedder's onToolResult/onToolInput callbacks with
5646
+ // attacker-controlled data.
5647
+ if (ev.source !== window.parent) return;
5648
+ var msg = ev.data;
5649
+ if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;
5650
+ if (typeof msg.id !== 'undefined' && pending[msg.id]) {
5651
+ var p = pending[msg.id];
5652
+ delete pending[msg.id];
5653
+ if (msg.error) p.reject(new Error((msg.error && msg.error.message) || 'request error'));
5654
+ else p.resolve(msg.result);
5655
+ return;
5656
+ }
5657
+ if (msg.method === 'ui/notifications/tool-input') {
5658
+ dispatch('toolInput', (msg.params && msg.params.arguments) || {});
5659
+ } else if (msg.method === 'ui/notifications/tool-input-partial') {
5660
+ dispatch('toolInputPartial', (msg.params && msg.params.arguments) || {});
5661
+ } else if (msg.method === 'ui/notifications/tool-result') {
5662
+ // tool-result params are the CallToolResult directly per the spec.
5663
+ dispatch('toolResult', msg.params || {});
5664
+ } else if (msg.method === 'ui/notifications/tool-cancelled') {
5665
+ // Clear cached input / partial / result so a late onToolResult
5666
+ // subscriber doesn't get the cancelled-then-stale value replayed.
5667
+ // hostContext stays — the environment hasn't changed.
5668
+ last.toolInput = undefined;
5669
+ last.toolInputPartial = undefined;
5670
+ last.toolResult = undefined;
5671
+ dispatch('toolCancelled', msg.params || {});
5672
+ } else if (msg.method === 'ui/notifications/host-context-changed') {
5673
+ dispatch('hostContext', msg.params || {});
5674
+ }
5675
+ });
5676
+
5677
+ sendRequest('ui/initialize', {
5678
+ appInfo: { name: 'sunpeak-inline-helper', version: '1.0.0' },
5679
+ appCapabilities: {},
5680
+ protocolVersion: '2026-01-26'
5681
+ }).then(function() {
5682
+ sendNotification('ui/notifications/initialized');
5683
+ }).catch(function(err) {
5684
+ console.warn('[sunpeak] ui/initialize failed:', err && err.message);
5685
+ });
5686
+ })();
5687
+ `.trim();
5549
5688
  //#endregion
5550
5689
  //#region src/inspector/iframe-resource.tsx
5551
5690
  /**
@@ -5684,6 +5823,23 @@ requestAnimationFrame(function(){
5684
5823
  e.source.postMessage({jsonrpc:"2.0",method:"sunpeak/fence-ack",params:{fenceId:fid}},"*");
5685
5824
  });}});`;
5686
5825
  /**
5826
+ * Inject the paint-fence responder and (optionally) a platform runtime script
5827
+ * into a user-provided HTML document. Used for the `html` prop mode where the
5828
+ * embedder hands us a complete document from `mcpClient.readResource(...)` and
5829
+ * we need to splice in the same infrastructure that the `scriptSrc` wrapper
5830
+ * provides. The injection is placed before `</head>` when present, falling
5831
+ * back to the start of `<body>` or the document start.
5832
+ */
5833
+ function injectInfraScripts(html, platformScript) {
5834
+ const fenceTag = `<script data-sunpeak-fence>${PAINT_FENCE_SCRIPT}<\/script>`;
5835
+ const injection = `${platformScript ? `<script>${platformScript}<\/script>` : ""}${fenceTag}${`<script data-sunpeak-helper>${SUNPEAK_INLINE_HELPER_SCRIPT}<\/script>`}`;
5836
+ const headMatch = html.match(/<\/head\s*>/i);
5837
+ if (headMatch) return html.replace(headMatch[0], `${injection}${headMatch[0]}`);
5838
+ const bodyMatch = html.match(/<body([^>]*)>/i);
5839
+ if (bodyMatch) return html.replace(bodyMatch[0], `${bodyMatch[0]}${injection}`);
5840
+ return injection + html;
5841
+ }
5842
+ /**
5687
5843
  * Generates HTML wrapper for a script URL.
5688
5844
  * The MCP Apps SDK in the loaded script handles communication via PostMessageTransport.
5689
5845
  */
@@ -5747,7 +5903,7 @@ function buildIframeAllow(permissions) {
5747
5903
  * connects via PostMessageTransport to window.parent. The parent side uses
5748
5904
  * McpAppHost (wrapping AppBridge) to communicate.
5749
5905
  */
5750
- function IframeResource({ src, scriptSrc, hostContext, toolInput, toolInputPartial, toolResult, hostOptions, csp, permissions, prefersBorder, className, style, onDisplayModeReady, debugInjectState, injectOpenAIRuntime, sandboxUrl }) {
5906
+ function IframeResource({ src, scriptSrc, html, hostContext, toolInput, toolInputPartial, toolResult, hostOptions, csp, permissions, prefersBorder, className, style, onDisplayModeReady, debugInjectState, injectOpenAIRuntime, sandboxUrl }) {
5751
5907
  const iframeRef = (0, react.useRef)(null);
5752
5908
  const hostRef = (0, react.useRef)(null);
5753
5909
  const resourceUrl = src ?? scriptSrc;
@@ -5755,6 +5911,8 @@ function IframeResource({ src, scriptSrc, hostContext, toolInput, toolInputParti
5755
5911
  srcRef.current = src;
5756
5912
  const scriptSrcRef = (0, react.useRef)(scriptSrc);
5757
5913
  scriptSrcRef.current = scriptSrc;
5914
+ const htmlRef = (0, react.useRef)(html);
5915
+ htmlRef.current = html;
5758
5916
  const cspRef = (0, react.useRef)(csp);
5759
5917
  cspRef.current = csp;
5760
5918
  const hostContextRef = (0, react.useRef)(hostContext);
@@ -5804,9 +5962,20 @@ function IframeResource({ src, scriptSrc, hostContext, toolInput, toolInputParti
5804
5962
  onSandboxReady: () => {
5805
5963
  const currentSrc = srcRef.current;
5806
5964
  const currentScriptSrc = scriptSrcRef.current;
5965
+ const currentHtml = htmlRef.current;
5807
5966
  const currentHost = hostRef.current;
5808
5967
  if (!currentHost) return;
5809
- if (currentScriptSrc) {
5968
+ if (currentHtml) {
5969
+ const finalHtml = injectInfraScripts(currentHtml, injectOpenAIRuntimeRef.current ? MOCK_OPENAI_RUNTIME_SCRIPT : void 0);
5970
+ currentHost.sendSandboxResourceReady({
5971
+ html: finalHtml,
5972
+ sandbox: "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
5973
+ });
5974
+ if (iframeRef.current) {
5975
+ iframeRef.current.style.opacity = "1";
5976
+ iframeRef.current.style.transition = "opacity 100ms";
5977
+ }
5978
+ } else if (currentScriptSrc) {
5810
5979
  const absoluteScriptSrc = currentScriptSrc.startsWith("/") ? `${window.location.origin}${currentScriptSrc}` : currentScriptSrc;
5811
5980
  const cspPolicy = generateCSP(cspRef.current, absoluteScriptSrc);
5812
5981
  const appHtml = generateScriptHtml(absoluteScriptSrc, hostContextRef.current?.theme ?? "dark", cspPolicy, injectOpenAIRuntimeRef.current ? MOCK_OPENAI_RUNTIME_SCRIPT : void 0);
@@ -5877,7 +6046,17 @@ function IframeResource({ src, scriptSrc, hostContext, toolInput, toolInputParti
5877
6046
  const borderStyle = prefersBorder ? { border: "1px solid var(--color-border-primary, #e5e7eb)" } : { border: "none" };
5878
6047
  const sandboxSrc = (0, react.useMemo)(() => {
5879
6048
  if (!sandboxUrl) return void 0;
5880
- const url = new URL("/proxy", sandboxUrl);
6049
+ let url;
6050
+ try {
6051
+ const parsed = new URL(sandboxUrl);
6052
+ url = parsed.pathname && parsed.pathname !== "/" ? parsed : new URL("/proxy", sandboxUrl);
6053
+ } catch {
6054
+ return;
6055
+ }
6056
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
6057
+ console.warn("[IframeResource] Ignoring non-http(s) sandboxUrl:", sandboxUrl);
6058
+ return;
6059
+ }
5881
6060
  if (injectOpenAIRuntime) url.searchParams.set("platform", "chatgpt");
5882
6061
  return url.toString();
5883
6062
  }, [sandboxUrl, injectOpenAIRuntime]);
@@ -6317,6 +6496,7 @@ function useInspectorState({ simulations, defaultHost = "chatgpt" }) {
6317
6496
  }, [toolResult, modelContext]);
6318
6497
  const resourceUrl = selectedSim?.resourceUrl;
6319
6498
  const resourceScript = selectedSim?.resourceScript;
6499
+ const resourceHtml = selectedSim?.resourceHtml;
6320
6500
  const csp = selectedSim?.resource ? extractResourceCSP(selectedSim.resource) : void 0;
6321
6501
  const resourceMeta = (selectedSim?.resource?._meta)?.ui;
6322
6502
  return {
@@ -6383,6 +6563,7 @@ function useInspectorState({ simulations, defaultHost = "chatgpt" }) {
6383
6563
  handleUpdateModelContext,
6384
6564
  resourceUrl,
6385
6565
  resourceScript,
6566
+ resourceHtml,
6386
6567
  csp,
6387
6568
  permissions: resourceMeta?.permissions,
6388
6569
  prefersBorder: resourceMeta?.prefersBorder ?? false,
@@ -6485,7 +6666,14 @@ function useMcpConnection(initialServerUrl) {
6485
6666
  //#endregion
6486
6667
  //#region src/inspector/theme-provider.tsx
6487
6668
  var ThemeProviderContext = react.createContext(void 0);
6488
- /** Default theme applier: sets data-theme attribute on document.documentElement */
6669
+ /**
6670
+ * Default theme applier: sets `data-theme` on `document.documentElement`.
6671
+ * Kept for callers using `ThemeProvider` outside the bundled `<Inspector />`
6672
+ * (e.g. custom-inspector builds composed from this package's primitives).
6673
+ * The bundled Inspector overrides this with a no-op applier and applies
6674
+ * theme to its own root element, so embedding it inside a host React app
6675
+ * leaves the host's document attributes untouched.
6676
+ */
6489
6677
  function defaultApplyTheme(theme) {
6490
6678
  if (typeof document !== "undefined") document.documentElement.setAttribute("data-theme", theme);
6491
6679
  }
@@ -6528,7 +6716,7 @@ function ChevronRightIcon() {
6528
6716
  })
6529
6717
  });
6530
6718
  }
6531
- function SimpleSidebar({ children, controls, headerRight }) {
6719
+ function SimpleSidebar({ children, controls, headerRight, rootRef, fillParent = false }) {
6532
6720
  const [isDrawerOpen, setIsDrawerOpen] = react.useState(false);
6533
6721
  const [sidebarWidth, setSidebarWidth] = react.useState(DEFAULT_SIDEBAR_WIDTH);
6534
6722
  const [isResizing, setIsResizing] = react.useState(false);
@@ -6553,7 +6741,8 @@ function SimpleSidebar({ children, controls, headerRight }) {
6553
6741
  };
6554
6742
  }, [isResizing]);
6555
6743
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6556
- className: "sunpeak-inspector-root flex h-screen w-full overflow-hidden relative",
6744
+ ref: rootRef,
6745
+ className: `sunpeak-inspector-root flex ${fillParent ? "h-full w-full" : "h-screen w-full"} overflow-hidden relative`,
6557
6746
  children: [
6558
6747
  isResizing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "fixed inset-0 z-50 cursor-col-resize" }),
6559
6748
  isDrawerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -6883,13 +7072,85 @@ function resolveServerToolResult(mock, args) {
6883
7072
  for (const entry of mock) if (Object.entries(entry.when).every(([key, value]) => args != null && args[key] === value)) return entry.result;
6884
7073
  }
6885
7074
  //#endregion
7075
+ //#region src/inspector/app-flatten.ts
7076
+ /** Pull the output-template URI off a tool's _meta, if present. */
7077
+ function getOutputTemplate(toolMeta) {
7078
+ if (!toolMeta || typeof toolMeta !== "object") return void 0;
7079
+ const openai = toolMeta.openai;
7080
+ if (!openai || typeof openai !== "object") return void 0;
7081
+ const template = openai.outputTemplate;
7082
+ return typeof template === "string" ? template : void 0;
7083
+ }
7084
+ /** Build an MCP-shaped `Resource` from the embedder's input. Used purely for
7085
+ * sidebar metadata (CSP, permissions, prefersBorder); the actual HTML render
7086
+ * goes through `resourceHtml` on the resulting Simulation. */
7087
+ function toMcpResource(r) {
7088
+ return {
7089
+ uri: r.uri,
7090
+ mimeType: r.mimeType ?? "text/html",
7091
+ name: r.uri,
7092
+ ...r._meta ? { _meta: r._meta } : {}
7093
+ };
7094
+ }
7095
+ /**
7096
+ * Flatten an `InspectorApp` to the `Record<string, Simulation>` shape the
7097
+ * Inspector consumes internally. Returns an empty map if `app` is missing.
7098
+ */
7099
+ function flattenAppToSimulations(app) {
7100
+ if (!app) return {};
7101
+ const result = {};
7102
+ const resourcesByUri = /* @__PURE__ */ new Map();
7103
+ for (const r of app.resources) {
7104
+ if (resourcesByUri.has(r.uri)) console.warn(`[Inspector] Duplicate resource URI '${r.uri}' in app.resources — the second entry replaces the first.`);
7105
+ resourcesByUri.set(r.uri, r);
7106
+ }
7107
+ for (const appTool of app.tools) {
7108
+ const uri = getOutputTemplate(appTool.tool._meta);
7109
+ if (!uri) continue;
7110
+ const resource = resourcesByUri.get(uri);
7111
+ if (!resource) continue;
7112
+ const mcpResource = toMcpResource(resource);
7113
+ const sims = appTool.simulations && appTool.simulations.length > 0 ? appTool.simulations : [{ name: appTool.tool.name }];
7114
+ for (const sim of sims) {
7115
+ const key = `${appTool.tool.name}__${sim.name}`;
7116
+ if (key in result) console.warn(`[Inspector] Duplicate simulation name '${sim.name}' under tool '${appTool.tool.name}' — the second entry replaces the first.`);
7117
+ result[key] = {
7118
+ name: key,
7119
+ displayName: sim.name,
7120
+ resourceHtml: resource.html,
7121
+ userMessage: sim.userMessage,
7122
+ tool: appTool.tool,
7123
+ resource: mcpResource,
7124
+ toolInput: sim.toolInput,
7125
+ toolResult: sim.toolResult,
7126
+ serverTools: sim.serverTools
7127
+ };
7128
+ }
7129
+ }
7130
+ return result;
7131
+ }
7132
+ //#endregion
6886
7133
  //#region src/inspector/inspector.tsx
6887
7134
  var DOCS_BASE_URL = "https://sunpeak.ai/docs";
6888
7135
  /** Check whether a simulation has user-authored fixture data. */
6889
7136
  function hasFixtureData(sim) {
6890
7137
  return sim.toolResult != null || sim.toolInput != null || sim.serverTools != null;
6891
7138
  }
6892
- function Inspector({ children, simulations: initialSimulations = {}, appName = "Sunpeak", appIcon, defaultHost = "chatgpt", onCallTool, onCallToolDirect, defaultProdResources = false, hideInspectorModes = false, demoMode = false, sandboxUrl, mcpServerUrl }) {
7139
+ var EMPTY_SIMULATIONS = Object.freeze({});
7140
+ function Inspector({ children, app, simulations: initialSimulationsProp = EMPTY_SIMULATIONS, appName: appNameProp, appIcon: appIconProp, defaultHost = "chatgpt", onCallTool, onCallToolDirect, defaultProdResources = false, hideInspectorModes = false, demoMode = false, sandboxUrl, mcpServerUrl }) {
7141
+ const initialSimulations = react.useMemo(() => app ? flattenAppToSimulations(app) : initialSimulationsProp, [app, initialSimulationsProp]);
7142
+ const appName = app?.name ?? appNameProp ?? "Sunpeak";
7143
+ const appIcon = app?.icon ?? appIconProp;
7144
+ const isEmbedded = !!app;
7145
+ const conflictWarnedRef = react.useRef(false);
7146
+ react.useEffect(() => {
7147
+ if (conflictWarnedRef.current) return;
7148
+ if (app && initialSimulationsProp && Object.keys(initialSimulationsProp).length > 0) {
7149
+ conflictWarnedRef.current = true;
7150
+ console.warn("[Inspector] Both `app` and `simulations` were provided. `app` takes precedence; `simulations` is ignored.");
7151
+ }
7152
+ }, [app, initialSimulationsProp]);
7153
+ const rootRef = react.useRef(null);
6893
7154
  const [simulations, setSimulations] = react.useState(initialSimulations);
6894
7155
  react.useEffect(() => {
6895
7156
  setSimulations(initialSimulations);
@@ -6972,7 +7233,7 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
6972
7233
  const [oauthClientSecret, setOauthClientSecret] = react.useState("");
6973
7234
  const [oauthStatus, setOauthStatus] = react.useState("none");
6974
7235
  const [oauthError, setOauthError] = react.useState();
6975
- const connection = useMcpConnection(mcpServerUrl || void 0);
7236
+ const connection = useMcpConnection(isEmbedded ? void 0 : mcpServerUrl || void 0);
6976
7237
  const [prodResources, setProdResources] = react.useState(state.urlProdResources ?? defaultProdResources);
6977
7238
  const showSidebar = state.urlSidebar !== false;
6978
7239
  const showDevOverlay = state.urlDevOverlay !== false;
@@ -7046,12 +7307,22 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7046
7307
  return;
7047
7308
  }
7048
7309
  if (data.status === "redirect" && data.authUrl) {
7310
+ let parsedAuthUrl = null;
7311
+ try {
7312
+ parsedAuthUrl = new URL(data.authUrl);
7313
+ } catch {}
7314
+ if (!parsedAuthUrl || parsedAuthUrl.protocol !== "http:" && parsedAuthUrl.protocol !== "https:") {
7315
+ popup?.close();
7316
+ setOauthError("OAuth authorization URL is not a valid http(s) URL.");
7317
+ setOauthStatus("error");
7318
+ return;
7319
+ }
7049
7320
  if (!popup || popup.closed) {
7050
7321
  setOauthError("Popup was blocked. Allow popups for this site and try again.");
7051
7322
  setOauthStatus("error");
7052
7323
  return;
7053
7324
  }
7054
- popup.location.href = data.authUrl;
7325
+ popup.location.href = parsedAuthUrl.toString();
7055
7326
  let checkClosed;
7056
7327
  let bc;
7057
7328
  const cleanup = () => {
@@ -7240,15 +7511,23 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7240
7511
  displayMode,
7241
7512
  setDisplayMode
7242
7513
  ]);
7243
- react.useLayoutEffect(() => {
7244
- const vars = activeShell?.styleVariables;
7245
- if (!vars) return;
7246
- const root = document.documentElement;
7247
- for (const [key, value] of Object.entries(vars)) if (value) root.style.setProperty(key, value);
7248
- }, [activeShell]);
7514
+ const prevStyleVarKeysRef = react.useRef([]);
7249
7515
  const prevPageStyleKeysRef = react.useRef([]);
7250
7516
  react.useLayoutEffect(() => {
7251
- const root = document.documentElement;
7517
+ const root = rootRef.current;
7518
+ if (!root) return;
7519
+ root.setAttribute("data-theme", state.theme);
7520
+ root.style.colorScheme = state.theme;
7521
+ for (const key of prevStyleVarKeysRef.current) root.style.removeProperty(key);
7522
+ const vars = activeShell?.styleVariables;
7523
+ if (vars) {
7524
+ const keys = [];
7525
+ for (const [key, value] of Object.entries(vars)) if (value) {
7526
+ root.style.setProperty(key, value);
7527
+ keys.push(key);
7528
+ }
7529
+ prevStyleVarKeysRef.current = keys;
7530
+ } else prevStyleVarKeysRef.current = [];
7252
7531
  for (const key of prevPageStyleKeysRef.current) root.style.removeProperty(key);
7253
7532
  const pageStyles = activeShell?.pageStyles;
7254
7533
  if (pageStyles) {
@@ -7259,7 +7538,7 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7259
7538
  }
7260
7539
  prevPageStyleKeysRef.current = keys;
7261
7540
  } else prevPageStyleKeysRef.current = [];
7262
- }, [activeShell]);
7541
+ }, [activeShell, state.theme]);
7263
7542
  react.useLayoutEffect(() => {
7264
7543
  const fontCss = activeShell?.fontCss;
7265
7544
  const id = "sunpeak-host-fonts";
@@ -7350,7 +7629,7 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7350
7629
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7351
7630
  className: "text-sm text-center max-w-xs",
7352
7631
  style: { color: "var(--color-text-secondary)" },
7353
- children: isError ? "Could not connect to MCP server" : isConnected ? "No tools with UI resources found on this server" : serverUrl ? "Connecting…" : "Enter an MCP server URL to get started"
7632
+ children: isEmbedded ? "No tools with UI resources in this app" : isError ? "Could not connect to MCP server" : isConnected ? "No tools with UI resources found on this server" : serverUrl ? "Connecting…" : "Enter an MCP server URL to get started"
7354
7633
  })
7355
7634
  });
7356
7635
  } else if (showEmptyState) content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -7375,6 +7654,30 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7375
7654
  children: "Building…"
7376
7655
  })
7377
7656
  });
7657
+ else if (state.resourceHtml) content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7658
+ className: "h-full w-full",
7659
+ style: { background: iframeBg },
7660
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IframeResource, {
7661
+ html: state.resourceHtml,
7662
+ hostContext,
7663
+ toolInput: state.toolInput,
7664
+ toolResult: state.effectiveToolResult,
7665
+ hostOptions: {
7666
+ hostInfo: activeShell?.hostInfo,
7667
+ hostCapabilities: activeShell?.hostCapabilities,
7668
+ onDisplayModeChange: state.handleDisplayModeChange,
7669
+ onUpdateModelContext: state.handleUpdateModelContext,
7670
+ onCallTool: handleCallTool
7671
+ },
7672
+ permissions: state.permissions,
7673
+ prefersBorder: state.prefersBorder,
7674
+ onDisplayModeReady: state.handleDisplayModeReady,
7675
+ debugInjectState: state.modelContext,
7676
+ injectOpenAIRuntime: state.activeHost === "chatgpt",
7677
+ sandboxUrl,
7678
+ className: "h-full w-full"
7679
+ }, `${state.activeHost}-${state.selectedSimulationName}-html`)
7680
+ });
7378
7681
  else if (effectiveResourceUrl) content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7379
7682
  className: "h-full w-full",
7380
7683
  style: { background: iframeBg },
@@ -7425,7 +7728,7 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7425
7728
  }, `${state.activeHost}-${state.selectedSimulationName}-${state.resourceScript}`)
7426
7729
  });
7427
7730
  else content = children;
7428
- const applyTheme = activeShell?.applyTheme;
7731
+ const applyTheme = react.useCallback((_theme) => {}, []);
7429
7732
  const runButton = !demoMode && onCallTool && currentSim && activeSimulationName === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7430
7733
  type: "button",
7431
7734
  onClick: handleRun,
@@ -7465,11 +7768,13 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7465
7768
  headerAction: runButton,
7466
7769
  children: content
7467
7770
  }) : content;
7771
+ const rootSizing = isEmbedded ? "h-full w-full" : "h-screen w-screen";
7468
7772
  if (!showSidebar) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThemeProvider, {
7469
7773
  theme: state.theme,
7470
7774
  applyTheme,
7471
7775
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7472
- className: "flex h-screen w-screen",
7776
+ ref: rootRef,
7777
+ className: `sunpeak-inspector-root flex ${rootSizing}`,
7473
7778
  children: conversationContent
7474
7779
  })
7475
7780
  });
@@ -7477,10 +7782,12 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7477
7782
  theme: state.theme,
7478
7783
  applyTheme,
7479
7784
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SimpleSidebar, {
7785
+ rootRef,
7786
+ fillParent: isEmbedded,
7480
7787
  controls: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7481
7788
  className: "space-y-1",
7482
7789
  children: [
7483
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SidebarControl, {
7790
+ !isEmbedded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SidebarControl, {
7484
7791
  label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7485
7792
  className: "flex items-center gap-1.5",
7486
7793
  children: ["MCP Server", serverUrl && !demoMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -7499,8 +7806,7 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7499
7806
  placeholder: "http://localhost:8000/mcp",
7500
7807
  disabled: demoMode
7501
7808
  })
7502
- }),
7503
- !demoMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SidebarCollapsibleControl, {
7809
+ }), !demoMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SidebarCollapsibleControl, {
7504
7810
  label: "Authentication",
7505
7811
  defaultCollapsed: authType === "none",
7506
7812
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -7585,8 +7891,8 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7585
7891
  })
7586
7892
  ]
7587
7893
  })
7588
- }, `auth-${authType === "none" ? "none" : "active"}`),
7589
- !hideInspectorModes && !demoMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SidebarCheckbox, {
7894
+ }, `auth-${authType === "none" ? "none" : "active"}`)] }),
7895
+ !hideInspectorModes && !demoMode && !isEmbedded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SidebarCheckbox, {
7590
7896
  checked: prodResources,
7591
7897
  onChange: setProdResources,
7592
7898
  label: "Prod Resources",
@@ -7637,7 +7943,7 @@ function Inspector({ children, simulations: initialSimulations = {}, appName = "
7637
7943
  label: selectedToolInfo && selectedToolInfo.fixtureSimNames.length > 0 ? "None (call server)" : "None"
7638
7944
  }], ...(selectedToolInfo?.fixtureSimNames ?? []).map((simName) => ({
7639
7945
  value: simName,
7640
- label: simName
7946
+ label: simulations[simName]?.displayName ?? simName
7641
7947
  }))]
7642
7948
  })
7643
7949
  })]
@@ -8113,6 +8419,12 @@ Object.defineProperty(exports, "extractResourceCSP", {
8113
8419
  return extractResourceCSP;
8114
8420
  }
8115
8421
  });
8422
+ Object.defineProperty(exports, "flattenAppToSimulations", {
8423
+ enumerable: true,
8424
+ get: function() {
8425
+ return flattenAppToSimulations;
8426
+ }
8427
+ });
8116
8428
  Object.defineProperty(exports, "getHostShell", {
8117
8429
  enumerable: true,
8118
8430
  get: function() {
@@ -8156,4 +8468,4 @@ Object.defineProperty(exports, "useThemeContext", {
8156
8468
  }
8157
8469
  });
8158
8470
 
8159
- //# sourceMappingURL=inspector-DtEighD9.cjs.map
8471
+ //# sourceMappingURL=inspector-DSX76Z-W.cjs.map