@agentproto/apps 0.11.0 → 0.12.0
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/dist/agents-overview/panel.mjs +25 -0
- package/dist/agents-overview/panel.mjs.map +1 -1
- package/dist/agents-overview.mjs +25 -0
- package/dist/agents-overview.mjs.map +1 -1
- package/dist/bureau-sessions/panel.mjs +25 -0
- package/dist/bureau-sessions/panel.mjs.map +1 -1
- package/dist/bureau-sessions.mjs +25 -0
- package/dist/bureau-sessions.mjs.map +1 -1
- package/dist/index.mjs +494 -12
- package/dist/index.mjs.map +1 -1
- package/dist/live-session/panel.mjs +25 -0
- package/dist/live-session/panel.mjs.map +1 -1
- package/dist/live-session.mjs +25 -0
- package/dist/live-session.mjs.map +1 -1
- package/dist/panel-bridge.d.ts.map +1 -1
- package/dist/session-chat/blob-embed.d.ts +53 -0
- package/dist/session-chat/blob-embed.d.ts.map +1 -0
- package/dist/session-chat/index.d.ts +13 -0
- package/dist/session-chat/index.d.ts.map +1 -1
- package/dist/session-chat/panel.d.ts +38 -2
- package/dist/session-chat/panel.d.ts.map +1 -1
- package/dist/session-chat/panel.mjs +609 -6
- package/dist/session-chat/panel.mjs.map +1 -1
- package/dist/session-chat.mjs +631 -13
- package/dist/session-chat.mjs.map +1 -1
- package/dist/session-story/panel.mjs +25 -0
- package/dist/session-story/panel.mjs.map +1 -1
- package/dist/session-story.mjs +25 -0
- package/dist/session-story.mjs.map +1 -1
- package/dist/sessions-panel/panel.mjs +25 -0
- package/dist/sessions-panel/panel.mjs.map +1 -1
- package/dist/sessions-panel.mjs +25 -0
- package/dist/sessions-panel.mjs.map +1 -1
- package/dist/work-board/panel.d.ts +1 -1
- package/dist/work-board/panel.d.ts.map +1 -1
- package/dist/work-board/panel.generated.d.ts +1 -1
- package/dist/work-board/panel.generated.d.ts.map +1 -1
- package/dist/work-board/panel.mjs +25 -0
- package/dist/work-board/panel.mjs.map +1 -1
- package/dist/work-board.mjs +25 -0
- package/dist/work-board.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2888,12 +2888,29 @@ function panelBridgeScript(appName) {
|
|
|
2888
2888
|
// JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
|
|
2889
2889
|
var _nextId = 1, _pending = {}, _notifyHandlers = [];
|
|
2890
2890
|
var _hostContext = null, _hostContextHandlers = [];
|
|
2891
|
+
var _hostCaps = null;
|
|
2891
2892
|
var _standaloneApp = null;
|
|
2892
2893
|
function _isStandalone(){
|
|
2893
2894
|
return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';
|
|
2894
2895
|
}
|
|
2896
|
+
// \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2897
|
+
// Panels that iframe the daemon's standalone app host carry this proof so
|
|
2898
|
+
// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for
|
|
2899
|
+
// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).
|
|
2900
|
+
// registerMcpApps replaces the placeholder with a real per-boot token when
|
|
2901
|
+
// serving the panel as a resource; every other render (tests, standalone
|
|
2902
|
+
// tab, VS Code HTTP-iframe panel \u2014 the latter two already pass
|
|
2903
|
+
// iframeEmbedAllowed's origin checks) keeps the literal, and
|
|
2904
|
+
// withEmbedToken() then degrades to an identity function.
|
|
2905
|
+
window.__AGENPROTO_EMBED_TOKEN__ = "__AGENPROTO_EMBED_TOKEN__";
|
|
2906
|
+
function withEmbedToken(url){
|
|
2907
|
+
var t = window.__AGENPROTO_EMBED_TOKEN__;
|
|
2908
|
+
if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;
|
|
2909
|
+
return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);
|
|
2910
|
+
}
|
|
2895
2911
|
function post(msg){ window.parent.postMessage(msg, '*'); }
|
|
2896
2912
|
function getHostContext(){ return _hostContext; }
|
|
2913
|
+
function getHostCapabilities(){ return _hostCaps; }
|
|
2897
2914
|
function onHostContext(cb){
|
|
2898
2915
|
_hostContextHandlers.push(cb);
|
|
2899
2916
|
// Replay the last context so a late subscriber isn't stuck blind.
|
|
@@ -2956,12 +2973,20 @@ function initBridge(){
|
|
|
2956
2973
|
// The initialize result carries the initial hostContext (displayMode +
|
|
2957
2974
|
// availableDisplayModes) \u2014 capture it before notifying the host.
|
|
2958
2975
|
if (result && result.hostContext) _setHostContext(result.hostContext);
|
|
2976
|
+
// McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 e.g. openLinks,
|
|
2977
|
+
// "Host supports opening external URLs". Captured once, read via
|
|
2978
|
+
// getHostCapabilities() by panels that need to branch on it.
|
|
2979
|
+
if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;
|
|
2959
2980
|
rpcNotify('ui/notifications/initialized', {});
|
|
2960
2981
|
});
|
|
2961
2982
|
}
|
|
2962
2983
|
function requestDisplayMode(mode){
|
|
2963
2984
|
return rpcRequest('ui/request-display-mode', {mode: mode});
|
|
2964
2985
|
}
|
|
2986
|
+
function openLink(url){
|
|
2987
|
+
if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);
|
|
2988
|
+
return rpcRequest('ui/open-link', {url: url});
|
|
2989
|
+
}
|
|
2965
2990
|
function callTool(name, args){
|
|
2966
2991
|
var raw = _standaloneApp
|
|
2967
2992
|
? _standaloneApp.callTool(name, args || {})
|
|
@@ -5507,14 +5532,89 @@ var liveSessionApp = defineApp({
|
|
|
5507
5532
|
}
|
|
5508
5533
|
});
|
|
5509
5534
|
|
|
5535
|
+
// src/session-chat/blob-embed.ts
|
|
5536
|
+
var BLOB_BOOT_MESSAGE_TYPE = "agentproto-blob-boot";
|
|
5537
|
+
function blobEmbedScript() {
|
|
5538
|
+
return `// \u2500\u2500 Blob pass-through transform (shared: session-chat/blob-embed.ts) \u2500\u2500
|
|
5539
|
+
function _blobEscapeAttr(s){ return String(s).replace(/&/g, '&').replace(/"/g, '"'); }
|
|
5540
|
+
// Rewrite the fetched chat html so it works as a blob: document: <base> on
|
|
5541
|
+
// the deep link, a fetch/EventSource shim that carries the embed token to
|
|
5542
|
+
// the daemon origin, and the standalone bridge's ./tool-call made absolute.
|
|
5543
|
+
function transformChatHtmlForBlob(html, url, token){
|
|
5544
|
+
var origin = new URL(url).origin;
|
|
5545
|
+
var tc = new URL('./tool-call', url);
|
|
5546
|
+
tc.searchParams.set('et', token);
|
|
5547
|
+
// The app-visible query for the blob doc: the deep link's search minus the
|
|
5548
|
+
// embed token (the token must not ride the app's visible params). The
|
|
5549
|
+
// blob document's own location has no params, so this is the ONLY channel
|
|
5550
|
+
// for ?session= / ?embed=1 \u2014 the app reads it via bootSearchParams().
|
|
5551
|
+
var bootUrl = new URL(url);
|
|
5552
|
+
bootUrl.searchParams.delete('et');
|
|
5553
|
+
var bootQuery = bootUrl.search;
|
|
5554
|
+
var shim = '<script>(function(){'
|
|
5555
|
+
+ 'try { window.parent.postMessage({ type: ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)} }, "*"); } catch (_) {}'
|
|
5556
|
+
+ 'var ORIGIN = ' + JSON.stringify(origin) + ', TOKEN = ' + JSON.stringify(token) + ';'
|
|
5557
|
+
+ 'window.__AGENTPROTO_BASEURL__ = ORIGIN;'
|
|
5558
|
+
+ 'var _h = window.history, _rs = _h.replaceState.bind(_h), _ps = _h.pushState.bind(_h);'
|
|
5559
|
+
+ 'function _sameOrLoc(u){'
|
|
5560
|
+
+ 'try {'
|
|
5561
|
+
+ 'var x = new URL(u == null ? window.location.href : u, document.baseURI);'
|
|
5562
|
+
+ 'return x.origin === window.location.origin ? u : window.location.href;'
|
|
5563
|
+
+ '} catch (_) { return window.location.href; }'
|
|
5564
|
+
+ '}'
|
|
5565
|
+
+ '_h.replaceState = function(s, t, u){'
|
|
5566
|
+
+ 'try { _rs(s, t, _sameOrLoc(u)); } catch (_) { try { _rs(s, t, window.location.href); } catch (_) {} }'
|
|
5567
|
+
+ '};'
|
|
5568
|
+
+ '_h.pushState = function(s, t, u){'
|
|
5569
|
+
+ 'try { _ps(s, t, _sameOrLoc(u)); } catch (_) { try { _ps(s, t, window.location.href); } catch (_) {} }'
|
|
5570
|
+
+ '};'
|
|
5571
|
+
+ 'window.__AGENTPROTO_BOOT_QUERY__ = ' + JSON.stringify(bootQuery) + ';'
|
|
5572
|
+
+ 'function withEt(u){'
|
|
5573
|
+
+ 'try {'
|
|
5574
|
+
+ 'var x = new URL(String(u), document.baseURI);'
|
|
5575
|
+
+ 'if (x.origin !== ORIGIN) return null;'
|
|
5576
|
+
+ 'if (!x.searchParams.has("et")) x.searchParams.set("et", TOKEN);'
|
|
5577
|
+
+ 'return x.href;'
|
|
5578
|
+
+ '} catch (_) { return null; }'
|
|
5579
|
+
+ '}'
|
|
5580
|
+
+ 'var _fetch = window.fetch;'
|
|
5581
|
+
+ 'if (typeof _fetch === "function") {'
|
|
5582
|
+
+ 'window.fetch = function(input, init){'
|
|
5583
|
+
+ 'var r;'
|
|
5584
|
+
+ 'if (typeof Request !== "undefined" && input instanceof Request) {'
|
|
5585
|
+
+ 'r = withEt(input.url);'
|
|
5586
|
+
+ 'if (r) { try { input = new Request(r, input); } catch (_) {} }'
|
|
5587
|
+
+ '} else {'
|
|
5588
|
+
+ 'r = withEt(input);'
|
|
5589
|
+
+ 'if (r) input = r;'
|
|
5590
|
+
+ '}'
|
|
5591
|
+
+ 'return _fetch.call(window, input, init);'
|
|
5592
|
+
+ '};'
|
|
5593
|
+
+ '}'
|
|
5594
|
+
+ 'var _ES = window.EventSource;'
|
|
5595
|
+
+ 'if (typeof _ES === "function") {'
|
|
5596
|
+
+ 'window.EventSource = function(u, cfg){ return new _ES(withEt(u) || u, cfg); };'
|
|
5597
|
+
+ 'window.EventSource.prototype = _ES.prototype;'
|
|
5598
|
+
+ '}'
|
|
5599
|
+
+ '})();<\\/script>';
|
|
5600
|
+
var head = '<base href="' + _blobEscapeAttr(url) + '">' + shim;
|
|
5601
|
+
var m = html.match(/<head[^>]*>/i);
|
|
5602
|
+
var out = m
|
|
5603
|
+
? html.slice(0, m.index + m[0].length) + head + html.slice(m.index + m[0].length)
|
|
5604
|
+
: head + html;
|
|
5605
|
+
return out.split('fetch("./tool-call"').join('fetch(' + JSON.stringify(tc.href));
|
|
5606
|
+
}`;
|
|
5607
|
+
}
|
|
5608
|
+
|
|
5510
5609
|
// src/session-chat/panel.ts
|
|
5511
5610
|
function escapeAttr(text) {
|
|
5512
5611
|
return text.replace(/&/g, "&").replace(/"/g, """);
|
|
5513
5612
|
}
|
|
5514
5613
|
function sessionChatEmbedHtml(initData) {
|
|
5515
|
-
const url = initData.url;
|
|
5516
|
-
const
|
|
5517
|
-
const
|
|
5614
|
+
const url = typeof initData.url === "string" && initData.url ? initData.url : null;
|
|
5615
|
+
const notInstalled = initData.installed === false;
|
|
5616
|
+
const link = url != null ? `<a href="${escapeAttr(url)}" target="_blank" rel="noreferrer">open in a tab</a>` : notInstalled ? "app not installed" : "connecting\u2026";
|
|
5617
|
+
const iframe = url != null ? `<iframe id="chat" title="Session Chat"></iframe>` : "";
|
|
5518
5618
|
return `<!DOCTYPE html>
|
|
5519
5619
|
<html lang="en">
|
|
5520
5620
|
<head>
|
|
@@ -5526,12 +5626,354 @@ function sessionChatEmbedHtml(initData) {
|
|
|
5526
5626
|
html,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3;overflow:hidden}
|
|
5527
5627
|
#bar{padding:6px 12px;font-size:12px;color:#8b949e;background:#161b22;border-bottom:1px solid #30363d}
|
|
5528
5628
|
#bar a{color:#58a6ff}
|
|
5529
|
-
#
|
|
5629
|
+
#mode{color:#6e7681}
|
|
5630
|
+
#stage{position:relative;height:calc(100% - 29px)}
|
|
5631
|
+
#chat{position:absolute;inset:0;z-index:2;border:0;display:block;width:100%;height:100%}
|
|
5632
|
+
#card{position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;text-align:center;padding:24px}
|
|
5633
|
+
#card h2{font-size:15px;font-weight:600}
|
|
5634
|
+
#card .sid{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:#8b949e;background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 8px}
|
|
5635
|
+
#card p{font-size:12px;color:#8b949e;max-width:420px;line-height:1.5}
|
|
5636
|
+
#card a{display:inline-block;margin-top:2px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:13px;padding:7px 14px;border-radius:6px;text-decoration:none}
|
|
5637
|
+
#card a:hover{border-color:#58a6ff}
|
|
5638
|
+
#notice{display:none;max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}
|
|
5639
|
+
#notice.show{display:block}
|
|
5640
|
+
#notice h1{font-size:18px;margin-bottom:12px}
|
|
5641
|
+
#notice code{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}
|
|
5642
|
+
#notice p{margin:8px 0;color:#8b949e;font-size:13px}
|
|
5530
5643
|
</style>
|
|
5531
5644
|
</head>
|
|
5532
5645
|
<body>
|
|
5533
|
-
<div id="bar">Session Chat ·
|
|
5534
|
-
|
|
5646
|
+
<div id="bar">Session Chat · <span id="link">${link}</span><span id="mode"></span></div>
|
|
5647
|
+
<div id="stage">${iframe}<div id="card">
|
|
5648
|
+
<h2>Session Chat</h2>
|
|
5649
|
+
<div id="card-session" class="sid">—</div>
|
|
5650
|
+
<p>Full embed isn't supported by this host — the chat opens in a browser tab.</p>
|
|
5651
|
+
<a id="card-open" href="#" target="_blank" rel="noreferrer">Open chat</a>
|
|
5652
|
+
</div><div id="notice"${notInstalled ? ' class="show"' : ""}>
|
|
5653
|
+
<h1>Session Chat is not installed</h1>
|
|
5654
|
+
<p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app —
|
|
5655
|
+
it does not bundle the chat UI itself.</p>
|
|
5656
|
+
<p>To use it, install the app into this daemon:</p>
|
|
5657
|
+
<p><code>agentproto app install @agentik/session-chat</code></p>
|
|
5658
|
+
</div></div>
|
|
5659
|
+
<script>
|
|
5660
|
+
window.__APP_INIT__ = ${JSON.stringify(initData)};
|
|
5661
|
+
|
|
5662
|
+
${panelBridgeScript("agentproto-session-chat")}
|
|
5663
|
+
|
|
5664
|
+
${blobEmbedScript()}
|
|
5665
|
+
|
|
5666
|
+
// \u2500\u2500 Session-chat launcher logic \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5667
|
+
var mountedUrl = (window.__APP_INIT__ && window.__APP_INIT__.url) || null;
|
|
5668
|
+
var pinnedSessionId = null; // session named by the host's tool-result push
|
|
5669
|
+
var pendingUrl = null; // url pushed by the host before the bridge was up
|
|
5670
|
+
var pendingNotInstalled = false;
|
|
5671
|
+
var bridged = false;
|
|
5672
|
+
var cardUrl = null; // deep link once resolved (drives the card button)
|
|
5673
|
+
var cardSessionEl = document.getElementById('card-session');
|
|
5674
|
+
var cardOpenEl = document.getElementById('card-open');
|
|
5675
|
+
var mountGen = 0; // bumps per mount(); async work checks it before touching the DOM
|
|
5676
|
+
var blobUrl = null; // live object url of the blob-mounted chat, if any
|
|
5677
|
+
var blobBootTimer = null;
|
|
5678
|
+
var blobBootOk = false;
|
|
5679
|
+
|
|
5680
|
+
function escapeHtml(text) {
|
|
5681
|
+
return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5684
|
+
function setMode(text) {
|
|
5685
|
+
document.getElementById('mode').textContent = text ? ' \\u00b7 ' + text : '';
|
|
5686
|
+
// The chat app is live inside the frame \u2014 the launcher bar (title, deep
|
|
5687
|
+
// link, mount mode) is noise on top of the real surface. Hide it and let
|
|
5688
|
+
// the frame own the full height. Only a failed mount (launcher card /
|
|
5689
|
+
// not-installed notice) shows the bar again.
|
|
5690
|
+
var bar = document.getElementById('bar');
|
|
5691
|
+
var stage = document.getElementById('stage');
|
|
5692
|
+
if (bar && stage) {
|
|
5693
|
+
bar.style.display = text ? 'none' : '';
|
|
5694
|
+
stage.style.height = text ? '100%' : 'calc(100% - 29px)';
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
|
|
5698
|
+
// One fresh #chat frame on the stage (over the card), the previous one gone.
|
|
5699
|
+
function newFrame() {
|
|
5700
|
+
var stage = document.getElementById('stage');
|
|
5701
|
+
var old = document.getElementById('chat');
|
|
5702
|
+
if (old) old.remove();
|
|
5703
|
+
var frame = document.createElement('iframe');
|
|
5704
|
+
frame.id = 'chat';
|
|
5705
|
+
frame.title = 'Session Chat';
|
|
5706
|
+
stage.insertBefore(frame, stage.firstChild);
|
|
5707
|
+
return frame;
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
function revokeBlob() {
|
|
5711
|
+
if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }
|
|
5712
|
+
if (blobUrl) {
|
|
5713
|
+
try { URL.revokeObjectURL(blobUrl); } catch (_) {}
|
|
5714
|
+
blobUrl = null;
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
|
|
5718
|
+
function mount(url) {
|
|
5719
|
+
if (!url || url === mountedUrl) return;
|
|
5720
|
+
mountedUrl = url;
|
|
5721
|
+
var gen = ++mountGen;
|
|
5722
|
+
revokeBlob();
|
|
5723
|
+
document.getElementById('notice').className = '';
|
|
5724
|
+
setCardUrl(url);
|
|
5725
|
+
document.getElementById('link').innerHTML =
|
|
5726
|
+
'<a href="' + escapeHtml(url) + '" target="_blank" rel="noreferrer">open in a tab</a>';
|
|
5727
|
+
var tokened = withEmbedToken(url);
|
|
5728
|
+
if (tokened === url) {
|
|
5729
|
+
// No per-boot token baked in (standalone tab, VS Code HTTP-iframe panel):
|
|
5730
|
+
// the direct frame already passes the daemon's origin checks, and the
|
|
5731
|
+
// blob path's daemon calls would have nothing to carry \u2014 skip it.
|
|
5732
|
+
mountDirect(url);
|
|
5733
|
+
return;
|
|
5734
|
+
}
|
|
5735
|
+
mountBlob(url, tokened, gen).catch(function (err) {
|
|
5736
|
+
if (gen !== mountGen) return;
|
|
5737
|
+
// The fetch itself was refused (host connect-src not merging our
|
|
5738
|
+
// connectDomains, PNA, daemon down, \u2026) \u2014 probe answered: no blob path.
|
|
5739
|
+
console.warn('[session-chat] blob mount failed, falling back to a direct frame:',
|
|
5740
|
+
err && err.message ? err.message : String(err));
|
|
5741
|
+
mountDirect(url);
|
|
5742
|
+
});
|
|
5743
|
+
}
|
|
5744
|
+
|
|
5745
|
+
// Direct-src iframe of the deep link, with the block probe \u2192 launcher card
|
|
5746
|
+
// fallback. The terminal mount path when the blob one is unavailable.
|
|
5747
|
+
function mountDirect(url) {
|
|
5748
|
+
var frame = newFrame();
|
|
5749
|
+
frame.src = withEmbedToken(url);
|
|
5750
|
+
setMode('direct frame');
|
|
5751
|
+
armBlockProbe(frame);
|
|
5752
|
+
}
|
|
5753
|
+
|
|
5754
|
+
// Blob pass-through: fetch the chat html through the embed token, rewrite it
|
|
5755
|
+
// for life as a blob: document (transformChatHtmlForBlob, blob-embed.ts) and
|
|
5756
|
+
// mount it \u2014 blob: passes a host frame-src that refuses the daemon origin.
|
|
5757
|
+
function mountBlob(url, tokened, gen) {
|
|
5758
|
+
return fetch(tokened, { credentials: 'omit' }).then(function (res) {
|
|
5759
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
5760
|
+
return res.text();
|
|
5761
|
+
}).then(function (html) {
|
|
5762
|
+
if (gen !== mountGen) return;
|
|
5763
|
+
var doc = transformChatHtmlForBlob(html, url, window.__AGENPROTO_EMBED_TOKEN__);
|
|
5764
|
+
blobUrl = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));
|
|
5765
|
+
var frame = newFrame();
|
|
5766
|
+
armBlobBootProbe(frame, url, gen);
|
|
5767
|
+
frame.src = blobUrl;
|
|
5768
|
+
setMode('embedded');
|
|
5769
|
+
});
|
|
5770
|
+
}
|
|
5771
|
+
|
|
5772
|
+
// The blob document's shim posts a boot ack as its first statement. No ack
|
|
5773
|
+
// within the settle window means the host refused the blob frame or its
|
|
5774
|
+
// inherited CSP killed the document's inline scripts \u2014 surface that through
|
|
5775
|
+
// the direct-frame path (whose own probe ends on the launcher card) rather
|
|
5776
|
+
// than leaving a silent blank.
|
|
5777
|
+
window.addEventListener('message', function (evt) {
|
|
5778
|
+
var d = evt.data;
|
|
5779
|
+
if (!d || typeof d !== 'object' || d.type !== ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)}) return;
|
|
5780
|
+
var frame = document.getElementById('chat');
|
|
5781
|
+
if (!frame || evt.source !== frame.contentWindow) return;
|
|
5782
|
+
blobBootOk = true;
|
|
5783
|
+
if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }
|
|
5784
|
+
});
|
|
5785
|
+
|
|
5786
|
+
function armBlobBootProbe(frame, url, gen) {
|
|
5787
|
+
blobBootOk = false;
|
|
5788
|
+
blobBootTimer = setTimeout(function () {
|
|
5789
|
+
blobBootTimer = null;
|
|
5790
|
+
if (gen !== mountGen || blobBootOk) return;
|
|
5791
|
+
console.warn('[session-chat] blob document never booted (host CSP?), falling back to a direct frame');
|
|
5792
|
+
revokeBlob();
|
|
5793
|
+
frame.remove();
|
|
5794
|
+
mountDirect(url);
|
|
5795
|
+
}, 5000);
|
|
5796
|
+
}
|
|
5797
|
+
|
|
5798
|
+
function showNotInstalled() {
|
|
5799
|
+
var old = document.getElementById('chat');
|
|
5800
|
+
if (old) old.remove();
|
|
5801
|
+
revokeBlob();
|
|
5802
|
+
setMode('');
|
|
5803
|
+
mountedUrl = null;
|
|
5804
|
+
document.getElementById('card').style.display = 'none';
|
|
5805
|
+
document.getElementById('notice').className = 'show';
|
|
5806
|
+
document.getElementById('link').textContent = 'app not installed';
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
// The body of an ext-apps tool-result notification, whichever wrapping
|
|
5810
|
+
// shape the host uses (params IS the CallToolResult, or nests it under
|
|
5811
|
+
// params.result). null for error results / non-JSON text.
|
|
5812
|
+
function parseToolResultBody(params) {
|
|
5813
|
+
if (!params || typeof params !== 'object') return null;
|
|
5814
|
+
var res = (params.result && typeof params.result === 'object') ? params.result : params;
|
|
5815
|
+
if (res.isError) return null;
|
|
5816
|
+
var content = Array.isArray(res.content) ? res.content : [];
|
|
5817
|
+
var item = content[0];
|
|
5818
|
+
if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;
|
|
5819
|
+
try { return JSON.parse(item.text); } catch (_) { return null; }
|
|
5820
|
+
}
|
|
5821
|
+
|
|
5822
|
+
// INLINED COPY of live-session/logic.ts extractToolResultSessionId \u2014 keep
|
|
5823
|
+
// in sync. \`sessionId\` (an agentproto_session_chat / live_session result)
|
|
5824
|
+
// wins over \`id\` (an agent_start result: the spawned session descriptor).
|
|
5825
|
+
function extractToolResultSessionId(params) {
|
|
5826
|
+
var body = parseToolResultBody(params);
|
|
5827
|
+
if (!body) return null;
|
|
5828
|
+
if (typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;
|
|
5829
|
+
if (typeof body.id === 'string' && body.id) return body.id;
|
|
5830
|
+
return null;
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
// \u2500\u2500 Embed-token refresh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5834
|
+
// The token baked into this page by registerMcpApps dies with the daemon
|
|
5835
|
+
// boot that minted it (runtime embed-tokens.ts), but hosts cache the
|
|
5836
|
+
// rendered widget: Claude Desktop keeps a conversation's srcdoc, so after
|
|
5837
|
+
// any daemon restart the cached page replays a dead token and every daemon
|
|
5838
|
+
// call 403s \u2014 the blob fetch fails, the direct frame is refused, and the
|
|
5839
|
+
// user is left on the launcher card forever. Every tool result carries a
|
|
5840
|
+
// live token (SessionChatOutput.embedToken), so adopt it before mounting.
|
|
5841
|
+
//
|
|
5842
|
+
// Only a render that WAS baked refreshes. A standalone tab or VS Code
|
|
5843
|
+
// HTTP-iframe panel still holds the literal placeholder and passes the
|
|
5844
|
+
// daemon's origin checks on its own; handing it a token would switch it
|
|
5845
|
+
// onto the blob path for no reason.
|
|
5846
|
+
function adoptEmbedToken(body) {
|
|
5847
|
+
var cur = window.__AGENPROTO_EMBED_TOKEN__;
|
|
5848
|
+
if (!cur || cur === '__AGENPROTO_EMBED_TOKEN__') return;
|
|
5849
|
+
if (!body || typeof body.embedToken !== 'string' || !body.embedToken) return;
|
|
5850
|
+
window.__AGENPROTO_EMBED_TOKEN__ = body.embedToken;
|
|
5851
|
+
}
|
|
5852
|
+
|
|
5853
|
+
// Turn a session id into the installed app's deep link over the bridge.
|
|
5854
|
+
function resolveSession(sessionId) {
|
|
5855
|
+
noteSession(sessionId);
|
|
5856
|
+
return callTool('agentproto_session_chat', sessionId ? { sessionId: sessionId } : {})
|
|
5857
|
+
.then(function(out) {
|
|
5858
|
+
adoptEmbedToken(out);
|
|
5859
|
+
if (out && out.installed && typeof out.url === 'string' && out.url) mount(out.url);
|
|
5860
|
+
else showNotInstalled();
|
|
5861
|
+
})
|
|
5862
|
+
.catch(function(err) {
|
|
5863
|
+
document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));
|
|
5864
|
+
});
|
|
5865
|
+
}
|
|
5866
|
+
|
|
5867
|
+
// \u2500\u2500 Launcher card (the frame-blocked fallback UI) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5868
|
+
// The iframe mounts OVER the card. When the host's own CSP refuses the
|
|
5869
|
+
// frame (Claude Desktop / Codex widget frames run frame-src 'self' blob:
|
|
5870
|
+
// data: and do not yet merge csp.frameDomains), the frame stays on a
|
|
5871
|
+
// same-origin about:blank \u2014 readable, unlike the loaded cross-origin chat \u2014
|
|
5872
|
+
// and is removed after a settle window so the card becomes the interactive
|
|
5873
|
+
// surface.
|
|
5874
|
+
//
|
|
5875
|
+
// The anchor carries target="_blank" rel="noreferrer" as the fallback path,
|
|
5876
|
+
// but that is a plain no-op in hosts that sandbox the widget iframe without
|
|
5877
|
+
// allow-popups (observed in Claude Desktop's side chat) \u2014 a normal click
|
|
5878
|
+
// on such a host does nothing. Where the host advertises the capability
|
|
5879
|
+
// (McpUiInitializeResult.hostCapabilities.openLinks in the ext-apps spec,
|
|
5880
|
+
// captured by the bridge's getHostCapabilities()), route through the
|
|
5881
|
+
// host-mediated ui/open-link request instead, which is not subject to the
|
|
5882
|
+
// iframe's popup sandboxing.
|
|
5883
|
+
cardOpenEl.addEventListener('click', function (evt) {
|
|
5884
|
+
if (!cardUrl) { evt.preventDefault(); return; }
|
|
5885
|
+
if (getHostCapabilities() && getHostCapabilities().openLinks) {
|
|
5886
|
+
evt.preventDefault();
|
|
5887
|
+
openLink(cardUrl).catch(function () { window.open(cardUrl, '_blank'); });
|
|
5888
|
+
}
|
|
5889
|
+
});
|
|
5890
|
+
|
|
5891
|
+
function noteSession(id) {
|
|
5892
|
+
if (id && cardSessionEl) cardSessionEl.textContent = id;
|
|
5893
|
+
}
|
|
5894
|
+
|
|
5895
|
+
function setCardUrl(u) {
|
|
5896
|
+
if (!u) return;
|
|
5897
|
+
cardUrl = u;
|
|
5898
|
+
cardOpenEl.setAttribute('href', u);
|
|
5899
|
+
}
|
|
5900
|
+
|
|
5901
|
+
// A blocked frame never leaves about:blank (same-origin with this page \u2014
|
|
5902
|
+
// readable); the loaded chat is cross-origin (the probe throws). A readable
|
|
5903
|
+
// frame that is STILL blank when the settle window closes = blocked.
|
|
5904
|
+
function isBlank(frame) {
|
|
5905
|
+
try { return frame.contentWindow.location.href === 'about:blank'; } catch (_) { return false; }
|
|
5906
|
+
}
|
|
5907
|
+
|
|
5908
|
+
function armBlockProbe(frame) {
|
|
5909
|
+
var settle = null;
|
|
5910
|
+
function probe() {
|
|
5911
|
+
if (settle) { clearTimeout(settle); settle = null; }
|
|
5912
|
+
if (!isBlank(frame)) return;
|
|
5913
|
+
// Still on about:blank right now \u2014 arm the settle window, but
|
|
5914
|
+
// re-check when it closes: a slow (not blocked) load may have landed
|
|
5915
|
+
// in the meantime, and only a frame STILL blank after the window gets
|
|
5916
|
+
// removed. load firing in between clears this timeout above.
|
|
5917
|
+
settle = setTimeout(function () {
|
|
5918
|
+
settle = null;
|
|
5919
|
+
if (isBlank(frame)) frame.remove();
|
|
5920
|
+
}, 2000);
|
|
5921
|
+
}
|
|
5922
|
+
setTimeout(function () {
|
|
5923
|
+
frame.addEventListener('load', probe);
|
|
5924
|
+
probe();
|
|
5925
|
+
}, 100);
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5928
|
+
// A url baked at render time (a host that re-renders per call with a real
|
|
5929
|
+
// initData) mounts on boot \u2014 through the same blob-first path as every
|
|
5930
|
+
// other mount, since the same opaque widget context frames it. The
|
|
5931
|
+
// pre-rendered #chat frame is replaced by mount()'s own.
|
|
5932
|
+
if (mountedUrl) {
|
|
5933
|
+
var bootUrl = mountedUrl;
|
|
5934
|
+
mountedUrl = null;
|
|
5935
|
+
adoptEmbedToken(window.__APP_INIT__);
|
|
5936
|
+
mount(bootUrl);
|
|
5937
|
+
}
|
|
5938
|
+
|
|
5939
|
+
// The host pushes the triggering tool call's result (ext-apps
|
|
5940
|
+
// ui/notifications/tool-result). For agent_start that result IS the spawned
|
|
5941
|
+
// session's descriptor \u2014 pin the widget to THAT session.
|
|
5942
|
+
onHostNotification(function(method, params) {
|
|
5943
|
+
if (method !== 'ui/notifications/tool-result') return;
|
|
5944
|
+
var body = parseToolResultBody(params);
|
|
5945
|
+
if (!body) return;
|
|
5946
|
+
if (typeof body.url === 'string' && body.url && body.installed !== false) {
|
|
5947
|
+
// Adopt now, not at mount time: the token doesn't depend on the bridge.
|
|
5948
|
+
adoptEmbedToken(body);
|
|
5949
|
+
if (bridged) mount(body.url); else pendingUrl = body.url;
|
|
5950
|
+
return;
|
|
5951
|
+
}
|
|
5952
|
+
if (body.installed === false) {
|
|
5953
|
+
if (bridged) showNotInstalled(); else pendingNotInstalled = true;
|
|
5954
|
+
return;
|
|
5955
|
+
}
|
|
5956
|
+
var id = extractToolResultSessionId(params);
|
|
5957
|
+
if (!id || id === pinnedSessionId) return;
|
|
5958
|
+
pinnedSessionId = id;
|
|
5959
|
+
noteSession(id);
|
|
5960
|
+
if (bridged) resolveSession(id);
|
|
5961
|
+
});
|
|
5962
|
+
|
|
5963
|
+
initBridge().then(function() {
|
|
5964
|
+
bridged = true;
|
|
5965
|
+
if (pendingUrl) { mount(pendingUrl); pendingUrl = null; return; }
|
|
5966
|
+
if (pendingNotInstalled) { showNotInstalled(); return; }
|
|
5967
|
+
if (mountedUrl) return; // rendered with a real url
|
|
5968
|
+
var init = window.__APP_INIT__ || {};
|
|
5969
|
+
if (init.installed === false) { showNotInstalled(); return; }
|
|
5970
|
+
// No url baked in (static ui:// render): resolve over the bridge \u2014 the
|
|
5971
|
+
// pinned session when the host named one, else the app's picker.
|
|
5972
|
+
return resolveSession(pinnedSessionId);
|
|
5973
|
+
}).catch(function(err) {
|
|
5974
|
+
document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));
|
|
5975
|
+
});
|
|
5976
|
+
</script>
|
|
5535
5977
|
</body>
|
|
5536
5978
|
</html>`;
|
|
5537
5979
|
}
|
|
@@ -5578,7 +6020,7 @@ function sessionChatAppUrl(httpBaseUrl, sessionId) {
|
|
|
5578
6020
|
params.set("embed", "1");
|
|
5579
6021
|
return `${base}?${params.toString()}`;
|
|
5580
6022
|
}
|
|
5581
|
-
function
|
|
6023
|
+
function daemonOrigin(httpBaseUrl) {
|
|
5582
6024
|
return new URL(httpBaseUrl).origin;
|
|
5583
6025
|
}
|
|
5584
6026
|
function makeSessionChatApp(ops) {
|
|
@@ -5588,12 +6030,27 @@ function makeSessionChatApp(ops) {
|
|
|
5588
6030
|
title: "Session Chat",
|
|
5589
6031
|
description: "Open the Session Chat app \u2014 a chat-thread view of any agentproto daemon session: full transcript replay, live follow-over SSE, and a composer that sends prompts into the same session. Requires the `@agentik/session-chat` app to be installed; without it the panel shows install instructions. Pass `sessionId` to deep-link straight into a known session.",
|
|
5590
6032
|
inputSchema: sessionChatInputSchema,
|
|
5591
|
-
execute: async (input) =>
|
|
5592
|
-
installed,
|
|
5593
|
-
|
|
5594
|
-
|
|
6033
|
+
execute: async (input) => {
|
|
6034
|
+
if (!installed) return { installed, url: null };
|
|
6035
|
+
const token = ops.mintEmbedToken?.();
|
|
6036
|
+
return {
|
|
6037
|
+
installed,
|
|
6038
|
+
url: sessionChatAppUrl(ops.httpBaseUrl, input.sessionId),
|
|
6039
|
+
// Re-arms a cached widget whose baked token died with an earlier
|
|
6040
|
+
// daemon boot (see SessionChatOutput.embedToken).
|
|
6041
|
+
...token ? { embedToken: token } : {}
|
|
6042
|
+
};
|
|
6043
|
+
},
|
|
5595
6044
|
html: (initData) => sessionChatEmbedHtml(initData),
|
|
5596
|
-
|
|
6045
|
+
// Spec-correct either way (ext-apps McpUiResourceMeta). Measured
|
|
6046
|
+
// 2026-09-18: Claude Desktop / Codex do NOT merge frameDomains into
|
|
6047
|
+
// their widget frame-src; whether they merge connectDomains is unknown,
|
|
6048
|
+
// so the panel probes it at runtime (blob fetch → fallback) rather than
|
|
6049
|
+
// assuming either way.
|
|
6050
|
+
csp: {
|
|
6051
|
+
frameDomains: [daemonOrigin(ops.httpBaseUrl)],
|
|
6052
|
+
connectDomains: [daemonOrigin(ops.httpBaseUrl)]
|
|
6053
|
+
}
|
|
5597
6054
|
};
|
|
5598
6055
|
}
|
|
5599
6056
|
var sessionChatApp = defineApp({
|
|
@@ -5623,12 +6080,29 @@ var WORK_BOARD_HTML_GENERATED = `<!DOCTYPE html>
|
|
|
5623
6080
|
// JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
|
|
5624
6081
|
var _nextId = 1, _pending = {}, _notifyHandlers = [];
|
|
5625
6082
|
var _hostContext = null, _hostContextHandlers = [];
|
|
6083
|
+
var _hostCaps = null;
|
|
5626
6084
|
var _standaloneApp = null;
|
|
5627
6085
|
function _isStandalone(){
|
|
5628
6086
|
return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';
|
|
5629
6087
|
}
|
|
6088
|
+
// \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6089
|
+
// Panels that iframe the daemon's standalone app host carry this proof so
|
|
6090
|
+
// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for
|
|
6091
|
+
// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).
|
|
6092
|
+
// registerMcpApps replaces the placeholder with a real per-boot token when
|
|
6093
|
+
// serving the panel as a resource; every other render (tests, standalone
|
|
6094
|
+
// tab, VS Code HTTP-iframe panel \u2014 the latter two already pass
|
|
6095
|
+
// iframeEmbedAllowed's origin checks) keeps the literal, and
|
|
6096
|
+
// withEmbedToken() then degrades to an identity function.
|
|
6097
|
+
window.__AGENPROTO_EMBED_TOKEN__ = "__AGENPROTO_EMBED_TOKEN__";
|
|
6098
|
+
function withEmbedToken(url){
|
|
6099
|
+
var t = window.__AGENPROTO_EMBED_TOKEN__;
|
|
6100
|
+
if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;
|
|
6101
|
+
return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);
|
|
6102
|
+
}
|
|
5630
6103
|
function post(msg){ window.parent.postMessage(msg, '*'); }
|
|
5631
6104
|
function getHostContext(){ return _hostContext; }
|
|
6105
|
+
function getHostCapabilities(){ return _hostCaps; }
|
|
5632
6106
|
function onHostContext(cb){
|
|
5633
6107
|
_hostContextHandlers.push(cb);
|
|
5634
6108
|
// Replay the last context so a late subscriber isn't stuck blind.
|
|
@@ -5691,12 +6165,20 @@ function initBridge(){
|
|
|
5691
6165
|
// The initialize result carries the initial hostContext (displayMode +
|
|
5692
6166
|
// availableDisplayModes) \u2014 capture it before notifying the host.
|
|
5693
6167
|
if (result && result.hostContext) _setHostContext(result.hostContext);
|
|
6168
|
+
// McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 e.g. openLinks,
|
|
6169
|
+
// "Host supports opening external URLs". Captured once, read via
|
|
6170
|
+
// getHostCapabilities() by panels that need to branch on it.
|
|
6171
|
+
if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;
|
|
5694
6172
|
rpcNotify('ui/notifications/initialized', {});
|
|
5695
6173
|
});
|
|
5696
6174
|
}
|
|
5697
6175
|
function requestDisplayMode(mode){
|
|
5698
6176
|
return rpcRequest('ui/request-display-mode', {mode: mode});
|
|
5699
6177
|
}
|
|
6178
|
+
function openLink(url){
|
|
6179
|
+
if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);
|
|
6180
|
+
return rpcRequest('ui/open-link', {url: url});
|
|
6181
|
+
}
|
|
5700
6182
|
function callTool(name, args){
|
|
5701
6183
|
var raw = _standaloneApp
|
|
5702
6184
|
? _standaloneApp.callTool(name, args || {})
|