@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.
@@ -105,6 +105,23 @@ function __applySizingCss(sizing) {
105
105
  }
106
106
  }
107
107
 
108
+ function __measureHeight() {
109
+ // <html> at fit-content includes body margins and collapsed child margins;
110
+ // content overflowing a fixed-height body is added; a px max-height clamps.
111
+ var de = document.documentElement;
112
+ var body = document.body;
113
+ if (!de || !body) return 0;
114
+ var prev = de.style.height;
115
+ de.style.height = 'fit-content';
116
+ var dr = de.getBoundingClientRect();
117
+ var br = body.getBoundingClientRect();
118
+ var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
119
+ de.style.height = prev;
120
+ var maxHeight = getComputedStyle(de).maxHeight;
121
+ var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
122
+ return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
123
+ }
124
+
108
125
  function __initAutoResize() {
109
126
  if (typeof window === 'undefined') return;
110
127
  // Idempotent: a re-injected IIFE must not stack observers.
@@ -127,23 +144,30 @@ function __initAutoResize() {
127
144
  if (typeof ResizeObserver === 'undefined') return;
128
145
 
129
146
  function startObserving() {
130
- var target = document.getElementById('root') || document.body;
131
- if (!target) return;
147
+ if (!document.body) return;
148
+ var targets = [document.documentElement, document.body];
149
+ var root = document.getElementById('root');
150
+ if (root) targets.push(root);
132
151
 
133
152
  var rafId = null;
134
153
  var lastReported = -1;
135
154
  function report() {
136
155
  rafId = null;
137
156
  try {
138
- var rect = target.getBoundingClientRect();
139
- var height = Math.ceil(rect.height);
140
- var width = Math.ceil(rect.width);
157
+ // Before the handshake an ext-apps host can't receive the report; the
158
+ // bridge:ready listener below sends it once the bridge is up.
159
+ if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
160
+ var height = __measureHeight();
161
+ var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
141
162
  if (height === lastReported || height <= 0) return;
142
163
  lastReported = height;
143
164
  var payload = { height: height, width: width };
144
165
  if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
145
166
  if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
146
- window.FrontMcpBridge.setSize(payload).catch(function() {});
167
+ window.FrontMcpBridge.setSize(payload).catch(function() {
168
+ // Forget a rejected report so the same height is sent again.
169
+ if (lastReported === height) lastReported = -1;
170
+ });
147
171
  }
148
172
  window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
149
173
  } catch (e) {}
@@ -164,9 +188,17 @@ function __initAutoResize() {
164
188
  window.__mcpResizeObserver.disconnect();
165
189
  }
166
190
  var ro = new ResizeObserver(function() { schedule(); });
167
- ro.observe(target);
191
+ for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
168
192
  window.__mcpResizeObserver = ro;
169
193
  } catch (e) {}
194
+ // Report once the bridge is ready (the report scheduled below is skipped
195
+ // while the handshake is still pending).
196
+ var readySent = false;
197
+ window.addEventListener('bridge:ready', function() {
198
+ if (readySent) return;
199
+ readySent = true;
200
+ schedule();
201
+ });
170
202
  // Report once on init so the host gets an initial measurement.
171
203
  schedule();
172
204
  }
@@ -214,12 +246,57 @@ function detectViewport() {
214
246
  return undefined;
215
247
  }
216
248
 
249
+ function __applyHostTheme(theme) {
250
+ // Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
251
+ // canvas and form controls (a widget's own :root color-scheme still wins),
252
+ // <html data-theme> is a styling hook. The OS fallback is never written.
253
+ if (theme !== 'light' && theme !== 'dark') return;
254
+ if (typeof document === 'undefined' || !document.documentElement) return;
255
+ try {
256
+ var meta = document.querySelector('meta[name="color-scheme"]');
257
+ var head = document.head || document.getElementsByTagName('head')[0];
258
+ if (!meta && head) {
259
+ meta = document.createElement('meta');
260
+ meta.setAttribute('name', 'color-scheme');
261
+ head.appendChild(meta);
262
+ }
263
+ if (meta) meta.setAttribute('content', theme);
264
+ document.documentElement.setAttribute('data-theme', theme);
265
+ } catch (e) {}
266
+ }
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
+
217
284
  function readInjectedData() {
218
285
  var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
219
286
  if (typeof window !== 'undefined') {
220
287
  if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
221
288
  if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
222
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
+ }
223
300
  }
224
301
  return data;
225
302
  }
@@ -268,7 +345,24 @@ var OpenAIAdapter = {
268
345
  if (sdk.displayMode) {
269
346
  context.hostContext.displayMode = sdk.displayMode;
270
347
  }
271
- // 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
+ });
272
366
  return Promise.resolve();
273
367
  },
