@frontmcp/uipack 1.8.5 → 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.
@@ -134,6 +134,23 @@ function __applySizingCss(sizing) {
134
134
  }
135
135
  }
136
136
 
137
+ function __measureHeight() {
138
+ // <html> at fit-content includes body margins and collapsed child margins;
139
+ // content overflowing a fixed-height body is added; a px max-height clamps.
140
+ var de = document.documentElement;
141
+ var body = document.body;
142
+ if (!de || !body) return 0;
143
+ var prev = de.style.height;
144
+ de.style.height = 'fit-content';
145
+ var dr = de.getBoundingClientRect();
146
+ var br = body.getBoundingClientRect();
147
+ var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
148
+ de.style.height = prev;
149
+ var maxHeight = getComputedStyle(de).maxHeight;
150
+ var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
151
+ return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
152
+ }
153
+
137
154
  function __initAutoResize() {
138
155
  if (typeof window === 'undefined') return;
139
156
  // Idempotent: a re-injected IIFE must not stack observers.
@@ -156,23 +173,30 @@ function __initAutoResize() {
156
173
  if (typeof ResizeObserver === 'undefined') return;
157
174
 
158
175
  function startObserving() {
159
- var target = document.getElementById('root') || document.body;
160
- if (!target) return;
176
+ if (!document.body) return;
177
+ var targets = [document.documentElement, document.body];
178
+ var root = document.getElementById('root');
179
+ if (root) targets.push(root);
161
180
 
162
181
  var rafId = null;
163
182
  var lastReported = -1;
164
183
  function report() {
165
184
  rafId = null;
166
185
  try {
167
- var rect = target.getBoundingClientRect();
168
- var height = Math.ceil(rect.height);
169
- var width = Math.ceil(rect.width);
186
+ // Before the handshake an ext-apps host can't receive the report; the
187
+ // bridge:ready listener below sends it once the bridge is up.
188
+ if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
189
+ var height = __measureHeight();
190
+ var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
170
191
  if (height === lastReported || height <= 0) return;
171
192
  lastReported = height;
172
193
  var payload = { height: height, width: width };
173
194
  if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
174
195
  if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
175
- window.FrontMcpBridge.setSize(payload).catch(function() {});
196
+ window.FrontMcpBridge.setSize(payload).catch(function() {
197
+ // Forget a rejected report so the same height is sent again.
198
+ if (lastReported === height) lastReported = -1;
199
+ });
176
200
  }
177
201
  window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
178
202
  } catch (e) {}
@@ -193,9 +217,17 @@ function __initAutoResize() {
193
217
  window.__mcpResizeObserver.disconnect();
194
218
  }
195
219
  var ro = new ResizeObserver(function() { schedule(); });
196
- ro.observe(target);
220
+ for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
197
221
  window.__mcpResizeObserver = ro;
198
222
  } catch (e) {}
223
+ // Report once the bridge is ready (the report scheduled below is skipped
224
+ // while the handshake is still pending).
225
+ var readySent = false;
226
+ window.addEventListener('bridge:ready', function() {
227
+ if (readySent) return;
228
+ readySent = true;
229
+ schedule();
230
+ });
199
231
  // Report once on init so the host gets an initial measurement.
200
232
  schedule();
201
233
  }
@@ -243,12 +275,57 @@ function detectViewport() {
243
275
  return undefined;
244
276
  }
245
277
 
278
+ function __applyHostTheme(theme) {
279
+ // Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
280
+ // canvas and form controls (a widget's own :root color-scheme still wins),
281
+ // <html data-theme> is a styling hook. The OS fallback is never written.
282
+ if (theme !== 'light' && theme !== 'dark') return;
283
+ if (typeof document === 'undefined' || !document.documentElement) return;
284
+ try {
285
+ var meta = document.querySelector('meta[name="color-scheme"]');
286
+ var head = document.head || document.getElementsByTagName('head')[0];
287
+ if (!meta && head) {
288
+ meta = document.createElement('meta');
289
+ meta.setAttribute('name', 'color-scheme');
290
+ head.appendChild(meta);
291
+ }
292
+ if (meta) meta.setAttribute('content', theme);
293
+ document.documentElement.setAttribute('data-theme', theme);
294
+ } catch (e) {}
295
+ }
296
+
297
+ function extractToolOutput(params) {
298
+ // A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
299
+ if (!params) return undefined;
300
+ if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
301
+ var content = params.content;
302
+ if (Array.isArray(content)) {
303
+ for (var i = 0; i < content.length; i++) {
304
+ var item = content[i];
305
+ if (item && item.type === 'text' && typeof item.text === 'string') {
306
+ try { return JSON.parse(item.text); } catch (e) { return item.text; }
307
+ }
308
+ }
309
+ }
310
+ return content;
311
+ }
312
+
246
313
  function readInjectedData() {
247
314
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
248
315
  if (typeof window !== 'undefined') {
249
316
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
250
317
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
251
318
  if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
319
+ // The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
320
+ // (toolOutput is the tool's structuredContent).
321
+ var oa = window.openai;
322
+ if (oa && typeof oa === 'object') {
323
+ if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
324
+ if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
325
+ if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
326
+ if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
327
+ }
328
+ }
252
329
  }
253
330
  return data;
254
331
  }
