@agentproto/apps 0.11.0 → 0.13.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.
Files changed (70) hide show
  1. package/dist/agents-overview/panel.mjs +39 -67
  2. package/dist/agents-overview/panel.mjs.map +1 -1
  3. package/dist/agents-overview.mjs +38 -68
  4. package/dist/agents-overview.mjs.map +1 -1
  5. package/dist/bin/sync.d.ts.map +1 -1
  6. package/dist/bin/sync.mjs +2374 -2
  7. package/dist/bin/sync.mjs.map +1 -1
  8. package/dist/bureau-sessions/panel.mjs +39 -67
  9. package/dist/bureau-sessions/panel.mjs.map +1 -1
  10. package/dist/bureau-sessions.mjs +38 -68
  11. package/dist/bureau-sessions.mjs.map +1 -1
  12. package/dist/config/fragment.d.ts +50 -0
  13. package/dist/config/fragment.d.ts.map +1 -0
  14. package/dist/config/index.d.ts +23 -0
  15. package/dist/config/index.d.ts.map +1 -0
  16. package/dist/config/ui.d.ts +43 -0
  17. package/dist/config/ui.d.ts.map +1 -0
  18. package/dist/config.mjs +2373 -0
  19. package/dist/config.mjs.map +1 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.mjs +3200 -150
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/live-session/logic.d.ts +2 -0
  25. package/dist/live-session/logic.d.ts.map +1 -1
  26. package/dist/live-session/panel.d.ts.map +1 -1
  27. package/dist/live-session/panel.mjs +52 -74
  28. package/dist/live-session/panel.mjs.map +1 -1
  29. package/dist/live-session.mjs +51 -75
  30. package/dist/live-session.mjs.map +1 -1
  31. package/dist/panel-bridge.d.ts +11 -6
  32. package/dist/panel-bridge.d.ts.map +1 -1
  33. package/dist/session-chat/blob-embed.d.ts +53 -0
  34. package/dist/session-chat/blob-embed.d.ts.map +1 -0
  35. package/dist/session-chat/index.d.ts +13 -0
  36. package/dist/session-chat/index.d.ts.map +1 -1
  37. package/dist/session-chat/panel.d.ts +38 -2
  38. package/dist/session-chat/panel.d.ts.map +1 -1
  39. package/dist/session-chat/panel.mjs +556 -6
  40. package/dist/session-chat/panel.mjs.map +1 -1
  41. package/dist/session-chat.mjs +591 -14
  42. package/dist/session-chat.mjs.map +1 -1
  43. package/dist/session-story/panel.mjs +39 -67
  44. package/dist/session-story/panel.mjs.map +1 -1
  45. package/dist/session-story.mjs +38 -68
  46. package/dist/session-story.mjs.map +1 -1
  47. package/dist/session-viewer/ui.d.ts.map +1 -1
  48. package/dist/session-viewer.mjs +10 -0
  49. package/dist/session-viewer.mjs.map +1 -1
  50. package/dist/sessions-panel/panel.d.ts.map +1 -1
  51. package/dist/sessions-panel/panel.mjs +40 -67
  52. package/dist/sessions-panel/panel.mjs.map +1 -1
  53. package/dist/sessions-panel.mjs +39 -68
  54. package/dist/sessions-panel.mjs.map +1 -1
  55. package/dist/work-board/panel.d.ts +1 -1
  56. package/dist/work-board/panel.d.ts.map +1 -1
  57. package/dist/work-board/panel.generated.d.ts +1 -1
  58. package/dist/work-board/panel.generated.d.ts.map +1 -1
  59. package/dist/work-board/panel.mjs +318 -62
  60. package/dist/work-board/panel.mjs.map +1 -1
  61. package/dist/work-board.mjs +318 -62
  62. package/dist/work-board.mjs.map +1 -1
  63. package/package.json +12 -4
  64. package/repo-maintenance/.agentproto/APP.md +22 -0
  65. package/repo-maintenance/.agentproto/agents/repo-maintenance-reviewer/AGENT.md +142 -0
  66. package/repo-maintenance/.agentproto/workflows/maintain/WORKFLOW.md +262 -0
  67. package/repo-maintenance/.agentproto/workflows/maintain/entry.mjs +672 -0
  68. package/repo-maintenance/README.md +79 -0
  69. package/repo-maintenance/routines/repo-maintenance-daily/ROUTINE.md +77 -0
  70. package/repo-maintenance/routines/repo-maintenance-weekly/ROUTINE.md +82 -0
@@ -1,20 +1,226 @@
1
1
  import { z } from 'zod';
2
2
  import { defineApp } from '@agentproto/app-kit';
3
+ import { DISPLAY_MODE_SCRIPT_BODY } from '@agentproto/app-client/display-mode';
3
4
 
