@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/esm/index.mjs CHANGED
@@ -398,12 +398,38 @@ function __applyHostTheme(theme) {
398
398
  } catch (e) {}
399
399
  }
400
400
 
401
+ function extractToolOutput(params) {
402
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
403
+ if (!params) return undefined;
404
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
405
+ var content = params.content;
406
+ if (Array.isArray(content)) {
407
+ for (var i = 0; i < content.length; i++) {
408
+ var item = content[i];
409
+ if (item && item.type === 'text' && typeof item.text === 'string') {
410
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
411
+ }
412
+ }
413
+ }
414
+ return content;
415
+ }
416
+
401
417
  function readInjectedData() {
402
418
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
403
419
  if (typeof window !== 'undefined') {
404
420
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
405
421
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
406
422
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
423
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
424
+ // (toolOutput is the tool's structuredContent).
425
+ var oa = window.openai;
426
+ if (oa && typeof oa === 'object') {
427
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
428
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
429
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
430
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
431
+ }
432
+ }
407
433
  }
408
434
  return data;
409
435
  }
@@ -452,7 +478,24 @@ var OpenAIAdapter = {
452
478
  if (sdk.displayMode) {
453
479
  context.hostContext.displayMode = sdk.displayMode;
454
480
  }
455
- // Note: OpenAI SDK does not have an onContextChange equivalent
481
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
482
+ window.addEventListener('openai:set_globals', function(event) {
483
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
484
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
485
+ context.toolInput = globals.toolInput;
486
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
487
+ }
488
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
489
+ context.structuredContent = globals.toolOutput;
490
+ context.notifyToolResult(globals.toolOutput);
491
+ }
492
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
493
+ context.notifyContextChange({ theme: globals.theme });
494
+ }
495
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
496
+ context.notifyContextChange({ displayMode: globals.displayMode });
497
+ }
498
+ });
456
499
  return Promise.resolve();
457
500
  },
458
501
  callTool: function(context, name, args) {
@@ -599,9 +642,10 @@ var ExtAppsAdapter = {
599
642
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
600
643
  break;
601
644
  case 'ui/notifications/tool-result':
602
- context.toolOutput = params.content;
645
+ var resultOutput = extractToolOutput(params);
646
+ context.toolOutput = resultOutput;
603
647
  context.structuredContent = params.structuredContent;
604
- context.notifyToolResult(params.content);
648
+ context.notifyToolResult(resultOutput);
605
649
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
606
650
  break;
607
651
  case 'ui/notifications/host-context-changed':
@@ -655,6 +699,15 @@ var ExtAppsAdapter = {
655
699
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
656
700
  });
657
701
  },
702
+ sendNotification: function(method, params) {
703
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
704
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
705
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
706
+ }
707
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
708
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
709
+ return Promise.resolve();
710
+ },
658
711
  sendHandshake: function(context) {
659
712
  // Send ui/initialize using '*' as target origin since TOFU hasn't
660
713
  // been established yet. The response from the host will establish
@@ -720,7 +773,7 @@ var ExtAppsAdapter = {
720
773
  var pending = this.pendingSize;
721
774
  if (!pending) return;
722
775
  this.pendingSize = null;
723
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
776
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
724
777
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
725
778
  }, function(err) {
726
779
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -747,13 +800,11 @@ var ExtAppsAdapter = {
747
800
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
748
801
  },
749
802
  setSize: function(context, size) {
750
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
751
- // measured/desired widget dimensions to the host.
752
- var params = {
753
- height: size && size.height,
754
- width: size && size.width,
755
- aspectRatio: size && size.aspectRatio
756
- };
803
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
804
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
805
+ var params = {};
806
+ if (size && typeof size.width === 'number') params.width = size.width;
807
+ if (size && typeof size.height === 'number') params.height = size.height;
757
808
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
758
809
  // No host origin yet: keep only the latest size and send it when the
759
810
  // handshake settles; every caller's promise follows that one request.
@@ -764,7 +815,7 @@ var ExtAppsAdapter = {
764
815
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
765
816
  });
766
817
  }
767
- return this.sendRequest('ui/setSize', params);
818
+ return this.sendNotification('ui/notifications/size-changed', params);
768
819
  },