@@ -297,7 +374,24 @@ var OpenAIAdapter = {
297
374
  if (sdk.displayMode) {
298
375
  context.hostContext.displayMode = sdk.displayMode;
299
376
  }
300
- // Note: OpenAI SDK does not have an onContextChange equivalent
377
+ // The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
378
+ window.addEventListener('openai:set_globals', function(event) {
379
+ var globals = (event && event.detail && event.detail.globals) || window.openai || {};
380
+ if (globals.toolInput !== undefined && globals.toolInput !== null) {
381
+ context.toolInput = globals.toolInput;
382
+ window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
383
+ }
384
+ if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
385
+ context.structuredContent = globals.toolOutput;
386
+ context.notifyToolResult(globals.toolOutput);
387
+ }
388
+ if (globals.theme && globals.theme !== context.hostContext.theme) {
389
+ context.notifyContextChange({ theme: globals.theme });
390
+ }
391
+ if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
392
+ context.notifyContextChange({ displayMode: globals.displayMode });
393
+ }
394
+ });
301
395
  return Promise.resolve();
302
396
  },
303
397
  callTool: function(context, name, args) {
@@ -349,6 +443,8 @@ var ExtAppsAdapter = {
349
443
  pendingRequests: {},
350
444
  requestId: 0,
351
445
  hostCapabilities: {},
446
+ handshakeSettled: false,
447
+ pendingSize: null,
352
448
  canHandle: function() {
353
449
  if (typeof window === 'undefined') return false;
354
450
  if (window.parent === window) return false;
@@ -442,9 +538,10 @@ var ExtAppsAdapter = {
442
538
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
443
539
  break;
444
540
  case 'ui/notifications/tool-result':
445
- context.toolOutput = params.content;
541
+ var resultOutput = extractToolOutput(params);
542
+ context.toolOutput = resultOutput;
446
543
  context.structuredContent = params.structuredContent;
447
- context.notifyToolResult(params.content);
544
+ context.notifyToolResult(resultOutput);
448
545
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
449
546
  break;
450
547
  case 'ui/notifications/host-context-changed':
@@ -498,6 +595,15 @@ var ExtAppsAdapter = {
498
595
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
499
596
  });
500
597
  },
598
+ sendNotification: function(method, params) {
599
+ // JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
600
+ if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
601
+ return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
602
+ }
603
+ var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
604
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
605
+ return Promise.resolve();
606
+ },
501
607
  sendHandshake: function(context) {
502
608
  // Send ui/initialize using '*' as target origin since TOFU hasn't
503
609
  // been established yet. The response from the host will establish
@@ -514,6 +620,7 @@ var ExtAppsAdapter = {
514
620
  var timeout = setTimeout(function() {
515
621
  delete self.pendingRequests[id];
516
622
  // Handshake timeout is non-fatal \u2014 notifications may still arrive
623
+ self.settleHandshake();
517
624
  resolve();
518
625
  }, 10000);
519
626
 
@@ -527,7 +634,9 @@ var ExtAppsAdapter = {
527
634
  supportsDisplayModes: true
528
635
  });
529
636
  if (result.hostContext) {
530
- Object.assign(context.hostContext, result.hostContext);
637
+ // Notify (not a silent merge) so onContextChange listeners and the
638
+ // host theme see the handshake's context.
639
+ context.notifyContextChange(result.hostContext);
531
640
  }
532
641
  // Send ui/notifications/initialized to tell the host the view is ready.
533
642
  // Per MCP Apps spec, the host waits for this before sending tool-result.
@@ -537,9 +646,10 @@ var ExtAppsAdapter = {
537
646
  method: 'ui/notifications/initialized',
538
647
  params: {}
539
648
  }, targetOrigin);
649
+ self.settleHandshake();
540
650
  resolve();
541
651
  },