274
368
  callTool: function(context, name, args) {
@@ -320,6 +414,8 @@ var ExtAppsAdapter = {
320
414
  pendingRequests: {},
321
415
  requestId: 0,
322
416
  hostCapabilities: {},
417
+ handshakeSettled: false,
418
+ pendingSize: null,
323
419
  canHandle: function() {
324
420
  if (typeof window === 'undefined') return false;
325
421
  if (window.parent === window) return false;
@@ -413,9 +509,10 @@ var ExtAppsAdapter = {
413
509
  window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
414
510
  break;
415
511
  case 'ui/notifications/tool-result':
416
- context.toolOutput = params.content;
512
+ var resultOutput = extractToolOutput(params);
513
+ context.toolOutput = resultOutput;
417
514
  context.structuredContent = params.structuredContent;
418
- context.notifyToolResult(params.content);
515
+ context.notifyToolResult(resultOutput);
419
516
  window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
420
517
  break;
421
518
  case 'ui/notifications/host-context-changed':
@@ -469,6 +566,15 @@ var ExtAppsAdapter = {
469
566
  window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
470
567
  });
471
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
+ },
472
578
  sendHandshake: function(context) {
473
579
  // Send ui/initialize using '*' as target origin since TOFU hasn't
474
580
  // been established yet. The response from the host will establish
@@ -485,6 +591,7 @@ var ExtAppsAdapter = {
485
591
  var timeout = setTimeout(function() {
486
592
  delete self.pendingRequests[id];
487
593
  // Handshake timeout is non-fatal \u2014 notifications may still arrive
594
+ self.settleHandshake();
488
595
  resolve();
489
596
  }, 10000);
490
597
 
@@ -498,7 +605,9 @@ var ExtAppsAdapter = {
498
605
  supportsDisplayModes: true
499
606
  });
500
607
  if (result.hostContext) {
501
- Object.assign(context.hostContext, result.hostContext);
608
+ // Notify (not a silent merge) so onContextChange listeners and the
609
+ // host theme see the handshake's context.
610
+ context.notifyContextChange(result.hostContext);
502
611
  }
503
612
  // Send ui/notifications/initialized to tell the host the view is ready.
504
613
  // Per MCP Apps spec, the host waits for this before sending tool-result.
@@ -508,9 +617,10 @@ var ExtAppsAdapter = {
508
617
  method: 'ui/notifications/initialized',
509
618
  params: {}
510
619
  }, targetOrigin);
620
+ self.settleHandshake();
511
621
  resolve();
512
622
  },
513
- reject: function(err) { resolve(); }, // Non-fatal
623
+ reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
514
624
  timeout: timeout
515
625
  };
516
626
 
@@ -524,6 +634,18 @@ var ExtAppsAdapter = {
524
634
  performHandshake: function(context) {
525
635
  return this.sendHandshake(context);
526
636
  },
637
+ settleHandshake: function() {
638
+ // Deliver the latest size requested while no host origin was trusted.
639
+ this.handshakeSettled = true;
640
+ var pending = this.pendingSize;
641
+ if (!pending) return;
642
+ this.pendingSize = null;
643
+ this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
644
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
645
+ }, function(err) {
646
+ for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
647
+ });
648
+ },
527
649
  callTool: function(context, name, args) {
528
650
  if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
529
651
  return Promise.reject(new Error('Server tool proxy not supported'));
@@ -545,13 +667,22 @@ var ExtAppsAdapter = {
545
667
  return this.sendRequest('ui/setDisplayMode', { mode: mode });
546
668
  },
547
669
  setSize: function(context, size) {
548
- // FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
549
- // measured/desired widget dimensions to the host.
550
- return this.sendRequest('ui/setSize', {
551
- height: size && size.height,
552
- width: size && size.width,
553
- aspectRatio: size && size.aspectRatio
554
- });
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;
675
+ if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
676
+ // No host origin yet: keep only the latest size and send it when the
677
+ // handshake settles; every caller's promise follows that one request.
678
+ var self = this;
679
+ if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
680
+ this.pendingSize.params = params;
681
+ return new Promise(function(resolve, reject) {
682
+ self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
683
+ });
684
+ }
685
+ return this.sendNotification('ui/notifications/size-changed', params);
555
686
  },
556
687
  requestClose: function(context) {
557
688
  return this.sendRequest('ui/close', {});
@@ -797,6 +928,7 @@ function FrontMcpBridge() {
797
928
  toolResultListeners: [],
798
929
  notifyContextChange: function(changes) {
799
930
  Object.assign(this.hostContext, changes);
931
+ if (changes && changes.theme) __applyHostTheme(changes.theme);
800
932
  for (var i = 0; i < this.contextListeners.length; i++) {
801
933
  try { this.contextListeners[i](changes); } catch(e) {}
802
934
  }
@@ -973,7 +1105,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
973
1105
 
974
1106
  // Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
975
1107
  // Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
976
- // 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.
977
1109
  FrontMcpBridge.prototype.setSize = function(size) {
978
1110
  if (!this._adapter) return Promise.reject(new Error('Not initialized'));
979
1111
  if (!this._adapter.setSize) return Promise.resolve();
@@ -1075,7 +1207,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
1075
1207
  target.classList.add('opacity-50', 'cursor-not-allowed');
1076
1208
 
1077
1209
  // Add spinner for buttons
1078
- 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>';
1079
1211
  if (target.tagName === 'BUTTON') {
1080
1212
  target.innerHTML = spinner + 'Loading...';
1081
1213
  }
@@ -1156,7 +1288,22 @@ function minifyJS(code) {
1156
1288
  if (code.length > MAX_MINIFY_CODE_LENGTH) {
1157
1289
  return code;
1158
1290
  }
1159
- 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");
1160
1307
  }
1161
1308
  function generatePlatformBundle(platform, options = {}) {
1162
1309
  const platformAdapters = {