@frontmcp/uipack 1.8.6 → 1.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -518,12 +518,38 @@ function __applyHostTheme(theme) {
518
518
  } catch (e) {}
519
519
  }
520
520
 
521
+ function extractToolOutput(params) {
522
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
523
+ if (!params) return undefined;
524
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
525
+ var content = params.content;
526
+ if (Array.isArray(content)) {
527
+ for (var i = 0; i < content.length; i++) {
528
+ var item = content[i];
529
+ if (item && item.type === 'text' && typeof item.text === 'string') {
530
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
531
+ }
532
+ }
533
+ }
534
+ return content;
535
+ }
536
+
521
537
  function readInjectedData() {
522
538
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
523
539
  if (typeof window !== 'undefined') {
524
540
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
525
541
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
526
542
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
543
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
544
+ // (toolOutput is the tool's structuredContent).
545
+ var oa = window.openai;
546
+ if (oa && typeof oa === 'object') {
547
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
548
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
549
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
550
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
551
+ }
552
+ }
527
553
  }
528
554
  return data;
529
555
  }
@@ -572,7 +598,24 @@ var OpenAIAdapter = {
572
598
  if (sdk.displayMode) {
573
599
  context.hostContext.displayMode = sdk.displayMode;
574
600
  }
575
- // Note: OpenAI SDK does not have an onContextChange equivalent
601
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
602
+ window.addEventListener('openai:set_globals', function(event) {
603
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
604
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
605
+ context.toolInput = globals.toolInput;
606
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
607
+ }
608
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
609
+ context.structuredContent = globals.toolOutput;
610
+ context.notifyToolResult(globals.toolOutput);
611
+ }
612
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
613
+ context.notifyContextChange({ theme: globals.theme });
614
+ }
615
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
616
+ context.notifyContextChange({ displayMode: globals.displayMode });
617
+ }
618
+ });
576
619
  return Promise.resolve();
577
620
  },
578
621
  callTool: function(context, name, args) {
@@ -719,9 +762,10 @@ var ExtAppsAdapter = {
719
762
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
720
763
  break;
721
764
  case 'ui/notifications/tool-result':
722
- context.toolOutput = params.content;
765
+ var resultOutput = extractToolOutput(params);
766
+ context.toolOutput = resultOutput;
723
767
  context.structuredContent = params.structuredContent;
724
- context.notifyToolResult(params.content);
768
+ context.notifyToolResult(resultOutput);
725
769
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
726
770
  break;
727
771
  case 'ui/notifications/host-context-changed':
@@ -775,6 +819,15 @@ var ExtAppsAdapter = {
775
819
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
776
820
  });
777
821
  },
822
+ sendNotification: function(method, params) {
823
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
824
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
825
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
826
+ }
827
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
828
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
829
+ return Promise.resolve();
830
+ },
778
831
  sendHandshake: function(context) {
779
832
  // Send ui/initialize using '*' as target origin since TOFU hasn't
780
833
  // been established yet. The response from the host will establish
@@ -840,7 +893,7 @@ var ExtAppsAdapter = {
840
893
  var pending = this.pendingSize;
841
894
  if (!pending) return;
842
895
  this.pendingSize = null;
843
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
896
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
844
897
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
845
898
  }, function(err) {
846
899
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -867,13 +920,11 @@ var ExtAppsAdapter = {
867
920
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
868
921
  },
869
922
  setSize: function(context, size) {
870
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
871
- // measured/desired widget dimensions to the host.
872
- var params = {
873
- height: size && size.height,
874
- width: size && size.width,
875
- aspectRatio: size && size.aspectRatio
876
- };
923
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
924
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
925
+ var params = {};
926
+ if (size && typeof size.width === 'number') params.width = size.width;
927
+ if (size && typeof size.height === 'number') params.height = size.height;
877
928
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
878
929
  // No host origin yet: keep only the latest size and send it when the
879
930
  // handshake settles; every caller's promise follows that one request.
@@ -884,7 +935,7 @@ var ExtAppsAdapter = {
884
935
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
885
936
  });
886
937
  }
887
- return this.sendRequest('ui/setSize', params);
938
+ return this.sendNotification('ui/notifications/size-changed', params);
888
939
  },