542
- reject: function(err) { resolve(); }, // Non-fatal
652
+ reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
543
653
  timeout: timeout
544
654
  };
545
655
 
@@ -553,6 +663,18 @@ var ExtAppsAdapter = {
553
663
  performHandshake: function(context) {
554
664
  return this.sendHandshake(context);
555
665
  },
666
+ settleHandshake: function() {
667
+ // Deliver the latest size requested while no host origin was trusted.
668
+ this.handshakeSettled = true;
669
+ var pending = this.pendingSize;
670
+ if (!pending) return;
671
+ this.pendingSize = null;
672
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
673
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
674
+ }, function(err) {
675
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
676
+ });
677
+ },
556
678
  callTool: function(context, name, args) {
557
679
  if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
558
680
  return Promise.reject(new Error('Server tool proxy not supported'));
@@ -574,13 +696,22 @@ var ExtAppsAdapter = {
574
696
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
575
697
  },
576
698
  setSize: function(context, size) {
577
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
578
- // measured/desired widget dimensions to the host.
579
- return this.sendRequest('ui/setSize', {
580
- height: size && size.height,
581
- width: size && size.width,
582
- aspectRatio: size && size.aspectRatio
583
- });
699
+ // Standard ext-apps sizing: the view tells the host its dimensions with the
700
+ // ui/notifications/size-changed notification ({ width?, height? } in px).
701
+ var params = {};
702
+ if (size && typeof size.width === 'number') params.width = size.width;
703
+ if (size && typeof size.height === 'number') params.height = size.height;
704
+ if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
705
+ // No host origin yet: keep only the latest size and send it when the
706
+ // handshake settles; every caller's promise follows that one request.
707
+ var self = this;
708
+ if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
709
+ this.pendingSize.params = params;
710
+ return new Promise(function(resolve, reject) {
711
+ self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
712
+ });
713
+ }
714
+ return this.sendNotification('ui/notifications/size-changed', params);
584
715
  },
585
716
  requestClose: function(context) {
586
717
  return this.sendRequest('ui/close', {});
@@ -826,6 +957,7 @@ function FrontMcpBridge() {
826
957
  toolResultListeners: [],
827
958
  notifyContextChange: function(changes) {
828
959
  Object.assign(this.hostContext, changes);
960
+ if (changes && changes.theme) __applyHostTheme(changes.theme);
829
961
  for (var i = 0; i < this.contextListeners.length; i++) {
830
962
  try { this.contextListeners[i](changes); } catch(e) {}
831
963
  }
@@ -1002,7 +1134,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
1002
1134
 
1003
1135
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
1004
1136
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
1005
- // ext-apps sends ui/setSize, OpenAI forwards to its SDK when available.
1137
+ // ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
1006
1138
  FrontMcpBridge.prototype.setSize = function(size) {
1007
1139
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
1008
1140
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1104,7 +1236,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1104
1236
  target.classList.add('opacity-50', 'cursor-not-allowed');
1105
1237
 
1106
1238
  // Add spinner for buttons
1107
- 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>';
1239
+ 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>';
1108
1240
  if (target.tagName === 'BUTTON') {
1109
1241
  target.innerHTML = spinner + 'Loading...';
1110
1242
  }
@@ -1185,7 +1317,22 @@ function minifyJS(code) {
1185
1317
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1186
1318
  return code;
1187
1319
  }
1188
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\s+/g, " ").replace(/\s*([{};,:()[\]])\s*/g, "$1").replace(/;\}/g, "}").trim();
1320
+ const out = [];
1321
+ let inBlockComment = false;
1322
+ for (const raw of code.split("\n")) {
1323
+ const line = raw.trim();
1324
+ if (inBlockComment) {
1325
+ if (line.includes("*/")) inBlockComment = false;
1326
+ continue;
1327
+ }
1328
+ if (line === "" || line.startsWith("//")) continue;
1329
+ if (line.startsWith("/*")) {
1330
+ if (!line.includes("*/")) inBlockComment = true;
1331
+ continue;
1332
+ }
1333
+ out.push(line);
1334
+ }
1335
+ return out.join("\n");
1189
1336
  }
1190
1337
  function generatePlatformBundle(platform, options = {}) {
1191
1338
  const platformAdapters = {