769
820
  requestClose: function(context) {
770
821
  return this.sendRequest('ui/close', {});
@@ -1187,7 +1238,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1187
1238
 
1188
1239
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1189
1240
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1190
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1241
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1191
1242
  FrontMcpBridge.prototype.setSize = function(size) {
1192
1243
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1193
1244
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1289,7 +1340,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1289
1340
  target.classList.add('opacity-50', 'cursor-not-allowed');
1290
1341
 
1291
1342
  // Add spinner for buttons
1292
- 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>';
1343
+ 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>';
1293
1344
  if (target.tagName === 'BUTTON') {
1294
1345
  target.innerHTML = spinner + 'Loading...';
1295
1346
  }
@@ -1370,7 +1421,22 @@ function minifyJS(code) {
1370
1421
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1371
1422
  return code;
1372
1423
  }
1373
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1424
+ const out = [];
1425
+ let inBlockComment = false;
1426
+ for (const raw of code.split("\n")) {
1427
+ const line = raw.trim();
1428
+ if (inBlockComment) {
1429
+ if (line.includes("*/")) inBlockComment = false;
1430
+ continue;
1431
+ }
1432
+ if (line === "" || line.startsWith("//")) continue;
1433
+ if (line.startsWith("/*")) {
1434
+ if (!line.includes("*/")) inBlockComment = true;
1435
+ continue;
1436
+ }
1437
+ out.push(line);
1438
+ }
1439
+ return out.join("\n");
1374
1440
  }
1375
1441
  function generatePlatformBundle(platform, options = {}) {
1376
1442
  const platformAdapters = {
@@ -4102,6 +4168,86 @@ function wrapDetectedContent(value) {
4102
4168
  }
4103
4169
  }
4104
4170
 
4171
+ // libs/uipack/src/adapters/markdown.ts
4172
+ var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
4173
+ function emphasis(text) {
4174
+ return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
4175
+ }
4176
+ function inline(text) {
4177
+ const stash = [];
4178
+ const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
4179
+ let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
4180
+ /`([^`]+)`/g,
4181
+ (_m, code) => hold(`<code>${code}</code>`)
4182
+ );
4183
+ out = out.replace(
4184
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
4185
+ (_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
4186
+ );
4187
+ out = emphasis(out);
4188
+ for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
4189
+ return out;
4190
+ }
4191
+ function markdownToHtml(source) {
4192
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
4193
+ const blocks = [];
4194
+ let paragraph = [];
4195
+ let list;
4196
+ const flushParagraph = () => {
4197
+ if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
4198
+ paragraph = [];
4199
+ };
4200
+ const flushList = () => {
4201
+ if (list)
4202
+ blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
4203
+ list = void 0;
4204
+ };
4205
+ for (let i = 0; i < lines.length; i++) {
4206
+ const line = lines[i];
4207
+ const fence = /^\s*(`{3,})/.exec(line);
4208
+ if (fence) {
4209
+ flushParagraph();
4210
+ flushList();
4211
+ const fenceLength = fence[1].length;
4212
+ const isClosing = (l) => {
4213
+ const m = /^\s*(`{3,})\s*$/.exec(l);
4214
+ return m !== null && m[1].length >= fenceLength;
4215
+ };
4216
+ const code = [];
4217
+ for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
4218
+ blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
4219
+ </code></pre>`);
4220
+ continue;
4221
+ }
4222
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
4223
+ if (heading) {
4224
+ flushParagraph();
4225
+ flushList();
4226
+ blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
4227
+ continue;
4228
+ }
4229
+ const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
4230
+ if (item) {
4231
+ flushParagraph();
4232
+ const tag = /\d/.test(item[1]) ? "ol" : "ul";
4233
+ if (list && list.tag !== tag) flushList();
4234
+ list ??= { tag, items: [] };
4235
+ list.items.push(item[2]);
4236
+ continue;
4237
+ }
4238
+ if (line.trim() === "") {
4239
+ flushParagraph();
4240
+ flushList();
4241
+ continue;
4242
+ }
4243
+ flushList();
4244
+ paragraph.push(line.trim());
4245
+ }
4246
+ flushParagraph();
4247
+ flushList();
4248
+ return blocks.join("\n");
4249
+ }
4250
+
4105
4251
  // libs/uipack/src/adapters/type-detector.ts