889
940
  requestClose: function(context) {
890
941
  return this.sendRequest('ui/close', {});
@@ -1307,7 +1358,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1307
1358
 
1308
1359
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1309
1360
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1310
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1361
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1311
1362
  FrontMcpBridge.prototype.setSize = function(size) {
1312
1363
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1313
1364
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1409,7 +1460,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1409
1460
  target.classList.add('opacity-50', 'cursor-not-allowed');
1410
1461
 
1411
1462
  // Add spinner for buttons
1412
- var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
1463
+ var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" style="width:1em;height:1em;vertical-align:-0.125em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
1413
1464
  if (target.tagName === 'BUTTON') {
1414
1465
  target.innerHTML = spinner + 'Loading...';
1415
1466
  }
@@ -1490,7 +1541,22 @@ function minifyJS(code) {
1490
1541
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1491
1542
  return code;
1492
1543
  }
1493
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1544
+ const out = [];
1545
+ let inBlockComment = false;
1546
+ for (const raw of code.split("\n")) {
1547
+ const line = raw.trim();
1548
+ if (inBlockComment) {
1549
+ if (line.includes("*/")) inBlockComment = false;
1550
+ continue;
1551
+ }
1552
+ if (line === "" || line.startsWith("//")) continue;
1553
+ if (line.startsWith("/*")) {
1554
+ if (!line.includes("*/")) inBlockComment = true;
1555
+ continue;
1556
+ }
1557
+ out.push(line);
1558
+ }
1559
+ return out.join("\n");
1494
1560
  }
1495
1561
  function generatePlatformBundle(platform, options = {}) {
1496
1562
  const platformAdapters = {
@@ -4222,6 +4288,86 @@ function wrapDetectedContent(value) {
4222
4288
  }
4223
4289
  }
4224
4290
 
4291
+ // libs/uipack/src/adapters/markdown.ts
4292
+ var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
4293
+ function emphasis(text) {
4294
+ return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
4295
+ }
4296
+ function inline(text) {
4297
+ const stash = [];
4298
+ const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
4299
+ let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
4300
+ /`([^`]+)`/g,
4301
+ (_m, code) => hold(`<code>${code}</code>`)
4302
+ );
4303
+ out = out.replace(
4304
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
4305
+ (_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
4306
+ );
4307
+ out = emphasis(out);
4308
+ for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
4309
+ return out;
4310
+ }
4311
+ function markdownToHtml(source) {
4312
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
4313
+ const blocks = [];
4314
+ let paragraph = [];
4315
+ let list;
4316
+ const flushParagraph = () => {
4317
+ if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
4318
+ paragraph = [];
4319
+ };
4320
+ const flushList = () => {
4321
+ if (list)
4322
+ blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
4323
+ list = void 0;
4324
+ };
4325
+ for (let i = 0; i < lines.length; i++) {
4326
+ const line = lines[i];
4327
+ const fence = /^\s*(`{3,})/.exec(line);
4328
+ if (fence) {
4329
+ flushParagraph();
4330
+ flushList();
4331
+ const fenceLength = fence[1].length;
4332
+ const isClosing = (l) => {
4333
+ const m = /^\s*(`{3,})\s*$/.exec(l);
4334
+ return m !== null && m[1].length >= fenceLength;
4335
+ };
4336
+ const code = [];
4337
+ for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
4338
+ blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
4339
+ </code></pre>`);
4340
+ continue;
4341
+ }
4342
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
4343
+ if (heading) {
4344
+ flushParagraph();
4345
+ flushList();
4346
+ blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
4347
+ continue;
4348
+ }
4349
+ const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
4350
+ if (item) {
4351
+ flushParagraph();
4352
+ const tag = /\d/.test(item[1]) ? "ol" : "ul";
4353
+ if (list && list.tag !== tag) flushList();
4354
+ list ??= { tag, items: [] };
4355
+ list.items.push(item[2]);
4356
+ continue;
4357
+ }
4358
+ if (line.trim() === "") {
4359
+ flushParagraph();
4360
+ flushList();
4361
+ continue;
4362
+ }
4363
+ flushList();
4364
+ paragraph.push(line.trim());
4365
+ }
4366
+ flushParagraph();
4367
+ flushList();
4368
+ return blocks.join("\n");
4369
+ }
4370
+
4225
4371
  // libs/uipack/src/adapters/type-detector.ts
