@frontmcp/uipack 1.8.4 → 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
package/adapters/index.js
CHANGED
|
@@ -669,6 +669,23 @@ function __applySizingCss(sizing) {
|
|
|
669
669
|
}
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
function __measureHeight() {
|
|
673
|
+
// <html> at fit-content includes body margins and collapsed child margins;
|
|
674
|
+
// content overflowing a fixed-height body is added; a px max-height clamps.
|
|
675
|
+
var de = document.documentElement;
|
|
676
|
+
var body = document.body;
|
|
677
|
+
if (!de || !body) return 0;
|
|
678
|
+
var prev = de.style.height;
|
|
679
|
+
de.style.height = 'fit-content';
|
|
680
|
+
var dr = de.getBoundingClientRect();
|
|
681
|
+
var br = body.getBoundingClientRect();
|
|
682
|
+
var h = Math.max(dr.height, br.top + body.scrollHeight + Math.max(0, dr.bottom - br.bottom) - dr.top);
|
|
683
|
+
de.style.height = prev;
|
|
684
|
+
var maxHeight = getComputedStyle(de).maxHeight;
|
|
685
|
+
var mx = maxHeight && maxHeight.slice(-2) === 'px' ? parseFloat(maxHeight) : NaN;
|
|
686
|
+
return Math.ceil(isNaN(mx) ? h : Math.min(h, mx));
|
|
687
|
+
}
|
|
688
|
+
|
|
672
689
|
function __initAutoResize() {
|
|
673
690
|
if (typeof window === 'undefined') return;
|
|
674
691
|
// Idempotent: a re-injected IIFE must not stack observers.
|
|
@@ -691,23 +708,30 @@ function __initAutoResize() {
|
|
|
691
708
|
if (typeof ResizeObserver === 'undefined') return;
|
|
692
709
|
|
|
693
710
|
function startObserving() {
|
|
694
|
-
|
|
695
|
-
|
|
711
|
+
if (!document.body) return;
|
|
712
|
+
var targets = [document.documentElement, document.body];
|
|
713
|
+
var root = document.getElementById('root');
|
|
714
|
+
if (root) targets.push(root);
|
|
696
715
|
|
|
697
716
|
var rafId = null;
|
|
698
717
|
var lastReported = -1;
|
|
699
718
|
function report() {
|
|
700
719
|
rafId = null;
|
|
701
720
|
try {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
721
|
+
// Before the handshake an ext-apps host can't receive the report; the
|
|
722
|
+
// bridge:ready listener below sends it once the bridge is up.
|
|
723
|
+
if (window.FrontMcpBridge && window.FrontMcpBridge.initialized === false) return;
|
|
724
|
+
var height = __measureHeight();
|
|
725
|
+
var width = Math.ceil(document.documentElement.getBoundingClientRect().width);
|
|
705
726
|
if (height === lastReported || height <= 0) return;
|
|
706
727
|
lastReported = height;
|
|
707
728
|
var payload = { height: height, width: width };
|
|
708
729
|
if (sizing.aspectRatio != null) payload.aspectRatio = sizing.aspectRatio;
|
|
709
730
|
if (window.FrontMcpBridge && typeof window.FrontMcpBridge.setSize === 'function') {
|
|
710
|
-
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
731
|
+
window.FrontMcpBridge.setSize(payload).catch(function() {
|
|
732
|
+
// Forget a rejected report so the same height is sent again.
|
|
733
|
+
if (lastReported === height) lastReported = -1;
|
|
734
|
+
});
|
|
711
735
|
}
|
|
712
736
|
window.dispatchEvent(new CustomEvent('widget:resize', { detail: payload }));
|
|
713
737
|
} catch (e) {}
|
|
@@ -728,9 +752,17 @@ function __initAutoResize() {
|
|
|
728
752
|
window.__mcpResizeObserver.disconnect();
|
|
729
753
|
}
|
|
730
754
|
var ro = new ResizeObserver(function() { schedule(); });
|
|
731
|
-
ro.observe(
|
|
755
|
+
for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
|
|
732
756
|
window.__mcpResizeObserver = ro;
|
|
733
757
|
} catch (e) {}
|
|
758
|
+
// Report once the bridge is ready (the report scheduled below is skipped
|
|
759
|
+
// while the handshake is still pending).
|
|
760
|
+
var readySent = false;
|
|
761
|
+
window.addEventListener('bridge:ready', function() {
|
|
762
|
+
if (readySent) return;
|
|
763
|
+
readySent = true;
|
|
764
|
+
schedule();
|
|
765
|
+
});
|
|
734
766
|
// Report once on init so the host gets an initial measurement.
|
|
735
767
|
schedule();
|
|
736
768
|
}
|
|
@@ -778,6 +810,25 @@ function detectViewport() {
|
|
|
778
810
|
return undefined;
|
|
779
811
|
}
|
|
780
812
|
|
|
813
|
+
function __applyHostTheme(theme) {
|
|
814
|
+
// Follow a host-supplied theme: <meta name="color-scheme"> sets the frame's
|
|
815
|
+
// canvas and form controls (a widget's own :root color-scheme still wins),
|
|
816
|
+
// <html data-theme> is a styling hook. The OS fallback is never written.
|
|
817
|
+
if (theme !== 'light' && theme !== 'dark') return;
|
|
818
|
+
if (typeof document === 'undefined' || !document.documentElement) return;
|
|
819
|
+
try {
|
|
820
|
+
var meta = document.querySelector('meta[name="color-scheme"]');
|
|
821
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
822
|
+
if (!meta && head) {
|
|
823
|
+
meta = document.createElement('meta');
|
|
824
|
+
meta.setAttribute('name', 'color-scheme');
|
|
825
|
+
head.appendChild(meta);
|
|
826
|
+
}
|
|
827
|
+
if (meta) meta.setAttribute('content', theme);
|
|
828
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
829
|
+
} catch (e) {}
|
|
830
|
+
}
|
|
831
|
+
|
|
781
832
|
function readInjectedData() {
|
|
782
833
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
783
834
|
if (typeof window !== 'undefined') {
|
|
@@ -884,6 +935,8 @@ var ExtAppsAdapter = {
|
|
|
884
935
|
pendingRequests: {},
|
|
885
936
|
requestId: 0,
|
|
886
937
|
hostCapabilities: {},
|
|
938
|
+
handshakeSettled: false,
|
|
939
|
+
pendingSize: null,
|
|
887
940
|
canHandle: function() {
|
|
888
941
|
if (typeof window === 'undefined') return false;
|
|
889
942
|
if (window.parent === window) return false;
|
|
@@ -1049,6 +1102,7 @@ var ExtAppsAdapter = {
|
|
|
1049
1102
|
var timeout = setTimeout(function() {
|
|
1050
1103
|
delete self.pendingRequests[id];
|
|
1051
1104
|
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
1105
|
+
self.settleHandshake();
|
|
1052
1106
|
resolve();
|
|
1053
1107
|
}, 10000);
|
|
1054
1108
|
|
|
@@ -1062,7 +1116,9 @@ var ExtAppsAdapter = {
|
|
|
1062
1116
|
supportsDisplayModes: true
|
|
1063
1117
|
});
|
|
1064
1118
|
if (result.hostContext) {
|
|
1065
|
-
|
|
1119
|
+
// Notify (not a silent merge) so onContextChange listeners and the
|
|
1120
|
+
// host theme see the handshake's context.
|
|
1121
|
+
context.notifyContextChange(result.hostContext);
|
|
1066
1122
|
}
|
|
1067
1123
|
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
1068
1124
|
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
@@ -1072,9 +1128,10 @@ var ExtAppsAdapter = {
|
|
|
1072
1128
|
method: 'ui/notifications/initialized',
|
|
1073
1129
|
params: {}
|
|
1074
1130
|
}, targetOrigin);
|
|
1131
|
+
self.settleHandshake();
|
|
1075
1132
|
resolve();
|
|
1076
1133
|
},
|
|
1077
|
-
reject: function(err) { resolve(); }, // Non-fatal
|
|
1134
|
+
reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
|
|
1078
1135
|
timeout: timeout
|
|
1079
1136
|
};
|
|
1080
1137
|
|
|
@@ -1088,6 +1145,18 @@ var ExtAppsAdapter = {
|
|
|
1088
1145
|
performHandshake: function(context) {
|
|
1089
1146
|
return this.sendHandshake(context);
|
|
1090
1147
|
},
|
|
1148
|
+
settleHandshake: function() {
|
|
1149
|
+
// Deliver the latest size requested while no host origin was trusted.
|
|
1150
|
+
this.handshakeSettled = true;
|
|
1151
|
+
var pending = this.pendingSize;
|
|
1152
|
+
if (!pending) return;
|
|
1153
|
+
this.pendingSize = null;
|
|
1154
|
+
this.sendRequest('ui/setSize', pending.params).then(function(result) {
|
|
1155
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
1156
|
+
}, function(err) {
|
|
1157
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
1158
|
+
});
|
|
1159
|
+
},
|
|
1091
1160
|
callTool: function(context, name, args) {
|
|
1092
1161
|
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
1093
1162
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
@@ -1111,11 +1180,22 @@ var ExtAppsAdapter = {
|
|
|
1111
1180
|
setSize: function(context, size) {
|
|
1112
1181
|
// FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
|
|
1113
1182
|
// measured/desired widget dimensions to the host.
|
|
1114
|
-
|
|
1183
|
+
var params = {
|
|
1115
1184
|
height: size && size.height,
|
|
1116
1185
|
width: size && size.width,
|
|
1117
1186
|
aspectRatio: size && size.aspectRatio
|
|
1118
|
-
}
|
|
1187
|
+
};
|
|
1188
|
+
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
1189
|
+
// No host origin yet: keep only the latest size and send it when the
|
|
1190
|
+
// handshake settles; every caller's promise follows that one request.
|
|
1191
|
+
var self = this;
|
|
1192
|
+
if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
|
|
1193
|
+
this.pendingSize.params = params;
|
|
1194
|
+
return new Promise(function(resolve, reject) {
|
|
1195
|
+
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
return this.sendRequest('ui/setSize', params);
|
|
1119
1199
|
},
|
|
1120
1200
|
requestClose: function(context) {
|
|
1121
1201
|
return this.sendRequest('ui/close', {});
|
|
@@ -1361,6 +1441,7 @@ function FrontMcpBridge() {
|
|
|
1361
1441
|
toolResultListeners: [],
|
|
1362
1442
|
notifyContextChange: function(changes) {
|
|
1363
1443
|
Object.assign(this.hostContext, changes);
|
|
1444
|
+
if (changes && changes.theme) __applyHostTheme(changes.theme);
|
|
1364
1445
|
for (var i = 0; i < this.contextListeners.length; i++) {
|
|
1365
1446
|
try { this.contextListeners[i](changes); } catch(e) {}
|
|
1366
1447
|
}
|
|
@@ -2145,8 +2226,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
|
|
|
2145
2226
|
}
|
|
2146
2227
|
|
|
2147
2228
|
// libs/uipack/src/component/transpiler.ts
|
|
2229
|
+
function loadEsbuild(filename) {
|
|
2230
|
+
try {
|
|
2231
|
+
return require("esbuild");
|
|
2232
|
+
} catch (err) {
|
|
2233
|
+
if (!isMissingEsbuild(err)) throw err;
|
|
2234
|
+
const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
|
|
2235
|
+
throw new Error(
|
|
2236
|
+
`${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.`,
|
|
2237
|
+
{ cause: err }
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function isMissingEsbuild(err) {
|
|
2242
|
+
if (!(err instanceof Error)) return false;
|
|
2243
|
+
return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
|
|
2244
|
+
}
|
|
2148
2245
|
function transpileReactSource(source, filename) {
|
|
2149
|
-
const esbuild =
|
|
2246
|
+
const esbuild = loadEsbuild(filename);
|
|
2150
2247
|
const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
|
|
2151
2248
|
const result = esbuild.transformSync(source, {
|
|
2152
2249
|
loader,
|
|
@@ -2165,7 +2262,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
|
|
|
2165
2262
|
`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.`
|
|
2166
2263
|
);
|
|
2167
2264
|
}
|
|
2168
|
-
const esbuild =
|
|
2265
|
+
const esbuild = loadEsbuild(filename);
|
|
2169
2266
|
const mountCode = `
|
|
2170
2267
|
// --- Auto-generated mount ---
|
|
2171
2268
|
import { createElement as __h } from 'react';
|
|
@@ -2262,7 +2359,8 @@ if (__root) {
|
|
|
2262
2359
|
} catch (err) {
|
|
2263
2360
|
const message = err instanceof Error ? err.message : String(err);
|
|
2264
2361
|
throw new Error(
|
|
2265
|
-
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project
|
|
2362
|
+
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
|
|
2363
|
+
{ cause: err }
|
|
2266
2364
|
);
|
|
2267
2365
|
}
|
|
2268
2366
|
}
|
|
@@ -2338,6 +2436,37 @@ function resolveImportSource(source) {
|
|
|
2338
2436
|
peerDependencies: []
|
|
2339
2437
|
};
|
|
2340
2438
|
}
|
|
2439
|
+
var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
|
|
2440
|
+
function missingWidgetError(filePath, cause) {
|
|
2441
|
+
const sourceWidget = findSourceWidget(filePath);
|
|
2442
|
+
const error = new Error(
|
|
2443
|
+
`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\`.` : ""),
|
|
2444
|
+
{ cause }
|
|
2445
|
+
);
|
|
2446
|
+
error.code = "ENOENT";
|
|
2447
|
+
error.path = filePath;
|
|
2448
|
+
return error;
|
|
2449
|
+
}
|
|
2450
|
+
function findSourceWidget(filePath) {
|
|
2451
|
+
const path = require("path");
|
|
2452
|
+
const fs = require("fs");
|
|
2453
|
+
const parts = filePath.split(path.sep);
|
|
2454
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
2455
|
+
if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
|
|
2456
|
+
const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
|
|
2457
|
+
if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
|
|
2458
|
+
candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
|
|
2459
|
+
for (const candidate of candidates) {
|
|
2460
|
+
const candidatePath = candidate.join(path.sep);
|
|
2461
|
+
try {
|
|
2462
|
+
if (fs.existsSync(candidatePath)) return candidatePath;
|
|
2463
|
+
} catch {
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return void 0;
|
|
2467
|
+
}
|
|
2468
|
+
return void 0;
|
|
2469
|
+
}
|
|
2341
2470
|
function resolveFileSource(source, options = {}) {
|
|
2342
2471
|
const path = require("path");
|
|
2343
2472
|
const ext = path.extname(source.file).toLowerCase();
|
|
@@ -2352,9 +2481,11 @@ function resolveFileSource(source, options = {}) {
|
|
|
2352
2481
|
const isNotFound = err?.code === "ENOENT";
|
|
2353
2482
|
if (isNotFound && wasRelative) {
|
|
2354
2483
|
throw new Error(
|
|
2355
|
-
`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)
|
|
2484
|
+
`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).`,
|
|
2485
|
+
{ cause: err }
|
|
2356
2486
|
);
|
|
2357
2487
|
}
|
|
2488
|
+
if (isNotFound) throw missingWidgetError(filePath, err);
|
|
2358
2489
|
throw err;
|
|
2359
2490
|
}
|
|
2360
2491
|
const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iife-generator.d.ts","sourceRoot":"","sources":["../../src/bridge-runtime/iife-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC;IACvE,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,wBAAwB;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CA8G7E;
|
|
1
|
+
{"version":3,"file":"iife-generator.d.ts","sourceRoot":"","sources":["../../src/bridge-runtime/iife-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC;IACvE,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,wBAAwB;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CA8G7E;AAguCD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,EACvD,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAM,GACnD,MAAM,CAYR;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAuB,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC"}
|
package/bridge-runtime/index.js
CHANGED
|
@@ -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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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(
|
|
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,6 +275,25 @@ 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
|
+
|
|
246
297
|
function readInjectedData() {
|
|
247
298
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
248
299
|
if (typeof window !== 'undefined') {
|
|
@@ -349,6 +400,8 @@ var ExtAppsAdapter = {
|
|
|
349
400
|
pendingRequests: {},
|
|
350
401
|
requestId: 0,
|
|
351
402
|
hostCapabilities: {},
|
|
403
|
+
handshakeSettled: false,
|
|
404
|
+
pendingSize: null,
|
|
352
405
|
canHandle: function() {
|
|
353
406
|
if (typeof window === 'undefined') return false;
|
|
354
407
|
if (window.parent === window) return false;
|
|
@@ -514,6 +567,7 @@ var ExtAppsAdapter = {
|
|
|
514
567
|
var timeout = setTimeout(function() {
|
|
515
568
|
delete self.pendingRequests[id];
|
|
516
569
|
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
570
|
+
self.settleHandshake();
|
|
517
571
|
resolve();
|
|
518
572
|
}, 10000);
|
|
519
573
|
|
|
@@ -527,7 +581,9 @@ var ExtAppsAdapter = {
|
|
|
527
581
|
supportsDisplayModes: true
|
|
528
582
|
});
|
|
529
583
|
if (result.hostContext) {
|
|
530
|
-
|
|
584
|
+
// Notify (not a silent merge) so onContextChange listeners and the
|
|
585
|
+
// host theme see the handshake's context.
|
|
586
|
+
context.notifyContextChange(result.hostContext);
|
|
531
587
|
}
|
|
532
588
|
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
533
589
|
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
@@ -537,9 +593,10 @@ var ExtAppsAdapter = {
|
|
|
537
593
|
method: 'ui/notifications/initialized',
|
|
538
594
|
params: {}
|
|
539
595
|
}, targetOrigin);
|
|
596
|
+
self.settleHandshake();
|
|
540
597
|
resolve();
|
|
541
598
|
},
|
|
542
|
-
reject: function(err) { resolve(); }, // Non-fatal
|
|
599
|
+
reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
|
|
543
600
|
timeout: timeout
|
|
544
601
|
};
|
|
545
602
|
|
|
@@ -553,6 +610,18 @@ var ExtAppsAdapter = {
|
|
|
553
610
|
performHandshake: function(context) {
|
|
554
611
|
return this.sendHandshake(context);
|
|
555
612
|
},
|
|
613
|
+
settleHandshake: function() {
|
|
614
|
+
// Deliver the latest size requested while no host origin was trusted.
|
|
615
|
+
this.handshakeSettled = true;
|
|
616
|
+
var pending = this.pendingSize;
|
|
617
|
+
if (!pending) return;
|
|
618
|
+
this.pendingSize = null;
|
|
619
|
+
this.sendRequest('ui/setSize', pending.params).then(function(result) {
|
|
620
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
621
|
+
}, function(err) {
|
|
622
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
623
|
+
});
|
|
624
|
+
},
|
|
556
625
|
callTool: function(context, name, args) {
|
|
557
626
|
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
558
627
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
@@ -576,11 +645,22 @@ var ExtAppsAdapter = {
|
|
|
576
645
|
setSize: function(context, size) {
|
|
577
646
|
// FrontMCP sizing channel \u2014 parallels ui/setDisplayMode. Reports the
|
|
578
647
|
// measured/desired widget dimensions to the host.
|
|
579
|
-
|
|
648
|
+
var params = {
|
|
580
649
|
height: size && size.height,
|
|
581
650
|
width: size && size.width,
|
|
582
651
|
aspectRatio: size && size.aspectRatio
|
|
583
|
-
}
|
|
652
|
+
};
|
|
653
|
+
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
654
|
+
// No host origin yet: keep only the latest size and send it when the
|
|
655
|
+
// handshake settles; every caller's promise follows that one request.
|
|
656
|
+
var self = this;
|
|
657
|
+
if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
|
|
658
|
+
this.pendingSize.params = params;
|
|
659
|
+
return new Promise(function(resolve, reject) {
|
|
660
|
+
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
return this.sendRequest('ui/setSize', params);
|
|
584
664
|
},
|
|
585
665
|
requestClose: function(context) {
|
|
586
666
|
return this.sendRequest('ui/close', {});
|
|
@@ -826,6 +906,7 @@ function FrontMcpBridge() {
|
|
|
826
906
|
toolResultListeners: [],
|
|
827
907
|
notifyContextChange: function(changes) {
|
|
828
908
|
Object.assign(this.hostContext, changes);
|
|
909
|
+
if (changes && changes.theme) __applyHostTheme(changes.theme);
|
|
829
910
|
for (var i = 0; i < this.contextListeners.length; i++) {
|
|
830
911
|
try { this.contextListeners[i](changes); } catch(e) {}
|
|
831
912
|
}
|