4106
4252
  function detectUIType(template) {
4107
4253
  if (template === null || template === void 0) {
@@ -4135,9 +4281,15 @@ function detectUIType(template) {
4135
4281
  }
4136
4282
 
4137
4283
  // libs/uipack/src/adapters/template-renderer.ts
4138
- function buildCspConfig(resolver) {
4284
+ function buildCspConfig(resolver, extra) {
4139
4285
  const cspResourceDomains = ["https://esm.sh"];
4140
4286
  const cspConnectDomains = ["https://esm.sh"];
4287
+ for (const domain of extra?.resourceDomains ?? []) {
4288
+ if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
4289
+ }
4290
+ for (const domain of extra?.connectDomains ?? []) {
4291
+ if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
4292
+ }
4141
4293
  if (resolver && "overrides" in resolver) {
4142
4294
  const overrides = resolver.overrides;
4143
4295
  if (overrides) {
@@ -4177,6 +4329,14 @@ function renderUnwrappedResult(rawResult, options) {
4177
4329
  }
4178
4330
  return rawResult;
4179
4331
  }
4332
+ function isReactElement(value) {
4333
+ return typeof value === "object" && value !== null && "$$typeof" in value;
4334
+ }
4335
+ function isDefinitelyReactComponent(template) {
4336
+ if (typeof template !== "function") return false;
4337
+ const proto = template.prototype;
4338
+ return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
4339
+ }
4180
4340
  function renderToolTemplate(options) {
4181
4341
  const { toolName, input, output, template, resolver, platformType, sizing } = options;
4182
4342
  const uiType = detectUIType(template);
@@ -4187,30 +4347,43 @@ function renderToolTemplate(options) {
4187
4347
  output,
4188
4348
  includeBridge: true,
4189
4349
  resolver,
4190
- sizing
4350
+ sizing,
4351
+ csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
4191
4352
  };
4192
4353
  let html2;
4193
4354
  let hash = "";
4194
4355
  let size = 0;
4195
4356
  if (typeof template === "object" && template !== null && "file" in template) {
4196
- const cspConfig = buildCspConfig(resolver);
4197
4357
  const result = renderComponent(
4198
4358
  { source: template, inlineReact: resourceMode === "inline" },
4199
- { ...shellConfig, csp: cspConfig }
4359
+ { ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
4200
4360
  );
4201
4361
  html2 = result.html;
4202
4362
  hash = result.hash;
4203
4363
  size = result.size;
4204
4364
  } else if (typeof template === "function") {
4205
- if (uiType === "react") {
4365
+ const helpers = createTemplateHelpers();
4366
+ const ctx = { input, output, helpers };
4367
+ let rawResult;
4368
+ let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
4369
+ if (isHtmlBuilder) {
4370
+ try {
4371
+ rawResult = template(ctx);
4372
+ isHtmlBuilder = !isReactElement(rawResult);
4373
+ } catch (error) {
4374
+ if (uiType !== "react") throw error;
4375
+ isHtmlBuilder = false;
4376
+ }
4377
+ }
4378
+ if (!isHtmlBuilder) {
4379
+ (options.logger ?? console).warn(
4380
+ `[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.`
4381
+ );
4206
4382
  const shellResult = buildShell('<div id="root"></div>', shellConfig);
4207
4383
  html2 = shellResult.html;
4208
4384
  hash = shellResult.hash;
4209
4385
  size = shellResult.size;
4210
4386
  } else {
4211
- const helpers = createTemplateHelpers();
4212
- const ctx = { input, output, helpers };
4213
- const rawResult = template(ctx);
4214
4387
  const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
4215
4388
  if (wrapped) {
4216
4389
  html2 = wrapped;
@@ -4222,7 +4395,7 @@ function renderToolTemplate(options) {
4222
4395
  }
4223
4396
  }
4224
4397
  } else if (typeof template === "string") {
4225
- const shellResult = buildShell(template, shellConfig);
4398
+ const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
4226
4399
  html2 = shellResult.html;
4227
4400
  hash = shellResult.hash;
4228
4401
  size = shellResult.size;
package/esm/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": {
@@ -265,12 +265,38 @@ function __applyHostTheme(theme) {
265
265
  } catch (e) {}
266
266
  }
267
267
 
268
+ function extractToolOutput(params) {
269
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
270
+ if (!params) return undefined;
271
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
272
+ var content = params.content;
273
+ if (Array.isArray(content)) {
274
+ for (var i = 0; i < content.length; i++) {
275
+ var item = content[i];
276
+ if (item && item.type === 'text' && typeof item.text === 'string') {
277
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
278
+ }
279
+ }
280
+ }
281
+ return content;
282
+ }
283
+
268
284
  function readInjectedData() {
269
285
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
270
286
  if (typeof window !== 'undefined') {
271
287
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
272
288
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
273
289
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
290
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
291
+ // (toolOutput is the tool's structuredContent).
292
+ var oa = window.openai;
293
+ if (oa && typeof oa === 'object') {
294
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
295
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
296
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
297
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
298
+ }
299
+ }
274
300
  }
275
301
  return data;
276
302
  }
@@ -319,7 +345,24 @@ var OpenAIAdapter = {
319
345
  if (sdk.displayMode) {
320
346
  context.hostContext.displayMode = sdk.displayMode;
321
347
  }
322
- // Note: OpenAI SDK does not have an onContextChange equivalent
348
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
349
+ window.addEventListener('openai:set_globals', function(event) {
350
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
351
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
352
+ context.toolInput = globals.toolInput;
353
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
354
+ }
355
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
356
+ context.structuredContent = globals.toolOutput;
357
+ context.notifyToolResult(globals.toolOutput);
358
+ }
359
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
360
+ context.notifyContextChange({ theme: globals.theme });
361
+ }
362
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
363
+ context.notifyContextChange({ displayMode: globals.displayMode });
364
+ }
365
+ });
323
366
  return Promise.resolve();
324
367
  },
325
368
  callTool: function(context, name, args) {
@@ -466,9 +509,10 @@ var ExtAppsAdapter = {
466
509
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
467
510
  break;
468
511
  case 'ui/notifications/tool-result':
469
- context.toolOutput = params.content;
512
+ var resultOutput = extractToolOutput(params);
513
+ context.toolOutput = resultOutput;
470
514
  context.structuredContent = params.structuredContent;
471
- context.notifyToolResult(params.content);
515
+ context.notifyToolResult(resultOutput);
472
516
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
473
517
  break;
474
518
  case 'ui/notifications/host-context-changed':
@@ -522,6 +566,15 @@ var ExtAppsAdapter = {
522
566
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
523
567
  });
524
568
  },
569
+ sendNotification: function(method, params) {
570
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
571
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
572
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
573
+ }
574
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
575
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
576
+ return Promise.resolve();
577
+ },
525
578
  sendHandshake: function(context) {
526
579
  // Send ui/initialize using '*' as target origin since TOFU hasn't
527
580
  // been established yet. The response from the host will establish
@@ -587,7 +640,7 @@ var ExtAppsAdapter = {
587
640
  var pending = this.pendingSize;
588
641
  if (!pending) return;
589
642
  this.pendingSize = null;
590
- this.sendRequest('ui/setSize', pending.params).then(function(result) {
643
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
591
644
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
592
645
  }, function(err) {
593
646
  for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
@@ -614,13 +667,11 @@ var ExtAppsAdapter = {
614
667
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
615
668
  },
616
669
  setSize: function(context, size) {
617
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
618
- // measured/desired widget dimensions to the host.
619
- var params = {
620
- height: size && size.height,
621
- width: size && size.width,
622
- aspectRatio: size && size.aspectRatio
623
- };
670
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
671
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
672
+ var params = {};
673
+ if (size && typeof size.width === 'number') params.width = size.width;
674
+ if (size && typeof size.height === 'number') params.height = size.height;
624
675
  if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
625
676
  // No host origin yet: keep only the latest size and send it when the
626
677
  // handshake settles; every caller's promise follows that one request.
@@ -631,7 +682,7 @@ var ExtAppsAdapter = {
631
682
  self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
632
683
  });
633
684
  }
634
- return this.sendRequest('ui/setSize', params);
685
+ return this.sendNotification('ui/notifications/size-changed', params);
635
686
  },
636
687
  requestClose: function(context) {
637
688
  return this.sendRequest('ui/close', {});
@@ -1054,7 +1105,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1054
1105
 
1055
1106
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1056
1107
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1057
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1108
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1058
1109
  FrontMcpBridge.prototype.setSize = function(size) {
1059
1110
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1060
1111
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1156,7 +1207,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1156
1207
  target.classList.add('opacity-50', 'cursor-not-allowed');
1157
1208
 
1158
1209
  // Add spinner for buttons
1159
- 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>';
1210
+ 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>';
1160
1211
  if (target.tagName === 'BUTTON') {
1161
1212
  target.innerHTML = spinner + 'Loading...';
1162
1213
  }
@@ -1237,7 +1288,22 @@ function minifyJS(code) {
1237
1288
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1238
1289
  return code;
1239
1290
  }
1240
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1291
+ const out = [];
1292
+ let inBlockComment = false;
1293
+ for (const raw of code.split("\n")) {
1294
+ const line = raw.trim();
1295
+ if (inBlockComment) {
1296
+ if (line.includes("*/")) inBlockComment = false;
1297
+ continue;
1298
+ }
1299
+ if (line === "" || line.startsWith("//")) continue;
1300
+ if (line.startsWith("/*")) {
1301
+ if (!line.includes("*/")) inBlockComment = true;
1302
+ continue;
1303
+ }
1304
+ out.push(line);
1305
+ }
1306
+ return out.join("\n");
1241
1307
  }
1242
1308
  function generatePlatformBundle(platform, options = {}) {
1243
1309
  const platformAdapters = {