4
5
  /**
5
6
  * @agentproto/apps v0.1.0
6
7
  * Ready-made agentproto apps (teams of agents + workflows) built on app-kit.
7
8
  */
8
9
 
10
+ function panelBridgeScript(appName) {
11
+ return `// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500
12
+ // JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
13
+ var _nextId = 1, _pending = {}, _notifyHandlers = [];
14
+ var _hostContext = null, _hostContextHandlers = [];
15
+ var _hostCaps = null;
16
+ var _standaloneApp = null;
17
+ function _isStandalone(){
18
+ return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';
19
+ }
20
+ // \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
21
+ // Panels that iframe the daemon's standalone app host carry this proof so
22
+ // GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for
23
+ // MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).
24
+ // registerMcpApps replaces the placeholder with a real per-boot token when
25
+ // serving the panel as a resource; every other render (tests, standalone
26
+ // tab, VS Code HTTP-iframe panel \u2014 the latter two already pass
27
+ // iframeEmbedAllowed's origin checks) keeps the literal, and
28
+ // withEmbedToken() then degrades to an identity function.
29
+ window.__AGENPROTO_EMBED_TOKEN__ = "__AGENPROTO_EMBED_TOKEN__";
30
+ function withEmbedToken(url){
31
+ var t = window.__AGENPROTO_EMBED_TOKEN__;
32
+ if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;
33
+ return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);
34
+ }
35
+ function post(msg){ window.parent.postMessage(msg, '*'); }
36
+ function getHostContext(){ return _hostContext; }
37
+ function getHostCapabilities(){ return _hostCaps; }
38
+ function onHostContext(cb){
39
+ _hostContextHandlers.push(cb);
40
+ // Replay the last context so a late subscriber isn't stuck blind.
41
+ if (_hostContext){ try { cb(_hostContext); } catch(_) {} }
42
+ }
43
+ function _setHostContext(ctx){
44
+ if (!ctx || typeof ctx !== 'object') return;
45
+ // ui/notifications/host-context-changed carries only the changed keys \u2014
46
+ // merge, matching the official ext-apps App behaviour.
47
+ _hostContext = Object.assign({}, _hostContext || {}, ctx);
48
+ for (var i = 0; i < _hostContextHandlers.length; i++){
49
+ try { _hostContextHandlers[i](_hostContext); } catch(_) {}
50
+ }
51
+ }
52
+ function rpcRequest(method, params){
53
+ return new Promise(function(resolve, reject){
54
+ var id = _nextId++;
55
+ _pending[id] = {resolve: resolve, reject: reject};
56
+ post({jsonrpc: '2.0', id: id, method: method, params: params || {}});
57
+ });
58
+ }
59
+ function rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }
60
+ function onHostNotification(cb){ _notifyHandlers.push(cb); }
61
+ window.addEventListener('message', function(evt){
62
+ var msg = evt.data;
63
+ if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;
64
+ if (msg.id != null && msg.method == null){
65
+ var p = _pending[msg.id];
66
+ if (!p) return;
67
+ delete _pending[msg.id];
68
+ if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));
69
+ else p.resolve(msg.result);
70
+ return;
71
+ }
72
+ if (msg.method){
73
+ if (msg.method === 'ui/notifications/host-context-changed'){
74
+ _setHostContext(msg.params || {});
75
+ }
76
+ for (var i = 0; i < _notifyHandlers.length; i++){
77
+ try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}
78
+ }
79
+ }
80
+ });
81
+ function initBridge(){
82
+ if (_isStandalone()){
83
+ return window.McpApp.connect().then(function(conn){
84
+ _standaloneApp = conn;
85
+ // No host to advertise a hostContext \u2014 default to inline with no
86
+ // other modes available, which keeps the display-mode toggle buttons
87
+ // hidden (they only show for modes hostContext.availableDisplayModes
88
+ // lists) instead of rendering two buttons with nothing to switch to.
89
+ _setHostContext({displayMode: 'inline', availableDisplayModes: []});
90
+ });
91
+ }
92
+ return rpcRequest('ui/initialize', {
93
+ appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},
94
+ appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},
95
+ protocolVersion: '2026-01-26'
96
+ }).then(function(result){
97
+ // The initialize result carries the initial hostContext (displayMode +
98
+ // availableDisplayModes) \u2014 capture it before notifying the host.
99
+ if (result && result.hostContext) _setHostContext(result.hostContext);
100
+ // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 e.g. openLinks,
101
+ // "Host supports opening external URLs". Captured once, read via
102
+ // getHostCapabilities() by panels that need to branch on it.
103
+ if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;
104
+ rpcNotify('ui/notifications/initialized', {});
105
+ });
106
+ }
107
+ function requestDisplayMode(mode){
108
+ return rpcRequest('ui/request-display-mode', {mode: mode});
109
+ }
110
+ function openLink(url){
111
+ if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);
112
+ return rpcRequest('ui/open-link', {url: url});
113
+ }
114
+ function callTool(name, args){
115
+ var raw = _standaloneApp
116
+ ? _standaloneApp.callTool(name, args || {})
117
+ : rpcRequest('tools/call', {name: name, arguments: args || {}});
118
+ return raw.then(function(result){
119
+ if (result.isError){
120
+ var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';
121
+ throw new Error(e);
122
+ }
123
+ var text = (result.content && result.content[0] && result.content[0].text) || '{}';
124
+ return JSON.parse(text);
125
+ });
126
+ }
127
+
128
+ // \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500
129
+ // The installer is inlined verbatim (it is self-contained and idempotent),
130
+ // then handed this panel's own JSON-RPC plumbing. NO auto-request: the
131
+ // panel stays inline until the user clicks, and the buttons only appear for
132
+ // the modes the host advertises in hostContext.availableDisplayModes.
133
+ ${DISPLAY_MODE_SCRIPT_BODY}
134
+ var displayMode = window.AgentprotoUI.installDisplayMode({
135
+ getHostContext: getHostContext,
136
+ onHostContext: onHostContext,
137
+ requestDisplayMode: requestDisplayMode
138
+ });`;
139
+ }
140
+
141
+ // src/session-chat/blob-embed.ts
142
+ var BLOB_BOOT_MESSAGE_TYPE = "agentproto-blob-boot";
143
+ function blobEmbedScript() {
144
+ return `// \u2500\u2500 Blob pass-through transform (shared: session-chat/blob-embed.ts) \u2500\u2500
145
+ function _blobEscapeAttr(s){ return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;'); }
146
+ // Rewrite the fetched chat html so it works as a blob: document: <base> on
147
+ // the deep link, a fetch/EventSource shim that carries the embed token to
148
+ // the daemon origin, and the standalone bridge's ./tool-call made absolute.
149
+ function transformChatHtmlForBlob(html, url, token){
150
+ var origin = new URL(url).origin;
151
+ var tc = new URL('./tool-call', url);
152
+ tc.searchParams.set('et', token);
153
+ // The app-visible query for the blob doc: the deep link's search minus the
154
+ // embed token (the token must not ride the app's visible params). The
155
+ // blob document's own location has no params, so this is the ONLY channel
156
+ // for ?session= / ?embed=1 \u2014 the app reads it via bootSearchParams().
157
+ var bootUrl = new URL(url);
158
+ bootUrl.searchParams.delete('et');
159
+ var bootQuery = bootUrl.search;
160
+ var shim = '<script>(function(){'
161
+ + 'try { window.parent.postMessage({ type: ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)} }, "*"); } catch (_) {}'
162
+ + 'var ORIGIN = ' + JSON.stringify(origin) + ', TOKEN = ' + JSON.stringify(token) + ';'
163
+ + 'window.__AGENTPROTO_BASEURL__ = ORIGIN;'
164
+ + 'var _h = window.history, _rs = _h.replaceState.bind(_h), _ps = _h.pushState.bind(_h);'
165
+ + 'function _sameOrLoc(u){'
166
+ + 'try {'
167
+ + 'var x = new URL(u == null ? window.location.href : u, document.baseURI);'
168
+ + 'return x.origin === window.location.origin ? u : window.location.href;'
169
+ + '} catch (_) { return window.location.href; }'
170
+ + '}'
171
+ + '_h.replaceState = function(s, t, u){'
172
+ + 'try { _rs(s, t, _sameOrLoc(u)); } catch (_) { try { _rs(s, t, window.location.href); } catch (_) {} }'
173
+ + '};'
174
+ + '_h.pushState = function(s, t, u){'
175
+ + 'try { _ps(s, t, _sameOrLoc(u)); } catch (_) { try { _ps(s, t, window.location.href); } catch (_) {} }'
176
+ + '};'
177
+ + 'window.__AGENTPROTO_BOOT_QUERY__ = ' + JSON.stringify(bootQuery) + ';'
178
+ + 'function withEt(u){'
179
+ + 'try {'
180
+ + 'var x = new URL(String(u), document.baseURI);'
181
+ + 'if (x.origin !== ORIGIN) return null;'
182
+ + 'if (!x.searchParams.has("et")) x.searchParams.set("et", TOKEN);'
183
+ + 'return x.href;'
184
+ + '} catch (_) { return null; }'
185
+ + '}'
186
+ + 'var _fetch = window.fetch;'
187
+ + 'if (typeof _fetch === "function") {'
188
+ + 'window.fetch = function(input, init){'
189
+ + 'var r;'
190
+ + 'if (typeof Request !== "undefined" && input instanceof Request) {'
191
+ + 'r = withEt(input.url);'
192
+ + 'if (r) { try { input = new Request(r, input); } catch (_) {} }'
193
+ + '} else {'
194
+ + 'r = withEt(input);'
195
+ + 'if (r) input = r;'
196
+ + '}'
197
+ + 'return _fetch.call(window, input, init);'
198
+ + '};'
199
+ + '}'
200
+ + 'var _ES = window.EventSource;'
201
+ + 'if (typeof _ES === "function") {'
202
+ + 'window.EventSource = function(u, cfg){ return new _ES(withEt(u) || u, cfg); };'
203
+ + 'window.EventSource.prototype = _ES.prototype;'
204
+ + '}'
205
+ + '})();<\\/script>';
206
+ var head = '<base href="' + _blobEscapeAttr(url) + '">' + shim;
207
+ var m = html.match(/<head[^>]*>/i);
208
+ var out = m
209
+ ? html.slice(0, m.index + m[0].length) + head + html.slice(m.index + m[0].length)
210
+ : head + html;
211
+ return out.split('fetch("./tool-call"').join('fetch(' + JSON.stringify(tc.href));
212
+ }`;
213
+ }
9
214
 
