@agentproto/apps 0.10.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.
Files changed (44) hide show
  1. package/dist/agents-overview/panel.mjs +43 -1
  2. package/dist/agents-overview/panel.mjs.map +1 -1
  3. package/dist/agents-overview.mjs +43 -1
  4. package/dist/agents-overview.mjs.map +1 -1
  5. package/dist/bureau-sessions/panel.mjs +43 -1
  6. package/dist/bureau-sessions/panel.mjs.map +1 -1
  7. package/dist/bureau-sessions.mjs +43 -1
  8. package/dist/bureau-sessions.mjs.map +1 -1
  9. package/dist/index.mjs +1116 -300
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/live-session/panel.mjs +43 -1
  12. package/dist/live-session/panel.mjs.map +1 -1
  13. package/dist/live-session.mjs +43 -1
  14. package/dist/live-session.mjs.map +1 -1
  15. package/dist/panel-bridge.d.ts +48 -0
  16. package/dist/panel-bridge.d.ts.map +1 -1
  17. package/dist/session-chat/blob-embed.d.ts +53 -0
  18. package/dist/session-chat/blob-embed.d.ts.map +1 -0
  19. package/dist/session-chat/index.d.ts +13 -0
  20. package/dist/session-chat/index.d.ts.map +1 -1
  21. package/dist/session-chat/panel.d.ts +38 -2
  22. package/dist/session-chat/panel.d.ts.map +1 -1
  23. package/dist/session-chat/panel.mjs +609 -6
  24. package/dist/session-chat/panel.mjs.map +1 -1
  25. package/dist/session-chat.mjs +631 -13
  26. package/dist/session-chat.mjs.map +1 -1
  27. package/dist/session-story/panel.mjs +43 -1
  28. package/dist/session-story/panel.mjs.map +1 -1
  29. package/dist/session-story.mjs +43 -1
  30. package/dist/session-story.mjs.map +1 -1
  31. package/dist/sessions-panel/panel.mjs +43 -1
  32. package/dist/sessions-panel/panel.mjs.map +1 -1
  33. package/dist/sessions-panel.mjs +43 -1
  34. package/dist/sessions-panel.mjs.map +1 -1
  35. package/dist/work-board/index.d.ts.map +1 -1
  36. package/dist/work-board/panel.d.ts +39 -6
  37. package/dist/work-board/panel.d.ts.map +1 -1
  38. package/dist/work-board/panel.generated.d.ts +13 -0
  39. package/dist/work-board/panel.generated.d.ts.map +1 -0
  40. package/dist/work-board/panel.mjs +494 -296
  41. package/dist/work-board/panel.mjs.map +1 -1
  42. package/dist/work-board.mjs +496 -298
  43. package/dist/work-board.mjs.map +1 -1
  44. package/package.json +12 -8
package/dist/index.mjs CHANGED
@@ -2888,8 +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;
2892
+ var _standaloneApp = null;
2893
+ function _isStandalone(){
2894
+ return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';
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
+ }
2891
2911
  function post(msg){ window.parent.postMessage(msg, '*'); }
2892
2912
  function getHostContext(){ return _hostContext; }
2913
+ function getHostCapabilities(){ return _hostCaps; }
2893
2914
  function onHostContext(cb){
2894
2915
  _hostContextHandlers.push(cb);
2895
2916
  // Replay the last context so a late subscriber isn't stuck blind.
@@ -2934,6 +2955,16 @@ window.addEventListener('message', function(evt){
2934
2955
  }
2935
2956
  });
2936
2957
  function initBridge(){
2958
+ if (_isStandalone()){
2959
+ return window.McpApp.connect().then(function(conn){
2960
+ _standaloneApp = conn;
2961
+ // No host to advertise a hostContext \u2014 default to inline with no
2962
+ // other modes available, which keeps the #dm/#pin toggle buttons
2963
+ // hidden (they only show for modes hostContext.availableDisplayModes
2964
+ // lists) instead of rendering two buttons with nothing to switch to.
2965
+ _setHostContext({displayMode: 'inline', availableDisplayModes: []});
2966
+ });
2967
+ }
2937
2968
  return rpcRequest('ui/initialize', {
2938
2969
  appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},
2939
2970
  appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},
@@ -2942,14 +2973,25 @@ function initBridge(){
2942
2973
  // The initialize result carries the initial hostContext (displayMode +
2943
2974
  // availableDisplayModes) \u2014 capture it before notifying the host.
2944
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;
2945
2980
  rpcNotify('ui/notifications/initialized', {});
2946
2981
  });
2947
2982
  }
