@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.
- package/adapters/index.js +339 -35
- package/adapters/markdown.d.ts +9 -0
- package/adapters/markdown.d.ts.map +1 -0
- package/adapters/template-renderer.d.ts +8 -0
- package/adapters/template-renderer.d.ts.map +1 -1
- package/bridge-runtime/iife-generator.d.ts.map +1 -1
- package/bridge-runtime/index.js +169 -22
- package/component/index.js +223 -26
- package/component/loader.d.ts.map +1 -1
- package/component/transpiler.d.ts.map +1 -1
- package/esm/adapters/index.mjs +339 -35
- package/esm/bridge-runtime/index.mjs +169 -22
- package/esm/component/index.mjs +223 -26
- package/esm/index.mjs +339 -35
- package/esm/package.json +8 -2
- package/esm/shell/index.mjs +169 -22
- package/index.js +339 -35
- package/package.json +8 -2
- package/shell/index.js +169 -22
- package/types/ui-config.d.ts +5 -3
- 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,12 +810,57 @@ 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
|
+
|
|
832
|
+
function extractToolOutput(params) {
|
|
833
|
+
// A host sends a CallToolResult: prefer structuredContent, then parsed text, then the raw content.
|
|
834
|
+
if (!params) return undefined;
|
|
835
|
+
if (params.structuredContent !== undefined && params.structuredContent !== null) return params.structuredContent;
|
|
836
|
+
var content = params.content;
|
|
837
|
+
if (Array.isArray(content)) {
|
|
838
|
+
for (var i = 0; i < content.length; i++) {
|
|
839
|
+
var item = content[i];
|
|
840
|
+
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
841
|
+
try { return JSON.parse(item.text); } catch (e) { return item.text; }
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return content;
|
|
846
|
+
}
|
|
847
|
+
|
|
781
848
|
function readInjectedData() {
|
|
782
849
|
var data = { toolInput: {}, toolOutput: undefined, structuredContent: undefined };
|
|
783
850
|
if (typeof window !== 'undefined') {
|
|
784
851
|
if (window.__mcpToolInput) data.toolInput = window.__mcpToolInput;
|
|
785
852
|
if (window.__mcpToolOutput) data.toolOutput = window.__mcpToolOutput;
|
|
786
853
|
if (window.__mcpStructuredContent) data.structuredContent = window.__mcpStructuredContent;
|
|
854
|
+
// The OpenAI Apps SDK hands the widget its data as window.openai.toolInput / toolOutput
|
|
855
|
+
// (toolOutput is the tool's structuredContent).
|
|
856
|
+
var oa = window.openai;
|
|
857
|
+
if (oa && typeof oa === 'object') {
|
|
858
|
+
if (oa.toolInput && !window.__mcpToolInput) data.toolInput = oa.toolInput;
|
|
859
|
+
if (oa.toolOutput !== undefined && oa.toolOutput !== null) {
|
|
860
|
+
if (data.toolOutput === undefined) data.toolOutput = oa.toolOutput;
|
|
861
|
+
if (data.structuredContent === undefined) data.structuredContent = oa.toolOutput;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
787
864
|
}
|
|
788
865
|
return data;
|
|
789
866
|
}
|
|
@@ -832,7 +909,24 @@ var OpenAIAdapter = {
|
|
|
832
909
|
if (sdk.displayMode) {
|
|
833
910
|
context.hostContext.displayMode = sdk.displayMode;
|
|
834
911
|
}
|
|
835
|
-
//
|
|
912
|
+
// The Apps SDK re-dispatches the changed globals in an 'openai:set_globals' event.
|
|
913
|
+
window.addEventListener('openai:set_globals', function(event) {
|
|
914
|
+
var globals = (event && event.detail && event.detail.globals) || window.openai || {};
|
|
915
|
+
if (globals.toolInput !== undefined && globals.toolInput !== null) {
|
|
916
|
+
context.toolInput = globals.toolInput;
|
|
917
|
+
window.dispatchEvent(new CustomEvent('tool:input', { detail: { arguments: context.toolInput } }));
|
|
918
|
+
}
|
|
919
|
+
if (globals.toolOutput !== undefined && globals.toolOutput !== null) {
|
|
920
|
+
context.structuredContent = globals.toolOutput;
|
|
921
|
+
context.notifyToolResult(globals.toolOutput);
|
|
922
|
+
}
|
|
923
|
+
if (globals.theme && globals.theme !== context.hostContext.theme) {
|
|
924
|
+
context.notifyContextChange({ theme: globals.theme });
|
|
925
|
+
}
|
|
926
|
+
if (globals.displayMode && globals.displayMode !== context.hostContext.displayMode) {
|
|
927
|
+
context.notifyContextChange({ displayMode: globals.displayMode });
|
|
928
|
+
}
|
|
929
|
+
});
|
|
836
930
|
return Promise.resolve();
|
|
837
931
|
},
|
|
838
932
|
callTool: function(context, name, args) {
|
|
@@ -884,6 +978,8 @@ var ExtAppsAdapter = {
|
|
|
884
978
|
pendingRequests: {},
|
|
885
979
|
requestId: 0,
|
|
886
980
|
hostCapabilities: {},
|
|
981
|
+
handshakeSettled: false,
|
|
982
|
+
pendingSize: null,
|
|
887
983
|
canHandle: function() {
|
|
888
984
|
if (typeof window === 'undefined') return false;
|
|
889
985
|
if (window.parent === window) return false;
|
|
@@ -977,9 +1073,10 @@ var ExtAppsAdapter = {
|
|
|
977
1073
|
window.dispatchEvent(new CustomEvent('tool:input-partial', { detail: { arguments: context.toolInput } }));
|
|
978
1074
|
break;
|
|
979
1075
|
case 'ui/notifications/tool-result':
|
|
980
|
-
|
|
1076
|
+
var resultOutput = extractToolOutput(params);
|
|
1077
|
+
context.toolOutput = resultOutput;
|
|
981
1078
|
context.structuredContent = params.structuredContent;
|
|
982
|
-
context.notifyToolResult(
|
|
1079
|
+
context.notifyToolResult(resultOutput);
|
|
983
1080
|
window.dispatchEvent(new CustomEvent('tool:result', { detail: params }));
|
|
984
1081
|
break;
|
|
985
1082
|
case 'ui/notifications/host-context-changed':
|
|
@@ -1033,6 +1130,15 @@ var ExtAppsAdapter = {
|
|
|
1033
1130
|
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, targetOrigin);
|
|
1034
1131
|
});
|
|
1035
1132
|
},
|
|
1133
|
+
sendNotification: function(method, params) {
|
|
1134
|
+
// JSON-RPC notification: no id, no response. Same origin rule as sendRequest.
|
|
1135
|
+
if (!this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
1136
|
+
return Promise.reject(new Error('Cannot send notification: no trusted origin established'));
|
|
1137
|
+
}
|
|
1138
|
+
var targetOrigin = this.trustedOrigin || this.trustedOrigins[0];
|
|
1139
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params }, targetOrigin);
|
|
1140
|
+
return Promise.resolve();
|
|
1141
|
+
},
|
|
1036
1142
|
sendHandshake: function(context) {
|
|
1037
1143
|
// Send ui/initialize using '*' as target origin since TOFU hasn't
|
|
1038
1144
|
// been established yet. The response from the host will establish
|
|
@@ -1049,6 +1155,7 @@ var ExtAppsAdapter = {
|
|
|
1049
1155
|
var timeout = setTimeout(function() {
|
|
1050
1156
|
delete self.pendingRequests[id];
|
|
1051
1157
|
// Handshake timeout is non-fatal \u2014 notifications may still arrive
|
|
1158
|
+
self.settleHandshake();
|
|
1052
1159
|
resolve();
|
|
1053
1160
|
}, 10000);
|
|
1054
1161
|
|
|
@@ -1062,7 +1169,9 @@ var ExtAppsAdapter = {
|
|
|
1062
1169
|
supportsDisplayModes: true
|
|
1063
1170
|
});
|
|
1064
1171
|
if (result.hostContext) {
|
|
1065
|
-
|
|
1172
|
+
// Notify (not a silent merge) so onContextChange listeners and the
|
|
1173
|
+
// host theme see the handshake's context.
|
|
1174
|
+
context.notifyContextChange(result.hostContext);
|
|
1066
1175
|
}
|
|
1067
1176
|
// Send ui/notifications/initialized to tell the host the view is ready.
|
|
1068
1177
|
// Per MCP Apps spec, the host waits for this before sending tool-result.
|
|
@@ -1072,9 +1181,10 @@ var ExtAppsAdapter = {
|
|
|
1072
1181
|
method: 'ui/notifications/initialized',
|
|
1073
1182
|
params: {}
|
|
1074
1183
|
}, targetOrigin);
|
|
1184
|
+
self.settleHandshake();
|
|
1075
1185
|
resolve();
|
|
1076
1186
|
},
|
|
1077
|
-
reject: function(err) { resolve(); }, // Non-fatal
|
|
1187
|
+
reject: function(err) { self.settleHandshake(); resolve(); }, // Non-fatal
|
|
1078
1188
|
timeout: timeout
|
|
1079
1189
|
};
|
|
1080
1190
|
|
|
@@ -1088,6 +1198,18 @@ var ExtAppsAdapter = {
|
|
|
1088
1198
|
performHandshake: function(context) {
|
|
1089
1199
|
return this.sendHandshake(context);
|
|
1090
1200
|
},
|
|
1201
|
+
settleHandshake: function() {
|
|
1202
|
+
// Deliver the latest size requested while no host origin was trusted.
|
|
1203
|
+
this.handshakeSettled = true;
|
|
1204
|
+
var pending = this.pendingSize;
|
|
1205
|
+
if (!pending) return;
|
|
1206
|
+
this.pendingSize = null;
|
|
1207
|
+
this.sendNotification('ui/notifications/size-changed', pending.params).then(function(result) {
|
|
1208
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].resolve(result);
|
|
1209
|
+
}, function(err) {
|
|
1210
|
+
for (var i = 0; i < pending.waiters.length; i++) pending.waiters[i].reject(err);
|
|
1211
|
+
});
|
|
1212
|
+
},
|
|
1091
1213
|
callTool: function(context, name, args) {
|
|
1092
1214
|
if (!this.hostCapabilities.serverTools && !this.hostCapabilities.serverToolProxy) {
|
|
1093
1215
|
return Promise.reject(new Error('Server tool proxy not supported'));
|
|
@@ -1109,13 +1231,22 @@ var ExtAppsAdapter = {
|
|
|
1109
1231
|
return this.sendRequest('ui/setDisplayMode', { mode: mode });
|
|
1110
1232
|
},
|
|
1111
1233
|
setSize: function(context, size) {
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1234
|
+
// Standard ext-apps sizing: the view tells the host its dimensions with the
|
|
1235
|
+
// ui/notifications/size-changed notification ({ width?, height? } in px).
|
|
1236
|
+
var params = {};
|
|
1237
|
+
if (size && typeof size.width === 'number') params.width = size.width;
|
|
1238
|
+
if (size && typeof size.height === 'number') params.height = size.height;
|
|
1239
|
+
if (!this.handshakeSettled && !this.trustedOrigin && this.trustedOrigins.length === 0) {
|
|
1240
|
+
// No host origin yet: keep only the latest size and send it when the
|
|
1241
|
+
// handshake settles; every caller's promise follows that one request.
|
|
1242
|
+
var self = this;
|
|
1243
|
+
if (!this.pendingSize) this.pendingSize = { params: params, waiters: [] };
|
|
1244
|
+
this.pendingSize.params = params;
|
|
1245
|
+
return new Promise(function(resolve, reject) {
|
|
1246
|
+
self.pendingSize.waiters.push({ resolve: resolve, reject: reject });
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
return this.sendNotification('ui/notifications/size-changed', params);
|
|
1119
1250
|
},
|
|
1120
1251
|
requestClose: function(context) {
|
|
1121
1252
|
return this.sendRequest('ui/close', {});
|
|
@@ -1361,6 +1492,7 @@ function FrontMcpBridge() {
|
|
|
1361
1492
|
toolResultListeners: [],
|
|
1362
1493
|
notifyContextChange: function(changes) {
|
|
1363
1494
|
Object.assign(this.hostContext, changes);
|
|
1495
|
+
if (changes && changes.theme) __applyHostTheme(changes.theme);
|
|
1364
1496
|
for (var i = 0; i < this.contextListeners.length; i++) {
|
|
1365
1497
|
try { this.contextListeners[i](changes); } catch(e) {}
|
|
1366
1498
|
}
|
|
@@ -1537,7 +1669,7 @@ FrontMcpBridge.prototype.requestDisplayMode = function(mode) {
|
|
|
1537
1669
|
|
|
1538
1670
|
// Report a desired widget size to the host. \`size\` is { height?, width?, aspectRatio? }.
|
|
1539
1671
|
// Per-adapter behaviour: Claude/generic no-op (host measures the DOM),
|
|
1540
|
-
// ext-apps sends ui/
|
|
1672
|
+
// ext-apps sends ui/notifications/size-changed, OpenAI forwards to its SDK when available.
|
|
1541
1673
|
FrontMcpBridge.prototype.setSize = function(size) {
|
|
1542
1674
|
if (!this._adapter) return Promise.reject(new Error('Not initialized'));
|
|
1543
1675
|
if (!this._adapter.setSize) return Promise.resolve();
|
|
@@ -1639,7 +1771,7 @@ FrontMcpBridge.prototype._setupDataToolCallHandler = function() {
|
|
|
1639
1771
|
target.classList.add('opacity-50', 'cursor-not-allowed');
|
|
1640
1772
|
|
|
1641
1773
|
// Add spinner for buttons
|
|
1642
|
-
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>';
|
|
1774
|
+
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>';
|
|
1643
1775
|
if (target.tagName === 'BUTTON') {
|
|
1644
1776
|
target.innerHTML = spinner + 'Loading...';
|
|
1645
1777
|
}
|
|
@@ -1720,7 +1852,22 @@ function minifyJS(code) {
|
|
|
1720
1852
|
if (code.length > MAX_MINIFY_CODE_LENGTH) {
|
|
1721
1853
|
return code;
|
|
1722
1854
|
}
|
|
1723
|
-
|
|
1855
|
+
const out = [];
|
|
1856
|
+
let inBlockComment = false;
|
|
1857
|
+
for (const raw of code.split("\n")) {
|
|
1858
|
+
const line = raw.trim();
|
|
1859
|
+
if (inBlockComment) {
|
|
1860
|
+
if (line.includes("*/")) inBlockComment = false;
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
1864
|
+
if (line.startsWith("/*")) {
|
|
1865
|
+
if (!line.includes("*/")) inBlockComment = true;
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
out.push(line);
|
|
1869
|
+
}
|
|
1870
|
+
return out.join("\n");
|
|
1724
1871
|
}
|
|
1725
1872
|
function generatePlatformBundle(platform, options = {}) {
|
|
1726
1873
|
const platformAdapters = {
|
|
@@ -2145,8 +2292,24 @@ function isFrontmcpUiResolvable(...candidatePaths) {
|
|
|
2145
2292
|
}
|
|
2146
2293
|
|
|
2147
2294
|
// libs/uipack/src/component/transpiler.ts
|
|
2295
|
+
function loadEsbuild(filename) {
|
|
2296
|
+
try {
|
|
2297
|
+
return require("esbuild");
|
|
2298
|
+
} catch (err) {
|
|
2299
|
+
if (!isMissingEsbuild(err)) throw err;
|
|
2300
|
+
const subject = filename ? `FileSource widget "${filename}"` : "A FileSource widget";
|
|
2301
|
+
throw new Error(
|
|
2302
|
+
`${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.`,
|
|
2303
|
+
{ cause: err }
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
function isMissingEsbuild(err) {
|
|
2308
|
+
if (!(err instanceof Error)) return false;
|
|
2309
|
+
return err.code === "MODULE_NOT_FOUND" && /Cannot find module ['"]esbuild['"]/.test(err.message);
|
|
2310
|
+
}
|
|
2148
2311
|
function transpileReactSource(source, filename) {
|
|
2149
|
-
const esbuild =
|
|
2312
|
+
const esbuild = loadEsbuild(filename);
|
|
2150
2313
|
const loader = filename?.endsWith(".tsx") ? "tsx" : "jsx";
|
|
2151
2314
|
const result = esbuild.transformSync(source, {
|
|
2152
2315
|
loader,
|
|
@@ -2165,7 +2328,7 @@ function bundleFileSource(source, filename, resolveDir, componentName, options =
|
|
|
2165
2328
|
`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
2329
|
);
|
|
2167
2330
|
}
|
|
2168
|
-
const esbuild =
|
|
2331
|
+
const esbuild = loadEsbuild(filename);
|
|
2169
2332
|
const mountCode = `
|
|
2170
2333
|
// --- Auto-generated mount ---
|
|
2171
2334
|
import { createElement as __h } from 'react';
|
|
@@ -2262,7 +2425,8 @@ if (__root) {
|
|
|
2262
2425
|
} catch (err) {
|
|
2263
2426
|
const message = err instanceof Error ? err.message : String(err);
|
|
2264
2427
|
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
|
|
2428
|
+
`Failed to bundle FileSource "${filename}": ${message}. If the error mentions @frontmcp/ui or @frontmcp/uipack, ensure both packages are installed in the consuming project.`,
|
|
2429
|
+
{ cause: err }
|
|
2266
2430
|
);
|
|
2267
2431
|
}
|
|
2268
2432
|
}
|
|
@@ -2338,6 +2502,37 @@ function resolveImportSource(source) {
|
|
|
2338
2502
|
peerDependencies: []
|
|
2339
2503
|
};
|
|
2340
2504
|
}
|
|
2505
|
+
var BUILD_DIR_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out"]);
|
|
2506
|
+
function missingWidgetError(filePath, cause) {
|
|
2507
|
+
const sourceWidget = findSourceWidget(filePath);
|
|
2508
|
+
const error = new Error(
|
|
2509
|
+
`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\`.` : ""),
|
|
2510
|
+
{ cause }
|
|
2511
|
+
);
|
|
2512
|
+
error.code = "ENOENT";
|
|
2513
|
+
error.path = filePath;
|
|
2514
|
+
return error;
|
|
2515
|
+
}
|
|
2516
|
+
function findSourceWidget(filePath) {
|
|
2517
|
+
const path = require("path");
|
|
2518
|
+
const fs = require("fs");
|
|
2519
|
+
const parts = filePath.split(path.sep);
|
|
2520
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
2521
|
+
if (!BUILD_DIR_SEGMENTS.has(parts[i])) continue;
|
|
2522
|
+
const candidates = [[...parts.slice(0, i), "src", ...parts.slice(i + 1)]];
|
|
2523
|
+
if (parts.length - i > 2) candidates.push([...parts.slice(0, i), "src", ...parts.slice(i + 2)]);
|
|
2524
|
+
candidates.push([...parts.slice(0, i), ...parts.slice(i + 1)]);
|
|
2525
|
+
for (const candidate of candidates) {
|
|
2526
|
+
const candidatePath = candidate.join(path.sep);
|
|
2527
|
+
try {
|
|
2528
|
+
if (fs.existsSync(candidatePath)) return candidatePath;
|
|
2529
|
+
} catch {
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
return void 0;
|
|
2533
|
+
}
|
|
2534
|
+
return void 0;
|
|
2535
|
+
}
|
|
2341
2536
|
function resolveFileSource(source, options = {}) {
|
|
2342
2537
|
const path = require("path");
|
|
2343
2538
|
const ext = path.extname(source.file).toLowerCase();
|
|
@@ -2352,9 +2547,11 @@ function resolveFileSource(source, options = {}) {
|
|
|
2352
2547
|
const isNotFound = err?.code === "ENOENT";
|
|
2353
2548
|
if (isNotFound && wasRelative) {
|
|
2354
2549
|
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)
|
|
2550
|
+
`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).`,
|
|
2551
|
+
{ cause: err }
|
|
2356
2552
|
);
|
|
2357
2553
|
}
|
|
2554
|
+
if (isNotFound) throw missingWidgetError(filePath, err);
|
|
2358
2555
|
throw err;
|
|
2359
2556
|
}
|
|
2360
2557
|
const componentName = source.exportName || extractDefaultExportName(rawSource) || "Component";
|
|
@@ -2732,6 +2929,86 @@ function wrapDetectedContent(value) {
|
|
|
2732
2929
|
}
|
|
2733
2930
|
}
|
|
2734
2931
|
|
|
2932
|
+
// libs/uipack/src/adapters/markdown.ts
|
|
2933
|
+
var SAFE_HREF = /^(https?:|mailto:|\/|#)/i;
|
|
2934
|
+
function emphasis(text) {
|
|
2935
|
+
return text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
2936
|
+
}
|
|
2937
|
+
function inline(text) {
|
|
2938
|
+
const stash = [];
|
|
2939
|
+
const hold = (html2) => `\uE000${stash.push(html2) - 1}\uE000`;
|
|
2940
|
+
let out = escapeHtml(text.replace(/\uE000/g, "")).replace(
|
|
2941
|
+
/`([^`]+)`/g,
|
|
2942
|
+
(_m, code) => hold(`<code>${code}</code>`)
|
|
2943
|
+
);
|
|
2944
|
+
out = out.replace(
|
|
2945
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
2946
|
+
(_m, label, href) => SAFE_HREF.test(href) ? hold(`<a href="${href}" rel="noopener noreferrer">${emphasis(label)}</a>`) : label
|
|
2947
|
+
);
|
|
2948
|
+
out = emphasis(out);
|
|
2949
|
+
for (let i = 0; i < 2; i++) out = out.replace(/\uE000(\d+)\uE000/g, (_m, n) => stash[Number(n)]);
|
|
2950
|
+
return out;
|
|
2951
|
+
}
|
|
2952
|
+
function markdownToHtml(source) {
|
|
2953
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
2954
|
+
const blocks = [];
|
|
2955
|
+
let paragraph = [];
|
|
2956
|
+
let list;
|
|
2957
|
+
const flushParagraph = () => {
|
|
2958
|
+
if (paragraph.length > 0) blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
|
|
2959
|
+
paragraph = [];
|
|
2960
|
+
};
|
|
2961
|
+
const flushList = () => {
|
|
2962
|
+
if (list)
|
|
2963
|
+
blocks.push(`<${list.tag}>${list.items.map((item) => `<li>${inline(item)}</li>`).join("")}</${list.tag}>`);
|
|
2964
|
+
list = void 0;
|
|
2965
|
+
};
|
|
2966
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2967
|
+
const line = lines[i];
|
|
2968
|
+
const fence = /^\s*(`{3,})/.exec(line);
|
|
2969
|
+
if (fence) {
|
|
2970
|
+
flushParagraph();
|
|
2971
|
+
flushList();
|
|
2972
|
+
const fenceLength = fence[1].length;
|
|
2973
|
+
const isClosing = (l) => {
|
|
2974
|
+
const m = /^\s*(`{3,})\s*$/.exec(l);
|
|
2975
|
+
return m !== null && m[1].length >= fenceLength;
|
|
2976
|
+
};
|
|
2977
|
+
const code = [];
|
|
2978
|
+
for (i++; i < lines.length && !isClosing(lines[i]); i++) code.push(lines[i]);
|
|
2979
|
+
blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}
|
|
2980
|
+
</code></pre>`);
|
|
2981
|
+
continue;
|
|
2982
|
+
}
|
|
2983
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
2984
|
+
if (heading) {
|
|
2985
|
+
flushParagraph();
|
|
2986
|
+
flushList();
|
|
2987
|
+
blocks.push(`<h${heading[1].length}>${inline(heading[2].trim())}</h${heading[1].length}>`);
|
|
2988
|
+
continue;
|
|
2989
|
+
}
|
|
2990
|
+
const item = /^\s*([-*+]|\d+\.)\s+(.*)$/.exec(line);
|
|
2991
|
+
if (item) {
|
|
2992
|
+
flushParagraph();
|
|
2993
|
+
const tag = /\d/.test(item[1]) ? "ol" : "ul";
|
|
2994
|
+
if (list && list.tag !== tag) flushList();
|
|
2995
|
+
list ??= { tag, items: [] };
|
|
2996
|
+
list.items.push(item[2]);
|
|
2997
|
+
continue;
|
|
2998
|
+
}
|
|
2999
|
+
if (line.trim() === "") {
|
|
3000
|
+
flushParagraph();
|
|
3001
|
+
flushList();
|
|
3002
|
+
continue;
|
|
3003
|
+
}
|
|
3004
|
+
flushList();
|
|
3005
|
+
paragraph.push(line.trim());
|
|
3006
|
+
}
|
|
3007
|
+
flushParagraph();
|
|
3008
|
+
flushList();
|
|
3009
|
+
return blocks.join("\n");
|
|
3010
|
+
}
|
|
3011
|
+
|
|
2735
3012
|
// libs/uipack/src/adapters/type-detector.ts
|
|
2736
3013
|
function detectUIType(template) {
|
|
2737
3014
|
if (template === null || template === void 0) {
|
|
@@ -2765,9 +3042,15 @@ function detectUIType(template) {
|
|
|
2765
3042
|
}
|
|
2766
3043
|
|
|
2767
3044
|
// libs/uipack/src/adapters/template-renderer.ts
|
|
2768
|
-
function buildCspConfig(resolver) {
|
|
3045
|
+
function buildCspConfig(resolver, extra) {
|
|
2769
3046
|
const cspResourceDomains = ["https://esm.sh"];
|
|
2770
3047
|
const cspConnectDomains = ["https://esm.sh"];
|
|
3048
|
+
for (const domain of extra?.resourceDomains ?? []) {
|
|
3049
|
+
if (!cspResourceDomains.includes(domain)) cspResourceDomains.push(domain);
|
|
3050
|
+
}
|
|
3051
|
+
for (const domain of extra?.connectDomains ?? []) {
|
|
3052
|
+
if (!cspConnectDomains.includes(domain)) cspConnectDomains.push(domain);
|
|
3053
|
+
}
|
|
2771
3054
|
if (resolver && "overrides" in resolver) {
|
|
2772
3055
|
const overrides = resolver.overrides;
|
|
2773
3056
|
if (overrides) {
|
|
@@ -2807,6 +3090,14 @@ function renderUnwrappedResult(rawResult, options) {
|
|
|
2807
3090
|
}
|
|
2808
3091
|
return rawResult;
|
|
2809
3092
|
}
|
|
3093
|
+
function isReactElement(value) {
|
|
3094
|
+
return typeof value === "object" && value !== null && "$$typeof" in value;
|
|
3095
|
+
}
|
|
3096
|
+
function isDefinitelyReactComponent(template) {
|
|
3097
|
+
if (typeof template !== "function") return false;
|
|
3098
|
+
const proto = template.prototype;
|
|
3099
|
+
return typeof proto?.render === "function" || template["$$typeof"] !== void 0;
|
|
3100
|
+
}
|
|
2810
3101
|
function renderToolTemplate(options) {
|
|
2811
3102
|
const { toolName, input, output, template, resolver, platformType, sizing } = options;
|
|
2812
3103
|
const uiType = detectUIType(template);
|
|
@@ -2817,30 +3108,43 @@ function renderToolTemplate(options) {
|
|
|
2817
3108
|
output,
|
|
2818
3109
|
includeBridge: true,
|
|
2819
3110
|
resolver,
|
|
2820
|
-
sizing
|
|
3111
|
+
sizing,
|
|
3112
|
+
csp: options.csp ? buildCspConfig(resolver, options.csp) : void 0
|
|
2821
3113
|
};
|
|
2822
3114
|
let html2;
|
|
2823
3115
|
let hash = "";
|
|
2824
3116
|
let size = 0;
|
|
2825
3117
|
if (typeof template === "object" && template !== null && "file" in template) {
|
|
2826
|
-
const cspConfig = buildCspConfig(resolver);
|
|
2827
3118
|
const result = renderComponent(
|
|
2828
3119
|
{ source: template, inlineReact: resourceMode === "inline" },
|
|
2829
|
-
{ ...shellConfig, csp:
|
|
3120
|
+
{ ...shellConfig, csp: buildCspConfig(resolver, options.csp) }
|
|
2830
3121
|
);
|
|
2831
3122
|
html2 = result.html;
|
|
2832
3123
|
hash = result.hash;
|
|
2833
3124
|
size = result.size;
|
|
2834
3125
|
} else if (typeof template === "function") {
|
|
2835
|
-
|
|
3126
|
+
const helpers = createTemplateHelpers();
|
|
3127
|
+
const ctx = { input, output, helpers };
|
|
3128
|
+
let rawResult;
|
|
3129
|
+
let isHtmlBuilder = uiType !== "react" || !isDefinitelyReactComponent(template);
|
|
3130
|
+
if (isHtmlBuilder) {
|
|
3131
|
+
try {
|
|
3132
|
+
rawResult = template(ctx);
|
|
3133
|
+
isHtmlBuilder = !isReactElement(rawResult);
|
|
3134
|
+
} catch (error) {
|
|
3135
|
+
if (uiType !== "react") throw error;
|
|
3136
|
+
isHtmlBuilder = false;
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
if (!isHtmlBuilder) {
|
|
3140
|
+
(options.logger ?? console).warn(
|
|
3141
|
+
`[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.`
|
|
3142
|
+
);
|
|
2836
3143
|
const shellResult = buildShell('<div id="root"></div>', shellConfig);
|
|
2837
3144
|
html2 = shellResult.html;
|
|
2838
3145
|
hash = shellResult.hash;
|
|
2839
3146
|
size = shellResult.size;
|
|
2840
3147
|
} else {
|
|
2841
|
-
const helpers = createTemplateHelpers();
|
|
2842
|
-
const ctx = { input, output, helpers };
|
|
2843
|
-
const rawResult = template(ctx);
|
|
2844
3148
|
const wrapped = isTrustedHtml(rawResult) ? void 0 : wrapDetectedContent(rawResult);
|
|
2845
3149
|
if (wrapped) {
|
|
2846
3150
|
html2 = wrapped;
|
|
@@ -2852,7 +3156,7 @@ function renderToolTemplate(options) {
|
|
|
2852
3156
|
}
|
|
2853
3157
|
}
|
|
2854
3158
|
} else if (typeof template === "string") {
|
|
2855
|
-
const shellResult = buildShell(template, shellConfig);
|
|
3159
|
+
const shellResult = buildShell(uiType === "markdown" ? markdownToHtml(template) : template, shellConfig);
|
|
2856
3160
|
html2 = shellResult.html;
|
|
2857
3161
|
hash = shellResult.hash;
|
|
2858
3162
|
size = shellResult.size;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, safe Markdown to HTML conversion for `ui.template` strings.
|
|
3
|
+
*
|
|
4
|
+
* Supports headings, paragraphs, fenced code, ordered/unordered lists and inline
|
|
5
|
+
* bold, italic, code and http(s)/mailto links. Everything else is escaped text; raw HTML in the
|
|
6
|
+
* source is never passed through.
|
|
7
|
+
*/
|
|
8
|
+
export declare function markdownToHtml(source: string): string;
|
|
9
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/adapters/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA2BH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAgErD"}
|
|
@@ -36,6 +36,14 @@ export interface RenderToolTemplateOptions {
|
|
|
36
36
|
* shell, and is mirrored onto the returned `meta` as `ui/preferredHeight` etc.
|
|
37
37
|
*/
|
|
38
38
|
sizing?: WidgetSizing;
|
|
39
|
+
/**
|
|
40
|
+
* Extra origins the widget may load from (`resourceDomains`) or call (`connectDomains`), from the
|
|
41
|
+
* tool's `ui.csp`. Merged into the page's own Content-Security-Policy on every template path.
|
|
42
|
+
*/
|
|
43
|
+
csp?: {
|
|
44
|
+
connectDomains?: string[];
|
|
45
|
+
resourceDomains?: string[];
|
|
46
|
+
};
|
|
39
47
|
/**
|
|
40
48
|
* HTML-escape a plain string returned by a template function; results built with
|
|
41
49
|
* `html` / `trustedHtml` stay markup. When unset, strings that look like HTML render as
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../../src/adapters/template-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../../src/adapters/template-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAQnD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,GAAG,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChE;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yEAAyE;IACzE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AA+ED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAsH/F"}
|
|
@@ -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;AA8xCD;;;;;;;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"}
|