10
215
  // src/session-chat/panel.ts
11
216
  function escapeAttr(text) {
12
217
  return text.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
13
218
  }
14
219
  function sessionChatEmbedHtml(initData) {
15
- const url = initData.url;
16
- const link = url != null ? `<a href="${escapeAttr(url)}" target="_blank" rel="noreferrer">open in a tab</a>` : "app not installed";
17
- const iframe = url != null ? `<iframe id="chat" title="Session Chat" src="${escapeAttr(url)}"></iframe>` : "";
220
+ const url = typeof initData.url === "string" && initData.url ? initData.url : null;
221
+ const notInstalled = initData.installed === false;
222
+ const link = url != null ? `<a href="${escapeAttr(url)}" target="_blank" rel="noreferrer">open in a tab</a>` : notInstalled ? "app not installed" : "connecting\u2026";
223
+ const iframe = url != null ? `<iframe id="chat" title="Session Chat"></iframe>` : "";
18
224
  return `<!DOCTYPE html>
19
225
  <html lang="en">
20
226
  <head>
@@ -26,12 +232,354 @@ function sessionChatEmbedHtml(initData) {
26
232
  html,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3;overflow:hidden}
27
233
  #bar{padding:6px 12px;font-size:12px;color:#8b949e;background:#161b22;border-bottom:1px solid #30363d}
28
234
  #bar a{color:#58a6ff}
29
- #chat{border:0;display:block;width:100%;height:calc(100% - 29px)}
235
+ #mode{color:#6e7681}
236
+ #stage{position:relative;height:calc(100% - 29px)}
237
+ #chat{position:absolute;inset:0;z-index:2;border:0;display:block;width:100%;height:100%}
238
+ #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}
239
+ #card h2{font-size:15px;font-weight:600}
240
+ #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}
241
+ #card p{font-size:12px;color:#8b949e;max-width:420px;line-height:1.5}
242
+ #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}
243
+ #card a:hover{border-color:#58a6ff}
244
+ #notice{display:none;max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}
245
+ #notice.show{display:block}
246
+ #notice h1{font-size:18px;margin-bottom:12px}
247
+ #notice code{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}
248
+ #notice p{margin:8px 0;color:#8b949e;font-size:13px}
30
249
  </style>
31
250
  </head>
32
251
  <body>
33
- <div id="bar">Session Chat &#183; ${link}</div>
34
- ${iframe}
252
+ <div id="bar">Session Chat &#183; <span id="link">${link}</span><span id="mode"></span></div>
253
+ <div id="stage">${iframe}<div id="card">
254
+ <h2>Session Chat</h2>
255
+ <div id="card-session" class="sid">&#8212;</div>
256
+ <p>Full embed isn't supported by this host &#8212; the chat opens in a browser tab.</p>
257
+ <a id="card-open" href="#" target="_blank" rel="noreferrer">Open chat</a>
258
+ </div><div id="notice"${notInstalled ? ' class="show"' : ""}>
259
+ <h1>Session Chat is not installed</h1>
260
+ <p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app &#8212;
261
+ it does not bundle the chat UI itself.</p>
262
+ <p>To use it, install the app into this daemon:</p>
263
+ <p><code>agentproto app install @agentik/session-chat</code></p>
264
+ </div></div>
265
+ <script>
266
+ window.__APP_INIT__ = ${JSON.stringify(initData)};
267
+
268
+ ${panelBridgeScript("agentproto-session-chat")}
269
+
270
+ ${blobEmbedScript()}
271
+
272
+ // \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
273
+ var mountedUrl = (window.__APP_INIT__ && window.__APP_INIT__.url) || null;
274
+ var pinnedSessionId = null; // session named by the host's tool-result push
275
+ var pendingUrl = null; // url pushed by the host before the bridge was up
276
+ var pendingNotInstalled = false;
277
+ var bridged = false;
278
+ var cardUrl = null; // deep link once resolved (drives the card button)
279
+ var cardSessionEl = document.getElementById('card-session');
280
+ var cardOpenEl = document.getElementById('card-open');
281
+ var mountGen = 0; // bumps per mount(); async work checks it before touching the DOM
282
+ var blobUrl = null; // live object url of the blob-mounted chat, if any
283
+ var blobBootTimer = null;
284
+ var blobBootOk = false;
285
+
286
+ function escapeHtml(text) {
287
+ return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
288
+ }
289
+
290
+ function setMode(text) {
291
+ document.getElementById('mode').textContent = text ? ' \\u00b7 ' + text : '';
292
+ // The chat app is live inside the frame \u2014 the launcher bar (title, deep
293
+ // link, mount mode) is noise on top of the real surface. Hide it and let
294
+ // the frame own the full height. Only a failed mount (launcher card /
295
+ // not-installed notice) shows the bar again.
296
+ var bar = document.getElementById('bar');
297
+ var stage = document.getElementById('stage');
298
+ if (bar && stage) {
299
+ bar.style.display = text ? 'none' : '';
300
+ stage.style.height = text ? '100%' : 'calc(100% - 29px)';
301
+ }
302
+ }
303
+
304
+ // One fresh #chat frame on the stage (over the card), the previous one gone.
305
+ function newFrame() {
306
+ var stage = document.getElementById('stage');
307
+ var old = document.getElementById('chat');
308
+ if (old) old.remove();
309
+ var frame = document.createElement('iframe');
310
+ frame.id = 'chat';
311
+ frame.title = 'Session Chat';
312
+ stage.insertBefore(frame, stage.firstChild);
313
+ return frame;
314
+ }
315
+
316
+ function revokeBlob() {
317
+ if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }
318
+ if (blobUrl) {
319
+ try { URL.revokeObjectURL(blobUrl); } catch (_) {}
320
+ blobUrl = null;
321
+ }
322
+ }
323
+
324
+ function mount(url) {
325
+ if (!url || url === mountedUrl) return;
326
+ mountedUrl = url;
327
+ var gen = ++mountGen;
328
+ revokeBlob();
329
+ document.getElementById('notice').className = '';
330
+ setCardUrl(url);
331
+ document.getElementById('link').innerHTML =
332
+ '<a href="' + escapeHtml(url) + '" target="_blank" rel="noreferrer">open in a tab</a>';
333
+ var tokened = withEmbedToken(url);
334
+ if (tokened === url) {
335
+ // No per-boot token baked in (standalone tab, VS Code HTTP-iframe panel):
336
+ // the direct frame already passes the daemon's origin checks, and the
337
+ // blob path's daemon calls would have nothing to carry \u2014 skip it.
338
+ mountDirect(url);
339
+ return;
340
+ }
341
+ mountBlob(url, tokened, gen).catch(function (err) {
342
+ if (gen !== mountGen) return;
343
+ // The fetch itself was refused (host connect-src not merging our
344
+ // connectDomains, PNA, daemon down, \u2026) \u2014 probe answered: no blob path.
345
+ console.warn('[session-chat] blob mount failed, falling back to a direct frame:',
346
+ err && err.message ? err.message : String(err));
347
+ mountDirect(url);
348
+ });
349
+ }
350
+
351
+ // Direct-src iframe of the deep link, with the block probe \u2192 launcher card
352
+ // fallback. The terminal mount path when the blob one is unavailable.
353
+ function mountDirect(url) {
354
+ var frame = newFrame();
355
+ frame.src = withEmbedToken(url);
356
+ setMode('direct frame');
357
+ armBlockProbe(frame);
358
+ }
359
+
360
+ // Blob pass-through: fetch the chat html through the embed token, rewrite it
361
+ // for life as a blob: document (transformChatHtmlForBlob, blob-embed.ts) and
362
+ // mount it \u2014 blob: passes a host frame-src that refuses the daemon origin.
363
+ function mountBlob(url, tokened, gen) {
364
+ return fetch(tokened, { credentials: 'omit' }).then(function (res) {
365
+ if (!res.ok) throw new Error('HTTP ' + res.status);
366
+ return res.text();
367
+ }).then(function (html) {
368
+ if (gen !== mountGen) return;
369
+ var doc = transformChatHtmlForBlob(html, url, window.__AGENPROTO_EMBED_TOKEN__);
370
+ blobUrl = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));
371
+ var frame = newFrame();
372
+ armBlobBootProbe(frame, url, gen);
373
+ frame.src = blobUrl;
374
+ setMode('embedded');
375
+ });
376
+ }
377
+
378
+ // The blob document's shim posts a boot ack as its first statement. No ack
379
+ // within the settle window means the host refused the blob frame or its
380
+ // inherited CSP killed the document's inline scripts \u2014 surface that through
381
+ // the direct-frame path (whose own probe ends on the launcher card) rather
382
+ // than leaving a silent blank.
383
+ window.addEventListener('message', function (evt) {
384
+ var d = evt.data;
385
+ if (!d || typeof d !== 'object' || d.type !== ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)}) return;
386
+ var frame = document.getElementById('chat');
387
+ if (!frame || evt.source !== frame.contentWindow) return;
388
+ blobBootOk = true;
389
+ if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }
390
+ });
391
+
392
+ function armBlobBootProbe(frame, url, gen) {
393
+ blobBootOk = false;
394
+ blobBootTimer = setTimeout(function () {
395
+ blobBootTimer = null;
396
+ if (gen !== mountGen || blobBootOk) return;
397
+ console.warn('[session-chat] blob document never booted (host CSP?), falling back to a direct frame');
398
+ revokeBlob();
399
+ frame.remove();
400
+ mountDirect(url);
401
+ }, 5000);
402
+ }
403
+
404
+ function showNotInstalled() {
405
+ var old = document.getElementById('chat');
406
+ if (old) old.remove();
407
+ revokeBlob();
408
+ setMode('');
409
+ mountedUrl = null;
410
+ document.getElementById('card').style.display = 'none';
411
+ document.getElementById('notice').className = 'show';
412
+ document.getElementById('link').textContent = 'app not installed';
413
+ }
414
+
415
+ // The body of an ext-apps tool-result notification, whichever wrapping
416
+ // shape the host uses (params IS the CallToolResult, or nests it under
417
+ // params.result). null for error results / non-JSON text.
418
+ function parseToolResultBody(params) {
419
+ if (!params || typeof params !== 'object') return null;
420
+ var res = (params.result && typeof params.result === 'object') ? params.result : params;
421
+ if (res.isError) return null;
422
+ var content = Array.isArray(res.content) ? res.content : [];
423
+ var item = content[0];
424
+ if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;
425
+ try { return JSON.parse(item.text); } catch (_) { return null; }
426
+ }
427
+
428
+ // INLINED COPY of live-session/logic.ts extractToolResultSessionId \u2014 keep
429
+ // in sync. \`sessionId\` (an agentproto_session_chat / live_session result)
430
+ // wins over \`id\` (an agent_start result: the spawned session descriptor).
431
+ function extractToolResultSessionId(params) {
432
+ var body = parseToolResultBody(params);
433
+ if (!body) return null;
434
+ if (typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;
435
+ if (typeof body.id === 'string' && body.id) return body.id;
436
+ return null;
437
+ }
438
+
439
+ // \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
440
+ // The token baked into this page by registerMcpApps dies with the daemon
441
+ // boot that minted it (runtime embed-tokens.ts), but hosts cache the
442
+ // rendered widget: Claude Desktop keeps a conversation's srcdoc, so after
443
+ // any daemon restart the cached page replays a dead token and every daemon
444
+ // call 403s \u2014 the blob fetch fails, the direct frame is refused, and the
445
+ // user is left on the launcher card forever. Every tool result carries a
446
+ // live token (SessionChatOutput.embedToken), so adopt it before mounting.
447
+ //
448
+ // Only a render that WAS baked refreshes. A standalone tab or VS Code
449
+ // HTTP-iframe panel still holds the literal placeholder and passes the
450
+ // daemon's origin checks on its own; handing it a token would switch it
451
+ // onto the blob path for no reason.
452
+ function adoptEmbedToken(body) {
453
+ var cur = window.__AGENPROTO_EMBED_TOKEN__;
454
+ if (!cur || cur === '__AGENPROTO_EMBED_TOKEN__') return;
455
+ if (!body || typeof body.embedToken !== 'string' || !body.embedToken) return;
456
+ window.__AGENPROTO_EMBED_TOKEN__ = body.embedToken;
457
+ }
458
+
459
+ // Turn a session id into the installed app's deep link over the bridge.
460
+ function resolveSession(sessionId) {
461
+ noteSession(sessionId);
462
+ return callTool('agentproto_session_chat', sessionId ? { sessionId: sessionId } : {})
463
+ .then(function(out) {
464
+ adoptEmbedToken(out);
465
+ if (out && out.installed && typeof out.url === 'string' && out.url) mount(out.url);
466
+ else showNotInstalled();
467
+ })
468
+ .catch(function(err) {
469
+ document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));
470
+ });
471
+ }
472
+
473
+ // \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
474
+ // The iframe mounts OVER the card. When the host's own CSP refuses the
475
+ // frame (Claude Desktop / Codex widget frames run frame-src 'self' blob:
476
+ // data: and do not yet merge csp.frameDomains), the frame stays on a
477
+ // same-origin about:blank \u2014 readable, unlike the loaded cross-origin chat \u2014
478
+ // and is removed after a settle window so the card becomes the interactive
479
+ // surface.
480
+ //
481
+ // The anchor carries target="_blank" rel="noreferrer" as the fallback path,
482
+ // but that is a plain no-op in hosts that sandbox the widget iframe without
483
+ // allow-popups (observed in Claude Desktop's side chat) \u2014 a normal click
484
+ // on such a host does nothing. Where the host advertises the capability
485
+ // (McpUiInitializeResult.hostCapabilities.openLinks in the ext-apps spec,
486
+ // captured by the bridge's getHostCapabilities()), route through the
487
+ // host-mediated ui/open-link request instead, which is not subject to the
488
+ // iframe's popup sandboxing.
489
+ cardOpenEl.addEventListener('click', function (evt) {
490
+ if (!cardUrl) { evt.preventDefault(); return; }
491
+ if (getHostCapabilities() && getHostCapabilities().openLinks) {
492
+ evt.preventDefault();
493
+ openLink(cardUrl).catch(function () { window.open(cardUrl, '_blank'); });
494
+ }
495
+ });
496
+
497
+ function noteSession(id) {
498
+ if (id && cardSessionEl) cardSessionEl.textContent = id;
499
+ }
500
+
501
+ function setCardUrl(u) {
502
+ if (!u) return;
503
+ cardUrl = u;
504
+ cardOpenEl.setAttribute('href', u);
505
+ }
506
+
507
+ // A blocked frame never leaves about:blank (same-origin with this page \u2014
508
+ // readable); the loaded chat is cross-origin (the probe throws). A readable
509
+ // frame that is STILL blank when the settle window closes = blocked.
510
+ function isBlank(frame) {
511
+ try { return frame.contentWindow.location.href === 'about:blank'; } catch (_) { return false; }
512
+ }
513
+
514
+ function armBlockProbe(frame) {
515
+ var settle = null;
516
+ function probe() {
517
+ if (settle) { clearTimeout(settle); settle = null; }
518
+ if (!isBlank(frame)) return;
519
+ // Still on about:blank right now \u2014 arm the settle window, but
520
+ // re-check when it closes: a slow (not blocked) load may have landed
521
+ // in the meantime, and only a frame STILL blank after the window gets
522
+ // removed. load firing in between clears this timeout above.
523
+ settle = setTimeout(function () {
524
+ settle = null;
525
+ if (isBlank(frame)) frame.remove();
526
+ }, 2000);
527
+ }
528
+ setTimeout(function () {
529
+ frame.addEventListener('load', probe);
530
+ probe();
531
+ }, 100);
532
+ }
533
+
534
+ // A url baked at render time (a host that re-renders per call with a real
535
+ // initData) mounts on boot \u2014 through the same blob-first path as every
536
+ // other mount, since the same opaque widget context frames it. The
537
+ // pre-rendered #chat frame is replaced by mount()'s own.
538
+ if (mountedUrl) {
539
+ var bootUrl = mountedUrl;
540
+ mountedUrl = null;
541
+ adoptEmbedToken(window.__APP_INIT__);
542
+ mount(bootUrl);
543
+ }
544
+
545
+ // The host pushes the triggering tool call's result (ext-apps
546
+ // ui/notifications/tool-result). For agent_start that result IS the spawned
547
+ // session's descriptor \u2014 pin the widget to THAT session.
548
+ onHostNotification(function(method, params) {
549
+ if (method !== 'ui/notifications/tool-result') return;
550
+ var body = parseToolResultBody(params);
551
+ if (!body) return;
552
+ if (typeof body.url === 'string' && body.url && body.installed !== false) {
553
+ // Adopt now, not at mount time: the token doesn't depend on the bridge.
554
+ adoptEmbedToken(body);
555
+ if (bridged) mount(body.url); else pendingUrl = body.url;
556
+ return;
557
+ }
558
+ if (body.installed === false) {
559
+ if (bridged) showNotInstalled(); else pendingNotInstalled = true;
560
+ return;
561
+ }
562
+ var id = extractToolResultSessionId(params);
563
+ if (!id || id === pinnedSessionId) return;
564
+ pinnedSessionId = id;
565
+ noteSession(id);
566
+ if (bridged) resolveSession(id);
567
+ });
568
+
569
+ initBridge().then(function() {
570
+ bridged = true;
571
+ if (pendingUrl) { mount(pendingUrl); pendingUrl = null; return; }
572
+ if (pendingNotInstalled) { showNotInstalled(); return; }
573
+ if (mountedUrl) return; // rendered with a real url
574
+ var init = window.__APP_INIT__ || {};
575
+ if (init.installed === false) { showNotInstalled(); return; }
576
+ // No url baked in (static ui:// render): resolve over the bridge \u2014 the
577
+ // pinned session when the host named one, else the app's picker.
578
+ return resolveSession(pinnedSessionId);
579
+ }).catch(function(err) {
580
+ document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));
581
+ });
582
+ </script>
35
583
  </body>
36
584
  </html>`;
37
585
  }