2948
2983
  function requestDisplayMode(mode){
2949
2984
  return rpcRequest('ui/request-display-mode', {mode: mode});
2950
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
+ }
2951
2990
  function callTool(name, args){
2952
- return rpcRequest('tools/call', {name: name, arguments: args || {}}).then(function(result){
2991
+ var raw = _standaloneApp
2992
+ ? _standaloneApp.callTool(name, args || {})
2993
+ : rpcRequest('tools/call', {name: name, arguments: args || {}});
2994
+ return raw.then(function(result){
2953
2995
  if (result.isError){
2954
2996
  var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';
2955
2997
  throw new Error(e);
@@ -5490,14 +5532,89 @@ var liveSessionApp = defineApp({
5490
5532
  }
5491
5533
  });
5492
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
+
5493
5609
  // src/session-chat/panel.ts
5494
5610
  function escapeAttr(text) {
5495
5611
  return text.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
5496
5612
  }
5497
5613
  function sessionChatEmbedHtml(initData) {
5498
- const url = initData.url;
5499
- const link = url != null ? `<a href="${escapeAttr(url)}" target="_blank" rel="noreferrer">open in a tab</a>` : "app not installed";
5500
- const iframe = url != null ? `<iframe id="chat" title="Session Chat" src="${escapeAttr(url)}"></iframe>` : "";
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>` : "";
5501
5618
  return `<!DOCTYPE html>
5502
5619
  <html lang="en">
5503
5620
  <head>
@@ -5509,12 +5626,354 @@ function sessionChatEmbedHtml(initData) {
5509
5626
  html,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3;overflow:hidden}
5510
5627
  #bar{padding:6px 12px;font-size:12px;color:#8b949e;background:#161b22;border-bottom:1px solid #30363d}
5511
5628
  #bar a{color:#58a6ff}
5512
- #chat{border:0;display:block;width:100%;height:calc(100% - 29px)}
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}
5513
5643
  </style>
5514
5644
  </head>
5515
5645
  <body>
5516
- <div id="bar">Session Chat &#183; ${link}</div>
5517
- ${iframe}
5646
+ <div id="bar">Session Chat &#183; <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">&#8212;</div>
5650
+ <p>Full embed isn't supported by this host &#8212; 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 &#8212;
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, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
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>
5518
5977
  </body>
5519
5978
  </html>`;
5520
5979
  }
@@ -5561,7 +6020,7 @@ function sessionChatAppUrl(httpBaseUrl, sessionId) {
5561
6020
  params.set("embed", "1");
5562
6021
  return `${base}?${params.toString()}`;
5563
6022
  }
5564
- function frameOrigin(httpBaseUrl) {
6023
+ function daemonOrigin(httpBaseUrl) {
5565
6024
  return new URL(httpBaseUrl).origin;
5566
6025
  }
5567
6026
  function makeSessionChatApp(ops) {
@@ -5571,12 +6030,27 @@ function makeSessionChatApp(ops) {
5571
6030
  title: "Session Chat",
5572
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.",
5573
6032
  inputSchema: sessionChatInputSchema,
5574
- execute: async (input) => ({
5575
- installed,
5576
- url: installed ? sessionChatAppUrl(ops.httpBaseUrl, input.sessionId) : null
5577
- }),
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
+ },
5578
6044
  html: (initData) => sessionChatEmbedHtml(initData),
5579
- csp: { frameDomains: [frameOrigin(ops.httpBaseUrl)] }
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
+ }
5580
6054
  };
5581
6055
  }
5582
6056
  var sessionChatApp = defineApp({
@@ -5591,317 +6065,659 @@ var sessionChatApp = defineApp({
5591
6065
  }
5592
6066
  });
5593
6067
 
5594
- // src/work-board/panel.ts
5595
- var WORK_BOARD_HTML = `<!DOCTYPE html>
6068
+ // src/work-board/panel.generated.ts
6069
+ var WORK_BOARD_HTML_GENERATED = `<!DOCTYPE html>
5596
6070
  <html lang="en">
5597
6071
  <head>
5598
- <meta charset="UTF-8">
5599
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6072
+ <meta charset="UTF-8" />
6073
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5600
6074
  <title>agentproto work board</title>
5601
- <style>
5602
- *{box-sizing:border-box;margin:0;padding:0}
5603
- :root{
5604
- --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
5605
- --text:#e6edf3;--text2:#8b949e;
5606
- --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
5607
- }
5608
- html,body{height:100%;font-family:Menlo,Monaco,'Courier New',monospace;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
5609
- #app{display:flex;flex-direction:column;height:100%}
5610
- #toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2);flex-shrink:0}
5611
- #board-label{font-size:11px;color:var(--text2)}
5612
- #board-id{font-weight:600;color:var(--text);font-size:12px}
5613
- #board-input{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 8px;border-radius:4px;font-size:12px;font-family:inherit;width:220px}
5614
- .tbtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}
5615
- .tbtn:hover{background:var(--border)}
5616
- #new-title{flex:1;min-width:120px;background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 8px;border-radius:4px;font-size:12px;font-family:inherit}
5617
- #columns{flex:1;display:flex;overflow-x:auto;overflow-y:hidden}
5618
- .col{width:260px;min-width:220px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border)}
5619
- .col-hdr{padding:8px 10px;border-bottom:1px solid var(--border);background:var(--bg2);font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.05em;display:flex;justify-content:space-between}
5620
- .col-body{flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:8px}
5621
- .card{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:8px 10px}
5622
- .c-title{font-size:12px;font-weight:500;margin-bottom:4px;word-break:break-word}
5623
- .c-id{font-size:10px;color:var(--text2);margin-bottom:6px}
5624
- .c-row{display:flex;flex-wrap:wrap;gap:4px;align-items:center;margin-bottom:6px}
5625
- .tag{display:inline-block;padding:1px 6px;border-radius:10px;font-size:10px;font-weight:600;white-space:nowrap}
5626
- .t-unclaimed{background:rgba(139,148,158,.15);color:var(--text2);border:1px dashed var(--text2)}
5627
- .t-owner{background:rgba(88,166,255,.15);color:var(--blue)}
5628
- .t-gate{background:rgba(63,185,80,.18);color:var(--green)}
5629
- .t-self{background:rgba(210,153,34,.18);color:var(--yellow)}
5630
- .t-human{background:rgba(88,166,255,.18);color:var(--blue)}
5631
- .t-gated{background:rgba(139,148,158,.12);color:var(--text2)}
5632
- .t-cancelled{background:rgba(139,148,158,.12);color:var(--text2)}
5633
- .c-err{font-size:10px;color:var(--red);margin-bottom:6px;word-break:break-word}
5634
- .c-actions{display:flex;flex-wrap:wrap;gap:4px}
5635
- .abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px;font-family:inherit}
5636
- .abtn:hover{background:var(--border)}
5637
- .abtn.danger{border-color:var(--red);color:var(--red)}
5638
- .abtn.primary{border-color:var(--green);color:var(--green)}
5639
- #statusbar{padding:4px 12px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
5640
- .empty{padding:16px 8px;color:var(--text2);font-size:11px;text-align:center}
5641
- </style>
5642
- </head>
5643
- <body>
5644
- <div id="app">
5645
- <div id="toolbar">
5646
- <span id="board-label">Board:</span>
5647
- <span id="board-id">\u2014</span>
5648
- <input id="board-input" type="text" placeholder="tree:&lt;sessionId&gt; or ws:&lt;slug&gt;">
5649
- <button class="tbtn" id="go-btn">Go</button>
5650
- <button class="tbtn" id="refresh-btn" title="Refresh">&#8635;</button>
5651
- <input id="new-title" type="text" placeholder="New task title&#8230;">
5652
- <button class="tbtn" id="add-btn">+ Add</button>
5653
- </div>
5654
- <div id="columns"></div>
5655
- <div id="statusbar">Connecting to bridge&#8230;</div>
5656
- </div>
5657
- <script>
5658
- ${panelBridgeScript("agentproto-work-board-panel")}
5659
-
5660
- // ============================================================
5661
- // State
5662
- // ============================================================
5663
-
5664
- var COLUMNS = [
5665
- {key: 'pending', label: 'Pending'},
5666
- {key: 'in_progress', label: 'In Progress'},
5667
- {key: 'done', label: 'Done'},
5668
- {key: 'failed', label: 'Failed'}
5669
- ];
5670
-
5671
- var boardId = null;
5672
- var tasks = [];
5673
- var pollTimer = null;
5674
- var pollActive = false;
5675
-
5676
- function setStatus(msg) {
5677
- document.getElementById('statusbar').textContent = msg;
5678
- }
5679
-
5680
- function esc(s) {
5681
- return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
6075
+ <!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by
6076
+ vite.config.ts's injectPanelBridge plugin \u2014 a classic (non-module)
6077
+ script so its \`initBridge\`/\`callTool\` land as globals before the
6078
+ deferred module script below runs. -->
6079
+ <script>// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500
6080
+ // JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
6081
+ var _nextId = 1, _pending = {}, _notifyHandlers = [];
6082
+ var _hostContext = null, _hostContextHandlers = [];
6083
+ var _hostCaps = null;
6084
+ var _standaloneApp = null;
6085
+ function _isStandalone(){
6086
+ return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';
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);
5682
6102
  }
5683
-
5684
- // A cancelled task folds into the Failed column, tagged distinctly \u2014 v1
5685
- // treats both as terminal scrap, and a fifth column for a rarely-used
5686
- // status isn't worth the width.
5687
- function columnOf(status) {
5688
- if (status === 'cancelled') return 'failed';
5689
- return status;
6103
+ function post(msg){ window.parent.postMessage(msg, '*'); }
6104
+ function getHostContext(){ return _hostContext; }
6105
+ function getHostCapabilities(){ return _hostCaps; }
6106
+ function onHostContext(cb){
6107
+ _hostContextHandlers.push(cb);
6108
+ // Replay the last context so a late subscriber isn't stuck blind.
6109
+ if (_hostContext){ try { cb(_hostContext); } catch(_) {} }
5690
6110
  }
5691
-
5692
- // ============================================================
5693
- // Verification tell \u2014 never render an unverified done as gate-passed.
5694
- // ============================================================
5695
-
5696
- function verificationTag(t) {
5697
- var v = t.verification;
5698
- if (v) {
5699
- if (v.kind === 'gate') return '<span class="tag t-gate">&#10003; gate</span>';
5700
- if (v.kind === 'self-report') return '<span class="tag t-self">self-report</span>';
5701
- if (v.kind === 'human') return '<span class="tag t-human">human</span>';
5702
- }
5703
- if (t.status === 'in_progress' && t.verify) {
5704
- return '<span class="tag t-gated">gated</span>';
6111
+ function _setHostContext(ctx){
6112
+ if (!ctx || typeof ctx !== 'object') return;
6113
+ // ui/notifications/host-context-changed carries only the changed keys \u2014
6114
+ // merge, matching the official ext-apps App behaviour.
6115
+ _hostContext = Object.assign({}, _hostContext || {}, ctx);
6116
+ for (var i = 0; i < _hostContextHandlers.length; i++){
6117
+ try { _hostContextHandlers[i](_hostContext); } catch(_) {}
5705
6118
  }
5706
- return '';
5707
6119
  }
5708
-
5709
- function ownerTag(t) {
5710
- if (!t.owner) return '<span class="tag t-unclaimed">Unclaimed</span>';
5711
- return '<span class="tag t-owner">' + esc(t.owner) + '</span>';
6120
+ function rpcRequest(method, params){
6121
+ return new Promise(function(resolve, reject){
6122
+ var id = _nextId++;
6123
+ _pending[id] = {resolve: resolve, reject: reject};
6124
+ post({jsonrpc: '2.0', id: id, method: method, params: params || {}});
6125
+ });
5712
6126
  }
5713
-
5714
- // ============================================================
5715
- // Card actions \u2014 explicit status moves, no drag-and-drop (read-only-safe
5716
- // first: see module docblock).
5717
- // ============================================================
5718
-
5719
- function actionsFor(t) {
5720
- var a = [];
5721
- if (t.status === 'pending') {
5722
- if (!t.owner) a.push({label: 'Claim', action: 'claim', cls: 'primary'});
5723
- else a.push({label: 'Start', action: 'start', cls: 'primary'});
5724
- a.push({label: 'Cancel', action: 'cancel', cls: 'danger'});
5725
- } else if (t.status === 'in_progress') {
5726
- a.push({label: 'Done', action: 'done', cls: 'primary'});
5727
- a.push({label: 'Fail', action: 'fail', cls: 'danger'});
5728
- a.push({label: 'Release', action: 'release', cls: ''});
5729
- a.push({label: 'Cancel', action: 'cancel', cls: 'danger'});
5730
- } else if (t.status === 'done') {
5731
- a.push({label: 'Reopen', action: 'reopen', cls: ''});
5732
- }
5733
- return a;
5734
- }
5735
-
5736
- function applyAction(taskId, action) {
5737
- var t = null;
5738
- for (var i = 0; i < tasks.length; i++) {
5739
- if (tasks[i].taskId === taskId) { t = tasks[i]; break; }
5740
- }
5741
- if (!t) return;
5742
- var p;
5743
- if (action === 'claim') {
5744
- p = callTool('task_claim', {taskId: t.taskId, rev: t.rev});
5745
- } else if (action === 'start') {
5746
- p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'in_progress'});
5747
- } else if (action === 'done') {
5748
- p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'done'});
5749
- } else if (action === 'fail') {
5750
- p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'failed'});
5751
- } else if (action === 'release') {
5752
- p = callTool('task_update', {taskId: t.taskId, rev: t.rev, owner: null});
5753
- } else if (action === 'cancel') {
5754
- p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'cancelled'});
5755
- } else if (action === 'reopen') {
5756
- p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'pending'});
5757
- } else {
6127
+ function rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }
6128
+ function onHostNotification(cb){ _notifyHandlers.push(cb); }
6129
+ window.addEventListener('message', function(evt){
6130
+ var msg = evt.data;
6131
+ if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;
6132
+ if (msg.id != null && msg.method == null){
6133
+ var p = _pending[msg.id];
6134
+ if (!p) return;
6135
+ delete _pending[msg.id];
6136
+ if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));
6137
+ else p.resolve(msg.result);
5758
6138
  return;
5759
6139
  }
5760
- p.then(function (result) {
5761
- if (result && result.conflict) {
5762
- setStatus('Someone else moved "' + t.title + '" first \u2014 refreshed.');
5763
- } else if (result && result.error) {
5764
- setStatus('Action failed: ' + result.error);
5765
- } else {
5766
- setStatus('Updated "' + t.title + '".');
6140
+ if (msg.method){
6141
+ if (msg.method === 'ui/notifications/host-context-changed'){
6142
+ _setHostContext(msg.params || {});
5767
6143
  }
5768
- return loadBoard();
5769
- }).catch(function (e) {
5770
- setStatus('Action failed: ' + e.message);
5771
- });
5772
- }
5773
-
5774
- // ============================================================
5775
- // Render
5776
- // ============================================================
5777
-
5778
- function renderCard(t) {
5779
- var html = '<div class="card" data-taskid="' + esc(t.taskId) + '">';
5780
- html += '<div class="c-title">' + esc(t.title) + '</div>';
5781
- html += '<div class="c-id">#' + esc(t.taskId) + '</div>';
5782
- html += '<div class="c-row">' + ownerTag(t) + verificationTag(t);
5783
- if (t.status === 'cancelled') html += '<span class="tag t-cancelled">cancelled</span>';
5784
- html += '</div>';
5785
- if (t.lastVerifyError) {
5786
- html += '<div class="c-err">verify failed: ' + esc(t.lastVerifyError) + '</div>';
5787
- }
5788
- var actions = actionsFor(t);
5789
- if (actions.length > 0) {
5790
- html += '<div class="c-actions">';
5791
- for (var i = 0; i < actions.length; i++) {
5792
- var a = actions[i];
5793
- html += '<button class="abtn ' + a.cls + '" data-taskid="' + esc(t.taskId) + '" data-action="' + a.action + '">' + a.label + '</button>';
6144
+ for (var i = 0; i < _notifyHandlers.length; i++){
6145
+ try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}
5794
6146
  }
5795
- html += '</div>';
5796
- }
5797
- html += '</div>';
5798
- return html;
5799
- }
5800
-
5801
- function render() {
5802
- document.getElementById('board-id').textContent = boardId || '\u2014';
5803
- if (!document.getElementById('board-input').value) {
5804
- document.getElementById('board-input').value = boardId || '';
5805
6147
  }
5806
- var el = document.getElementById('columns');
5807
- var html = '';
5808
- for (var c = 0; c < COLUMNS.length; c++) {
5809
- var col = COLUMNS[c];
5810
- var rows = tasks.filter(function (t) { return columnOf(t.status) === col.key; });
5811
- html += '<div class="col"><div class="col-hdr"><span>' + col.label + '</span><span>' + rows.length + '</span></div><div class="col-body">';
5812
- if (rows.length === 0) {
5813
- html += '<div class="empty">No tasks</div>';
5814
- } else {
5815
- for (var i = 0; i < rows.length; i++) html += renderCard(rows[i]);
5816
- }
5817
- html += '</div></div>';
6148
+ });
6149
+ function initBridge(){
6150
+ if (_isStandalone()){
6151
+ return window.McpApp.connect().then(function(conn){
6152
+ _standaloneApp = conn;
6153
+ // No host to advertise a hostContext \u2014 default to inline with no
6154
+ // other modes available, which keeps the #dm/#pin toggle buttons
6155
+ // hidden (they only show for modes hostContext.availableDisplayModes
6156
+ // lists) instead of rendering two buttons with nothing to switch to.
6157
+ _setHostContext({displayMode: 'inline', availableDisplayModes: []});
6158
+ });
5818
6159
  }
5819
- el.innerHTML = html;
5820
- }
5821
-
5822
- // ============================================================
5823
- // Data
5824
- // ============================================================
5825
-
5826
- function loadBoard() {
5827
- var args = {includeClosed: true, full: true};
5828
- if (boardId) args.boardId = boardId;
5829
- return callTool('task_list', args).then(function (data) {
5830
- boardId = data.boardId || boardId;
5831
- tasks = data.tasks || [];
5832
- render();
5833
- setStatus(tasks.length + ' task' + (tasks.length === 1 ? '' : 's') + ' \xB7 ' + new Date().toLocaleTimeString());
5834
- }).catch(function (e) {
5835
- setStatus('Error: ' + e.message);
6160
+ return rpcRequest('ui/initialize', {
6161
+ appInfo: {name: "agentproto-work-board-panel", version: '0.1.0'},
6162
+ appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},
6163
+ protocolVersion: '2026-01-26'
6164
+ }).then(function(result){
6165
+ // The initialize result carries the initial hostContext (displayMode +
6166
+ // availableDisplayModes) \u2014 capture it before notifying the host.
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;
6172
+ rpcNotify('ui/notifications/initialized', {});
5836
6173
  });
5837
6174
  }
5838
-
5839
- function doPoll() {
5840
- if (pollActive) return;
5841
- pollActive = true;
5842
- loadBoard().then(function () {
5843
- pollActive = false;
5844
- pollTimer = setTimeout(doPoll, 4000);
5845
- }).catch(function () {
5846
- pollActive = false;
5847
- pollTimer = setTimeout(doPoll, 4000);
6175
+ function requestDisplayMode(mode){
6176
+ return rpcRequest('ui/request-display-mode', {mode: mode});
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
+ }
6182
+ function callTool(name, args){
6183
+ var raw = _standaloneApp
6184
+ ? _standaloneApp.callTool(name, args || {})
6185
+ : rpcRequest('tools/call', {name: name, arguments: args || {}});
6186
+ return raw.then(function(result){
6187
+ if (result.isError){
6188
+ var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';
6189
+ throw new Error(e);
6190
+ }
6191
+ var text = (result.content && result.content[0] && result.content[0].text) || '{}';
6192
+ return JSON.parse(text);
5848
6193
  });
5849
6194
  }
5850
6195
 
5851
- // ============================================================
5852
- // Toolbar wiring
5853
- // ============================================================
6196
+ // \u2500\u2500 Display-mode toggle buttons (NO auto-request) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6197
+ // Injected by the shared bridge so every panel gets them without touching
6198
+ // its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the
6199
+ // panel stays inline by default; the user expands on demand. Buttons only
6200
+ // appear for modes the host advertises in hostContext.availableDisplayModes.
6201
+ (function(){
6202
+ function mount(){
6203
+ var style = document.createElement('style');
6204
+ style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'
6205
+ + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'
6206
+ + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'
6207
+ + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'
6208
+ + '#dm{right:8px}#pin{right:118px}'
6209
+ + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'
6210
+ + '@media (prefers-color-scheme:dark){'
6211
+ + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'
6212
+ + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';
6213
+ document.head.appendChild(style);
5854
6214
 
5855
- document.getElementById('columns').addEventListener('click', function (evt) {
5856
- var btn = evt.target.closest ? evt.target.closest('button[data-action]') : null;
5857
- if (!btn) return;
5858
- applyAction(btn.getAttribute('data-taskid'), btn.getAttribute('data-action'));
5859
- });
6215
+ var btn = document.createElement('button');
6216
+ btn.id = 'dm'; btn.type = 'button'; btn.title = "Basculer l'affichage";
6217
+ var pin = document.createElement('button');
6218
+ pin.id = 'pin'; pin.type = 'button'; pin.title = '\xC9pingler sur le c\xF4t\xE9 (pip)';
6219
+ document.body.appendChild(pin);
6220
+ document.body.appendChild(btn);
5860
6221
 
5861
- document.getElementById('refresh-btn').addEventListener('click', function () {
5862
- loadBoard();
5863
- });
6222
+ function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }
5864
6223
 
5865
- document.getElementById('go-btn').addEventListener('click', function () {
5866
- var v = document.getElementById('board-input').value.trim();
5867
- if (!v) return;
5868
- boardId = v;
5869
- loadBoard();
5870
- });
6224
+ // Re-sync button visibility + label from the current host context.
6225
+ function syncBtn(ctx){
6226
+ ctx = ctx || {};
6227
+ var avail = ctx.availableDisplayModes;
6228
+ // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)
6229
+ console.log('[mcp-app] displayMode=', ctx.displayMode,
6230
+ 'availableDisplayModes=', avail);
5871
6231
 
5872
- document.getElementById('add-btn').addEventListener('click', function () {
5873
- var input = document.getElementById('new-title');
5874
- var title = input.value.trim();
5875
- if (!title) return;
5876
- var args = {title: title};
5877
- if (boardId) args.boardId = boardId;
5878
- callTool('task_create', args).then(function (result) {
5879
- if (result && result.error) {
5880
- setStatus('Create failed: ' + result.error);
5881
- return;
6232
+ // Fullscreen toggle button.
6233
+ if (has(avail, 'fullscreen')){
6234
+ btn.style.display = 'block';
6235
+ btn.textContent = (ctx.displayMode === 'fullscreen') ? '\u2921 R\xE9duire' : '\u2922 Agrandir';
6236
+ } else { btn.style.display = 'none'; }
6237
+
6238
+ // Dedicated pip ("pinned on side") button \u2014 only if the host advertises pip.
6239
+ if (has(avail, 'pip')){
6240
+ pin.style.display = 'block';
6241
+ pin.textContent = (ctx.displayMode === 'pip') ? '\u2921 D\xE9tacher' : '\u{1F4CC} \xC9pingler';
6242
+ } else { pin.style.display = 'none'; }
5882
6243
  }
5883
- input.value = '';
5884
- setStatus('Created "' + title + '".');
5885
- return loadBoard();
5886
- }).catch(function (e) {
5887
- setStatus('Create failed: ' + e.message);
5888
- });
5889
- });
5890
6244
 
5891
- // ============================================================
5892
- // Boot
5893
- // ============================================================
6245
+ onHostContext(syncBtn);
5894
6246
 
5895
- initBridge().then(function () {
5896
- return loadBoard();
5897
- }).then(function () {
5898
- pollTimer = setTimeout(doPoll, 4000);
5899
- }).catch(function (e) {
5900
- setStatus('Bridge error: ' + e.message);
5901
- });
5902
- </script>
6247
+ btn.addEventListener('click', function(){
6248
+ var ctx = getHostContext() || {};
6249
+ var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');
6250
+ requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});
6251
+ });
6252
+
6253
+ pin.addEventListener('click', function(){
6254
+ var ctx = getHostContext() || {};
6255
+ requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});
6256
+ });
6257
+ }
6258
+ if (document.body) mount();
6259
+ else document.addEventListener('DOMContentLoaded', mount);
6260
+ })();</script>
6261
+ <script>(function() {
6262
+ "use strict";
6263
+ const COLUMN_STATUSES = ["pending", "in_progress", "done", "failed"];
6264
+ const COLUMN_LABELS = {
6265
+ pending: "Pending",
6266
+ in_progress: "In Progress",
6267
+ done: "Done",
6268
+ failed: "Failed"
6269
+ };
6270
+ function esc(value) {
6271
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6272
+ }
6273
+ function columnOf(status) {
6274
+ return status === "cancelled" ? "failed" : status;
6275
+ }
6276
+ function verificationTag(task) {
6277
+ const v = task.verification;
6278
+ if (v) {
6279
+ if (v.kind === "gate") return '<span class="tag t-gate">&#10003; gate</span>';
6280
+ if (v.kind === "self-report") return '<span class="tag t-self">self-report</span>';
6281
+ if (v.kind === "human") return '<span class="tag t-human">human</span>';
6282
+ }
6283
+ if (task.status === "in_progress" && task.verify) {
6284
+ return '<span class="tag t-gated">gated</span>';
6285
+ }
6286
+ return "";
6287
+ }
6288
+ function ownerTag(task) {
6289
+ if (!task.owner) return '<span class="tag t-unclaimed">Unclaimed</span>';
6290
+ return \`<span class="tag t-owner">\${esc(task.owner)}</span>\`;
6291
+ }
6292
+ function actionsFor(task) {
6293
+ if (task.status === "pending") {
6294
+ return [
6295
+ task.owner ? { label: "Start", action: "start", cls: "primary" } : { label: "Claim", action: "claim", cls: "primary" },
6296
+ { label: "Cancel", action: "cancel", cls: "danger" }
6297
+ ];
6298
+ }
6299
+ if (task.status === "in_progress") {
6300
+ return [
6301
+ { label: "Done", action: "done", cls: "primary" },
6302
+ { label: "Fail", action: "fail", cls: "danger" },
6303
+ { label: "Release", action: "release", cls: "" },
6304
+ { label: "Cancel", action: "cancel", cls: "danger" }
6305
+ ];
6306
+ }
6307
+ if (task.status === "done") {
6308
+ return [{ label: "Reopen", action: "reopen", cls: "" }];
6309
+ }
6310
+ return [];
6311
+ }
6312
+ function renderCard(task) {
6313
+ let html = \`<div class="card" data-taskid="\${esc(task.taskId)}">\`;
6314
+ html += \`<div class="c-title">\${esc(task.title)}</div>\`;
6315
+ html += \`<div class="c-id">#\${esc(task.taskId)}</div>\`;
6316
+ html += \`<div class="c-row">\${ownerTag(task)}\${verificationTag(task)}\`;
6317
+ if (task.status === "cancelled") html += '<span class="tag t-cancelled">cancelled</span>';
6318
+ html += "</div>";
6319
+ if (task.lastVerifyError) {
6320
+ html += \`<div class="c-err">verify failed: \${esc(task.lastVerifyError)}</div>\`;
6321
+ }
6322
+ const actions = actionsFor(task);
6323
+ if (actions.length > 0) {
6324
+ html += '<div class="c-actions">';
6325
+ for (const a of actions) {
6326
+ html += \`<button class="abtn \${a.cls}" data-taskid="\${esc(task.taskId)}" data-action="\${a.action}">\${a.label}</button>\`;
6327
+ }
6328
+ html += "</div>";
6329
+ }
6330
+ html += "</div>";
6331
+ return html;
6332
+ }
6333
+ function renderColumns(tasks2) {
6334
+ let html = "";
6335
+ for (const status of COLUMN_STATUSES) {
6336
+ const rows = tasks2.filter((t) => columnOf(t.status) === status);
6337
+ html += \`<div class="col"><div class="col-hdr"><span>\${COLUMN_LABELS[status]}</span><span>\${rows.length}</span></div><div class="col-body">\`;
6338
+ html += rows.length === 0 ? '<div class="empty">No tasks</div>' : rows.map(renderCard).join("");
6339
+ html += "</div></div>";
6340
+ }
6341
+ return html;
6342
+ }
6343
+ function getEl(id) {
6344
+ const el = document.getElementById(id);
6345
+ if (!el) throw new Error(\`work board: missing #\${id}\`);
6346
+ return el;
6347
+ }
6348
+ function getInput(id) {
6349
+ const el = document.getElementById(id);
6350
+ if (!(el instanceof HTMLInputElement)) throw new Error(\`work board: missing input #\${id}\`);
6351
+ return el;
6352
+ }
6353
+ function isCardAction(value) {
6354
+ switch (value) {
6355
+ case "claim":
6356
+ case "start":
6357
+ case "done":
6358
+ case "fail":
6359
+ case "release":
6360
+ case "cancel":
6361
+ case "reopen":
6362
+ return true;
6363
+ default:
6364
+ return false;
6365
+ }
6366
+ }
6367
+ let boardId = null;
6368
+ let tasks = [];
6369
+ let pollActive = false;
6370
+ function boot() {
6371
+ const columnsEl = getEl("columns");
6372
+ const statusbarEl = getEl("statusbar");
6373
+ const boardIdEl = getEl("board-id");
6374
+ const boardInputEl = getInput("board-input");
6375
+ const newTitleEl = getInput("new-title");
6376
+ function setStatus(msg) {
6377
+ statusbarEl.textContent = msg;
6378
+ }
6379
+ function render() {
6380
+ boardIdEl.textContent = boardId ?? "\u2014";
6381
+ if (!boardInputEl.value) boardInputEl.value = boardId ?? "";
6382
+ columnsEl.innerHTML = renderColumns(tasks);
6383
+ }
6384
+ function findTask(taskId) {
6385
+ return tasks.find((t) => t.taskId === taskId);
6386
+ }
6387
+ function loadBoard() {
6388
+ const args = {
6389
+ includeClosed: true,
6390
+ full: true,
6391
+ ...boardId ? { boardId } : {}
6392
+ };
6393
+ return callTool("task_list", args).then((data) => {
6394
+ boardId = data.boardId ?? boardId;
6395
+ tasks = data.tasks ?? [];
6396
+ render();
6397
+ setStatus(\`\${tasks.length} task\${tasks.length === 1 ? "" : "s"} \xB7 \${(/* @__PURE__ */ new Date()).toLocaleTimeString()}\`);
6398
+ }).catch((e) => {
6399
+ setStatus(\`Error: \${e.message}\`);
6400
+ });
6401
+ }
6402
+ function applyAction(taskId, action) {
6403
+ const task = findTask(taskId);
6404
+ if (!task) return;
6405
+ const base = { taskId: task.taskId, rev: task.rev };
6406
+ let promise;
6407
+ if (action === "claim") {
6408
+ promise = callTool("task_claim", base);
6409
+ } else if (action === "start") {
6410
+ promise = callTool("task_update", { ...base, status: "in_progress" });
6411
+ } else if (action === "done") {
6412
+ promise = callTool("task_update", { ...base, status: "done" });
6413
+ } else if (action === "fail") {
6414
+ promise = callTool("task_update", { ...base, status: "failed" });
6415
+ } else if (action === "release") {
6416
+ promise = callTool("task_update", { ...base, owner: null });
6417
+ } else if (action === "cancel") {
6418
+ promise = callTool("task_update", { ...base, status: "cancelled" });
6419
+ } else {
6420
+ promise = callTool("task_update", { ...base, status: "pending" });
6421
+ }
6422
+ promise.then((result) => {
6423
+ if ("conflict" in result) {
6424
+ setStatus(\`Someone else moved "\${task.title}" first \u2014 refreshed.\`);
6425
+ } else if ("error" in result) {
6426
+ setStatus(\`Action failed: \${result.error}\`);
6427
+ } else {
6428
+ setStatus(\`Updated "\${task.title}".\`);
6429
+ }
6430
+ return loadBoard();
6431
+ }).catch((e) => {
6432
+ setStatus(\`Action failed: \${e.message}\`);
6433
+ });
6434
+ }
6435
+ function doPoll() {
6436
+ if (pollActive) return;
6437
+ pollActive = true;
6438
+ loadBoard().finally(() => {
6439
+ pollActive = false;
6440
+ setTimeout(doPoll, 4e3);
6441
+ });
6442
+ }
6443
+ columnsEl.addEventListener("click", (evt) => {
6444
+ if (!(evt.target instanceof Element)) return;
6445
+ const btn = evt.target.closest("button[data-action]");
6446
+ if (!(btn instanceof HTMLButtonElement)) return;
6447
+ const taskId = btn.getAttribute("data-taskid");
6448
+ const action = btn.getAttribute("data-action");
6449
+ if (!taskId || !action || !isCardAction(action)) return;
6450
+ applyAction(taskId, action);
6451
+ });
6452
+ getEl("refresh-btn").addEventListener("click", () => {
6453
+ void loadBoard();
6454
+ });
6455
+ getEl("go-btn").addEventListener("click", () => {
6456
+ const value = boardInputEl.value.trim();
6457
+ if (!value) return;
6458
+ boardId = value;
6459
+ void loadBoard();
6460
+ });
6461
+ getEl("add-btn").addEventListener("click", () => {
6462
+ const title = newTitleEl.value.trim();
6463
+ if (!title) return;
6464
+ const args = boardId ? { title, boardId } : { title };
6465
+ callTool("task_create", args).then((result) => {
6466
+ if (result.error) {
6467
+ setStatus(\`Create failed: \${result.error}\`);
6468
+ return void 0;
6469
+ }
6470
+ newTitleEl.value = "";
6471
+ setStatus(\`Created "\${title}".\`);
6472
+ return loadBoard();
6473
+ }).catch((e) => {
6474
+ setStatus(\`Create failed: \${e.message}\`);
6475
+ });
6476
+ });
6477
+ initBridge().then(() => loadBoard()).then(() => {
6478
+ setTimeout(doPoll, 4e3);
6479
+ }).catch((e) => {
6480
+ setStatus(\`Bridge error: \${e.message}\`);
6481
+ });
6482
+ }
6483
+ if (document.readyState === "loading") {
6484
+ document.addEventListener("DOMContentLoaded", boot);
6485
+ } else {
6486
+ boot();
6487
+ }
6488
+ })();</script>
6489
+ <style rel="stylesheet" crossorigin>/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
6490
+ Semantic tokens + runtime mapping sheets.
6491
+
6492
+ Every rule below the next section reads ONLY the --wb-* custom
6493
+ properties. Each one resolves through TWO mapping sheets, chosen at
6494
+ PAINT time via plain CSS var() fallback \u2014 no build step, no JS, so a
6495
+ host that swaps its theme live (VS Code) is never frozen at whatever
6496
+ theme happened to be active when this bundle was built:
6497
+
6498
+ 1. embedded \u2014 VS Code injects a live --vscode-* custom-property set
6499
+ into the webview and swaps it on theme change. Whenever the
6500
+ relevant --vscode-* variable exists, it wins \u2014 in a VS Code
6501
+ \`srcdoc\` relay panel AND (structurally identically) any other host
6502
+ that happened to expose the same names, though none is known to.
6503
+ 2. standalone \u2014 the agentproto brand tokens (see
6504
+ projects/agentproto/site/src/app/brand.css), used whenever no
6505
+ --vscode-* variable is set \u2014 a plain MCP-Apps host or the daemon's
6506
+ own \`/apps/.../ui\` tab. \`--phos\`/\`--amber\`/\`--crimson\` are
6507
+ THEME-INVARIANT in brand.css (the phosphor terminal palette), so
6508
+ they're identical in both blocks below; the rest follow brand.css's
6509
+ light/dark split via \`prefers-color-scheme\` \u2014 a standalone tab has
6510
+ no host to pick a theme for it, unlike VS Code's own.
6511
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
6512
+
6513
+ :root {
6514
+ --wb-bg: var(--vscode-editor-background, #f5f6f3);
6515
+ --wb-surface: var(--vscode-sideBar-background, #ffffff);
6516
+ --wb-ink: var(--vscode-editor-foreground, #171a1e);
6517
+ --wb-ink-muted: var(--vscode-descriptionForeground, #5c6470);
6518
+ --wb-border: var(--vscode-panel-border, #dde0da);
6519
+ --wb-accent: var(--vscode-button-background, #0f62fe);
6520
+
6521
+ /* Status colors \u2014 phos/amber/crimson are the theme-invariant terminal
6522
+ palette in brand.css, so light and dark standalone fallbacks match. */
6523
+ --wb-status-pending: var(--vscode-descriptionForeground, #5c6470);
6524
+ --wb-status-in-progress: var(--vscode-charts-yellow, #f0b429);
6525
+ --wb-status-done: var(--vscode-charts-green, #3edc81);
6526
+ --wb-status-failed: var(--vscode-charts-red, #fa4d56);
6527
+ --wb-status-cancelled: var(--vscode-descriptionForeground, #5c6470);
6528
+ --wb-status-cancelled-weak: color-mix(in srgb, var(--wb-status-cancelled) 55%, transparent);
6529
+ }
6530
+
6531
+ @media (prefers-color-scheme: dark) {
6532
+ :root {
6533
+ --wb-bg: var(--vscode-editor-background, #101214);
6534
+ --wb-surface: var(--vscode-sideBar-background, #16191d);
6535
+ --wb-ink: var(--vscode-editor-foreground, #e8eaed);
6536
+ --wb-ink-muted: var(--vscode-descriptionForeground, #99a2ad);
6537
+ --wb-border: var(--vscode-panel-border, #272c33);
6538
+ --wb-accent: var(--vscode-button-background, #4589ff);
6539
+
6540
+ --wb-status-pending: var(--vscode-descriptionForeground, #99a2ad);
6541
+ --wb-status-in-progress: var(--vscode-charts-yellow, #f0b429);
6542
+ --wb-status-done: var(--vscode-charts-green, #3edc81);
6543
+ --wb-status-failed: var(--vscode-charts-red, #fa4d56);
6544
+ --wb-status-cancelled: var(--vscode-descriptionForeground, #99a2ad);
6545
+ --wb-status-cancelled-weak: color-mix(in srgb, var(--wb-status-cancelled) 55%, transparent);
6546
+ }
6547
+ }
6548
+
6549
+ /* \u2500\u2500 Base \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6550
+
6551
+ * { box-sizing: border-box; margin: 0; padding: 0; }
6552
+
6553
+ html, body {
6554
+ height: 100%;
6555
+ font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
6556
+ font-size: 13px;
6557
+ background: var(--wb-bg);
6558
+ color: var(--wb-ink);
6559
+ overflow: hidden;
6560
+ }
6561
+
6562
+ .mono {
6563
+ font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace;
6564
+ }
6565
+
6566
+ #app { display: flex; flex-direction: column; height: 100%; }
6567
+
6568
+ #toolbar {
6569
+ padding: 8px 12px;
6570
+ border-bottom: 1px solid var(--wb-border);
6571
+ display: flex;
6572
+ align-items: center;
6573
+ gap: 8px;
6574
+ background: var(--wb-surface);
6575
+ flex-shrink: 0;
6576
+ }
6577
+ #board-label { font-size: 11px; color: var(--wb-ink-muted); }
6578
+ #board-id { font-weight: 600; color: var(--wb-ink); font-size: 12px; font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace; }
6579
+ #board-input {
6580
+ background: var(--wb-bg);
6581
+ border: 1px solid var(--wb-border);
6582
+ color: var(--wb-ink);
6583
+ padding: 4px 8px;
6584
+ border-radius: 4px;
6585
+ font-size: 12px;
6586
+ font-family: inherit;
6587
+ width: 220px;
6588
+ }
6589
+ .tbtn {
6590
+ background: var(--wb-bg);
6591
+ border: 1px solid var(--wb-border);
6592
+ color: var(--wb-ink);
6593
+ padding: 4px 10px;
6594
+ border-radius: 4px;
6595
+ cursor: pointer;
6596
+ font-size: 12px;
6597
+ font-family: inherit;
6598
+ }
6599
+ .tbtn:hover { background: var(--wb-border); }
6600
+ #new-title {
6601
+ flex: 1;
6602
+ min-width: 120px;
6603
+ background: var(--wb-bg);
6604
+ border: 1px solid var(--wb-border);
6605
+ color: var(--wb-ink);
6606
+ padding: 4px 8px;
6607
+ border-radius: 4px;
6608
+ font-size: 12px;
6609
+ font-family: inherit;
6610
+ }
6611
+
6612
+ #columns { flex: 1; display: flex; overflow-x: auto; overflow-y: hidden; }
6613
+ .col {
6614
+ width: 260px;
6615
+ min-width: 220px;
6616
+ flex-shrink: 0;
6617
+ display: flex;
6618
+ flex-direction: column;
6619
+ border-right: 1px solid var(--wb-border);
6620
+ }
6621
+ .col-hdr {
6622
+ padding: 8px 10px;
6623
+ border-bottom: 1px solid var(--wb-border);
6624
+ background: var(--wb-surface);
6625
+ font-size: 11px;
6626
+ font-weight: 600;
6627
+ color: var(--wb-ink-muted);
6628
+ text-transform: uppercase;
6629
+ letter-spacing: 0.05em;
6630
+ display: flex;
6631
+ justify-content: space-between;
6632
+ }
6633
+ .col-body { flex: 1; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px; }
6634
+
6635
+ .card {
6636
+ background: var(--wb-surface);
6637
+ border: 1px solid var(--wb-border);
6638
+ border-radius: 6px;
6639
+ padding: 8px 10px;
6640
+ }
6641
+ .c-title { font-size: 12px; font-weight: 500; margin-bottom: 4px; word-break: break-word; }
6642
+ .c-id {
6643
+ font-size: 10px;
6644
+ color: var(--wb-ink-muted);
6645
+ margin-bottom: 6px;
6646
+ font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace;
6647
+ }
6648
+ .c-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin-bottom: 6px; }
6649
+
6650
+ .tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }
6651
+ .t-unclaimed { background: color-mix(in srgb, var(--wb-ink-muted) 15%, transparent); color: var(--wb-ink-muted); border: 1px dashed var(--wb-ink-muted); }
6652
+ .t-owner { background: color-mix(in srgb, var(--wb-accent) 15%, transparent); color: var(--wb-accent); }
6653
+
6654
+ /* Verification tell: gate solid green, self-report amber outline, human
6655
+ blue, a declared verify with no verification yet grey "gated" \u2014 never
6656
+ render an unverified done as gate-passed (see render.ts's verificationTag). */
6657
+ .t-gate { background: color-mix(in srgb, var(--wb-status-done) 18%, transparent); color: var(--wb-status-done); }
6658
+ .t-self { background: transparent; color: var(--wb-status-in-progress); border: 1px solid var(--wb-status-in-progress); }
6659
+ .t-human { background: color-mix(in srgb, var(--wb-accent) 18%, transparent); color: var(--wb-accent); }
6660
+ .t-gated { background: color-mix(in srgb, var(--wb-ink-muted) 12%, transparent); color: var(--wb-ink-muted); }
6661
+ .t-cancelled {
6662
+ background: color-mix(in srgb, var(--wb-status-cancelled) 10%, transparent);
6663
+ color: var(--wb-status-cancelled-weak);
6664
+ border: 1px dashed color-mix(in srgb, var(--wb-status-cancelled) 40%, transparent);
6665
+ }
6666
+
6667
+ .c-err { font-size: 10px; color: var(--wb-status-failed); margin-bottom: 6px; word-break: break-word; }
6668
+ .c-actions { display: flex; flex-wrap: wrap; gap: 4px; }
6669
+ .abtn {
6670
+ background: var(--wb-bg);
6671
+ border: 1px solid var(--wb-border);
6672
+ color: var(--wb-ink);
6673
+ padding: 3px 8px;
6674
+ border-radius: 4px;
6675
+ cursor: pointer;
6676
+ font-size: 11px;
6677
+ font-family: inherit;
6678
+ }
6679
+ .abtn:hover { background: var(--wb-border); }
6680
+ .abtn.danger { border-color: var(--wb-status-failed); color: var(--wb-status-failed); }
6681
+ .abtn.primary { border-color: var(--wb-status-done); color: var(--wb-status-done); }
6682
+
6683
+ #statusbar {
6684
+ padding: 4px 12px;
6685
+ font-size: 11px;
6686
+ color: var(--wb-ink-muted);
6687
+ border-top: 1px solid var(--wb-border);
6688
+ background: var(--wb-surface);
6689
+ flex-shrink: 0;
6690
+ }
6691
+ .empty { padding: 16px 8px; color: var(--wb-ink-muted); font-size: 11px; text-align: center; }</style>
6692
+ </head>
6693
+ <body>
6694
+ <div id="app">
6695
+ <div id="toolbar">
6696
+ <span id="board-label">Board:</span>
6697
+ <span id="board-id">\u2014</span>
6698
+ <input id="board-input" type="text" placeholder="tree:&lt;sessionId&gt; or ws:&lt;slug&gt;">
6699
+ <button class="tbtn" id="go-btn">Go</button>
6700
+ <button class="tbtn" id="refresh-btn" title="Refresh">&#8635;</button>
6701
+ <input id="new-title" type="text" placeholder="New task title&#8230;">
6702
+ <button class="tbtn" id="add-btn">+ Add</button>
6703
+ </div>
6704
+ <div id="columns"></div>
6705
+ <div id="statusbar">Connecting to bridge&#8230;</div>
6706
+ </div>
5903
6707
  </body>
5904
- </html>`;
6708
+ </html>
6709
+ `;
6710
+
6711
+ // src/work-board/panel.ts
6712
+ var WORK_BOARD_APP_ID = "@agentproto/work-board";
6713
+ var WORK_BOARD_TOOL_ID = "agentproto_work_board";
6714
+ var WORK_BOARD_UI_TOOLS = [
6715
+ "task_list",
6716
+ "task_claim",
6717
+ "task_update",
6718
+ "task_create"
6719
+ ];
6720
+ var WORK_BOARD_HTML = WORK_BOARD_HTML_GENERATED;
5905
6721
 
5906
6722
  // src/work-board/index.ts
5907
6723
  var workBoardInputSchema = z.object({
@@ -5911,7 +6727,7 @@ var workBoardInputSchema = z.object({
5911
6727
  });
5912
6728
  function makeWorkBoardApp(ops) {
5913
6729
  return {
5914
- id: "agentproto_work_board",
6730
+ id: WORK_BOARD_TOOL_ID,
5915
6731
  title: "Work Board",
5916
6732
  description: "Open the agentproto work board \u2014 a kanban over the daemon's Task ledger (columns: pending/in_progress/done/failed, cancelled folded into failed). Swimlane is the ledger's own board id (`tree:<root>` or `ws:<slug>`). Each card shows its owner (or \"Unclaimed\") and the verification tell \u2014 a gate-passed done reads visually distinct from a self-reported one. Pass `boardId` to open a specific board.",
5917
6733
  inputSchema: workBoardInputSchema,
@@ -5920,14 +6736,14 @@ function makeWorkBoardApp(ops) {
5920
6736
  };
5921
6737
  }
5922
6738
  var workBoardApp = defineApp({
5923
- id: "@agentproto/work-board",
6739
+ id: WORK_BOARD_APP_ID,
5924
6740
  name: "Work Board",
5925
6741
  description: "Open the agentproto work board \u2014 a kanban over the daemon's Task ledger, scoped by board id (`tree:<rootSessionId>` or `ws:<slug>`), with an explicit status action per card.",
5926
6742
  agents: [],
5927
6743
  ui: {
5928
6744
  html: WORK_BOARD_HTML,
5929
6745
  title: "Work Board",
5930
- tools: ["task_list", "task_claim", "task_update", "task_create"]
6746
+ tools: [...WORK_BOARD_UI_TOOLS]
5931
6747
  }
5932
6748
  });
5933
6749