@frontmcp/uipack 1.8.5 → 1.8.6
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/adapters/index.js +146 -15
- package/bridge-runtime/iife-generator.d.ts.map +1 -1
- package/bridge-runtime/index.js +92 -11
- package/component/index.js +146 -15
- package/component/loader.d.ts.map +1 -1
- package/component/transpiler.d.ts.map +1 -1
- package/esm/adapters/index.mjs +146 -15
- package/esm/bridge-runtime/index.mjs +92 -11
- package/esm/component/index.mjs +146 -15
- package/esm/index.mjs +146 -15
- package/esm/package.json +8 -2
- package/esm/shell/index.mjs +92 -11
- package/index.js +146 -15
- package/package.json +8 -2
- package/shell/index.js +92 -11
- package/types/ui-config.d.ts +4 -2
- package/types/ui-config.d.ts.map +1 -1
|
@@ -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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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(
|
|
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,6 +246,25 @@ 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
|
+
|
|
217
268
|
function readInjectedData() {
|
|
218
269
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
219
270
|
if (typeof window !== 'undefined') {
|
|
@@ -320,6 +371,8 @@ var ExtAppsAdapter = {
|
|
|
320
371
|
pendingRequests: {},
|
|
321
372
|
requestId: 0,
|
|
322
373
|
hostCapabilities: {},
|
|
374
|
+
handshakeSettled: false,
|
|
375
|
+
pendingSize: null,
|
|
323
376
|
canHandle: function() {
|
|
324
377
|
if (typeof window === 'undefined') return false;
|
|
325
378
|
if (window.parent === window) return false;
|
|
@@ -485,6 +538,7 @@ var ExtAppsAdapter = {
|
|
|
485
538
|
var timeout = setTimeout(function() {
|
|
486
539
|
delete self.pendingRequests[id];
|
|
487
540
|
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
541
|
+
self.settleHandshake();
|
|
488
542
|
resolve();
|
|
489
543
|
}, 10000);
|
|
490
544
|
|
|
@@ -498,7 +552,9 @@ var ExtAppsAdapter = {
|
|
|
498
552
|
supportsDisplayModes: true
|
|
499
553
|
});
|
|
500
554
|
if (result.hostContext) {
|
|
501
|
-
|
|
555
|
+
// Notify (not a silent merge) so onContextChange listeners and the
|
|
556
|
+
// host theme see the handshake's context.
|
|
557
|
+
context.notifyContextChange(result.hostContext);
|
|
502
558
|
}
|
|
503
559
|
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
504
560
|
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
@@ -508,9 +564,10 @@ var ExtAppsAdapter = {
|
|
|
508
564
|
method: 'ui/notifications/initialized',
|
|
509
565
|
params: {}
|
|
510
566
|
}, targetOrigin);
|
|
567
|
+
self.settleHandshake();
|
|
511
568
|
resolve();
|
|
512
569
|
},
|
|
513
|
-
reject: function(err) { resolve(); }, // Non-fatal
|
|
570
|
+
reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
|
|
514
571
|
timeout: timeout
|
|
515
572
|
};
|
|
516
573
|
|
|
@@ -524,6 +581,18 @@ var ExtAppsAdapter = {
|
|
|
524
581
|
performHandshake: function(context) {
|
|
525
582
|
return this.sendHandshake(context);
|
|
526
583
|
},
|
|
584
|
+
settleHandshake: function() {
|
|
585
|
+
// Deliver the latest size requested while no host origin was trusted.
|
|
586
|
+
this.handshakeSettled = true;
|
|
587
|
+
var pending = this.pendingSize;
|
|
588
|
+
if (!pending) return;
|
|
589
|
+
this.pendingSize = null;
|
|
590
|
+
this.sendRequest('ui/setSize', pending.params).then(function(result) {
|
|
591
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
592
|
+
}, function(err) {
|
|
593
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
594
|
+
});
|
|
595
|
+
},
|
|
527
596
|
callTool: function(context, name, args) {
|
|
528
597
|
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
529
598
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
@@ -547,11 +616,22 @@ var ExtAppsAdapter = {
|
|
|
547
616
|
setSize: function(context, size) {
|
|
548
617
|
// FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
|
|
549
618
|
// measured/desired widget dimensions to the host.
|
|
550
|
-
|
|
619
|
+
var params = {
|
|
551
620
|
height: size && size.height,
|
|
552
621
|
width: size && size.width,
|
|
553
622
|
aspectRatio: size && size.aspectRatio
|
|
554
|
-
}
|
|
623
|
+
};
|
|
624
|
+
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
625
|
+
// No host origin yet: keep only the latest size and send it when the
|
|
626
|
+
// handshake settles; every caller's promise follows that one request.
|
|
627
|
+
var self = this;
|
|
628
|
+
if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
|
|
629
|
+
this.pendingSize.params = params;
|
|
630
|
+
return new Promise(function(resolve, reject) {
|
|
631
|
+
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return this.sendRequest('ui/setSize', params);
|
|
555
635
|
},
|
|
556
636
|
requestClose: function(context) {
|
|
557
637
|
return this.sendRequest('ui/close', {});
|
|
@@ -797,6 +877,7 @@ function FrontMcpBridge() {
|
|
|
797
877
|
toolResultListeners: [],
|
|
798
878
|
notifyContextChange: function(changes) {
|
|
799
879
|
Object.assign(this.hostContext, changes);
|
|
880
|
+
if (changes && changes.theme) __applyHostTheme(changes.theme);
|
|
800
881
|
for (var i = 0; i < this.contextListeners.length; i++) {
|
|
801
882
|
try { this.contextListeners[i](changes); } catch(e) {}
|
|
802
883
|
}
|
package/esm/component/index.mjs
CHANGED
|
@@ -469,8 +469,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
|
|
|
469
469
|
}
|
|
470
470
|
|
|
471
471
|
// libs/uipack/src/component/transpiler.ts
|
|
472
|
+
function loadEsbuild(filename) {
|
|
473
|
+
try {
|
|
474
|
+
return __require("esbuild");
|
|
475
|
+
} catch (err) {
|
|
476
|
+
if (!isMissingEsbuild(err)) throw err;
|
|
477
|
+
const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
|
|
478
|
+
throw new Error(
|
|
479
|
+
`${subject} needs esbuild, which is not installed. @frontmcp/uipack loads esbuild on demand to bundle FileSource widgets (.tsx/.jsx) when the tool is called, so it must be installed where the server runs: \`npm install esbuild\` (in "dependencies", not "devDependencies"). Projects created with \`frontmcp create\` already get it through the \`frontmcp\` package.`,
|
|
480
|
+
{ cause: err }
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function isMissingEsbuild(err) {
|
|
485
|
+
if (!(err instanceof Error)) return false;
|
|
486
|
+
return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
|
|
487
|
+
}
|
|
472
488
|
function transpileReactSource(source, filename) {
|
|
473
|
-
const esbuild =
|
|
489
|
+
const esbuild = loadEsbuild(filename);
|
|
474
490
|
const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
|
|
475
491
|
const result = esbuild.transformSync(source, {
|
|
476
492
|
loader,
|
|
@@ -489,7 +505,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
|
|
|
489
505
|
`FileSource widget "${filename}" requires the @frontmcp/ui package, which provides the React bridge mount that's injected at bundle time. Install it (e.g. \`npm install @frontmcp/ui\` or \`yarn add @frontmcp/ui\`) and try again.`
|
|
490
506
|
);
|
|
491
507
|
}
|
|
492
|
-
const esbuild =
|
|
508
|
+
const esbuild = loadEsbuild(filename);
|
|
493
509
|
const mountCode = `
|
|
494
510
|
// --- Auto-generated mount ---
|
|
495
511
|
import { createElement as __h } from 'react';
|
|
@@ -586,7 +602,8 @@ if (__root) {
|
|
|
586
602
|
} catch (err) {
|
|
587
603
|
const message = err instanceof Error ? err.message : String(err);
|
|
588
604
|
throw new Error(
|
|
589
|
-
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project
|
|
605
|
+
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
|
|
606
|
+
{ cause: err }
|
|
590
607
|
);
|
|
591
608
|
}
|
|
592
609
|
}
|
|
@@ -648,6 +665,37 @@ function resolveImportSource(source) {
|
|
|
648
665
|
peerDependencies: []
|
|
649
666
|
};
|
|
650
667
|
}
|
|
668
|
+
var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
|
|
669
|
+
function missingWidgetError(filePath, cause) {
|
|
670
|
+
const sourceWidget = findSourceWidget(filePath);
|
|
671
|
+
const error = new Error(
|
|
672
|
+
`FileSource widget not found (ENOENT): no file at "${filePath}". The widget source is read from this path when the tool is called, so it has to exist where the compiled tool runs \u2014 a path built from \`__dirname\` or \`import.meta.url\` points into the build output once the tool is compiled. tsc does not copy \`*.widget.tsx\` / \`*.widget.jsx\` files into its output; \`frontmcp build\` does, and any other build needs a step that copies them next to the compiled tool.` + (sourceWidget ? ` The source widget exists at "${sourceWidget}" \u2014 copy it to "${filePath}", or build with \`frontmcp build\`.` : ""),
|
|
673
|
+
{ cause }
|
|
674
|
+
);
|
|
675
|
+
error.code = "ENOENT";
|
|
676
|
+
error.path = filePath;
|
|
677
|
+
return error;
|
|
678
|
+
}
|
|
679
|
+
function findSourceWidget(filePath) {
|
|
680
|
+
const path = __require("path");
|
|
681
|
+
const fs = __require("fs");
|
|
682
|
+
const parts = filePath.split(path.sep);
|
|
683
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
684
|
+
if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
|
|
685
|
+
const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
|
|
686
|
+
if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
|
|
687
|
+
candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
|
|
688
|
+
for (const candidate of candidates) {
|
|
689
|
+
const candidatePath = candidate.join(path.sep);
|
|
690
|
+
try {
|
|
691
|
+
if (fs.existsSync(candidatePath)) return candidatePath;
|
|
692
|
+
} catch {
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return void 0;
|
|
696
|
+
}
|
|
697
|
+
return void 0;
|
|
698
|
+
}
|
|
651
699
|
function resolveFileSource(source, options = {}) {
|
|
652
700
|
const path = __require("path");
|
|
653
701
|
const ext = path.extname(source.file).toLowerCase();
|
|
@@ -662,9 +710,11 @@ function resolveFileSource(source, options = {}) {
|
|
|
662
710
|
const isNotFound = err?.code === "ENOENT";
|
|
663
711
|
if (isNotFound && wasRelative) {
|
|
664
712
|
throw new Error(
|
|
665
|
-
`FileSource widget "${source.file}" not found at "${filePath}". Relative paths are resolved against process.cwd() ("${process.cwd()}"), not the tool file's directory. To anchor the path to the tool file, pass an absolute path \u2014 e.g. \`{ file: fileURLToPath(new URL('./widget.tsx', import.meta.url)) }\` from \`node:url\` (see issue #444)
|
|
713
|
+
`FileSource widget "${source.file}" not found at "${filePath}". Relative paths are resolved against process.cwd() ("${process.cwd()}"), not the tool file's directory. To anchor the path to the tool file, pass an absolute path \u2014 e.g. \`{ file: fileURLToPath(new URL('./widget.tsx', import.meta.url)) }\` from \`node:url\` (see issue #444).`,
|
|
714
|
+
{ cause: err }
|
|
666
715
|
);
|
|
667
716
|
}
|
|
717
|
+
if (isNotFound) throw missingWidgetError(filePath, err);
|
|
668
718
|
throw err;
|
|
669
719
|
}
|
|
670
720
|
const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
|
|
@@ -882,6 +932,23 @@ function __applySizingCss(sizing) {
|
|
|
882
932
|
}
|
|
883
933
|
}
|
|
884
934
|
|
|
935
|
+
function __measureHeight() {
|
|
936
|
+
// <html> at fit-content includes body margins and collapsed child margins;
|
|
937
|
+
// content overflowing a fixed-height body is added; a px max-height clamps.
|
|
938
|
+
var de = document.documentElement;
|
|
939
|
+
var body = document.body;
|
|
940
|
+
if (!de || !body) return 0;
|
|
941
|
+
var prev = de.style.height;
|
|
942
|
+
de.style.height = 'fit-content';
|
|
943
|
+
var dr = de.getBoundingClientRect();
|
|
944
|
+
var br = body.getBoundingClientRect();
|
|
945
|
+
var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
|
|
946
|
+
de.style.height = prev;
|
|
947
|
+
var maxHeight = getComputedStyle(de).maxHeight;
|
|
948
|
+
var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
|
|
949
|
+
return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
|
|
950
|
+
}
|
|
951
|
+
|
|
885
952
|
function __initAutoResize() {
|
|
886
953
|
if (typeof window === 'undefined') return;
|
|
887
954
|
// Idempotent: a re-injected IIFE must not stack observers.
|
|
@@ -904,23 +971,30 @@ function __initAutoResize() {
|
|
|
904
971
|
if (typeof ResizeObserver === 'undefined') return;
|
|
905
972
|
|
|
906
973
|
function startObserving() {
|
|
907
|
-
|
|
908
|
-
|
|
974
|
+
if (!document.body) return;
|
|
975
|
+
var targets = [document.documentElement, document.body];
|
|
976
|
+
var root = document.getElementById('root');
|
|
977
|
+
if (root) targets.push(root);
|
|
909
978
|
|
|
910
979
|
var rafId = null;
|
|
911
980
|
var lastReported = -1;
|
|
912
981
|
function report() {
|
|
913
982
|
rafId = null;
|
|
914
983
|
try {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
984
|
+
// Before the handshake an ext-apps host can't receive the report; the
|
|
985
|
+
// bridge:ready listener below sends it once the bridge is up.
|
|
986
|
+
if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
|
|
987
|
+
var height = __measureHeight();
|
|
988
|
+
var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
|
|
918
989
|
if (height === lastReported || height <= 0) return;
|
|
919
990
|
lastReported = height;
|
|
920
991
|
var payload = { height: height, width: width };
|
|
921
992
|
if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
|
|
922
993
|
if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
|
|
923
|
-
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
994
|
+
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
995
|
+
// Forget a rejected report so the same height is sent again.
|
|
996
|
+
if (lastReported === height) lastReported = -1;
|
|
997
|
+
});
|
|
924
998
|
}
|
|
925
999
|
window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
|
|
926
1000
|
} catch (e) {}
|
|
@@ -941,9 +1015,17 @@ function __initAutoResize() {
|
|
|
941
1015
|
window.__mcpResizeObserver.disconnect();
|
|
942
1016
|
}
|
|
943
1017
|
var ro = new ResizeObserver(function() { schedule(); });
|
|
944
|
-
ro.observe(
|
|
1018
|
+
for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
|
|
945
1019
|
window.__mcpResizeObserver = ro;
|
|
946
1020
|
} catch (e) {}
|
|
1021
|
+
// Report once the bridge is ready (the report scheduled below is skipped
|
|
1022
|
+
// while the handshake is still pending).
|
|
1023
|
+
var readySent = false;
|
|
1024
|
+
window.addEventListener('bridge:ready', function() {
|
|
1025
|
+
if (readySent) return;
|
|
1026
|
+
readySent = true;
|
|
1027
|
+
schedule();
|
|
1028
|
+
});
|
|
947
1029
|
// Report once on init so the host gets an initial measurement.
|
|
948
1030
|
schedule();
|
|
949
1031
|
}
|
|
@@ -991,6 +1073,25 @@ function detectViewport() {
|
|
|
991
1073
|
return undefined;
|
|
992
1074
|
}
|
|
993
1075
|
|
|
1076
|
+
function __applyHostTheme(theme) {
|
|
1077
|
+
// Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
|
|
1078
|
+
// canvas and form controls (a widget's own :root color-scheme still wins),
|
|
1079
|
+
// <html data-theme> is a styling hook. The OS fallback is never written.
|
|
1080
|
+
if (theme !== 'light' && theme !== 'dark') return;
|
|
1081
|
+
if (typeof document === 'undefined' || !document.documentElement) return;
|
|
1082
|
+
try {
|
|
1083
|
+
var meta = document.querySelector('meta[name="color-scheme"]');
|
|
1084
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
1085
|
+
if (!meta && head) {
|
|
1086
|
+
meta = document.createElement('meta');
|
|
1087
|
+
meta.setAttribute('name', 'color-scheme');
|
|
1088
|
+
head.appendChild(meta);
|
|
1089
|
+
}
|
|
1090
|
+
if (meta) meta.setAttribute('content', theme);
|
|
1091
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
1092
|
+
} catch (e) {}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
994
1095
|
function readInjectedData() {
|
|
995
1096
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
996
1097
|
if (typeof window !== 'undefined') {
|
|
@@ -1097,6 +1198,8 @@ var ExtAppsAdapter = {
|
|
|
1097
1198
|
pendingRequests: {},
|
|
1098
1199
|
requestId: 0,
|
|
1099
1200
|
hostCapabilities: {},
|
|
1201
|
+
handshakeSettled: false,
|
|
1202
|
+
pendingSize: null,
|
|
1100
1203
|
canHandle: function() {
|
|
1101
1204
|
if (typeof window === 'undefined') return false;
|
|
1102
1205
|
if (window.parent === window) return false;
|
|
@@ -1262,6 +1365,7 @@ var ExtAppsAdapter = {
|
|
|
1262
1365
|
var timeout = setTimeout(function() {
|
|
1263
1366
|
delete self.pendingRequests[id];
|
|
1264
1367
|
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
1368
|
+
self.settleHandshake();
|
|
1265
1369
|
resolve();
|
|
1266
1370
|
}, 10000);
|
|
1267
1371
|
|
|
@@ -1275,7 +1379,9 @@ var ExtAppsAdapter = {
|
|
|
1275
1379
|
supportsDisplayModes: true
|
|
1276
1380
|
});
|
|
1277
1381
|
if (result.hostContext) {
|
|
1278
|
-
|
|
1382
|
+
// Notify (not a silent merge) so onContextChange listeners and the
|
|
1383
|
+
// host theme see the handshake's context.
|
|
1384
|
+
context.notifyContextChange(result.hostContext);
|
|
1279
1385
|
}
|
|
1280
1386
|
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
1281
1387
|
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
@@ -1285,9 +1391,10 @@ var ExtAppsAdapter = {
|
|
|
1285
1391
|
method: 'ui/notifications/initialized',
|
|
1286
1392
|
params: {}
|
|
1287
1393
|
}, targetOrigin);
|
|
1394
|
+
self.settleHandshake();
|
|
1288
1395
|
resolve();
|
|
1289
1396
|
},
|
|
1290
|
-
reject: function(err) { resolve(); }, // Non-fatal
|
|
1397
|
+
reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
|
|
1291
1398
|
timeout: timeout
|
|
1292
1399
|
};
|
|
1293
1400
|
|
|
@@ -1301,6 +1408,18 @@ var ExtAppsAdapter = {
|
|
|
1301
1408
|
performHandshake: function(context) {
|
|
1302
1409
|
return this.sendHandshake(context);
|
|
1303
1410
|
},
|
|
1411
|
+
settleHandshake: function() {
|
|
1412
|
+
// Deliver the latest size requested while no host origin was trusted.
|
|
1413
|
+
this.handshakeSettled = true;
|
|
1414
|
+
var pending = this.pendingSize;
|
|
1415
|
+
if (!pending) return;
|
|
1416
|
+
this.pendingSize = null;
|
|
1417
|
+
this.sendRequest('ui/setSize', pending.params).then(function(result) {
|
|
1418
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
1419
|
+
}, function(err) {
|
|
1420
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
1421
|
+
});
|
|
1422
|
+
},
|
|
1304
1423
|
callTool: function(context, name, args) {
|
|
1305
1424
|
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
1306
1425
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
@@ -1324,11 +1443,22 @@ var ExtAppsAdapter = {
|
|
|
1324
1443
|
setSize: function(context, size) {
|
|
1325
1444
|
// FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
|
|
1326
1445
|
// measured/desired widget dimensions to the host.
|
|
1327
|
-
|
|
1446
|
+
var params = {
|
|
1328
1447
|
height: size && size.height,
|
|
1329
1448
|
width: size && size.width,
|
|
1330
1449
|
aspectRatio: size && size.aspectRatio
|
|
1331
|
-
}
|
|
1450
|
+
};
|
|
1451
|
+
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
1452
|
+
// No host origin yet: keep only the latest size and send it when the
|
|
1453
|
+
// handshake settles; every caller's promise follows that one request.
|
|
1454
|
+
var self = this;
|
|
1455
|
+
if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
|
|
1456
|
+
this.pendingSize.params = params;
|
|
1457
|
+
return new Promise(function(resolve, reject) {
|
|
1458
|
+
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
return this.sendRequest('ui/setSize', params);
|
|
1332
1462
|
},
|
|
1333
1463
|
requestClose: function(context) {
|
|
1334
1464
|
return this.sendRequest('ui/close', {});
|
|
@@ -1574,6 +1704,7 @@ function FrontMcpBridge() {
|
|
|
1574
1704
|
toolResultListeners: [],
|
|
1575
1705
|
notifyContextChange: function(changes) {
|
|
1576
1706
|
Object.assign(this.hostContext, changes);
|
|
1707
|
+
if (changes && changes.theme) __applyHostTheme(changes.theme);
|
|
1577
1708
|
for (var i = 0; i < this.contextListeners.length; i++) {
|
|
1578
1709
|
try { this.contextListeners[i](changes); } catch(e) {}
|
|
1579
1710
|
}
|