@@ -78,7 +626,7 @@ function sessionChatAppUrl(httpBaseUrl, sessionId) {
78
626
  params.set("embed", "1");
79
627
  return `${base}?${params.toString()}`;
80
628
  }
81
- function frameOrigin(httpBaseUrl) {
629
+ function daemonOrigin(httpBaseUrl) {
82
630
  return new URL(httpBaseUrl).origin;
83
631
  }
84
632
  function makeSessionChatApp(ops) {
@@ -88,12 +636,27 @@ function makeSessionChatApp(ops) {
88
636
  title: "Session Chat",
89
637
  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.",
90
638
  inputSchema: sessionChatInputSchema,
91
- execute: async (input) => ({
92
- installed,
93
- url: installed ? sessionChatAppUrl(ops.httpBaseUrl, input.sessionId) : null
94
- }),
639
+ execute: async (input) => {
640
+ if (!installed) return { installed, url: null };
641
+ const token = ops.mintEmbedToken?.();
642
+ return {
643
+ installed,
644
+ url: sessionChatAppUrl(ops.httpBaseUrl, input.sessionId),
645
+ // Re-arms a cached widget whose baked token died with an earlier
646
+ // daemon boot (see SessionChatOutput.embedToken).
647
+ ...token ? { embedToken: token } : {}
648
+ };
649
+ },
95
650
  html: (initData) => sessionChatEmbedHtml(initData),
96
- csp: { frameDomains: [frameOrigin(ops.httpBaseUrl)] }
651
+ // Spec-correct either way (ext-apps McpUiResourceMeta). Measured
652
+ // 2026-09-18: Claude Desktop / Codex do NOT merge frameDomains into
653
+ // their widget frame-src; whether they merge connectDomains is unknown,
654
+ // so the panel probes it at runtime (blob fetch → fallback) rather than
655
+ // assuming either way.
656
+ csp: {
657
+ frameDomains: [daemonOrigin(ops.httpBaseUrl)],
658
+ connectDomains: [daemonOrigin(ops.httpBaseUrl)]
659
+ }
97
660
  };
98
661
  }
99
662
  var sessionChatApp = defineApp({
@@ -104,10 +667,24 @@ var sessionChatApp = defineApp({
104
667
  ui: {
105
668
  html: SESSION_CHAT_FALLBACK_HTML,
106
669
  title: "Session Chat",
107
- tools: ["session_list", "agent_start", "adapter_list", "conversation_read"]
670
+ // `session_restart` is what makes an ENDED session (completed / killed /
671
+ // exited / crashed) recoverable from the chat instead of a dead end: the
672
+ // UI offers "Restart session" in place of the composer, and the tool
673
+ // resumes the same conversation (ACP-level resume via the adapter session
674
+ // id, fresh spawn as fallback) and returns a NEW session id the chat
675
+ // navigates to. Kept in step with the installed app's own allowlist
676
+ // (`@agentik/session-chat`'s APP.md `ui.tools`) — a tool missing from
677
+ // EITHER list is refused for the surface that reads it.
678
+ tools: [
679
+ "session_list",
680
+ "agent_start",
681
+ "adapter_list",
682
+ "conversation_read",
683
+ "session_restart"
684
+ ]
108
685
  }
109
686
  });
110
687
 
111
- export { SESSION_CHAT_APP_ID, SESSION_CHAT_FALLBACK_HTML, makeSessionChatApp, sessionChatApp, sessionChatAppUrl, sessionChatEmbedHtml, sessionChatInputSchema };
688
+ export { BLOB_BOOT_MESSAGE_TYPE, SESSION_CHAT_APP_ID, SESSION_CHAT_FALLBACK_HTML, blobEmbedScript, makeSessionChatApp, sessionChatApp, sessionChatAppUrl, sessionChatEmbedHtml, sessionChatInputSchema };
112
689
  //# sourceMappingURL=session-chat.mjs.map
113
690
  //# sourceMappingURL=session-chat.mjs.map