4226
4372
  function detectUIType(template) {
4227
4373
  if (template === null || template === void 0) {
@@ -4255,9 +4401,15 @@ function detectUIType(template) {
4255
4401
  }
4256
4402
 
4257
4403
  // libs/uipack/src/adapters/template-renderer.ts
4258
- function buildCspConfig(resolver) {
4404
+ function buildCspConfig(resolver, extra) {
4259
4405
  const cspResourceDomains = ["https://esm.sh"];
4260
4406
  const cspConnectDomains = ["https://esm.sh"];
4407
+ for (const domain of extra?.resourceDomains ?? []) {
4408
+ if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
4409
+ }
4410
+ for (const domain of extra?.connectDomains ?? []) {
4411
+ if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
4412
+ }
4261
4413
  if (resolver && "overrides" in resolver) {
4262
4414
  const overrides = resolver.overrides;
4263
4415
  if (overrides) {
@@ -4297,6 +4449,14 @@ function renderUnwrappedResult(rawResult, options) {
4297
4449
  }
4298
4450
  return rawResult;
4299
4451
  }
4452
+ function isReactElement(value) {
4453
+ return typeof value === "object" && value !== null && "$$typeof" in value;
4454
+ }
4455
+ function isDefinitelyReactComponent(template) {
4456
+ if (typeof template !== "function") return false;
4457
+ const proto = template.prototype;
4458
+ return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
4459
+ }
4300
4460
  function renderToolTemplate(options) {
4301
4461
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
4302
4462
  const uiType = detectUIType(template);
@@ -4307,30 +4467,43 @@ function renderToolTemplate(options) {
4307
4467
  output,
4308
4468
  includeBridge: true,
4309
4469
  resolver,
4310
- sizing
4470
+ sizing,
4471
+ csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
4311
4472
  };
4312
4473
  let html2;
4313
4474
  let hash = "";
4314
4475
  let size = 0;
4315
4476
  if (typeof template === "object" && template !== null && "file" in template) {
4316
- const cspConfig = buildCspConfig(resolver);
4317
4477
  const result = renderComponent(
4318
4478
  { source: template, inlineReact: resourceMode === "inline" },
4319
- { ...shellConfig, csp: cspConfig }
4479
+ { ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
4320
4480
  );
4321
4481
  html2 = result.html;
4322
4482
  hash = result.hash;
4323
4483
  size = result.size;
4324
4484
  } else if (typeof template === "function") {
4325
- if (uiType === "react") {
4485
+ const helpers = createTemplateHelpers();
4486
+ const ctx = { input, output, helpers };
4487
+ let rawResult;
4488
+ let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
4489
+ if (isHtmlBuilder) {
4490
+ try {
4491
+ rawResult = template(ctx);
4492
+ isHtmlBuilder = !isReactElement(rawResult);
4493
+ } catch (error) {
4494
+ if (uiType !== "react") throw error;
4495
+ isHtmlBuilder = false;
4496
+ }
4497
+ }
4498
+ if (!isHtmlBuilder) {
4499
+ (options.logger ?? console).warn(
4500
+ `[FrontMCP] Tool "${toolName}": \`ui.template\` is a React component reference, which cannot be bundled (there is no source file to compile), so the widget would render an empty page. Use \`template: { file: './widget.tsx' }\` instead.`
4501
+ );
4326
4502
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
4327
4503
  html2 = shellResult.html;
4328
4504
  hash = shellResult.hash;
4329
4505
  size = shellResult.size;
4330
4506
  } else {
4331
- const helpers = createTemplateHelpers();
4332
- const ctx = { input, output, helpers };
4333
- const rawResult = template(ctx);
4334
4507
  const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
4335
4508
  if (wrapped) {
4336
4509
  html2 = wrapped;
@@ -4342,7 +4515,7 @@ function renderToolTemplate(options) {
4342
4515
  }
4343
4516
  }
4344
4517
  } else if (typeof template === "string") {
4345
- const shellResult = buildShell(template, shellConfig);
4518
+ const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
4346
4519
  html2 = shellResult.html;
4347
4520
  hash = shellResult.hash;
4348
4521
  size = shellResult.size;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/uipack",
3
- "version": "1.8.6",
3
+ "version": "1.8.7",
4
4
  "description": "FrontMCP UIpack - HTML shell builder, pluggable import resolver, and NPM component loader for MCP UI (React-free core)",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",
@@ -61,7 +61,7 @@
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@frontmcp/lazy-zod": "1.8.6",
64
+ "@frontmcp/lazy-zod": "1.8.7",
65
65
  "esbuild": ">=0.27.0 <1"
66
66
  },
67
67
  "peerDependenciesMeta": {
package/shell/index.js CHANGED
@@ -317,12 +317,38 @@ function __applyHostTheme(theme) {
317
317
  } catch (e) {}
318
318
  }
319
319
 
320
+ function extractToolOutput(params) {
321
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
322
+ if (!params) return undefined;
323
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
324
+ var content = params.content;
325
+ if (Array.isArray(content)) {
326
+ for (var i = 0; i < content.length; i++) {
327
+ var item = content[i];
328
+ if (item && item.type === 'text' && typeof item.text === 'string') {
329
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
330
+ }
331
+ }
332
+ }
333
+ return content;
334
+ }
335
+
320
336
  function readInjectedData() {
321
337
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
322
338
  if (typeof window !== 'undefined') {
323
339
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
324
340
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
325
341
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
342
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
343
+ // (toolOutput is the tool's structuredContent).
344
+ var oa = window.openai;
345
+ if (oa && typeof oa === 'object') {
346
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
347
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
348
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
349
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
350
+ }
351
+ }
326
352
  }
327
353
  return data;
328
354
  }
@@ -371,7 +397,24 @@ var OpenAIAdapter = {
371
397
  if (sdk.displayMode) {
372
398
  context.hostContext.displayMode = sdk.displayMode;
373
399
  }
374
- // Note: OpenAI SDK does not have an onContextChange equivalent
400
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
401
+ window.addEventListener('openai:set_globals', function(event) {
402
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
403
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
404
+ context.toolInput = globals.toolInput;
405
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
406
+ }
407
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
408
+ context.structuredContent = globals.toolOutput;
409
+ context.notifyToolResult(globals.toolOutput);
410
+ }
411
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
412
+ context.notifyContextChange({ theme: globals.theme });
413
+ }
414
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
415
+ context.notifyContextChange({ displayMode: globals.displayMode });
416
+ }
417
+ });
375
418
  return Promise.resolve();
376
419
  },
377
420
  callTool: function(context, name, args) {
@@ -518,9 +561,10 @@ var ExtAppsAdapter = {
518
561
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
519
562
  break;
520
563
  case 'ui/notifications/tool-result':
521
- context.toolOutput = params.content;
564
+ var resultOutput = extractToolOutput(params);
565
+ context.toolOutput = resultOutput;
522
566
  context.structuredContent = params.structuredContent;
523
- context.notifyToolResult(params.content);
567
+ context.notifyToolResult(resultOutput);
524
568
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
525
569
  break;
526
570
  case 'ui/notifications/host-context-changed':
@@ -574,6 +618,15 @@ var ExtAppsAdapter = {
574
618
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
575
619
  });
576
620
  },
621
+ sendNotification: function(method, params) {
622
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
623
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
624
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
625
+ }
626
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
627
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
628
+ return Promise.resolve();
629
+ },
577
630
  sendHandshake: function(context) {
578
631
  // Send ui/initialize using '*' as target origin since TOFU hasn't
579
632
  // been established yet. The response from the host will establish
@@ -639,7 +692,7 @@ var ExtAppsAdapter = {
639
692
  var pending = this.pendingSize;
640
693
  if (!pending) return;
641
694
  this.pendingSize = null;
642
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
695
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
643
696
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
644
697
  }, function(err) {
645
698
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -666,13 +719,11 @@ var ExtAppsAdapter = {
666
719
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
667
720
  },
668
721
  setSize: function(context, size) {
669
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
670
- // measured/desired widget dimensions to the host.
671
- var params = {
672
- height: size && size.height,
673
- width: size && size.width,
674
- aspectRatio: size && size.aspectRatio
675
- };
722
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
723
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
724
+ var params = {};
725
+ if (size && typeof size.width === 'number') params.width = size.width;
726
+ if (size && typeof size.height === 'number') params.height = size.height;
676
727
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
677
728
  // No host origin yet: keep only the latest size and send it when the
678
729
  // handshake settles; every caller's promise follows that one request.
@@ -683,7 +734,7 @@ var ExtAppsAdapter = {
683
734
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
684
735
  });
685
736
  }
686
- return this.sendRequest('ui/setSize', params);
737
+ return this.sendNotification('ui/notifications/size-changed', params);
687
738
  },
688
739
  requestClose: function(context) {
689
740
  return this.sendRequest('ui/close', {});
@@ -1106,7 +1157,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1106
1157
 
1107
1158
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1108
1159
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1109
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1160
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1110
1161
  FrontMcpBridge.prototype.setSize = function(size) {
1111
1162
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1112
1163
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1208,7 +1259,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1208
1259
  target.classList.add('opacity-50', 'cursor-not-allowed');
1209
1260
 
1210
1261
  // Add spinner for buttons
1211
- var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
1262
+ var spinner = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 inline" style="width:1em;height:1em;vertical-align:-0.125em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
1212
1263
  if (target.tagName === 'BUTTON') {
1213
1264
  target.innerHTML = spinner + 'Loading...';
1214
1265
  }
@@ -1289,7 +1340,22 @@ function minifyJS(code) {
1289
1340
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1290
1341
  return code;
1291
1342
  }
1292
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1343
+ const out = [];
1344
+ let inBlockComment = false;
1345
+ for (const raw of code.split("\n")) {
1346
+ const line = raw.trim();
1347
+ if (inBlockComment) {
1348
+ if (line.includes("*/")) inBlockComment = false;
1349
+ continue;
1350
+ }
1351
+ if (line === "" || line.startsWith("//")) continue;
1352
+ if (line.startsWith("/*")) {
1353
+ if (!line.includes("*/")) inBlockComment = true;
1354
+ continue;
1355
+ }
1356
+ out.push(line);
1357
+ }
1358
+ return out.join("\n");
1293
1359
  }
1294
1360
  function generatePlatformBundle(platform, options = {}) {
1295
1361
  const platformAdapters = {
@@ -418,7 +418,7 @@ export interface UITemplateConfig<In = unknown, Out = unknown> {
418
418
  * - Claude / static widgets: the host measures DOM height itself, so this is
419
419
  * a CSS-only no-op.
420
420
  * - OpenAI: forwarded through the Apps SDK sizing API.
421
- * - ext-apps hosts: reported via the `ui/setSize` request.
421
+ * - ext-apps hosts: reported with the standard `ui/notifications/size-changed` notification.
422
422
  *
423
423
  * @default true (when the widget configures sizing)
424
424
  */