@agentproto/apps 0.20.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents-overview/panel.mjs +21 -8
- package/dist/agents-overview/panel.mjs.map +1 -1
- package/dist/agents-overview.mjs +21 -8
- package/dist/agents-overview.mjs.map +1 -1
- package/dist/bin/sync.mjs +19 -2
- package/dist/bin/sync.mjs.map +1 -1
- package/dist/bureau-sessions/panel.mjs +21 -8
- package/dist/bureau-sessions/panel.mjs.map +1 -1
- package/dist/bureau-sessions.mjs +21 -8
- package/dist/bureau-sessions.mjs.map +1 -1
- package/dist/index.mjs +63 -20
- package/dist/index.mjs.map +1 -1
- package/dist/live-session/panel.mjs +21 -8
- package/dist/live-session/panel.mjs.map +1 -1
- package/dist/live-session.mjs +21 -8
- package/dist/live-session.mjs.map +1 -1
- package/dist/ops-panel/ui.d.ts +1 -1
- package/dist/ops-panel/ui.d.ts.map +1 -1
- package/dist/ops-panel.mjs +19 -2
- package/dist/ops-panel.mjs.map +1 -1
- package/dist/panel-bridge.d.ts.map +1 -1
- package/dist/review-panel/panel.d.ts +1 -1
- package/dist/review-panel/panel.d.ts.map +1 -1
- package/dist/review-panel/panel.generated.d.ts +1 -1
- package/dist/review-panel/panel.generated.d.ts.map +1 -1
- package/dist/review-panel/panel.mjs +1 -1
- package/dist/review-panel/panel.mjs.map +1 -1
- package/dist/review-panel.mjs +1 -1
- package/dist/review-panel.mjs.map +1 -1
- package/dist/session-chat/panel.mjs +21 -8
- package/dist/session-chat/panel.mjs.map +1 -1
- package/dist/session-chat.mjs +21 -8
- package/dist/session-chat.mjs.map +1 -1
- package/dist/session-story/panel.mjs +21 -8
- package/dist/session-story/panel.mjs.map +1 -1
- package/dist/session-story.mjs +21 -8
- package/dist/session-story.mjs.map +1 -1
- package/dist/sessions-panel/panel.mjs +21 -8
- package/dist/sessions-panel/panel.mjs.map +1 -1
- package/dist/sessions-panel.mjs +21 -8
- package/dist/sessions-panel.mjs.map +1 -1
- package/dist/store/panel.d.ts +1 -1
- package/dist/store/panel.d.ts.map +1 -1
- package/dist/store/panel.generated.d.ts +1 -1
- package/dist/store/panel.generated.d.ts.map +1 -1
- package/dist/store/panel.mjs +1 -1
- package/dist/store/panel.mjs.map +1 -1
- package/dist/store.mjs +1 -1
- package/dist/store.mjs.map +1 -1
- package/dist/work-board/panel.d.ts +1 -1
- package/dist/work-board/panel.d.ts.map +1 -1
- package/dist/work-board/panel.generated.d.ts +1 -1
- package/dist/work-board/panel.generated.d.ts.map +1 -1
- package/dist/work-board/panel.mjs +21 -8
- package/dist/work-board/panel.mjs.map +1 -1
- package/dist/work-board.mjs +21 -8
- package/dist/work-board.mjs.map +1 -1
- package/package.json +2 -2
- package/repo-maintenance/.agentproto/workflows/maintain/WORKFLOW.md +17 -1
- package/repo-maintenance/.agentproto/workflows/maintain/entry.mjs +80 -5
- package/session-steward/.agentproto/workflows/session-steward/WORKFLOW.md +6 -3
- package/session-steward/.agentproto/workflows/session-steward/entry.mjs +7 -3
- package/session-steward/.agentproto/workflows/session-steward/origin-policy.mjs +2 -2
- package/session-steward/README.md +1 -1
- package/session-steward/routines/session-steward-hourly/ROUTINE.md +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/review-panel/panel.generated.ts","../src/review-panel/panel.ts","../src/review-panel/index.ts"],"names":[],"mappings":";;;;;;;;;;AAYO,IAAM,2BAAA,GAA8B,6uxCAAA;;;ACsCpC,IAAM,mBAAA,GAAsB,0BAAA;AAI5B,IAAM,oBAAA,GAAuB,oBAAA;AAO7B,IAAM,qBAAA,GAAwB;AAAA,EACnC,eAAA;AAAA,EACA,eAAA;AAAA,EACA,eAAA;AAAA,EACA,YAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAA;AAEO,IAAM,iBAAA,GAAoB,2BAAA;;;AC/B1B,IAAM,sBAAA,GAAyB,EAAE,MAAA,CAAO;AAAA,EAC7C,KAAK,CAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,iFAA4E,CAAA;AAAA,EAChH,YAAY,CAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,yDAAyD,CAAA;AAAA,EACpG,OAAO,CAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,+EAA+E,CAAA;AAAA,EACrH,oBAAoB,CAAA,CACjB,MAAA,GACA,QAAA,EAAS,CACT,SAAS,kEAAkE;AAChF,CAAC;AA+BM,SAAS,mBAAmB,GAAA,EAAsE;AACvG,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,oBAAA;AAAA,IACJ,KAAA,EAAO,SAAA;AAAA,IACP,WAAA,EACE,wXAAA;AAAA,IAKF,WAAA,EAAa,sBAAA;AAAA,IACb,OAAA,EAAS,OAAM,KAAA,KAAS;AACtB,MAAA,MAAM,QAAA,GAAW,MAAM,GAAA,CAAI,WAAA,CAAY,KAAK,CAAA;AAC5C,MAAA,OAAO,EAAE,GAAG,QAAA,EAAU,GAAI,KAAA,CAAM,KAAA,GAAQ,EAAE,UAAA,EAAY,KAAA,CAAM,KAAA,EAAM,GAAI,EAAC,EAAG;AAAA,IAC5E,CAAA;AAAA,IACA,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,iBAA4B,SAAA,CAAU;AAAA,EACjD,EAAA,EAAI,mBAAA;AAAA,EACJ,IAAA,EAAM,SAAA;AAAA,EACN,WAAA,EACE,0JAAA;AAAA,EAEF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,iBAAA;AAAA,IACN,KAAA,EAAO,SAAA;AAAA,IACP,KAAA,EAAO,CAAC,GAAG,qBAAqB;AAAA;AAEpC,CAAC","file":"review-panel.mjs","sourcesContent":["/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/review-panel/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-review-panel-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui:reviews\n *\n * `pnpm --filter @agentproto/apps run build:ui:reviews:check` (wired into\n * CI) fails if this file drifts from what the Vite sources actually produce.\n */\n\nexport const REVIEW_PANEL_HTML_GENERATED = \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"UTF-8\\\" />\\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\" />\\n<title>agentproto reviews</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-review-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(function(result){\\n if (result.isError){\\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\\n throw new Error(e);\\n }\\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\\n return JSON.parse(text);\\n });\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n function esc(value) {\\n return value.replace(/&/g, \\\"&\\\").replace(/</g, \\\"<\\\").replace(/>/g, \\\">\\\").replace(/\\\"/g, \\\""\\\");\\n }\\n function liveSessionUrl(origin, sessionId) {\\n return `${origin}/apps/@agentproto/live-session/ui?sessionId=${encodeURIComponent(sessionId)}`;\\n }\\n function shortRange(baseSha, headSha) {\\n if (!baseSha || !headSha) return \\\"—\\\";\\n return `${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`;\\n }\\n function ageOf(createdAt, now = Date.now()) {\\n const ms = now - new Date(createdAt).getTime();\\n if (!Number.isFinite(ms) || ms < 0) return \\\"just now\\\";\\n const mins = Math.floor(ms / 6e4);\\n if (mins < 1) return \\\"just now\\\";\\n if (mins < 60) return `${mins}m`;\\n const hours = Math.floor(mins / 60);\\n if (hours < 24) return `${hours}h`;\\n return `${Math.floor(hours / 24)}d`;\\n }\\n function verdictChip(status) {\\n return `<span class=\\\"chip chip-verdict v-${esc(status)}\\\">${esc(status)}</span>`;\\n }\\n const LANE_STATUS_LABEL = {\\n pass: \\\"pass\\\",\\n fail: \\\"fail\\\",\\n skipped: \\\"skipped\\\",\\n timeout: \\\"timeout\\\"\\n };\\n function laneStatusChip(status) {\\n return `<span class=\\\"chip chip-lane l-${esc(status)}\\\">${esc(LANE_STATUS_LABEL[status])}</span>`;\\n }\\n function severityTag(severity) {\\n return `<span class=\\\"tag sev-${esc(severity)}\\\">${esc(severity)}</span>`;\\n }\\n function requesterLabel(row) {\\n const r = row.requester;\\n if (!r) return \\\"—\\\";\\n const id = r.sessionId ? esc(r.sessionId) : \\\"\\\";\\n const author = r.gitAuthor ? esc(r.gitAuthor.name) : \\\"\\\";\\n if (id && author) return `${id} <span class=\\\"muted\\\">(${author})</span>`;\\n return id || author || \\\"—\\\";\\n }\\n function prLabel(row) {\\n if (!row.pr) return \\\"—\\\";\\n const state = row.prState ? ` <span class=\\\"tag pr-${esc(row.prState)}\\\">${esc(row.prState)}</span>` : \\\"\\\";\\n return `<a href=\\\"${esc(row.pr.url)}\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">#${row.pr.number}</a>${state}`;\\n }\\n function renderRow(row) {\\n const status = row.status ?? row.verdict ?? \\\"incomplete\\\";\\n const flags = [];\\n if (row.dirty) flags.push('<span class=\\\"tag t-dirty\\\">dirty</span>');\\n if (row.cached) flags.push('<span class=\\\"tag t-cached\\\">cached</span>');\\n if (row.status !== \\\"running\\\") {\\n flags.push(row.signed ? '<span class=\\\"tag t-signed\\\">signed</span>' : '<span class=\\\"tag t-unsigned\\\">unsigned</span>');\\n }\\n return `<tr class=\\\"row\\\" data-runid=\\\"${esc(row.runId)}\\\" tabindex=\\\"0\\\"><td>${verdictChip(status)}</td><td class=\\\"mono\\\">${esc(shortRange(row.baseSha, row.headSha))}</td><td>${esc(row.binding ?? \\\"—\\\")}</td><td class=\\\"mono\\\">${esc(row.repoRemote ?? \\\"—\\\")}</td><td>${requesterLabel(row)}</td><td>${prLabel(row)}</td><td>${flags.join(\\\" \\\") || \\\"—\\\"}</td><td>${esc(ageOf(row.createdAt))}</td></tr>`;\\n }\\n function renderList(rows2) {\\n if (rows2.length === 0) return '<div class=\\\"empty\\\">No reviews recorded yet.</div>';\\n const header = \\\"<tr><th>Verdict</th><th>Range</th><th>Binding</th><th>Repo</th><th>Requester</th><th>PR</th><th>Flags</th><th>Age</th></tr>\\\";\\n return `<table class=\\\"list\\\"><thead>${header}</thead><tbody>${rows2.map(renderRow).join(\\\"\\\")}</tbody></table>`;\\n }\\n function findingLine(f) {\\n const loc = f.file ? `${esc(f.file)}${f.line ? `:${f.line}` : \\\"\\\"}` : \\\"\\\";\\n return `<details class=\\\"finding\\\"><summary>${severityTag(f.severity)} ${esc(f.title)}${loc ? ` <span class=\\\"mono muted\\\">${loc}</span>` : \\\"\\\"}</summary><div class=\\\"finding-detail\\\">${esc(f.detail)}</div></details>`;\\n }\\n function renderLaneDetail(lane, rubrics = []) {\\n const parts = [];\\n parts.push(`<div class=\\\"lane-hdr\\\">`);\\n parts.push(`<span class=\\\"lane-id\\\">${esc(lane.id)}</span>`);\\n parts.push(laneStatusChip(lane.status));\\n parts.push(`<span class=\\\"tag ${lane.blocking ? \\\"t-blocking\\\" : \\\"t-advisory\\\"}\\\">${lane.blocking ? \\\"blocking\\\" : \\\"advisory\\\"}</span>`);\\n if (lane.durationMs !== void 0) parts.push(`<span class=\\\"muted\\\">${(lane.durationMs / 1e3).toFixed(1)}s</span>`);\\n parts.push(`</div>`);\\n if (lane.error) parts.push(`<div class=\\\"lane-error\\\">${esc(lane.error)}</div>`);\\n if (lane.sessionId || lane.preset || lane.model || lane.fallbacks?.length) {\\n const rubric = rubrics.find((r) => r.check === lane.id);\\n const bits = [];\\n if (lane.model) bits.push(`model: ${esc(lane.model)}`);\\n if (lane.preset) bits.push(`preset: ${esc(lane.preset)}`);\\n if (lane.fallbacks?.length) {\\n bits.push(`after unavailable: ${lane.fallbacks.map((f) => `${esc(f.preset)} (${esc(f.error)})`).join(\\\", \\\")}`);\\n }\\n if (rubric) bits.push(`rubric: <span class=\\\"mono\\\">${esc(rubric.sha256.slice(0, 12))}</span>`);\\n if (lane.sessionId) {\\n bits.push(\\n `reviewer: <button class=\\\"sess-link\\\" data-session-id=\\\"${esc(lane.sessionId)}\\\">${esc(lane.sessionId)}</button>`\\n );\\n }\\n parts.push(`<div class=\\\"lane-agent-meta\\\">${bits.join(\\\" · \\\")}</div>`);\\n }\\n if (lane.findings.length > 0) parts.push(`<div class=\\\"findings\\\">${lane.findings.map(findingLine).join(\\\"\\\")}</div>`);\\n return `<div class=\\\"lane\\\">${parts.join(\\\"\\\")}</div>`;\\n }\\n function renderDetail(detail2) {\\n const status = detail2.status === \\\"done\\\" ? detail2.verdict ?? \\\"incomplete\\\" : detail2.status;\\n const header = [`<div class=\\\"detail-hdr\\\">`, verdictChip(status), `<span class=\\\"mono\\\">${esc(detail2.runId)}</span>`];\\n if (detail2.binding) header.push(`<span class=\\\"tag\\\">${esc(detail2.binding)}</span>`);\\n if (detail2.cached) header.push('<span class=\\\"tag t-cached\\\">cached</span>');\\n if (detail2.status === \\\"done\\\" && detail2.attestation) {\\n const sig = detail2.attestation.attestor?.signature;\\n header.push(\\n sig ? `<span class=\\\"tag t-signed\\\" title=\\\"${esc(sig.keyFingerprint)}\\\">signed: ${esc(sig.principal)}</span>` : '<span class=\\\"tag t-unsigned\\\">unsigned</span>'\\n );\\n }\\n header.push(`</div>`);\\n if (detail2.error) header.push(`<div class=\\\"detail-error\\\">${esc(detail2.error)}</div>`);\\n if (detail2.supersededBy) header.push(`<div class=\\\"muted\\\">superseded by ${esc(detail2.supersededBy)}</div>`);\\n const fullLanes = detail2.attestation?.lanes;\\n const rubrics = detail2.attestation?.rubrics ?? [];\\n let body;\\n if (fullLanes) {\\n body = fullLanes.map((l) => renderLaneDetail(l, rubrics)).join(\\\"\\\");\\n } else if (detail2.lanes) {\\n body = detail2.lanes.map((l) => `<div class=\\\"lane\\\"><div class=\\\"lane-hdr\\\"><span class=\\\"lane-id\\\">${esc(l.id)}</span>${laneStatusChip(l.status)}</div></div>`).join(\\\"\\\");\\n } else {\\n body = \\\"\\\";\\n }\\n return `<div class=\\\"detail\\\">${header.join(\\\"\\\")}${body}</div>`;\\n }\\n const POLL_RUNNING_MS = 4e3;\\n const POLL_IDLE_MS = 15e3;\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`review panel: missing #${id}`);\\n return el;\\n }\\n let rows = [];\\n let selectedRunId = null;\\n let detail = null;\\n let pollTimer = null;\\n let loadInFlight = false;\\n function boot() {\\n const listPaneEl = getEl(\\\"list-pane\\\");\\n const detailPaneEl = getEl(\\\"detail-pane\\\");\\n const detailBodyEl = getEl(\\\"detail-body\\\");\\n const statusbarEl = getEl(\\\"statusbar\\\");\\n const cancelBtn = getEl(\\\"cancel-btn\\\");\\n const rerunBtn = getEl(\\\"rerun-btn\\\");\\n const prBtn = getEl(\\\"pr-btn\\\");\\n const exportBtn = getEl(\\\"export-btn\\\");\\n function setStatus(msg) {\\n statusbarEl.textContent = msg;\\n }\\n function findRow(runId) {\\n return rows.find((r) => r.runId === runId);\\n }\\n function renderListPane() {\\n listPaneEl.innerHTML = renderList(rows);\\n if (!selectedRunId) return;\\n listPaneEl.querySelectorAll(\\\"tr[data-runid]\\\").forEach((tr) => {\\n if (tr.dataset.runid === selectedRunId) tr.classList.add(\\\"selected\\\");\\n });\\n }\\n function renderDetailPane() {\\n if (!selectedRunId || !detail) {\\n detailPaneEl.classList.remove(\\\"open\\\");\\n return;\\n }\\n detailPaneEl.classList.add(\\\"open\\\");\\n detailBodyEl.innerHTML = renderDetail(detail);\\n const running = detail.status === \\\"running\\\";\\n cancelBtn.disabled = !running;\\n cancelBtn.style.display = running ? \\\"\\\" : \\\"none\\\";\\n detailBodyEl.querySelectorAll(\\\".sess-link\\\").forEach((btn) => {\\n btn.addEventListener(\\\"click\\\", () => openSession(btn.dataset.sessionId ?? \\\"\\\"));\\n });\\n }\\n function openSession(sessionId) {\\n if (!sessionId) return;\\n const origin = window.location.origin && window.location.origin !== \\\"null\\\" ? window.location.origin : \\\"\\\";\\n const url = liveSessionUrl(origin, sessionId);\\n const caps = getHostCapabilities();\\n if (caps?.openLinks) {\\n openLink(url).catch(() => window.open(url, \\\"_blank\\\"));\\n } else {\\n window.open(url, \\\"_blank\\\");\\n }\\n }\\n function loadList() {\\n if (loadInFlight) return Promise.resolve();\\n loadInFlight = true;\\n return callTool(\\\"review_ledger\\\", { includeRunning: true }).then((result) => {\\n rows = result.attestations;\\n renderListPane();\\n const runningCount = rows.filter((r) => r.status === \\\"running\\\").length;\\n setStatus(`${result.total} review${result.total === 1 ? \\\"\\\" : \\\"s\\\"} · ${runningCount} running · ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\\n if (selectedRunId && findRow(selectedRunId)) return loadDetail(selectedRunId);\\n return void 0;\\n }).catch((e) => setStatus(`Error: ${e.message}`)).finally(() => {\\n loadInFlight = false;\\n });\\n }\\n function loadDetail(runId) {\\n return callTool(\\\"review_status\\\", { runId }).then((result) => {\\n detail = result;\\n renderDetailPane();\\n }).catch((e) => setStatus(`Error loading detail: ${e.message}`));\\n }\\n function openDetail(runId) {\\n selectedRunId = runId;\\n renderListPane();\\n void loadDetail(runId);\\n }\\n function closeDetail() {\\n selectedRunId = null;\\n detail = null;\\n renderListPane();\\n renderDetailPane();\\n }\\n listPaneEl.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const tr = evt.target.closest(\\\"tr[data-runid]\\\");\\n if (!(tr instanceof HTMLElement)) return;\\n const runId = tr.dataset.runid;\\n if (runId) openDetail(runId);\\n });\\n listPaneEl.addEventListener(\\\"keydown\\\", (evt) => {\\n if (evt.key !== \\\"Enter\\\" && evt.key !== \\\" \\\") return;\\n if (!(evt.target instanceof Element)) return;\\n const tr = evt.target.closest(\\\"tr[data-runid]\\\");\\n if (!(tr instanceof HTMLElement)) return;\\n evt.preventDefault();\\n const runId = tr.dataset.runid;\\n if (runId) openDetail(runId);\\n });\\n getEl(\\\"detail-close\\\").addEventListener(\\\"click\\\", closeDetail);\\n getEl(\\\"refresh-btn\\\").addEventListener(\\\"click\\\", () => {\\n void loadList();\\n });\\n cancelBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_cancel\\\", { runId: selectedRunId }).then(() => {\\n setStatus(\\\"Cancelled.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Cancel failed: ${e.message}`));\\n });\\n rerunBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n const row = findRow(selectedRunId);\\n const cwd = row?.cwd;\\n if (!cwd) {\\n setStatus(\\\"Re-run failed: this row has no known checkout path.\\\");\\n return;\\n }\\n callTool(\\\"review_run\\\", {\\n cwd,\\n ...row?.binding ? { binding: row.binding } : {},\\n nocache: true,\\n wait: false,\\n supersede: true\\n }).then(() => {\\n setStatus(\\\"Re-run started.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Re-run failed: ${e.message}`));\\n });\\n prBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_pr\\\", { runId: selectedRunId }).then(() => {\\n setStatus(\\\"Fetched PR status.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Fetch PR status failed: ${e.message}`));\\n });\\n exportBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_export\\\", { runId: selectedRunId }).then((result) => {\\n setStatus(result.path ? `Exported to ${result.path}` : \\\"Exported.\\\");\\n }).catch((e) => setStatus(`Export failed: ${e.message}`));\\n });\\n function doPoll() {\\n if (pollTimer) clearTimeout(pollTimer);\\n loadList().finally(() => {\\n const anyRunning = rows.some((r) => r.status === \\\"running\\\");\\n pollTimer = setTimeout(doPoll, anyRunning ? POLL_RUNNING_MS : POLL_IDLE_MS);\\n });\\n }\\n initBridge().then(() => loadList()).then(() => {\\n pollTimer = setTimeout(doPoll, POLL_RUNNING_MS);\\n }).catch((e) => setStatus(`Bridge error: ${e.message}`));\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n Semantic tokens + runtime mapping sheets — same scheme as work-board's\\n style.css (see that file's header doc for the two-mapping-sheet\\n rationale): every rule below reads ONLY the --rp-* custom properties,\\n each resolving through a live --vscode-* var() first, then the\\n agentproto brand fallback.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --rp-bg: var(--vscode-editor-background, #f5f6f3);\\n --rp-surface: var(--vscode-sideBar-background, #ffffff);\\n --rp-ink: var(--vscode-editor-foreground, #171a1e);\\n --rp-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --rp-border: var(--vscode-panel-border, #dde0da);\\n --rp-accent: var(--vscode-button-background, #0f62fe);\\n\\n --rp-pass: var(--vscode-charts-green, #3edc81);\\n --rp-block: var(--vscode-charts-red, #fa4d56);\\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\\n --rp-running: var(--vscode-charts-blue, #4589ff);\\n --rp-cancelled: var(--vscode-descriptionForeground, #5c6470);\\n --rp-failed: var(--vscode-charts-red, #fa4d56);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --rp-bg: var(--vscode-editor-background, #101214);\\n --rp-surface: var(--vscode-sideBar-background, #16191d);\\n --rp-ink: var(--vscode-editor-foreground, #e8eaed);\\n --rp-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --rp-border: var(--vscode-panel-border, #272c33);\\n --rp-accent: var(--vscode-button-background, #4589ff);\\n\\n --rp-pass: var(--vscode-charts-green, #3edc81);\\n --rp-block: var(--vscode-charts-red, #fa4d56);\\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\\n --rp-running: var(--vscode-charts-blue, #4589ff);\\n --rp-cancelled: var(--vscode-descriptionForeground, #99a2ad);\\n --rp-failed: var(--vscode-charts-red, #fa4d56);\\n }\\n}\\n\\n/* ── Base ─────────────────────────────────────────────────────────── */\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--rp-bg);\\n color: var(--rp-ink);\\n overflow: hidden;\\n}\\n\\n.mono {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.muted { color: var(--rp-ink-muted); }\\n\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#toolbar {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--rp-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--rp-surface);\\n flex-shrink: 0;\\n}\\n#toolbar-label { font-size: 11px; color: var(--rp-ink-muted); }\\n.tbtn {\\n background: var(--rp-bg);\\n border: 1px solid var(--rp-border);\\n color: var(--rp-ink);\\n padding: 4px 10px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n.tbtn:hover { background: var(--rp-border); }\\n.tbtn:disabled { opacity: 0.5; cursor: default; }\\n.tbtn.danger { border-color: var(--rp-block); color: var(--rp-block); }\\n\\n#main { flex: 1; overflow: hidden; display: flex; }\\n#list-pane { flex: 1; overflow-y: auto; }\\n#detail-pane {\\n width: 420px;\\n min-width: 320px;\\n border-left: 1px solid var(--rp-border);\\n overflow-y: auto;\\n display: none;\\n flex-direction: column;\\n}\\n#detail-pane.open { display: flex; }\\n#detail-close { align-self: flex-end; margin: 6px; }\\n\\ntable.list { width: 100%; border-collapse: collapse; }\\ntable.list th {\\n text-align: left;\\n font-size: 10px;\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n color: var(--rp-ink-muted);\\n padding: 6px 8px;\\n border-bottom: 1px solid var(--rp-border);\\n position: sticky;\\n top: 0;\\n background: var(--rp-surface);\\n}\\ntable.list td { padding: 6px 8px; border-bottom: 1px solid var(--rp-border); font-size: 12px; vertical-align: middle; }\\ntr.row { cursor: pointer; }\\ntr.row:hover, tr.row:focus { background: color-mix(in srgb, var(--rp-accent) 8%, transparent); outline: none; }\\ntr.row.selected { background: color-mix(in srgb, var(--rp-accent) 14%, transparent); }\\n\\n.empty { padding: 24px 12px; color: var(--rp-ink-muted); font-size: 12px; text-align: center; }\\n\\n.chip { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }\\n.chip-verdict.v-pass { background: color-mix(in srgb, var(--rp-pass) 18%, transparent); color: var(--rp-pass); }\\n.chip-verdict.v-block { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\\n.chip-verdict.v-incomplete { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\\n.chip-verdict.v-running { background: color-mix(in srgb, var(--rp-running) 18%, transparent); color: var(--rp-running); }\\n.chip-verdict.v-cancelled { background: color-mix(in srgb, var(--rp-cancelled) 15%, transparent); color: var(--rp-cancelled); }\\n.chip-verdict.v-failed { background: color-mix(in srgb, var(--rp-failed) 18%, transparent); color: var(--rp-failed); }\\n\\n.chip-lane.l-pass { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\\n.chip-lane.l-fail { background: color-mix(in srgb, var(--rp-block) 15%, transparent); color: var(--rp-block); }\\n.chip-lane.l-skipped { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.chip-lane.l-timeout { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\\n\\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\\n.t-dirty { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\\n.t-cached { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.t-signed { background: color-mix(in srgb, var(--rp-pass) 12%, transparent); color: var(--rp-pass); }\\n.t-unsigned { background: color-mix(in srgb, var(--rp-ink-muted) 10%, transparent); color: var(--rp-ink-muted); }\\n.t-blocking { background: color-mix(in srgb, var(--rp-block) 12%, transparent); color: var(--rp-block); }\\n.t-advisory { background: color-mix(in srgb, var(--rp-ink-muted) 12%, transparent); color: var(--rp-ink-muted); }\\n.pr-open { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\\n.pr-merged { background: color-mix(in srgb, var(--rp-accent) 15%, transparent); color: var(--rp-accent); }\\n.pr-closed { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.sev-high { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\\n.sev-medium { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\\n.sev-low { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n\\n.detail { padding: 10px; display: flex; flex-direction: column; gap: 10px; }\\n.detail-hdr { display: flex; align-items: center; gap: 8px; }\\n.detail-error { color: var(--rp-block); font-size: 12px; word-break: break-word; }\\n.detail-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 10px 10px; }\\n\\n.lane {\\n border: 1px solid var(--rp-border);\\n border-radius: 6px;\\n padding: 8px;\\n display: flex;\\n flex-direction: column;\\n gap: 6px;\\n}\\n.lane-hdr { display: flex; align-items: center; gap: 6px; }\\n.lane-id { font-weight: 600; font-size: 12px; }\\n.lane-error { color: var(--rp-block); font-size: 11px; word-break: break-word; }\\n.lane-agent-meta { font-size: 11px; color: var(--rp-ink-muted); }\\n.sess-link {\\n background: none;\\n border: none;\\n color: var(--rp-accent);\\n cursor: pointer;\\n font: inherit;\\n padding: 0;\\n text-decoration: underline;\\n}\\n\\n.findings { display: flex; flex-direction: column; gap: 4px; }\\n.finding summary { cursor: pointer; font-size: 11px; list-style: none; }\\n.finding summary::-webkit-details-marker { display: none; }\\n.finding-detail { font-size: 11px; color: var(--rp-ink-muted); padding: 4px 0 0 4px; white-space: pre-wrap; word-break: break-word; }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--rp-ink-muted);\\n border-top: 1px solid var(--rp-border);\\n background: var(--rp-surface);\\n flex-shrink: 0;\\n}</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"toolbar\\\">\\n <span id=\\\"toolbar-label\\\">Reviews</span>\\n <button class=\\\"tbtn\\\" id=\\\"refresh-btn\\\" title=\\\"Refresh\\\">↻</button>\\n </div>\\n <div id=\\\"main\\\">\\n <div id=\\\"list-pane\\\"></div>\\n <div id=\\\"detail-pane\\\">\\n <button class=\\\"tbtn\\\" id=\\\"detail-close\\\">× Close</button>\\n <div id=\\\"detail-body\\\"></div>\\n <div class=\\\"detail-actions\\\">\\n <button class=\\\"tbtn danger\\\" id=\\\"cancel-btn\\\">Cancel</button>\\n <button class=\\\"tbtn\\\" id=\\\"rerun-btn\\\">Re-run fresh</button>\\n <button class=\\\"tbtn\\\" id=\\\"pr-btn\\\">Fetch PR status</button>\\n <button class=\\\"tbtn\\\" id=\\\"export-btn\\\">Export</button>\\n </div>\\n </div>\\n </div>\\n <div id=\\\"statusbar\\\">Connecting to bridge…</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto review-panel — the `REVIEW_PANEL_HTML`\n * served as the `ui://agentproto_reviews/view` resource by `registerMcpApps`\n * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via\n * the `agentproto_reviews` tool defined in ./index.ts.\n *\n * A verdict panel over the daemon's review ledger\n * (`packages/runtime/src/review-ledger.ts` + `review-runner.ts`'s in-flight\n * runs) — list view (running first, then newest) and a per-run detail view\n * (lanes, findings, agent-lane provenance).\n *\n * `REVIEW_PANEL_HTML` is a real single-file Vite build, not a hand-authored\n * template string — same technique as work-board (see that package's\n * ./panel.ts docblock): the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run build:ui:reviews`\n * (scripts/build-review-panel-ui.mjs) — so this package still builds with\n * plain `pnpm build` on a fresh clone, no Vite step in the critical path.\n * `build:ui:reviews:check` (wired into CI) fails if that generated file\n * drifts from what the Vite sources actually produce.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts)\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → review_ledger (`includeRunning: true`) on a poll\n * that speeds up to ~4s while any row is running, ~15s otherwise; a\n * selected row's detail comes from review_status (runId)\n * – Actions: tools/call → review_cancel / review_run (nocache + wait:false\n * + supersede, \"Re-run fresh\") / review_pr / review_export — real tool\n * names only, no second write path\n * – Deep link: an agent lane's reviewer-session id opens (openLink, or\n * window.open as a fallback) `/apps/@agentproto/live-session/ui\n * ?sessionId=<id>` — runtime's `handleAppUiPage` validates and bakes\n * that query param into the live-session widget's initData\n * (builtin-apps.ts's `isValidDeepLinkSessionId`), so it boots already\n * pinned to the exact reviewer session instead of self-discovering the\n * newest running one. See ./ui/render.ts's `liveSessionUrl`.\n *\n * The verdict/status chip (never render a running/cancelled run as if it\n * had a verdict): pass/block/incomplete/running/cancelled/failed, one\n * visual language everywhere it appears — see ./ui/render.ts's `verdictChip`.\n */\n\nimport { REVIEW_PANEL_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph can still name the panel from source (mirrors\n * work-board/panel.ts's `WORK_BOARD_APP_ID`). */\nexport const REVIEW_PANEL_APP_ID = \"@agentproto/review-panel\"\n\n/** The MCP tool the panel is registered under (./index.ts\n * `makeReviewPanelApp`), hence its resource uri `ui://agentproto_reviews/view`. */\nexport const REVIEW_PANEL_TOOL_ID = \"agentproto_reviews\"\n\n/** The daemon tools this panel's html calls, in order of first use. Consumed\n * as `ui.tools` by ./index.ts and as the client-side allowlist by any host\n * that dispatches the panel's `tools/call` directly. The reviewer-session\n * deep link (./ui/render.ts's `liveSessionUrl`) is a plain `openLink`/\n * `window.open` navigation, not a tools/call, so it needs no entry here. */\nexport const REVIEW_PANEL_UI_TOOLS = [\n \"review_ledger\",\n \"review_status\",\n \"review_cancel\",\n \"review_run\",\n \"review_pr\",\n \"review_export\",\n] as const\n\nexport const REVIEW_PANEL_HTML = REVIEW_PANEL_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto review verdict panel — a list +\n * detail view over the daemon's review ledger\n * (`packages/runtime/src/review-ledger.ts`) and its in-flight runs\n * (`review-runner.ts`'s `ReviewRunner.list()`).\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape, no\n * @agstudio/mcp-apps / Mastra dependency), same as sessions-panel /\n * work-board — the host (@agentproto/runtime) wires this factory's output\n * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the\n * mounted list assembled in runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_reviews` is called by the host with the optional\n * `{cwd, repoRemote, runId, requesterSessionId}` scoping input.\n * 2. execute() answers an initial `review_ledger`-shaped snapshot\n * (`includeRunning: true`) via `ops.listReviews` — the SAME body\n * `review_ledger` itself runs (`reviewLedgerView`, review-tools.ts),\n * so a non-interactive caller sees exactly what the tool would return.\n * 3. The HTML panel — a static work-board-style Vite build, so it does\n * NOT thread this execute() input through — opens its own bridge and\n * polls `review_ledger({includeRunning: true})` unscoped (mirrors\n * work-board's own boardId-from-execute() omission), then\n * `review_status(runId)` for whichever row is selected. Actions go\n * through `review_cancel`/`review_run`/`review_pr`/`review_export` —\n * real tool names only, no second write path.\n *\n * This module also exports `reviewPanelApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeReviewPanelApp` above, which the daemon mounts\n * directly at boot: that factory closes over the LIVE ledger/runner read,\n * something a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { REVIEW_PANEL_APP_ID, REVIEW_PANEL_HTML, REVIEW_PANEL_TOOL_ID, REVIEW_PANEL_UI_TOOLS } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const reviewPanelInputSchema = z.object({\n cwd: z.string().optional().describe(\"A directory inside a git repo — scopes the initial snapshot to its remote.\"),\n repoRemote: z.string().optional().describe(\"Normalized repo remote (e.g. github.com/agentproto/ts).\"),\n runId: z.string().optional().describe(\"A specific run id known at open time (informational on the initial snapshot).\"),\n requesterSessionId: z\n .string()\n .optional()\n .describe(\"Scope the initial snapshot to reviews requested by this session.\"),\n})\n\nexport type ReviewPanelInput = z.infer<typeof reviewPanelInputSchema>\n\nexport interface ReviewPanelOutput {\n total: number\n attestations: Record<string, unknown>[]\n /** Echoed back from the input — the row this snapshot's caller already\n * had in mind, when known. Not consumed by the panel's own client JS\n * (see this file's docblock on why the static build doesn't thread it). */\n focusRunId?: string\n}\n\n/**\n * Generic host wiring for `makeReviewPanelApp` — the daemon supplies\n * `listReviews` bound to `reviewLedgerView(reviewRunner, ...)`\n * (review-tools.ts), the exact function `review_ledger` itself calls, so\n * the panel's initial snapshot and the tool's own output can never drift\n * apart into two implementations.\n */\nexport interface ReviewPanelOps {\n listReviews(input: ReviewPanelInput): Promise<ReviewPanelOutput> | ReviewPanelOutput\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the ledger/runner read so execute() doesn't need an\n * AppContext (agentproto has no userId/guildId concept) — mirrors\n * makeWorkBoardApp.\n */\nexport function makeReviewPanelApp(ops: ReviewPanelOps): AgnoMcpApp<ReviewPanelInput, ReviewPanelOutput> {\n return {\n id: REVIEW_PANEL_TOOL_ID,\n title: \"Reviews\",\n description:\n \"Open the agentproto review panel — a verdict list (running reviews first, then \" +\n \"newest) over the daemon's review ledger, with a per-run detail view (lanes, \" +\n \"findings, agent-lane provenance) and actions (cancel, re-run fresh, fetch PR \" +\n \"status, export). Pass `cwd`/`repoRemote`/`requesterSessionId` to scope the initial \" +\n \"snapshot; the panel's own live view re-polls unscoped.\",\n inputSchema: reviewPanelInputSchema,\n execute: async input => {\n const snapshot = await ops.listReviews(input)\n return { ...snapshot, ...(input.runId ? { focusRunId: input.runId } : {}) }\n },\n html: REVIEW_PANEL_HTML,\n }\n}\n\nexport const reviewPanelApp: AppHandle = defineApp({\n id: REVIEW_PANEL_APP_ID,\n name: \"Reviews\",\n description:\n \"Open the agentproto review panel — a verdict list + detail view over the daemon's \" +\n \"review ledger, with cancel / re-run / PR-status / export actions.\",\n agents: [],\n ui: {\n html: REVIEW_PANEL_HTML,\n title: \"Reviews\",\n tools: [...REVIEW_PANEL_UI_TOOLS],\n },\n})\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/review-panel/panel.generated.ts","../src/review-panel/panel.ts","../src/review-panel/index.ts"],"names":[],"mappings":";;;;;;;;;;AAYO,IAAM,2BAAA,GAA8B,w6yCAAA;;;ACsCpC,IAAM,mBAAA,GAAsB,0BAAA;AAI5B,IAAM,oBAAA,GAAuB,oBAAA;AAO7B,IAAM,qBAAA,GAAwB;AAAA,EACnC,eAAA;AAAA,EACA,eAAA;AAAA,EACA,eAAA;AAAA,EACA,YAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAA;AAEO,IAAM,iBAAA,GAAoB,2BAAA;;;AC/B1B,IAAM,sBAAA,GAAyB,EAAE,MAAA,CAAO;AAAA,EAC7C,KAAK,CAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,iFAA4E,CAAA;AAAA,EAChH,YAAY,CAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,yDAAyD,CAAA;AAAA,EACpG,OAAO,CAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,+EAA+E,CAAA;AAAA,EACrH,oBAAoB,CAAA,CACjB,MAAA,GACA,QAAA,EAAS,CACT,SAAS,kEAAkE;AAChF,CAAC;AA+BM,SAAS,mBAAmB,GAAA,EAAsE;AACvG,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,oBAAA;AAAA,IACJ,KAAA,EAAO,SAAA;AAAA,IACP,WAAA,EACE,wXAAA;AAAA,IAKF,WAAA,EAAa,sBAAA;AAAA,IACb,OAAA,EAAS,OAAM,KAAA,KAAS;AACtB,MAAA,MAAM,QAAA,GAAW,MAAM,GAAA,CAAI,WAAA,CAAY,KAAK,CAAA;AAC5C,MAAA,OAAO,EAAE,GAAG,QAAA,EAAU,GAAI,KAAA,CAAM,KAAA,GAAQ,EAAE,UAAA,EAAY,KAAA,CAAM,KAAA,EAAM,GAAI,EAAC,EAAG;AAAA,IAC5E,CAAA;AAAA,IACA,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,iBAA4B,SAAA,CAAU;AAAA,EACjD,EAAA,EAAI,mBAAA;AAAA,EACJ,IAAA,EAAM,SAAA;AAAA,EACN,WAAA,EACE,0JAAA;AAAA,EAEF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,iBAAA;AAAA,IACN,KAAA,EAAO,SAAA;AAAA,IACP,KAAA,EAAO,CAAC,GAAG,qBAAqB;AAAA;AAEpC,CAAC","file":"review-panel.mjs","sourcesContent":["/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/review-panel/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-review-panel-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui:reviews\n *\n * `pnpm --filter @agentproto/apps run build:ui:reviews:check` (wired into\n * CI) fails if this file drifts from what the Vite sources actually produce.\n */\n\nexport const REVIEW_PANEL_HTML_GENERATED = \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"UTF-8\\\" />\\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\" />\\n<title>agentproto reviews</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-review-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\n// An MCP tool-result envelope: {content: [{type: 'text', text}, ...]}.\\nfunction _isEnvelope(v){\\n return !!v && typeof v === 'object' && Array.isArray(v.content) &&\\n !!v.content[0] && v.content[0].type === 'text' && typeof v.content[0].text === 'string';\\n}\\n// Peels every envelope layer. The standalone path (POST /apps/:id/tool-call,\\n// runtime app-tools.ts dispatchAllowlistedAppTool) wraps an already-wrapped\\n// builtin result a second time, and an inner isError rides inside that outer\\n// success — so isError is honoured at each layer, not just the first.\\nfunction _unwrapToolResult(result){\\n var env = result;\\n while (true){\\n var first = env && env.content && env.content[0];\\n var text = (first && first.text) || '';\\n if (env && env.isError) throw new Error(text || 'tool error');\\n try { var value = JSON.parse(text || '{}'); } catch(_) { return text; }\\n if (!_isEnvelope(value)) return value;\\n env = value;\\n }\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(_unwrapToolResult);\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n function esc(value) {\\n return value.replace(/&/g, \\\"&\\\").replace(/</g, \\\"<\\\").replace(/>/g, \\\">\\\").replace(/\\\"/g, \\\""\\\");\\n }\\n function liveSessionUrl(origin, sessionId) {\\n return `${origin}/apps/@agentproto/live-session/ui?sessionId=${encodeURIComponent(sessionId)}`;\\n }\\n function shortRange(baseSha, headSha) {\\n if (!baseSha || !headSha) return \\\"—\\\";\\n return `${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`;\\n }\\n function ageOf(createdAt, now = Date.now()) {\\n const ms = now - new Date(createdAt).getTime();\\n if (!Number.isFinite(ms) || ms < 0) return \\\"just now\\\";\\n const mins = Math.floor(ms / 6e4);\\n if (mins < 1) return \\\"just now\\\";\\n if (mins < 60) return `${mins}m`;\\n const hours = Math.floor(mins / 60);\\n if (hours < 24) return `${hours}h`;\\n return `${Math.floor(hours / 24)}d`;\\n }\\n function verdictChip(status) {\\n return `<span class=\\\"chip chip-verdict v-${esc(status)}\\\">${esc(status)}</span>`;\\n }\\n const LANE_STATUS_LABEL = {\\n pass: \\\"pass\\\",\\n fail: \\\"fail\\\",\\n skipped: \\\"skipped\\\",\\n timeout: \\\"timeout\\\"\\n };\\n function laneStatusChip(status) {\\n return `<span class=\\\"chip chip-lane l-${esc(status)}\\\">${esc(LANE_STATUS_LABEL[status])}</span>`;\\n }\\n function severityTag(severity) {\\n return `<span class=\\\"tag sev-${esc(severity)}\\\">${esc(severity)}</span>`;\\n }\\n function requesterLabel(row) {\\n const r = row.requester;\\n if (!r) return \\\"—\\\";\\n const id = r.sessionId ? esc(r.sessionId) : \\\"\\\";\\n const author = r.gitAuthor ? esc(r.gitAuthor.name) : \\\"\\\";\\n if (id && author) return `${id} <span class=\\\"muted\\\">(${author})</span>`;\\n return id || author || \\\"—\\\";\\n }\\n function prLabel(row) {\\n if (!row.pr) return \\\"—\\\";\\n const state = row.prState ? ` <span class=\\\"tag pr-${esc(row.prState)}\\\">${esc(row.prState)}</span>` : \\\"\\\";\\n return `<a href=\\\"${esc(row.pr.url)}\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">#${row.pr.number}</a>${state}`;\\n }\\n function renderRow(row) {\\n const status = row.status ?? row.verdict ?? \\\"incomplete\\\";\\n const flags = [];\\n if (row.dirty) flags.push('<span class=\\\"tag t-dirty\\\">dirty</span>');\\n if (row.cached) flags.push('<span class=\\\"tag t-cached\\\">cached</span>');\\n if (row.status !== \\\"running\\\") {\\n flags.push(row.signed ? '<span class=\\\"tag t-signed\\\">signed</span>' : '<span class=\\\"tag t-unsigned\\\">unsigned</span>');\\n }\\n return `<tr class=\\\"row\\\" data-runid=\\\"${esc(row.runId)}\\\" tabindex=\\\"0\\\"><td>${verdictChip(status)}</td><td class=\\\"mono\\\">${esc(shortRange(row.baseSha, row.headSha))}</td><td>${esc(row.binding ?? \\\"—\\\")}</td><td class=\\\"mono\\\">${esc(row.repoRemote ?? \\\"—\\\")}</td><td>${requesterLabel(row)}</td><td>${prLabel(row)}</td><td>${flags.join(\\\" \\\") || \\\"—\\\"}</td><td>${esc(ageOf(row.createdAt))}</td></tr>`;\\n }\\n function renderList(rows2) {\\n if (rows2.length === 0) return '<div class=\\\"empty\\\">No reviews recorded yet.</div>';\\n const header = \\\"<tr><th>Verdict</th><th>Range</th><th>Binding</th><th>Repo</th><th>Requester</th><th>PR</th><th>Flags</th><th>Age</th></tr>\\\";\\n return `<table class=\\\"list\\\"><thead>${header}</thead><tbody>${rows2.map(renderRow).join(\\\"\\\")}</tbody></table>`;\\n }\\n function findingLine(f) {\\n const loc = f.file ? `${esc(f.file)}${f.line ? `:${f.line}` : \\\"\\\"}` : \\\"\\\";\\n return `<details class=\\\"finding\\\"><summary>${severityTag(f.severity)} ${esc(f.title)}${loc ? ` <span class=\\\"mono muted\\\">${loc}</span>` : \\\"\\\"}</summary><div class=\\\"finding-detail\\\">${esc(f.detail)}</div></details>`;\\n }\\n function renderLaneDetail(lane, rubrics = []) {\\n const parts = [];\\n parts.push(`<div class=\\\"lane-hdr\\\">`);\\n parts.push(`<span class=\\\"lane-id\\\">${esc(lane.id)}</span>`);\\n parts.push(laneStatusChip(lane.status));\\n parts.push(`<span class=\\\"tag ${lane.blocking ? \\\"t-blocking\\\" : \\\"t-advisory\\\"}\\\">${lane.blocking ? \\\"blocking\\\" : \\\"advisory\\\"}</span>`);\\n if (lane.durationMs !== void 0) parts.push(`<span class=\\\"muted\\\">${(lane.durationMs / 1e3).toFixed(1)}s</span>`);\\n parts.push(`</div>`);\\n if (lane.error) parts.push(`<div class=\\\"lane-error\\\">${esc(lane.error)}</div>`);\\n if (lane.sessionId || lane.preset || lane.model || lane.fallbacks?.length) {\\n const rubric = rubrics.find((r) => r.check === lane.id);\\n const bits = [];\\n if (lane.model) bits.push(`model: ${esc(lane.model)}`);\\n if (lane.preset) bits.push(`preset: ${esc(lane.preset)}`);\\n if (lane.fallbacks?.length) {\\n bits.push(`after unavailable: ${lane.fallbacks.map((f) => `${esc(f.preset)} (${esc(f.error)})`).join(\\\", \\\")}`);\\n }\\n if (rubric) bits.push(`rubric: <span class=\\\"mono\\\">${esc(rubric.sha256.slice(0, 12))}</span>`);\\n if (lane.sessionId) {\\n bits.push(\\n `reviewer: <button class=\\\"sess-link\\\" data-session-id=\\\"${esc(lane.sessionId)}\\\">${esc(lane.sessionId)}</button>`\\n );\\n }\\n parts.push(`<div class=\\\"lane-agent-meta\\\">${bits.join(\\\" · \\\")}</div>`);\\n }\\n if (lane.findings.length > 0) parts.push(`<div class=\\\"findings\\\">${lane.findings.map(findingLine).join(\\\"\\\")}</div>`);\\n return `<div class=\\\"lane\\\">${parts.join(\\\"\\\")}</div>`;\\n }\\n function renderDetail(detail2) {\\n const status = detail2.status === \\\"done\\\" ? detail2.verdict ?? \\\"incomplete\\\" : detail2.status;\\n const header = [`<div class=\\\"detail-hdr\\\">`, verdictChip(status), `<span class=\\\"mono\\\">${esc(detail2.runId)}</span>`];\\n if (detail2.binding) header.push(`<span class=\\\"tag\\\">${esc(detail2.binding)}</span>`);\\n if (detail2.cached) header.push('<span class=\\\"tag t-cached\\\">cached</span>');\\n if (detail2.status === \\\"done\\\" && detail2.attestation) {\\n const sig = detail2.attestation.attestor?.signature;\\n header.push(\\n sig ? `<span class=\\\"tag t-signed\\\" title=\\\"${esc(sig.keyFingerprint)}\\\">signed: ${esc(sig.principal)}</span>` : '<span class=\\\"tag t-unsigned\\\">unsigned</span>'\\n );\\n }\\n header.push(`</div>`);\\n if (detail2.error) header.push(`<div class=\\\"detail-error\\\">${esc(detail2.error)}</div>`);\\n if (detail2.supersededBy) header.push(`<div class=\\\"muted\\\">superseded by ${esc(detail2.supersededBy)}</div>`);\\n const fullLanes = detail2.attestation?.lanes;\\n const rubrics = detail2.attestation?.rubrics ?? [];\\n let body;\\n if (fullLanes) {\\n body = fullLanes.map((l) => renderLaneDetail(l, rubrics)).join(\\\"\\\");\\n } else if (detail2.lanes) {\\n body = detail2.lanes.map((l) => `<div class=\\\"lane\\\"><div class=\\\"lane-hdr\\\"><span class=\\\"lane-id\\\">${esc(l.id)}</span>${laneStatusChip(l.status)}</div></div>`).join(\\\"\\\");\\n } else {\\n body = \\\"\\\";\\n }\\n return `<div class=\\\"detail\\\">${header.join(\\\"\\\")}${body}</div>`;\\n }\\n const POLL_RUNNING_MS = 4e3;\\n const POLL_IDLE_MS = 15e3;\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`review panel: missing #${id}`);\\n return el;\\n }\\n let rows = [];\\n let selectedRunId = null;\\n let detail = null;\\n let pollTimer = null;\\n let loadInFlight = false;\\n function boot() {\\n const listPaneEl = getEl(\\\"list-pane\\\");\\n const detailPaneEl = getEl(\\\"detail-pane\\\");\\n const detailBodyEl = getEl(\\\"detail-body\\\");\\n const statusbarEl = getEl(\\\"statusbar\\\");\\n const cancelBtn = getEl(\\\"cancel-btn\\\");\\n const rerunBtn = getEl(\\\"rerun-btn\\\");\\n const prBtn = getEl(\\\"pr-btn\\\");\\n const exportBtn = getEl(\\\"export-btn\\\");\\n function setStatus(msg) {\\n statusbarEl.textContent = msg;\\n }\\n function findRow(runId) {\\n return rows.find((r) => r.runId === runId);\\n }\\n function renderListPane() {\\n listPaneEl.innerHTML = renderList(rows);\\n if (!selectedRunId) return;\\n listPaneEl.querySelectorAll(\\\"tr[data-runid]\\\").forEach((tr) => {\\n if (tr.dataset.runid === selectedRunId) tr.classList.add(\\\"selected\\\");\\n });\\n }\\n function renderDetailPane() {\\n if (!selectedRunId || !detail) {\\n detailPaneEl.classList.remove(\\\"open\\\");\\n return;\\n }\\n detailPaneEl.classList.add(\\\"open\\\");\\n detailBodyEl.innerHTML = renderDetail(detail);\\n const running = detail.status === \\\"running\\\";\\n cancelBtn.disabled = !running;\\n cancelBtn.style.display = running ? \\\"\\\" : \\\"none\\\";\\n detailBodyEl.querySelectorAll(\\\".sess-link\\\").forEach((btn) => {\\n btn.addEventListener(\\\"click\\\", () => openSession(btn.dataset.sessionId ?? \\\"\\\"));\\n });\\n }\\n function openSession(sessionId) {\\n if (!sessionId) return;\\n const origin = window.location.origin && window.location.origin !== \\\"null\\\" ? window.location.origin : \\\"\\\";\\n const url = liveSessionUrl(origin, sessionId);\\n const caps = getHostCapabilities();\\n if (caps?.openLinks) {\\n openLink(url).catch(() => window.open(url, \\\"_blank\\\"));\\n } else {\\n window.open(url, \\\"_blank\\\");\\n }\\n }\\n function loadList() {\\n if (loadInFlight) return Promise.resolve();\\n loadInFlight = true;\\n return callTool(\\\"review_ledger\\\", { includeRunning: true }).then((result) => {\\n rows = result.attestations;\\n renderListPane();\\n const runningCount = rows.filter((r) => r.status === \\\"running\\\").length;\\n setStatus(`${result.total} review${result.total === 1 ? \\\"\\\" : \\\"s\\\"} · ${runningCount} running · ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\\n if (selectedRunId && findRow(selectedRunId)) return loadDetail(selectedRunId);\\n return void 0;\\n }).catch((e) => setStatus(`Error: ${e.message}`)).finally(() => {\\n loadInFlight = false;\\n });\\n }\\n function loadDetail(runId) {\\n return callTool(\\\"review_status\\\", { runId }).then((result) => {\\n detail = result;\\n renderDetailPane();\\n }).catch((e) => setStatus(`Error loading detail: ${e.message}`));\\n }\\n function openDetail(runId) {\\n selectedRunId = runId;\\n renderListPane();\\n void loadDetail(runId);\\n }\\n function closeDetail() {\\n selectedRunId = null;\\n detail = null;\\n renderListPane();\\n renderDetailPane();\\n }\\n listPaneEl.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const tr = evt.target.closest(\\\"tr[data-runid]\\\");\\n if (!(tr instanceof HTMLElement)) return;\\n const runId = tr.dataset.runid;\\n if (runId) openDetail(runId);\\n });\\n listPaneEl.addEventListener(\\\"keydown\\\", (evt) => {\\n if (evt.key !== \\\"Enter\\\" && evt.key !== \\\" \\\") return;\\n if (!(evt.target instanceof Element)) return;\\n const tr = evt.target.closest(\\\"tr[data-runid]\\\");\\n if (!(tr instanceof HTMLElement)) return;\\n evt.preventDefault();\\n const runId = tr.dataset.runid;\\n if (runId) openDetail(runId);\\n });\\n getEl(\\\"detail-close\\\").addEventListener(\\\"click\\\", closeDetail);\\n getEl(\\\"refresh-btn\\\").addEventListener(\\\"click\\\", () => {\\n void loadList();\\n });\\n cancelBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_cancel\\\", { runId: selectedRunId }).then(() => {\\n setStatus(\\\"Cancelled.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Cancel failed: ${e.message}`));\\n });\\n rerunBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n const row = findRow(selectedRunId);\\n const cwd = row?.cwd;\\n if (!cwd) {\\n setStatus(\\\"Re-run failed: this row has no known checkout path.\\\");\\n return;\\n }\\n callTool(\\\"review_run\\\", {\\n cwd,\\n ...row?.binding ? { binding: row.binding } : {},\\n nocache: true,\\n wait: false,\\n supersede: true\\n }).then(() => {\\n setStatus(\\\"Re-run started.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Re-run failed: ${e.message}`));\\n });\\n prBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_pr\\\", { runId: selectedRunId }).then(() => {\\n setStatus(\\\"Fetched PR status.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Fetch PR status failed: ${e.message}`));\\n });\\n exportBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_export\\\", { runId: selectedRunId }).then((result) => {\\n setStatus(result.path ? `Exported to ${result.path}` : \\\"Exported.\\\");\\n }).catch((e) => setStatus(`Export failed: ${e.message}`));\\n });\\n function doPoll() {\\n if (pollTimer) clearTimeout(pollTimer);\\n loadList().finally(() => {\\n const anyRunning = rows.some((r) => r.status === \\\"running\\\");\\n pollTimer = setTimeout(doPoll, anyRunning ? POLL_RUNNING_MS : POLL_IDLE_MS);\\n });\\n }\\n initBridge().then(() => loadList()).then(() => {\\n pollTimer = setTimeout(doPoll, POLL_RUNNING_MS);\\n }).catch((e) => setStatus(`Bridge error: ${e.message}`));\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n Semantic tokens + runtime mapping sheets — same scheme as work-board's\\n style.css (see that file's header doc for the two-mapping-sheet\\n rationale): every rule below reads ONLY the --rp-* custom properties,\\n each resolving through a live --vscode-* var() first, then the\\n agentproto brand fallback.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --rp-bg: var(--vscode-editor-background, #f5f6f3);\\n --rp-surface: var(--vscode-sideBar-background, #ffffff);\\n --rp-ink: var(--vscode-editor-foreground, #171a1e);\\n --rp-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --rp-border: var(--vscode-panel-border, #dde0da);\\n --rp-accent: var(--vscode-button-background, #0f62fe);\\n\\n --rp-pass: var(--vscode-charts-green, #3edc81);\\n --rp-block: var(--vscode-charts-red, #fa4d56);\\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\\n --rp-running: var(--vscode-charts-blue, #4589ff);\\n --rp-cancelled: var(--vscode-descriptionForeground, #5c6470);\\n --rp-failed: var(--vscode-charts-red, #fa4d56);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --rp-bg: var(--vscode-editor-background, #101214);\\n --rp-surface: var(--vscode-sideBar-background, #16191d);\\n --rp-ink: var(--vscode-editor-foreground, #e8eaed);\\n --rp-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --rp-border: var(--vscode-panel-border, #272c33);\\n --rp-accent: var(--vscode-button-background, #4589ff);\\n\\n --rp-pass: var(--vscode-charts-green, #3edc81);\\n --rp-block: var(--vscode-charts-red, #fa4d56);\\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\\n --rp-running: var(--vscode-charts-blue, #4589ff);\\n --rp-cancelled: var(--vscode-descriptionForeground, #99a2ad);\\n --rp-failed: var(--vscode-charts-red, #fa4d56);\\n }\\n}\\n\\n/* ── Base ─────────────────────────────────────────────────────────── */\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--rp-bg);\\n color: var(--rp-ink);\\n overflow: hidden;\\n}\\n\\n.mono {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.muted { color: var(--rp-ink-muted); }\\n\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#toolbar {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--rp-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--rp-surface);\\n flex-shrink: 0;\\n}\\n#toolbar-label { font-size: 11px; color: var(--rp-ink-muted); }\\n.tbtn {\\n background: var(--rp-bg);\\n border: 1px solid var(--rp-border);\\n color: var(--rp-ink);\\n padding: 4px 10px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n.tbtn:hover { background: var(--rp-border); }\\n.tbtn:disabled { opacity: 0.5; cursor: default; }\\n.tbtn.danger { border-color: var(--rp-block); color: var(--rp-block); }\\n\\n#main { flex: 1; overflow: hidden; display: flex; }\\n#list-pane { flex: 1; overflow-y: auto; }\\n#detail-pane {\\n width: 420px;\\n min-width: 320px;\\n border-left: 1px solid var(--rp-border);\\n overflow-y: auto;\\n display: none;\\n flex-direction: column;\\n}\\n#detail-pane.open { display: flex; }\\n#detail-close { align-self: flex-end; margin: 6px; }\\n\\ntable.list { width: 100%; border-collapse: collapse; }\\ntable.list th {\\n text-align: left;\\n font-size: 10px;\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n color: var(--rp-ink-muted);\\n padding: 6px 8px;\\n border-bottom: 1px solid var(--rp-border);\\n position: sticky;\\n top: 0;\\n background: var(--rp-surface);\\n}\\ntable.list td { padding: 6px 8px; border-bottom: 1px solid var(--rp-border); font-size: 12px; vertical-align: middle; }\\ntr.row { cursor: pointer; }\\ntr.row:hover, tr.row:focus { background: color-mix(in srgb, var(--rp-accent) 8%, transparent); outline: none; }\\ntr.row.selected { background: color-mix(in srgb, var(--rp-accent) 14%, transparent); }\\n\\n.empty { padding: 24px 12px; color: var(--rp-ink-muted); font-size: 12px; text-align: center; }\\n\\n.chip { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }\\n.chip-verdict.v-pass { background: color-mix(in srgb, var(--rp-pass) 18%, transparent); color: var(--rp-pass); }\\n.chip-verdict.v-block { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\\n.chip-verdict.v-incomplete { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\\n.chip-verdict.v-running { background: color-mix(in srgb, var(--rp-running) 18%, transparent); color: var(--rp-running); }\\n.chip-verdict.v-cancelled { background: color-mix(in srgb, var(--rp-cancelled) 15%, transparent); color: var(--rp-cancelled); }\\n.chip-verdict.v-failed { background: color-mix(in srgb, var(--rp-failed) 18%, transparent); color: var(--rp-failed); }\\n\\n.chip-lane.l-pass { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\\n.chip-lane.l-fail { background: color-mix(in srgb, var(--rp-block) 15%, transparent); color: var(--rp-block); }\\n.chip-lane.l-skipped { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.chip-lane.l-timeout { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\\n\\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\\n.t-dirty { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\\n.t-cached { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.t-signed { background: color-mix(in srgb, var(--rp-pass) 12%, transparent); color: var(--rp-pass); }\\n.t-unsigned { background: color-mix(in srgb, var(--rp-ink-muted) 10%, transparent); color: var(--rp-ink-muted); }\\n.t-blocking { background: color-mix(in srgb, var(--rp-block) 12%, transparent); color: var(--rp-block); }\\n.t-advisory { background: color-mix(in srgb, var(--rp-ink-muted) 12%, transparent); color: var(--rp-ink-muted); }\\n.pr-open { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\\n.pr-merged { background: color-mix(in srgb, var(--rp-accent) 15%, transparent); color: var(--rp-accent); }\\n.pr-closed { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.sev-high { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\\n.sev-medium { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\\n.sev-low { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n\\n.detail { padding: 10px; display: flex; flex-direction: column; gap: 10px; }\\n.detail-hdr { display: flex; align-items: center; gap: 8px; }\\n.detail-error { color: var(--rp-block); font-size: 12px; word-break: break-word; }\\n.detail-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 10px 10px; }\\n\\n.lane {\\n border: 1px solid var(--rp-border);\\n border-radius: 6px;\\n padding: 8px;\\n display: flex;\\n flex-direction: column;\\n gap: 6px;\\n}\\n.lane-hdr { display: flex; align-items: center; gap: 6px; }\\n.lane-id { font-weight: 600; font-size: 12px; }\\n.lane-error { color: var(--rp-block); font-size: 11px; word-break: break-word; }\\n.lane-agent-meta { font-size: 11px; color: var(--rp-ink-muted); }\\n.sess-link {\\n background: none;\\n border: none;\\n color: var(--rp-accent);\\n cursor: pointer;\\n font: inherit;\\n padding: 0;\\n text-decoration: underline;\\n}\\n\\n.findings { display: flex; flex-direction: column; gap: 4px; }\\n.finding summary { cursor: pointer; font-size: 11px; list-style: none; }\\n.finding summary::-webkit-details-marker { display: none; }\\n.finding-detail { font-size: 11px; color: var(--rp-ink-muted); padding: 4px 0 0 4px; white-space: pre-wrap; word-break: break-word; }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--rp-ink-muted);\\n border-top: 1px solid var(--rp-border);\\n background: var(--rp-surface);\\n flex-shrink: 0;\\n}</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"toolbar\\\">\\n <span id=\\\"toolbar-label\\\">Reviews</span>\\n <button class=\\\"tbtn\\\" id=\\\"refresh-btn\\\" title=\\\"Refresh\\\">↻</button>\\n </div>\\n <div id=\\\"main\\\">\\n <div id=\\\"list-pane\\\"></div>\\n <div id=\\\"detail-pane\\\">\\n <button class=\\\"tbtn\\\" id=\\\"detail-close\\\">× Close</button>\\n <div id=\\\"detail-body\\\"></div>\\n <div class=\\\"detail-actions\\\">\\n <button class=\\\"tbtn danger\\\" id=\\\"cancel-btn\\\">Cancel</button>\\n <button class=\\\"tbtn\\\" id=\\\"rerun-btn\\\">Re-run fresh</button>\\n <button class=\\\"tbtn\\\" id=\\\"pr-btn\\\">Fetch PR status</button>\\n <button class=\\\"tbtn\\\" id=\\\"export-btn\\\">Export</button>\\n </div>\\n </div>\\n </div>\\n <div id=\\\"statusbar\\\">Connecting to bridge…</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto review-panel — the `REVIEW_PANEL_HTML`\n * served as the `ui://agentproto_reviews/view` resource by `registerMcpApps`\n * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via\n * the `agentproto_reviews` tool defined in ./index.ts.\n *\n * A verdict panel over the daemon's review ledger\n * (`packages/runtime/src/review-ledger.ts` + `review-runner.ts`'s in-flight\n * runs) — list view (running first, then newest) and a per-run detail view\n * (lanes, findings, agent-lane provenance).\n *\n * `REVIEW_PANEL_HTML` is a real single-file Vite build, not a hand-authored\n * template string — same technique as work-board (see that package's\n * ./panel.ts docblock): the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run build:ui:reviews`\n * (scripts/build-review-panel-ui.mjs) — so this package still builds with\n * plain `pnpm build` on a fresh clone, no Vite step in the critical path.\n * `build:ui:reviews:check` (wired into CI) fails if that generated file\n * drifts from what the Vite sources actually produce.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts)\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → review_ledger (`includeRunning: true`) on a poll\n * that speeds up to ~4s while any row is running, ~15s otherwise; a\n * selected row's detail comes from review_status (runId)\n * – Actions: tools/call → review_cancel / review_run (nocache + wait:false\n * + supersede, \"Re-run fresh\") / review_pr / review_export — real tool\n * names only, no second write path\n * – Deep link: an agent lane's reviewer-session id opens (openLink, or\n * window.open as a fallback) `/apps/@agentproto/live-session/ui\n * ?sessionId=<id>` — runtime's `handleAppUiPage` validates and bakes\n * that query param into the live-session widget's initData\n * (builtin-apps.ts's `isValidDeepLinkSessionId`), so it boots already\n * pinned to the exact reviewer session instead of self-discovering the\n * newest running one. See ./ui/render.ts's `liveSessionUrl`.\n *\n * The verdict/status chip (never render a running/cancelled run as if it\n * had a verdict): pass/block/incomplete/running/cancelled/failed, one\n * visual language everywhere it appears — see ./ui/render.ts's `verdictChip`.\n */\n\nimport { REVIEW_PANEL_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph can still name the panel from source (mirrors\n * work-board/panel.ts's `WORK_BOARD_APP_ID`). */\nexport const REVIEW_PANEL_APP_ID = \"@agentproto/review-panel\"\n\n/** The MCP tool the panel is registered under (./index.ts\n * `makeReviewPanelApp`), hence its resource uri `ui://agentproto_reviews/view`. */\nexport const REVIEW_PANEL_TOOL_ID = \"agentproto_reviews\"\n\n/** The daemon tools this panel's html calls, in order of first use. Consumed\n * as `ui.tools` by ./index.ts and as the client-side allowlist by any host\n * that dispatches the panel's `tools/call` directly. The reviewer-session\n * deep link (./ui/render.ts's `liveSessionUrl`) is a plain `openLink`/\n * `window.open` navigation, not a tools/call, so it needs no entry here. */\nexport const REVIEW_PANEL_UI_TOOLS = [\n \"review_ledger\",\n \"review_status\",\n \"review_cancel\",\n \"review_run\",\n \"review_pr\",\n \"review_export\",\n] as const\n\nexport const REVIEW_PANEL_HTML = REVIEW_PANEL_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto review verdict panel — a list +\n * detail view over the daemon's review ledger\n * (`packages/runtime/src/review-ledger.ts`) and its in-flight runs\n * (`review-runner.ts`'s `ReviewRunner.list()`).\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape, no\n * @agstudio/mcp-apps / Mastra dependency), same as sessions-panel /\n * work-board — the host (@agentproto/runtime) wires this factory's output\n * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the\n * mounted list assembled in runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_reviews` is called by the host with the optional\n * `{cwd, repoRemote, runId, requesterSessionId}` scoping input.\n * 2. execute() answers an initial `review_ledger`-shaped snapshot\n * (`includeRunning: true`) via `ops.listReviews` — the SAME body\n * `review_ledger` itself runs (`reviewLedgerView`, review-tools.ts),\n * so a non-interactive caller sees exactly what the tool would return.\n * 3. The HTML panel — a static work-board-style Vite build, so it does\n * NOT thread this execute() input through — opens its own bridge and\n * polls `review_ledger({includeRunning: true})` unscoped (mirrors\n * work-board's own boardId-from-execute() omission), then\n * `review_status(runId)` for whichever row is selected. Actions go\n * through `review_cancel`/`review_run`/`review_pr`/`review_export` —\n * real tool names only, no second write path.\n *\n * This module also exports `reviewPanelApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeReviewPanelApp` above, which the daemon mounts\n * directly at boot: that factory closes over the LIVE ledger/runner read,\n * something a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { REVIEW_PANEL_APP_ID, REVIEW_PANEL_HTML, REVIEW_PANEL_TOOL_ID, REVIEW_PANEL_UI_TOOLS } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const reviewPanelInputSchema = z.object({\n cwd: z.string().optional().describe(\"A directory inside a git repo — scopes the initial snapshot to its remote.\"),\n repoRemote: z.string().optional().describe(\"Normalized repo remote (e.g. github.com/agentproto/ts).\"),\n runId: z.string().optional().describe(\"A specific run id known at open time (informational on the initial snapshot).\"),\n requesterSessionId: z\n .string()\n .optional()\n .describe(\"Scope the initial snapshot to reviews requested by this session.\"),\n})\n\nexport type ReviewPanelInput = z.infer<typeof reviewPanelInputSchema>\n\nexport interface ReviewPanelOutput {\n total: number\n attestations: Record<string, unknown>[]\n /** Echoed back from the input — the row this snapshot's caller already\n * had in mind, when known. Not consumed by the panel's own client JS\n * (see this file's docblock on why the static build doesn't thread it). */\n focusRunId?: string\n}\n\n/**\n * Generic host wiring for `makeReviewPanelApp` — the daemon supplies\n * `listReviews` bound to `reviewLedgerView(reviewRunner, ...)`\n * (review-tools.ts), the exact function `review_ledger` itself calls, so\n * the panel's initial snapshot and the tool's own output can never drift\n * apart into two implementations.\n */\nexport interface ReviewPanelOps {\n listReviews(input: ReviewPanelInput): Promise<ReviewPanelOutput> | ReviewPanelOutput\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the ledger/runner read so execute() doesn't need an\n * AppContext (agentproto has no userId/guildId concept) — mirrors\n * makeWorkBoardApp.\n */\nexport function makeReviewPanelApp(ops: ReviewPanelOps): AgnoMcpApp<ReviewPanelInput, ReviewPanelOutput> {\n return {\n id: REVIEW_PANEL_TOOL_ID,\n title: \"Reviews\",\n description:\n \"Open the agentproto review panel — a verdict list (running reviews first, then \" +\n \"newest) over the daemon's review ledger, with a per-run detail view (lanes, \" +\n \"findings, agent-lane provenance) and actions (cancel, re-run fresh, fetch PR \" +\n \"status, export). Pass `cwd`/`repoRemote`/`requesterSessionId` to scope the initial \" +\n \"snapshot; the panel's own live view re-polls unscoped.\",\n inputSchema: reviewPanelInputSchema,\n execute: async input => {\n const snapshot = await ops.listReviews(input)\n return { ...snapshot, ...(input.runId ? { focusRunId: input.runId } : {}) }\n },\n html: REVIEW_PANEL_HTML,\n }\n}\n\nexport const reviewPanelApp: AppHandle = defineApp({\n id: REVIEW_PANEL_APP_ID,\n name: \"Reviews\",\n description:\n \"Open the agentproto review panel — a verdict list + detail view over the daemon's \" +\n \"review ledger, with cancel / re-run / PR-status / export actions.\",\n agents: [],\n ui: {\n html: REVIEW_PANEL_HTML,\n title: \"Reviews\",\n tools: [...REVIEW_PANEL_UI_TOOLS],\n },\n})\n"]}
|
|
@@ -109,18 +109,31 @@ function openLink(url){
|
|
|
109
109
|
if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);
|
|
110
110
|
return rpcRequest('ui/open-link', {url: url});
|
|
111
111
|
}
|
|
112
|
+
// An MCP tool-result envelope: {content: [{type: 'text', text}, ...]}.
|
|
113
|
+
function _isEnvelope(v){
|
|
114
|
+
return !!v && typeof v === 'object' && Array.isArray(v.content) &&
|
|
115
|
+
!!v.content[0] && v.content[0].type === 'text' && typeof v.content[0].text === 'string';
|
|
116
|
+
}
|
|
117
|
+
// Peels every envelope layer. The standalone path (POST /apps/:id/tool-call,
|
|
118
|
+
// runtime app-tools.ts dispatchAllowlistedAppTool) wraps an already-wrapped
|
|
119
|
+
// builtin result a second time, and an inner isError rides inside that outer
|
|
120
|
+
// success \u2014 so isError is honoured at each layer, not just the first.
|
|
121
|
+
function _unwrapToolResult(result){
|
|
122
|
+
var env = result;
|
|
123
|
+
while (true){
|
|
124
|
+
var first = env && env.content && env.content[0];
|
|
125
|
+
var text = (first && first.text) || '';
|
|
126
|
+
if (env && env.isError) throw new Error(text || 'tool error');
|
|
127
|
+
try { var value = JSON.parse(text || '{}'); } catch(_) { return text; }
|
|
128
|
+
if (!_isEnvelope(value)) return value;
|
|
129
|
+
env = value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
112
132
|
function callTool(name, args){
|
|
113
133
|
var raw = _standaloneApp
|
|
114
134
|
? _standaloneApp.callTool(name, args || {})
|
|
115
135
|
: rpcRequest('tools/call', {name: name, arguments: args || {}});
|
|
116
|
-
return raw.then(
|
|
117
|
-
if (result.isError){
|
|
118
|
-
var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';
|
|
119
|
-
throw new Error(e);
|
|
120
|
-
}
|
|
121
|
-
var text = (result.content && result.content[0] && result.content[0].text) || '{}';
|
|
122
|
-
return JSON.parse(text);
|
|
123
|
-
});
|
|
136
|
+
return raw.then(_unwrapToolResult);
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
// \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/panel-bridge.ts","../../src/session-chat/blob-embed.ts","../../src/session-chat/panel.ts"],"names":[],"mappings":"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","file":"panel.mjs","sourcesContent":["/**\n * Shared MCP Apps client bridge for agentproto panels.\n *\n * One spec-correct copy of the JSON-RPC-over-postMessage bridge that every\n * ui:// panel embeds in its <script> block. Mirrors the behaviour of\n * @agstudio/mcp-apps' mcpAppClientScript() — which we cannot import here:\n * agentproto is a standalone pnpm workspace with no @agstudio/* dependency\n * (same isolation invariant as mcp-apps-adapter.ts).\n *\n * Handshake (MCP Apps ext spec 2026-01-26, ext-apps McpUiInitializeRequest):\n * params REQUIRE `appInfo` (Implementation: {name, version}),\n * `appCapabilities`, and `protocolVersion`. Sending `clientInfo` instead\n * of `appInfo` fails the host's schema validation — hosts either reply\n * with an error or silently drop the request, so the panel's initBridge()\n * promise never resolves and the panel renders blank while every\n * server-side surface (tools, resources) works. That exact bug shipped in\n * 4 copy-pasted bridges; this module is the single corrected source.\n *\n * The script defines, in panel scope:\n * post / rpcRequest / rpcNotify — raw JSON-RPC plumbing\n * onHostNotification(cb) — subscribe to host-pushed\n * notifications (method, params)\n * initBridge() — ui/initialize → ui/notifications/initialized\n * callTool(name, args) — tools/call + JSON text unwrap\n * getHostContext() / onHostContext(cb) — host context (displayMode, theme, …),\n * captured from the initialize result and\n * merged on ui/notifications/host-context-changed\n * requestDisplayMode(mode) — ui/request-display-mode (inline|fullscreen|pip)\n *\n * Display-mode toggle: the bridge also mounts two floating buttons\n * (fullscreen + pip) in the top-right corner of every panel, shown only for\n * the modes the host actually advertises via hostContext.availableDisplayModes.\n * No auto-request: panels stay inline until the user clicks. The buttons\n * themselves are NOT implemented here — `@agentproto/app-client/display-mode`\n * is the one implementation, shared with the `window.McpApp` bridge injected\n * into installed apps (`packages/runtime/src/app-ui-apps.ts`); this module\n * only hands it `getHostContext`/`onHostContext`/`requestDisplayMode`. It\n * also leaves `window.AgentprotoUI.installDisplayMode` defined in the panel,\n * which is what a panel with its own header calls to place the toggle\n * inline instead (see that module's `mountToggle`).\n *\n * Standalone HTTP mode: `GET /apps/:appId/ui` (packages/runtime\n * http-server.ts) serves this exact script with NO postMessage host on the\n * other end of `window.parent` — it's a plain browser tab (or a VS Code\n * *HTTP-iframe* panel), so `window.parent === window` and nothing ever\n * answers `initBridge()`'s `ui/initialize` request, leaving every panel\n * stuck on \"Connecting to bridge…\" forever. `injectStandaloneAppBridge`\n * (app-ui-apps.ts) papers over exactly this for installed apps by injecting\n * `window.McpApp.connect() -> Promise<{callTool, ...}>`, a REST-backed\n * stand-in whose `callTool` POSTs to the sibling `./tool-call` route — but\n * this bridge never consumed `window.McpApp`, so builtin panels (the only\n * consumers of this module) were left hanging regardless.\n *\n * Fixed here by detecting that same standalone shape and short-circuiting\n * both `initBridge()` (resolves locally, no round trip) and `callTool()`\n * (routes through `window.McpApp.connect()` instead of `tools/call` over\n * postMessage). The detection requires BOTH `window.parent === window` (no\n * host to talk to — this is the actual condition standalone mode means) AND\n * `window.McpApp` being present with a `connect` function (the capability\n * this bridge needs from it): `window.McpApp` presence alone was considered\n * and rejected as the sole signal, even though it happens to also hold today\n * — the VS Code webview's `srcdoc` relay (packages/vscode\n * appPanel.ts/appPanelController.ts) answers this bridge's raw postMessage\n * JSON-RPC directly and never defines `window.McpApp` for a builtin panel\n * (only `createUiHtmlCache`'s `injectMcpAppBridge`, used for INSTALLED\n * apps' `ui.path` html, defines it) — but that's an accident of today's\n * wiring, not a guarantee; `window.parent === window` is the direct,\n * load-bearing signal (\"is there anyone to postMessage?\") and doesn't\n * depend on which injector happened to run. There is no host in standalone\n * mode, so `initBridge()` seeds a default `hostContext` of `{displayMode:\n * 'inline', availableDisplayModes: []}` — an empty `availableDisplayModes`\n * is what keeps the display-mode toggle buttons hidden (they only show for\n * modes `hostContext.availableDisplayModes` lists), rather than rendering\n * two buttons with nothing to switch to.\n *\n * The real postMessage-host path (a compliant MCP-Apps host, or the VS Code\n * srcdoc relay) is untouched byte-for-byte: same `rpcRequest`/`rpcNotify`\n * over `window.parent.postMessage`, same handshake, same display-mode\n * plumbing.\n *\n * Asymmetry not fixed here (flagged, not papered over): `window.McpApp`'s\n * `sendMessage`/`updateModelContext` (STANDALONE_REST_BRIDGE_SCRIPT,\n * app-ui-apps.ts) reject with \"no host (standalone mode)\", while\n * `appPanelController.ts`'s `ui/message`/`ui/update-model-context` handlers\n * accept-and-drop (`return {}`). This bridge doesn't currently expose either\n * call, so the difference is latent, but it's the last behavioural delta\n * between the two panel paths and worth a follow-up if this bridge ever\n * grows those methods.\n */\n\nimport { DISPLAY_MODE_SCRIPT_BODY } from \"@agentproto/app-client/display-mode\"\n\nexport function panelBridgeScript(appName: string): string {\n return `// ── MCP Apps bridge (shared: panel-bridge.ts) ──\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nvar _hostCaps = null;\nvar _standaloneApp = null;\nfunction _isStandalone(){\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\n}\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\n// Panels that iframe the daemon's standalone app host carry this proof so\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\n// registerMcpApps replaces the placeholder with a real per-boot token when\n// serving the panel as a resource; every other render (tests, standalone\n// tab, VS Code HTTP-iframe panel — the latter two already pass\n// iframeEmbedAllowed's origin checks) keeps the literal, and\n// withEmbedToken() then degrades to an identity function.\nwindow.__AGENPROTO_EMBED_TOKEN__ = \"__AGENPROTO_EMBED_TOKEN__\";\nfunction withEmbedToken(url){\n var t = window.__AGENPROTO_EMBED_TOKEN__;\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\n}\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\nfunction getHostContext(){ return _hostContext; }\nfunction getHostCapabilities(){ return _hostCaps; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== 'object') return;\n // ui/notifications/host-context-changed carries only the changed keys —\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener('message', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === 'ui/notifications/host-context-changed'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n if (_isStandalone()){\n return window.McpApp.connect().then(function(conn){\n _standaloneApp = conn;\n // No host to advertise a hostContext — default to inline with no\n // other modes available, which keeps the display-mode toggle buttons\n // hidden (they only show for modes hostContext.availableDisplayModes\n // lists) instead of rendering two buttons with nothing to switch to.\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\n });\n }\n return rpcRequest('ui/initialize', {\n appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\n protocolVersion: '2026-01-26'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) — capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\n // \"Host supports opening external URLs\". Captured once, read via\n // getHostCapabilities() by panels that need to branch on it.\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\n rpcNotify('ui/notifications/initialized', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest('ui/request-display-mode', {mode: mode});\n}\nfunction openLink(url){\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\n return rpcRequest('ui/open-link', {url: url});\n}\nfunction callTool(name, args){\n var raw = _standaloneApp\n ? _standaloneApp.callTool(name, args || {})\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\n return raw.then(function(result){\n if (result.isError){\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\n throw new Error(e);\n }\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\n return JSON.parse(text);\n });\n}\n\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\n// The installer is inlined verbatim (it is self-contained and idempotent),\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\n// panel stays inline until the user clicks, and the buttons only appear for\n// the modes the host advertises in hostContext.availableDisplayModes.\n${DISPLAY_MODE_SCRIPT_BODY}\nvar displayMode = window.AgentprotoUI.installDisplayMode({\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n requestDisplayMode: requestDisplayMode\n});`\n}\n","/**\n * Blob pass-through for the session-chat widget (see ./panel.ts `mountBlob`).\n *\n * Claude Desktop and Codex apply their own CSP to an MCP-Apps widget frame:\n * `frame-src 'self' blob: data:`. Neither merges the resource's declared\n * `_meta.ui.csp.frameDomains` into it (measured 2026-09-18), so a direct\n * `<iframe src=\"http://127.0.0.1:18790/apps/@agentik/session-chat/ui?…\">` is\n * refused host-side before the request ever reaches the daemon. `blob:` IS\n * on that list — so the panel fetches the chat html itself (a fetch, not a\n * frame navigation; the per-boot `?et=` embed token gets it through the\n * daemon's origin gates and PNA preflight), rewrites it with the function\n * below, and mounts the result as a `blob:` document.\n *\n * What the rewrite has to fix, given that the document no longer lives on\n * the daemon origin:\n *\n * 1. `<base href=\"<deep link>\">` right after `<head>`, so every relative\n * url in the app (the injected standalone bridge's `./tool-call`, any\n * relative asset) resolves exactly as it would have on the daemon.\n * NOTE: the base is the document url itself (`…/session-chat/ui?…`),\n * not `…/ui/` — a trailing slash would add a path segment and make\n * `./tool-call` resolve to `…/ui/tool-call`, which routes nowhere.\n * 2. The blob document's origin is opaque (Claude's widget context is a\n * sandboxed frame; the blob inherits that) — every daemon request it\n * makes carries `Origin: null`, which the daemon's browser gates\n * (`guardBrowserOrigin`, `authorizeMcp`, `checkSessionsToken`) refuse\n * unless the request ALSO carries the embed token. The chat UI talks to\n * the daemon with absolute urls (`${DAEMON_URL}/mcp`, `/sessions/*`,\n * its SSE stream via `fetch`, `/sessions/:id/chat`) that we don't\n * control, so a small shim is injected ahead of every app script: it\n * wraps `window.fetch` / `window.EventSource` to append `et=` to any\n * request aimed at the daemon origin, and points\n * `window.__AGENTPROTO_BASEURL__` at the daemon origin the deep link\n * names (so a non-default port works too).\n * 3. The standalone bridge's `fetch(\"./tool-call\"` (runtime\n * app-ui-apps.ts `STANDALONE_REST_BRIDGE_SCRIPT`) is additionally\n * rewritten to the absolute tokened url — belt and braces on top of\n * the shim, and the one rewrite that is exact-string-testable.\n * 4. The shim's FIRST statement posts `{ type: \"agentproto-blob-boot\" }`\n * to its parent. A blob document inherits its creator's CSP, so if the\n * host's widget CSP ever kills inline scripts, that ack never arrives\n * and the panel tears the blob down and falls back to the direct frame\n * + launcher card (panel.ts `armBlobBootProbe`) instead of leaving a\n * silent blank. No CSP `<meta>` is added to the blob html: the\n * creator's CSP wins regardless, it would only be noise.\n *\n * Shipped as a JS string (same shape as ../panel-bridge.ts) so the panel\n * page embeds the exact code the test evaluates — no TS twin to drift from.\n */\n\n/** `postMessage` type the injected shim sends its parent on boot. */\nexport const BLOB_BOOT_MESSAGE_TYPE = \"agentproto-blob-boot\"\n\nexport function blobEmbedScript(): string {\n return `// ── Blob pass-through transform (shared: session-chat/blob-embed.ts) ──\nfunction _blobEscapeAttr(s){ return String(s).replace(/&/g, '&').replace(/\"/g, '"'); }\n// Rewrite the fetched chat html so it works as a blob: document: <base> on\n// the deep link, a fetch/EventSource shim that carries the embed token to\n// the daemon origin, and the standalone bridge's ./tool-call made absolute.\nfunction transformChatHtmlForBlob(html, url, token){\n var origin = new URL(url).origin;\n var tc = new URL('./tool-call', url);\n tc.searchParams.set('et', token);\n // The app-visible query for the blob doc: the deep link's search minus the\n // embed token (the token must not ride the app's visible params). The\n // blob document's own location has no params, so this is the ONLY channel\n // for ?session= / ?embed=1 — the app reads it via bootSearchParams().\n var bootUrl = new URL(url);\n bootUrl.searchParams.delete('et');\n var bootQuery = bootUrl.search;\n var shim = '<script>(function(){'\n + 'try { window.parent.postMessage({ type: ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)} }, \"*\"); } catch (_) {}'\n + 'var ORIGIN = ' + JSON.stringify(origin) + ', TOKEN = ' + JSON.stringify(token) + ';'\n + 'window.__AGENTPROTO_BASEURL__ = ORIGIN;'\n + 'var _h = window.history, _rs = _h.replaceState.bind(_h), _ps = _h.pushState.bind(_h);'\n + 'function _sameOrLoc(u){'\n + 'try {'\n + 'var x = new URL(u == null ? window.location.href : u, document.baseURI);'\n + 'return x.origin === window.location.origin ? u : window.location.href;'\n + '} catch (_) { return window.location.href; }'\n + '}'\n + '_h.replaceState = function(s, t, u){'\n + 'try { _rs(s, t, _sameOrLoc(u)); } catch (_) { try { _rs(s, t, window.location.href); } catch (_) {} }'\n + '};'\n + '_h.pushState = function(s, t, u){'\n + 'try { _ps(s, t, _sameOrLoc(u)); } catch (_) { try { _ps(s, t, window.location.href); } catch (_) {} }'\n + '};'\n + 'window.__AGENTPROTO_BOOT_QUERY__ = ' + JSON.stringify(bootQuery) + ';'\n + 'function withEt(u){'\n + 'try {'\n + 'var x = new URL(String(u), document.baseURI);'\n + 'if (x.origin !== ORIGIN) return null;'\n + 'if (!x.searchParams.has(\"et\")) x.searchParams.set(\"et\", TOKEN);'\n + 'return x.href;'\n + '} catch (_) { return null; }'\n + '}'\n + 'var _fetch = window.fetch;'\n + 'if (typeof _fetch === \"function\") {'\n + 'window.fetch = function(input, init){'\n + 'var r;'\n + 'if (typeof Request !== \"undefined\" && input instanceof Request) {'\n + 'r = withEt(input.url);'\n + 'if (r) { try { input = new Request(r, input); } catch (_) {} }'\n + '} else {'\n + 'r = withEt(input);'\n + 'if (r) input = r;'\n + '}'\n + 'return _fetch.call(window, input, init);'\n + '};'\n + '}'\n + 'var _ES = window.EventSource;'\n + 'if (typeof _ES === \"function\") {'\n + 'window.EventSource = function(u, cfg){ return new _ES(withEt(u) || u, cfg); };'\n + 'window.EventSource.prototype = _ES.prototype;'\n + '}'\n + '})();<\\\\/script>';\n var head = '<base href=\"' + _blobEscapeAttr(url) + '\">' + shim;\n var m = html.match(/<head[^>]*>/i);\n var out = m\n ? html.slice(0, m.index + m[0].length) + head + html.slice(m.index + m[0].length)\n : head + html;\n return out.split('fetch(\"./tool-call\"').join('fetch(' + JSON.stringify(tc.href));\n}`\n}\n","/**\n * HTML bundles for the session-chat builtin widget — the thin bridge page\n * (`sessionChatEmbedHtml`) and the not-installed notice\n * (`SESSION_CHAT_FALLBACK_HTML`) served as the\n * `ui://agentproto_session_chat/view` resource by `registerMcpApps` (see\n * ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via the\n * `agentproto_session_chat` tool defined in ./index.ts, AND bound to\n * `agent_start` (runtime agent-tools.ts `_meta.ui.resourceUri`) so a launch\n * card opens the chat of the session it just spawned.\n *\n * Deliberately NOT a chat UI reimplementation: the real chat lives in the\n * installed `@agentik/session-chat` app, served by the daemon's standalone\n * app host at `GET /apps/@agentik/session-chat/ui`. When the app is\n * installed, this page is a full-viewport iframe of that URL deep-linked\n * with `?session=<id>&embed=1` — the exact spelling the daemon's standalone\n * route relaxes its `frame-ancestors 'none'` for (see runtime\n * http-server.ts handleAppUiPage) and the VS Code chat-panel webview\n * already uses. No second chat bundle exists anywhere in agentproto/ts.\n *\n * Self-bootstrapping (same pattern as live-session/panel.ts): the static\n * ui:// resource is rendered ONCE at server-registration time with EMPTY\n * initData (mcp-apps-adapter.ts registerMcpApps), so the deep link can't\n * be baked in. The page resolves it at runtime, in priority order:\n * 1. `initData.url` when the html was rendered with a real tool output\n * (tests, and any host that re-renders per call) — iframe is inline.\n * 2. The host's `ui/notifications/tool-result` push of the tool call that\n * mounted the widget. An `agent_start` result IS the spawned session's\n * descriptor (`{ id: \"sess_…\", … }`) → the widget calls\n * `agentproto_session_chat({ sessionId })` over the bridge to turn that\n * id into the deep link; an `agentproto_session_chat` result already\n * carries `{ installed, url }` and mounts directly.\n * 3. Otherwise (opened bare from the catalog) `agentproto_session_chat({})`\n * → the app's session picker.\n * Both orders of (2) vs. bridge init are handled: a result that lands\n * before `initBridge()` resolves is held and applied once it does.\n *\n * Mount order (see `mount()` below):\n * 1. Blob pass-through, when a per-boot embed token is baked in (i.e. an\n * MCP-Apps host rendered this resource): fetch the chat html with the\n * token, rewrite it (./blob-embed.ts `transformChatHtmlForBlob`) and\n * mount it as a `blob:` document. Claude Desktop / Codex widget frames\n * run `frame-src 'self' blob: data:` and do not merge our declared\n * `csp.frameDomains`, so a direct daemon iframe is refused host-side —\n * `blob:` is the pass-through. The blob document must post a boot ack\n * within a settle window (`armBlobBootProbe`); a missing ack (host CSP\n * killed its inline scripts, or refused the blob frame) tears it down\n * and falls through to 2.\n * 2. Direct-src iframe of the deep link (VS Code HTTP-iframe panels, a\n * standalone tab — no token, and their origin already passes the\n * daemon's checks; or the blob path's fallback). If the host refuses\n * the frame, `armBlockProbe` removes it and the launcher card becomes\n * the interactive surface.\n *\n * A persistent header with the deep link stays visible above the iframe, so\n * if the frame is refused (older daemon without the embed relaxation, or a\n * host whose own CSP blocks daemon frames) the user still has a one-click\n * path to the chat in a tab. When the app is NOT installed the page is a\n * small readable notice pointing at `agentproto app install` — the\n * fallback branch, never a vendored UI.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\nimport { BLOB_BOOT_MESSAGE_TYPE, blobEmbedScript } from \"./blob-embed.js\"\nimport type { SessionChatOutput } from \"./index.js\"\n\n/** Only `&` and `\"` are significant inside a double-quoted attribute value. */\nfunction escapeAttr(text: string): string {\n return text.replace(/&/g, \"&\").replace(/\"/g, \""\")\n}\n\nexport function sessionChatEmbedHtml(initData: Partial<SessionChatOutput>): string {\n const url = typeof initData.url === \"string\" && initData.url ? initData.url : null\n const notInstalled = initData.installed === false\n const storeUrl =\n typeof initData.storeUrl === \"string\" && initData.storeUrl\n ? initData.storeUrl\n : \"/store?install=%40agentik%2Fsession-chat\"\n const link =\n url != null\n ? `<a href=\"${escapeAttr(url)}\" target=\"_blank\" rel=\"noreferrer\">open in a tab</a>`\n : notInstalled\n ? \"app not installed\"\n : \"connecting…\"\n const iframe =\n url != null\n // No inline src: the url mounts through the bridge script below, via\n // withEmbedToken(), so the frame never loads without the per-boot\n // embed proof — whichever render path (static resource or a host\n // re-render with a real initData) produced it.\n ? `<iframe id=\"chat\" title=\"Session Chat\"></iframe>`\n : \"\"\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Session Chat</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3;overflow:hidden}\n#bar{padding:6px 12px;font-size:12px;color:#8b949e;background:#161b22;border-bottom:1px solid #30363d}\n#bar a{color:#58a6ff}\n#mode{color:#6e7681}\n#stage{position:relative;height:calc(100% - 29px)}\n#chat{position:absolute;inset:0;z-index:2;border:0;display:block;width:100%;height:100%}\n#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}\n#card h2{font-size:15px;font-weight:600}\n#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}\n#card p{font-size:12px;color:#8b949e;max-width:420px;line-height:1.5}\n#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}\n#card a:hover{border-color:#58a6ff}\n#notice{display:none;max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}\n#notice.show{display:block}\n#notice h1{font-size:18px;margin-bottom:12px}\n#notice code{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}\n#notice p{margin:8px 0;color:#8b949e;font-size:13px}\n</style>\n</head>\n<body>\n<div id=\"bar\">Session Chat · <span id=\"link\">${link}</span><span id=\"mode\"></span></div>\n<div id=\"stage\">${iframe}<div id=\"card\">\n <h2>Session Chat</h2>\n <div id=\"card-session\" class=\"sid\">—</div>\n <p>Full embed isn't supported by this host — the chat opens in a browser tab.</p>\n <a id=\"card-open\" href=\"#\" target=\"_blank\" rel=\"noreferrer\">Open chat</a>\n</div><div id=\"notice\"${notInstalled ? ' class=\"show\"' : \"\"}>\n <h1>Session Chat is not installed</h1>\n <p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app —\n it does not bundle the chat UI itself.</p>\n <p>To use it, install the app into this daemon:</p>\n <p><code>agentproto app install @agentik/session-chat</code></p>\n <p>or open <a href=\"${escapeAttr(storeUrl)}\" target=\"_blank\" rel=\"noreferrer\">the App Store</a>\n (the daemon's <code>/store</code> panel, with the entry pre-selected).</p>\n</div></div>\n<script>\nwindow.__APP_INIT__ = ${JSON.stringify(initData)};\n\n${panelBridgeScript(\"agentproto-session-chat\")}\n\n${blobEmbedScript()}\n\n// ── Session-chat launcher logic ──────────────────────────────────────────\nvar mountedUrl = (window.__APP_INIT__ && window.__APP_INIT__.url) || null;\nvar pinnedSessionId = null; // session named by the host's tool-result push\nvar pendingUrl = null; // url pushed by the host before the bridge was up\nvar pendingNotInstalled = false;\nvar bridged = false;\nvar cardUrl = null; // deep link once resolved (drives the card button)\nvar cardSessionEl = document.getElementById('card-session');\nvar cardOpenEl = document.getElementById('card-open');\nvar mountGen = 0; // bumps per mount(); async work checks it before touching the DOM\nvar blobUrl = null; // live object url of the blob-mounted chat, if any\nvar blobBootTimer = null;\nvar blobBootOk = false;\n\nfunction escapeHtml(text) {\n return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/\"/g, '"');\n}\n\nfunction setMode(text) {\n document.getElementById('mode').textContent = text ? ' \\\\u00b7 ' + text : '';\n // The chat app is live inside the frame — the launcher bar (title, deep\n // link, mount mode) is noise on top of the real surface. Hide it and let\n // the frame own the full height. Only a failed mount (launcher card /\n // not-installed notice) shows the bar again.\n var bar = document.getElementById('bar');\n var stage = document.getElementById('stage');\n if (bar && stage) {\n bar.style.display = text ? 'none' : '';\n stage.style.height = text ? '100%' : 'calc(100% - 29px)';\n }\n}\n\n// One fresh #chat frame on the stage (over the card), the previous one gone.\nfunction newFrame() {\n var stage = document.getElementById('stage');\n var old = document.getElementById('chat');\n if (old) old.remove();\n var frame = document.createElement('iframe');\n frame.id = 'chat';\n frame.title = 'Session Chat';\n stage.insertBefore(frame, stage.firstChild);\n return frame;\n}\n\nfunction revokeBlob() {\n if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }\n if (blobUrl) {\n try { URL.revokeObjectURL(blobUrl); } catch (_) {}\n blobUrl = null;\n }\n}\n\nfunction mount(url) {\n if (!url || url === mountedUrl) return;\n mountedUrl = url;\n var gen = ++mountGen;\n revokeBlob();\n document.getElementById('notice').className = '';\n setCardUrl(url);\n document.getElementById('link').innerHTML =\n '<a href=\"' + escapeHtml(url) + '\" target=\"_blank\" rel=\"noreferrer\">open in a tab</a>';\n var tokened = withEmbedToken(url);\n if (tokened === url) {\n // No per-boot token baked in (standalone tab, VS Code HTTP-iframe panel):\n // the direct frame already passes the daemon's origin checks, and the\n // blob path's daemon calls would have nothing to carry — skip it.\n mountDirect(url);\n return;\n }\n mountBlob(url, tokened, gen).catch(function (err) {\n if (gen !== mountGen) return;\n // The fetch itself was refused (host connect-src not merging our\n // connectDomains, PNA, daemon down, …) — probe answered: no blob path.\n console.warn('[session-chat] blob mount failed, falling back to a direct frame:',\n err && err.message ? err.message : String(err));\n mountDirect(url);\n });\n}\n\n// Direct-src iframe of the deep link, with the block probe → launcher card\n// fallback. The terminal mount path when the blob one is unavailable.\nfunction mountDirect(url) {\n var frame = newFrame();\n frame.src = withEmbedToken(url);\n setMode('direct frame');\n armBlockProbe(frame);\n}\n\n// Blob pass-through: fetch the chat html through the embed token, rewrite it\n// for life as a blob: document (transformChatHtmlForBlob, blob-embed.ts) and\n// mount it — blob: passes a host frame-src that refuses the daemon origin.\nfunction mountBlob(url, tokened, gen) {\n return fetch(tokened, { credentials: 'omit' }).then(function (res) {\n if (!res.ok) throw new Error('HTTP ' + res.status);\n return res.text();\n }).then(function (html) {\n if (gen !== mountGen) return;\n var doc = transformChatHtmlForBlob(html, url, window.__AGENPROTO_EMBED_TOKEN__);\n blobUrl = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));\n var frame = newFrame();\n armBlobBootProbe(frame, url, gen);\n frame.src = blobUrl;\n setMode('embedded');\n });\n}\n\n// The blob document's shim posts a boot ack as its first statement. No ack\n// within the settle window means the host refused the blob frame or its\n// inherited CSP killed the document's inline scripts — surface that through\n// the direct-frame path (whose own probe ends on the launcher card) rather\n// than leaving a silent blank.\nwindow.addEventListener('message', function (evt) {\n var d = evt.data;\n if (!d || typeof d !== 'object' || d.type !== ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)}) return;\n var frame = document.getElementById('chat');\n if (!frame || evt.source !== frame.contentWindow) return;\n blobBootOk = true;\n if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }\n});\n\nfunction armBlobBootProbe(frame, url, gen) {\n blobBootOk = false;\n blobBootTimer = setTimeout(function () {\n blobBootTimer = null;\n if (gen !== mountGen || blobBootOk) return;\n console.warn('[session-chat] blob document never booted (host CSP?), falling back to a direct frame');\n revokeBlob();\n frame.remove();\n mountDirect(url);\n }, 5000);\n}\n\nfunction showNotInstalled() {\n var old = document.getElementById('chat');\n if (old) old.remove();\n revokeBlob();\n setMode('');\n mountedUrl = null;\n document.getElementById('card').style.display = 'none';\n document.getElementById('notice').className = 'show';\n document.getElementById('link').textContent = 'app not installed';\n}\n\n// The body of an ext-apps tool-result notification, whichever wrapping\n// shape the host uses (params IS the CallToolResult, or nests it under\n// params.result). null for error results / non-JSON text.\nfunction parseToolResultBody(params) {\n if (!params || typeof params !== 'object') return null;\n var res = (params.result && typeof params.result === 'object') ? params.result : params;\n if (res.isError) return null;\n var content = Array.isArray(res.content) ? res.content : [];\n var item = content[0];\n if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;\n try { return JSON.parse(item.text); } catch (_) { return null; }\n}\n\n// INLINED COPY of live-session/logic.ts extractToolResultSessionId — keep\n// in sync. \\`sessionId\\` (an agentproto_session_chat / live_session result)\n// wins over \\`id\\` (an agent_start result: the spawned session descriptor).\nfunction extractToolResultSessionId(params) {\n var body = parseToolResultBody(params);\n if (!body) return null;\n if (typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;\n if (typeof body.id === 'string' && body.id) return body.id;\n return null;\n}\n\n// ── Embed-token refresh ──────────────────────────────────────────────────\n// The token baked into this page by registerMcpApps dies with the daemon\n// boot that minted it (runtime embed-tokens.ts), but hosts cache the\n// rendered widget: Claude Desktop keeps a conversation's srcdoc, so after\n// any daemon restart the cached page replays a dead token and every daemon\n// call 403s — the blob fetch fails, the direct frame is refused, and the\n// user is left on the launcher card forever. Every tool result carries a\n// live token (SessionChatOutput.embedToken), so adopt it before mounting.\n//\n// Only a render that WAS baked refreshes. A standalone tab or VS Code\n// HTTP-iframe panel still holds the literal placeholder and passes the\n// daemon's origin checks on its own; handing it a token would switch it\n// onto the blob path for no reason.\nfunction adoptEmbedToken(body) {\n var cur = window.__AGENPROTO_EMBED_TOKEN__;\n if (!cur || cur === '__AGENPROTO_EMBED_TOKEN__') return;\n if (!body || typeof body.embedToken !== 'string' || !body.embedToken) return;\n window.__AGENPROTO_EMBED_TOKEN__ = body.embedToken;\n}\n\n// Turn a session id into the installed app's deep link over the bridge.\nfunction resolveSession(sessionId) {\n noteSession(sessionId);\n return callTool('agentproto_session_chat', sessionId ? { sessionId: sessionId } : {})\n .then(function(out) {\n adoptEmbedToken(out);\n if (out && out.installed && typeof out.url === 'string' && out.url) mount(out.url);\n else showNotInstalled();\n })\n .catch(function(err) {\n document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));\n });\n}\n\n// ── Launcher card (the frame-blocked fallback UI) ────────────────────────\n// The iframe mounts OVER the card. When the host's own CSP refuses the\n// frame (Claude Desktop / Codex widget frames run frame-src 'self' blob:\n// data: and do not yet merge csp.frameDomains), the frame stays on a\n// same-origin about:blank — readable, unlike the loaded cross-origin chat —\n// and is removed after a settle window so the card becomes the interactive\n// surface.\n//\n// The anchor carries target=\"_blank\" rel=\"noreferrer\" as the fallback path,\n// but that is a plain no-op in hosts that sandbox the widget iframe without\n// allow-popups (observed in Claude Desktop's side chat) — a normal click\n// on such a host does nothing. Where the host advertises the capability\n// (McpUiInitializeResult.hostCapabilities.openLinks in the ext-apps spec,\n// captured by the bridge's getHostCapabilities()), route through the\n// host-mediated ui/open-link request instead, which is not subject to the\n// iframe's popup sandboxing.\ncardOpenEl.addEventListener('click', function (evt) {\n if (!cardUrl) { evt.preventDefault(); return; }\n if (getHostCapabilities() && getHostCapabilities().openLinks) {\n evt.preventDefault();\n openLink(cardUrl).catch(function () { window.open(cardUrl, '_blank'); });\n }\n});\n\nfunction noteSession(id) {\n if (id && cardSessionEl) cardSessionEl.textContent = id;\n}\n\nfunction setCardUrl(u) {\n if (!u) return;\n cardUrl = u;\n cardOpenEl.setAttribute('href', u);\n}\n\n// A blocked frame never leaves about:blank (same-origin with this page —\n// readable); the loaded chat is cross-origin (the probe throws). A readable\n// frame that is STILL blank when the settle window closes = blocked.\nfunction isBlank(frame) {\n try { return frame.contentWindow.location.href === 'about:blank'; } catch (_) { return false; }\n}\n\nfunction armBlockProbe(frame) {\n var settle = null;\n function probe() {\n if (settle) { clearTimeout(settle); settle = null; }\n if (!isBlank(frame)) return;\n // Still on about:blank right now — arm the settle window, but\n // re-check when it closes: a slow (not blocked) load may have landed\n // in the meantime, and only a frame STILL blank after the window gets\n // removed. load firing in between clears this timeout above.\n settle = setTimeout(function () {\n settle = null;\n if (isBlank(frame)) frame.remove();\n }, 2000);\n }\n setTimeout(function () {\n frame.addEventListener('load', probe);\n probe();\n }, 100);\n}\n\n// A url baked at render time (a host that re-renders per call with a real\n// initData) mounts on boot — through the same blob-first path as every\n// other mount, since the same opaque widget context frames it. The\n// pre-rendered #chat frame is replaced by mount()'s own.\nif (mountedUrl) {\n var bootUrl = mountedUrl;\n mountedUrl = null;\n adoptEmbedToken(window.__APP_INIT__);\n mount(bootUrl);\n}\n\n// The host pushes the triggering tool call's result (ext-apps\n// ui/notifications/tool-result). For agent_start that result IS the spawned\n// session's descriptor — pin the widget to THAT session.\nonHostNotification(function(method, params) {\n if (method !== 'ui/notifications/tool-result') return;\n var body = parseToolResultBody(params);\n if (!body) return;\n if (typeof body.url === 'string' && body.url && body.installed !== false) {\n // Adopt now, not at mount time: the token doesn't depend on the bridge.\n adoptEmbedToken(body);\n if (bridged) mount(body.url); else pendingUrl = body.url;\n return;\n }\n if (body.installed === false) {\n if (bridged) showNotInstalled(); else pendingNotInstalled = true;\n return;\n }\n var id = extractToolResultSessionId(params);\n if (!id || id === pinnedSessionId) return;\n pinnedSessionId = id;\n noteSession(id);\n if (bridged) resolveSession(id);\n});\n\ninitBridge().then(function() {\n bridged = true;\n if (pendingUrl) { mount(pendingUrl); pendingUrl = null; return; }\n if (pendingNotInstalled) { showNotInstalled(); return; }\n if (mountedUrl) return; // rendered with a real url\n var init = window.__APP_INIT__ || {};\n if (init.installed === false) { showNotInstalled(); return; }\n // No url baked in (static ui:// render): resolve over the bridge — the\n // pinned session when the host named one, else the app's picker.\n return resolveSession(pinnedSessionId);\n}).catch(function(err) {\n document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));\n});\n</script>\n</body>\n</html>`\n}\n\nexport const SESSION_CHAT_FALLBACK_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Session Chat — not installed</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3}\n#wrap{max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}\nh1{font-size:18px;margin-bottom:12px}\ncode{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}\np{margin:8px 0;color:#8b949e;font-size:13px}\n</style>\n</head>\n<body>\n<div id=\"wrap\">\n <h1>Session Chat is not installed</h1>\n <p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app —\n it does not bundle the chat UI itself.</p>\n <p>To use it, install the app into this daemon:</p>\n <p><code>agentproto app install @agentik/session-chat</code></p>\n <p>or open <a href=\"/store?install=%40agentik%2Fsession-chat\">the App Store</a>\n (the daemon's <code>/store</code> panel, with the entry pre-selected).</p>\n <p>Once installed, call <code>agentproto_session_chat</code> again (optionally with a\n <code>sessionId</code>) and the panel will embed the app's chat UI deep-linked to that\n session.</p>\n</div>\n</body>\n</html>`\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/panel-bridge.ts","../../src/session-chat/blob-embed.ts","../../src/session-chat/panel.ts"],"names":[],"mappings":"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","file":"panel.mjs","sourcesContent":["/**\n * Shared MCP Apps client bridge for agentproto panels.\n *\n * One spec-correct copy of the JSON-RPC-over-postMessage bridge that every\n * ui:// panel embeds in its <script> block. Mirrors the behaviour of\n * @agstudio/mcp-apps' mcpAppClientScript() — which we cannot import here:\n * agentproto is a standalone pnpm workspace with no @agstudio/* dependency\n * (same isolation invariant as mcp-apps-adapter.ts).\n *\n * Handshake (MCP Apps ext spec 2026-01-26, ext-apps McpUiInitializeRequest):\n * params REQUIRE `appInfo` (Implementation: {name, version}),\n * `appCapabilities`, and `protocolVersion`. Sending `clientInfo` instead\n * of `appInfo` fails the host's schema validation — hosts either reply\n * with an error or silently drop the request, so the panel's initBridge()\n * promise never resolves and the panel renders blank while every\n * server-side surface (tools, resources) works. That exact bug shipped in\n * 4 copy-pasted bridges; this module is the single corrected source.\n *\n * The script defines, in panel scope:\n * post / rpcRequest / rpcNotify — raw JSON-RPC plumbing\n * onHostNotification(cb) — subscribe to host-pushed\n * notifications (method, params)\n * initBridge() — ui/initialize → ui/notifications/initialized\n * callTool(name, args) — tools/call + JSON text unwrap\n * getHostContext() / onHostContext(cb) — host context (displayMode, theme, …),\n * captured from the initialize result and\n * merged on ui/notifications/host-context-changed\n * requestDisplayMode(mode) — ui/request-display-mode (inline|fullscreen|pip)\n *\n * Display-mode toggle: the bridge also mounts two floating buttons\n * (fullscreen + pip) in the top-right corner of every panel, shown only for\n * the modes the host actually advertises via hostContext.availableDisplayModes.\n * No auto-request: panels stay inline until the user clicks. The buttons\n * themselves are NOT implemented here — `@agentproto/app-client/display-mode`\n * is the one implementation, shared with the `window.McpApp` bridge injected\n * into installed apps (`packages/runtime/src/app-ui-apps.ts`); this module\n * only hands it `getHostContext`/`onHostContext`/`requestDisplayMode`. It\n * also leaves `window.AgentprotoUI.installDisplayMode` defined in the panel,\n * which is what a panel with its own header calls to place the toggle\n * inline instead (see that module's `mountToggle`).\n *\n * Standalone HTTP mode: `GET /apps/:appId/ui` (packages/runtime\n * http-server.ts) serves this exact script with NO postMessage host on the\n * other end of `window.parent` — it's a plain browser tab (or a VS Code\n * *HTTP-iframe* panel), so `window.parent === window` and nothing ever\n * answers `initBridge()`'s `ui/initialize` request, leaving every panel\n * stuck on \"Connecting to bridge…\" forever. `injectStandaloneAppBridge`\n * (app-ui-apps.ts) papers over exactly this for installed apps by injecting\n * `window.McpApp.connect() -> Promise<{callTool, ...}>`, a REST-backed\n * stand-in whose `callTool` POSTs to the sibling `./tool-call` route — but\n * this bridge never consumed `window.McpApp`, so builtin panels (the only\n * consumers of this module) were left hanging regardless.\n *\n * Fixed here by detecting that same standalone shape and short-circuiting\n * both `initBridge()` (resolves locally, no round trip) and `callTool()`\n * (routes through `window.McpApp.connect()` instead of `tools/call` over\n * postMessage). The detection requires BOTH `window.parent === window` (no\n * host to talk to — this is the actual condition standalone mode means) AND\n * `window.McpApp` being present with a `connect` function (the capability\n * this bridge needs from it): `window.McpApp` presence alone was considered\n * and rejected as the sole signal, even though it happens to also hold today\n * — the VS Code webview's `srcdoc` relay (packages/vscode\n * appPanel.ts/appPanelController.ts) answers this bridge's raw postMessage\n * JSON-RPC directly and never defines `window.McpApp` for a builtin panel\n * (only `createUiHtmlCache`'s `injectMcpAppBridge`, used for INSTALLED\n * apps' `ui.path` html, defines it) — but that's an accident of today's\n * wiring, not a guarantee; `window.parent === window` is the direct,\n * load-bearing signal (\"is there anyone to postMessage?\") and doesn't\n * depend on which injector happened to run. There is no host in standalone\n * mode, so `initBridge()` seeds a default `hostContext` of `{displayMode:\n * 'inline', availableDisplayModes: []}` — an empty `availableDisplayModes`\n * is what keeps the display-mode toggle buttons hidden (they only show for\n * modes `hostContext.availableDisplayModes` lists), rather than rendering\n * two buttons with nothing to switch to.\n *\n * The real postMessage-host path (a compliant MCP-Apps host, or the VS Code\n * srcdoc relay) is untouched byte-for-byte: same `rpcRequest`/`rpcNotify`\n * over `window.parent.postMessage`, same handshake, same display-mode\n * plumbing.\n *\n * Asymmetry not fixed here (flagged, not papered over): `window.McpApp`'s\n * `sendMessage`/`updateModelContext` (STANDALONE_REST_BRIDGE_SCRIPT,\n * app-ui-apps.ts) reject with \"no host (standalone mode)\", while\n * `appPanelController.ts`'s `ui/message`/`ui/update-model-context` handlers\n * accept-and-drop (`return {}`). This bridge doesn't currently expose either\n * call, so the difference is latent, but it's the last behavioural delta\n * between the two panel paths and worth a follow-up if this bridge ever\n * grows those methods.\n */\n\nimport { DISPLAY_MODE_SCRIPT_BODY } from \"@agentproto/app-client/display-mode\"\n\nexport function panelBridgeScript(appName: string): string {\n return `// ── MCP Apps bridge (shared: panel-bridge.ts) ──\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nvar _hostCaps = null;\nvar _standaloneApp = null;\nfunction _isStandalone(){\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\n}\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\n// Panels that iframe the daemon's standalone app host carry this proof so\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\n// registerMcpApps replaces the placeholder with a real per-boot token when\n// serving the panel as a resource; every other render (tests, standalone\n// tab, VS Code HTTP-iframe panel — the latter two already pass\n// iframeEmbedAllowed's origin checks) keeps the literal, and\n// withEmbedToken() then degrades to an identity function.\nwindow.__AGENPROTO_EMBED_TOKEN__ = \"__AGENPROTO_EMBED_TOKEN__\";\nfunction withEmbedToken(url){\n var t = window.__AGENPROTO_EMBED_TOKEN__;\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\n}\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\nfunction getHostContext(){ return _hostContext; }\nfunction getHostCapabilities(){ return _hostCaps; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== 'object') return;\n // ui/notifications/host-context-changed carries only the changed keys —\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener('message', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === 'ui/notifications/host-context-changed'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n if (_isStandalone()){\n return window.McpApp.connect().then(function(conn){\n _standaloneApp = conn;\n // No host to advertise a hostContext — default to inline with no\n // other modes available, which keeps the display-mode toggle buttons\n // hidden (they only show for modes hostContext.availableDisplayModes\n // lists) instead of rendering two buttons with nothing to switch to.\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\n });\n }\n return rpcRequest('ui/initialize', {\n appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\n protocolVersion: '2026-01-26'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) — capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\n // \"Host supports opening external URLs\". Captured once, read via\n // getHostCapabilities() by panels that need to branch on it.\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\n rpcNotify('ui/notifications/initialized', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest('ui/request-display-mode', {mode: mode});\n}\nfunction openLink(url){\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\n return rpcRequest('ui/open-link', {url: url});\n}\n// An MCP tool-result envelope: {content: [{type: 'text', text}, ...]}.\nfunction _isEnvelope(v){\n return !!v && typeof v === 'object' && Array.isArray(v.content) &&\n !!v.content[0] && v.content[0].type === 'text' && typeof v.content[0].text === 'string';\n}\n// Peels every envelope layer. The standalone path (POST /apps/:id/tool-call,\n// runtime app-tools.ts dispatchAllowlistedAppTool) wraps an already-wrapped\n// builtin result a second time, and an inner isError rides inside that outer\n// success — so isError is honoured at each layer, not just the first.\nfunction _unwrapToolResult(result){\n var env = result;\n while (true){\n var first = env && env.content && env.content[0];\n var text = (first && first.text) || '';\n if (env && env.isError) throw new Error(text || 'tool error');\n try { var value = JSON.parse(text || '{}'); } catch(_) { return text; }\n if (!_isEnvelope(value)) return value;\n env = value;\n }\n}\nfunction callTool(name, args){\n var raw = _standaloneApp\n ? _standaloneApp.callTool(name, args || {})\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\n return raw.then(_unwrapToolResult);\n}\n\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\n// The installer is inlined verbatim (it is self-contained and idempotent),\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\n// panel stays inline until the user clicks, and the buttons only appear for\n// the modes the host advertises in hostContext.availableDisplayModes.\n${DISPLAY_MODE_SCRIPT_BODY}\nvar displayMode = window.AgentprotoUI.installDisplayMode({\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n requestDisplayMode: requestDisplayMode\n});`\n}\n","/**\n * Blob pass-through for the session-chat widget (see ./panel.ts `mountBlob`).\n *\n * Claude Desktop and Codex apply their own CSP to an MCP-Apps widget frame:\n * `frame-src 'self' blob: data:`. Neither merges the resource's declared\n * `_meta.ui.csp.frameDomains` into it (measured 2026-09-18), so a direct\n * `<iframe src=\"http://127.0.0.1:18790/apps/@agentik/session-chat/ui?…\">` is\n * refused host-side before the request ever reaches the daemon. `blob:` IS\n * on that list — so the panel fetches the chat html itself (a fetch, not a\n * frame navigation; the per-boot `?et=` embed token gets it through the\n * daemon's origin gates and PNA preflight), rewrites it with the function\n * below, and mounts the result as a `blob:` document.\n *\n * What the rewrite has to fix, given that the document no longer lives on\n * the daemon origin:\n *\n * 1. `<base href=\"<deep link>\">` right after `<head>`, so every relative\n * url in the app (the injected standalone bridge's `./tool-call`, any\n * relative asset) resolves exactly as it would have on the daemon.\n * NOTE: the base is the document url itself (`…/session-chat/ui?…`),\n * not `…/ui/` — a trailing slash would add a path segment and make\n * `./tool-call` resolve to `…/ui/tool-call`, which routes nowhere.\n * 2. The blob document's origin is opaque (Claude's widget context is a\n * sandboxed frame; the blob inherits that) — every daemon request it\n * makes carries `Origin: null`, which the daemon's browser gates\n * (`guardBrowserOrigin`, `authorizeMcp`, `checkSessionsToken`) refuse\n * unless the request ALSO carries the embed token. The chat UI talks to\n * the daemon with absolute urls (`${DAEMON_URL}/mcp`, `/sessions/*`,\n * its SSE stream via `fetch`, `/sessions/:id/chat`) that we don't\n * control, so a small shim is injected ahead of every app script: it\n * wraps `window.fetch` / `window.EventSource` to append `et=` to any\n * request aimed at the daemon origin, and points\n * `window.__AGENTPROTO_BASEURL__` at the daemon origin the deep link\n * names (so a non-default port works too).\n * 3. The standalone bridge's `fetch(\"./tool-call\"` (runtime\n * app-ui-apps.ts `STANDALONE_REST_BRIDGE_SCRIPT`) is additionally\n * rewritten to the absolute tokened url — belt and braces on top of\n * the shim, and the one rewrite that is exact-string-testable.\n * 4. The shim's FIRST statement posts `{ type: \"agentproto-blob-boot\" }`\n * to its parent. A blob document inherits its creator's CSP, so if the\n * host's widget CSP ever kills inline scripts, that ack never arrives\n * and the panel tears the blob down and falls back to the direct frame\n * + launcher card (panel.ts `armBlobBootProbe`) instead of leaving a\n * silent blank. No CSP `<meta>` is added to the blob html: the\n * creator's CSP wins regardless, it would only be noise.\n *\n * Shipped as a JS string (same shape as ../panel-bridge.ts) so the panel\n * page embeds the exact code the test evaluates — no TS twin to drift from.\n */\n\n/** `postMessage` type the injected shim sends its parent on boot. */\nexport const BLOB_BOOT_MESSAGE_TYPE = \"agentproto-blob-boot\"\n\nexport function blobEmbedScript(): string {\n return `// ── Blob pass-through transform (shared: session-chat/blob-embed.ts) ──\nfunction _blobEscapeAttr(s){ return String(s).replace(/&/g, '&').replace(/\"/g, '"'); }\n// Rewrite the fetched chat html so it works as a blob: document: <base> on\n// the deep link, a fetch/EventSource shim that carries the embed token to\n// the daemon origin, and the standalone bridge's ./tool-call made absolute.\nfunction transformChatHtmlForBlob(html, url, token){\n var origin = new URL(url).origin;\n var tc = new URL('./tool-call', url);\n tc.searchParams.set('et', token);\n // The app-visible query for the blob doc: the deep link's search minus the\n // embed token (the token must not ride the app's visible params). The\n // blob document's own location has no params, so this is the ONLY channel\n // for ?session= / ?embed=1 — the app reads it via bootSearchParams().\n var bootUrl = new URL(url);\n bootUrl.searchParams.delete('et');\n var bootQuery = bootUrl.search;\n var shim = '<script>(function(){'\n + 'try { window.parent.postMessage({ type: ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)} }, \"*\"); } catch (_) {}'\n + 'var ORIGIN = ' + JSON.stringify(origin) + ', TOKEN = ' + JSON.stringify(token) + ';'\n + 'window.__AGENTPROTO_BASEURL__ = ORIGIN;'\n + 'var _h = window.history, _rs = _h.replaceState.bind(_h), _ps = _h.pushState.bind(_h);'\n + 'function _sameOrLoc(u){'\n + 'try {'\n + 'var x = new URL(u == null ? window.location.href : u, document.baseURI);'\n + 'return x.origin === window.location.origin ? u : window.location.href;'\n + '} catch (_) { return window.location.href; }'\n + '}'\n + '_h.replaceState = function(s, t, u){'\n + 'try { _rs(s, t, _sameOrLoc(u)); } catch (_) { try { _rs(s, t, window.location.href); } catch (_) {} }'\n + '};'\n + '_h.pushState = function(s, t, u){'\n + 'try { _ps(s, t, _sameOrLoc(u)); } catch (_) { try { _ps(s, t, window.location.href); } catch (_) {} }'\n + '};'\n + 'window.__AGENTPROTO_BOOT_QUERY__ = ' + JSON.stringify(bootQuery) + ';'\n + 'function withEt(u){'\n + 'try {'\n + 'var x = new URL(String(u), document.baseURI);'\n + 'if (x.origin !== ORIGIN) return null;'\n + 'if (!x.searchParams.has(\"et\")) x.searchParams.set(\"et\", TOKEN);'\n + 'return x.href;'\n + '} catch (_) { return null; }'\n + '}'\n + 'var _fetch = window.fetch;'\n + 'if (typeof _fetch === \"function\") {'\n + 'window.fetch = function(input, init){'\n + 'var r;'\n + 'if (typeof Request !== \"undefined\" && input instanceof Request) {'\n + 'r = withEt(input.url);'\n + 'if (r) { try { input = new Request(r, input); } catch (_) {} }'\n + '} else {'\n + 'r = withEt(input);'\n + 'if (r) input = r;'\n + '}'\n + 'return _fetch.call(window, input, init);'\n + '};'\n + '}'\n + 'var _ES = window.EventSource;'\n + 'if (typeof _ES === \"function\") {'\n + 'window.EventSource = function(u, cfg){ return new _ES(withEt(u) || u, cfg); };'\n + 'window.EventSource.prototype = _ES.prototype;'\n + '}'\n + '})();<\\\\/script>';\n var head = '<base href=\"' + _blobEscapeAttr(url) + '\">' + shim;\n var m = html.match(/<head[^>]*>/i);\n var out = m\n ? html.slice(0, m.index + m[0].length) + head + html.slice(m.index + m[0].length)\n : head + html;\n return out.split('fetch(\"./tool-call\"').join('fetch(' + JSON.stringify(tc.href));\n}`\n}\n","/**\n * HTML bundles for the session-chat builtin widget — the thin bridge page\n * (`sessionChatEmbedHtml`) and the not-installed notice\n * (`SESSION_CHAT_FALLBACK_HTML`) served as the\n * `ui://agentproto_session_chat/view` resource by `registerMcpApps` (see\n * ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via the\n * `agentproto_session_chat` tool defined in ./index.ts, AND bound to\n * `agent_start` (runtime agent-tools.ts `_meta.ui.resourceUri`) so a launch\n * card opens the chat of the session it just spawned.\n *\n * Deliberately NOT a chat UI reimplementation: the real chat lives in the\n * installed `@agentik/session-chat` app, served by the daemon's standalone\n * app host at `GET /apps/@agentik/session-chat/ui`. When the app is\n * installed, this page is a full-viewport iframe of that URL deep-linked\n * with `?session=<id>&embed=1` — the exact spelling the daemon's standalone\n * route relaxes its `frame-ancestors 'none'` for (see runtime\n * http-server.ts handleAppUiPage) and the VS Code chat-panel webview\n * already uses. No second chat bundle exists anywhere in agentproto/ts.\n *\n * Self-bootstrapping (same pattern as live-session/panel.ts): the static\n * ui:// resource is rendered ONCE at server-registration time with EMPTY\n * initData (mcp-apps-adapter.ts registerMcpApps), so the deep link can't\n * be baked in. The page resolves it at runtime, in priority order:\n * 1. `initData.url` when the html was rendered with a real tool output\n * (tests, and any host that re-renders per call) — iframe is inline.\n * 2. The host's `ui/notifications/tool-result` push of the tool call that\n * mounted the widget. An `agent_start` result IS the spawned session's\n * descriptor (`{ id: \"sess_…\", … }`) → the widget calls\n * `agentproto_session_chat({ sessionId })` over the bridge to turn that\n * id into the deep link; an `agentproto_session_chat` result already\n * carries `{ installed, url }` and mounts directly.\n * 3. Otherwise (opened bare from the catalog) `agentproto_session_chat({})`\n * → the app's session picker.\n * Both orders of (2) vs. bridge init are handled: a result that lands\n * before `initBridge()` resolves is held and applied once it does.\n *\n * Mount order (see `mount()` below):\n * 1. Blob pass-through, when a per-boot embed token is baked in (i.e. an\n * MCP-Apps host rendered this resource): fetch the chat html with the\n * token, rewrite it (./blob-embed.ts `transformChatHtmlForBlob`) and\n * mount it as a `blob:` document. Claude Desktop / Codex widget frames\n * run `frame-src 'self' blob: data:` and do not merge our declared\n * `csp.frameDomains`, so a direct daemon iframe is refused host-side —\n * `blob:` is the pass-through. The blob document must post a boot ack\n * within a settle window (`armBlobBootProbe`); a missing ack (host CSP\n * killed its inline scripts, or refused the blob frame) tears it down\n * and falls through to 2.\n * 2. Direct-src iframe of the deep link (VS Code HTTP-iframe panels, a\n * standalone tab — no token, and their origin already passes the\n * daemon's checks; or the blob path's fallback). If the host refuses\n * the frame, `armBlockProbe` removes it and the launcher card becomes\n * the interactive surface.\n *\n * A persistent header with the deep link stays visible above the iframe, so\n * if the frame is refused (older daemon without the embed relaxation, or a\n * host whose own CSP blocks daemon frames) the user still has a one-click\n * path to the chat in a tab. When the app is NOT installed the page is a\n * small readable notice pointing at `agentproto app install` — the\n * fallback branch, never a vendored UI.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\nimport { BLOB_BOOT_MESSAGE_TYPE, blobEmbedScript } from \"./blob-embed.js\"\nimport type { SessionChatOutput } from \"./index.js\"\n\n/** Only `&` and `\"` are significant inside a double-quoted attribute value. */\nfunction escapeAttr(text: string): string {\n return text.replace(/&/g, \"&\").replace(/\"/g, \""\")\n}\n\nexport function sessionChatEmbedHtml(initData: Partial<SessionChatOutput>): string {\n const url = typeof initData.url === \"string\" && initData.url ? initData.url : null\n const notInstalled = initData.installed === false\n const storeUrl =\n typeof initData.storeUrl === \"string\" && initData.storeUrl\n ? initData.storeUrl\n : \"/store?install=%40agentik%2Fsession-chat\"\n const link =\n url != null\n ? `<a href=\"${escapeAttr(url)}\" target=\"_blank\" rel=\"noreferrer\">open in a tab</a>`\n : notInstalled\n ? \"app not installed\"\n : \"connecting…\"\n const iframe =\n url != null\n // No inline src: the url mounts through the bridge script below, via\n // withEmbedToken(), so the frame never loads without the per-boot\n // embed proof — whichever render path (static resource or a host\n // re-render with a real initData) produced it.\n ? `<iframe id=\"chat\" title=\"Session Chat\"></iframe>`\n : \"\"\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Session Chat</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3;overflow:hidden}\n#bar{padding:6px 12px;font-size:12px;color:#8b949e;background:#161b22;border-bottom:1px solid #30363d}\n#bar a{color:#58a6ff}\n#mode{color:#6e7681}\n#stage{position:relative;height:calc(100% - 29px)}\n#chat{position:absolute;inset:0;z-index:2;border:0;display:block;width:100%;height:100%}\n#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}\n#card h2{font-size:15px;font-weight:600}\n#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}\n#card p{font-size:12px;color:#8b949e;max-width:420px;line-height:1.5}\n#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}\n#card a:hover{border-color:#58a6ff}\n#notice{display:none;max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}\n#notice.show{display:block}\n#notice h1{font-size:18px;margin-bottom:12px}\n#notice code{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}\n#notice p{margin:8px 0;color:#8b949e;font-size:13px}\n</style>\n</head>\n<body>\n<div id=\"bar\">Session Chat · <span id=\"link\">${link}</span><span id=\"mode\"></span></div>\n<div id=\"stage\">${iframe}<div id=\"card\">\n <h2>Session Chat</h2>\n <div id=\"card-session\" class=\"sid\">—</div>\n <p>Full embed isn't supported by this host — the chat opens in a browser tab.</p>\n <a id=\"card-open\" href=\"#\" target=\"_blank\" rel=\"noreferrer\">Open chat</a>\n</div><div id=\"notice\"${notInstalled ? ' class=\"show\"' : \"\"}>\n <h1>Session Chat is not installed</h1>\n <p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app —\n it does not bundle the chat UI itself.</p>\n <p>To use it, install the app into this daemon:</p>\n <p><code>agentproto app install @agentik/session-chat</code></p>\n <p>or open <a href=\"${escapeAttr(storeUrl)}\" target=\"_blank\" rel=\"noreferrer\">the App Store</a>\n (the daemon's <code>/store</code> panel, with the entry pre-selected).</p>\n</div></div>\n<script>\nwindow.__APP_INIT__ = ${JSON.stringify(initData)};\n\n${panelBridgeScript(\"agentproto-session-chat\")}\n\n${blobEmbedScript()}\n\n// ── Session-chat launcher logic ──────────────────────────────────────────\nvar mountedUrl = (window.__APP_INIT__ && window.__APP_INIT__.url) || null;\nvar pinnedSessionId = null; // session named by the host's tool-result push\nvar pendingUrl = null; // url pushed by the host before the bridge was up\nvar pendingNotInstalled = false;\nvar bridged = false;\nvar cardUrl = null; // deep link once resolved (drives the card button)\nvar cardSessionEl = document.getElementById('card-session');\nvar cardOpenEl = document.getElementById('card-open');\nvar mountGen = 0; // bumps per mount(); async work checks it before touching the DOM\nvar blobUrl = null; // live object url of the blob-mounted chat, if any\nvar blobBootTimer = null;\nvar blobBootOk = false;\n\nfunction escapeHtml(text) {\n return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/\"/g, '"');\n}\n\nfunction setMode(text) {\n document.getElementById('mode').textContent = text ? ' \\\\u00b7 ' + text : '';\n // The chat app is live inside the frame — the launcher bar (title, deep\n // link, mount mode) is noise on top of the real surface. Hide it and let\n // the frame own the full height. Only a failed mount (launcher card /\n // not-installed notice) shows the bar again.\n var bar = document.getElementById('bar');\n var stage = document.getElementById('stage');\n if (bar && stage) {\n bar.style.display = text ? 'none' : '';\n stage.style.height = text ? '100%' : 'calc(100% - 29px)';\n }\n}\n\n// One fresh #chat frame on the stage (over the card), the previous one gone.\nfunction newFrame() {\n var stage = document.getElementById('stage');\n var old = document.getElementById('chat');\n if (old) old.remove();\n var frame = document.createElement('iframe');\n frame.id = 'chat';\n frame.title = 'Session Chat';\n stage.insertBefore(frame, stage.firstChild);\n return frame;\n}\n\nfunction revokeBlob() {\n if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }\n if (blobUrl) {\n try { URL.revokeObjectURL(blobUrl); } catch (_) {}\n blobUrl = null;\n }\n}\n\nfunction mount(url) {\n if (!url || url === mountedUrl) return;\n mountedUrl = url;\n var gen = ++mountGen;\n revokeBlob();\n document.getElementById('notice').className = '';\n setCardUrl(url);\n document.getElementById('link').innerHTML =\n '<a href=\"' + escapeHtml(url) + '\" target=\"_blank\" rel=\"noreferrer\">open in a tab</a>';\n var tokened = withEmbedToken(url);\n if (tokened === url) {\n // No per-boot token baked in (standalone tab, VS Code HTTP-iframe panel):\n // the direct frame already passes the daemon's origin checks, and the\n // blob path's daemon calls would have nothing to carry — skip it.\n mountDirect(url);\n return;\n }\n mountBlob(url, tokened, gen).catch(function (err) {\n if (gen !== mountGen) return;\n // The fetch itself was refused (host connect-src not merging our\n // connectDomains, PNA, daemon down, …) — probe answered: no blob path.\n console.warn('[session-chat] blob mount failed, falling back to a direct frame:',\n err && err.message ? err.message : String(err));\n mountDirect(url);\n });\n}\n\n// Direct-src iframe of the deep link, with the block probe → launcher card\n// fallback. The terminal mount path when the blob one is unavailable.\nfunction mountDirect(url) {\n var frame = newFrame();\n frame.src = withEmbedToken(url);\n setMode('direct frame');\n armBlockProbe(frame);\n}\n\n// Blob pass-through: fetch the chat html through the embed token, rewrite it\n// for life as a blob: document (transformChatHtmlForBlob, blob-embed.ts) and\n// mount it — blob: passes a host frame-src that refuses the daemon origin.\nfunction mountBlob(url, tokened, gen) {\n return fetch(tokened, { credentials: 'omit' }).then(function (res) {\n if (!res.ok) throw new Error('HTTP ' + res.status);\n return res.text();\n }).then(function (html) {\n if (gen !== mountGen) return;\n var doc = transformChatHtmlForBlob(html, url, window.__AGENPROTO_EMBED_TOKEN__);\n blobUrl = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));\n var frame = newFrame();\n armBlobBootProbe(frame, url, gen);\n frame.src = blobUrl;\n setMode('embedded');\n });\n}\n\n// The blob document's shim posts a boot ack as its first statement. No ack\n// within the settle window means the host refused the blob frame or its\n// inherited CSP killed the document's inline scripts — surface that through\n// the direct-frame path (whose own probe ends on the launcher card) rather\n// than leaving a silent blank.\nwindow.addEventListener('message', function (evt) {\n var d = evt.data;\n if (!d || typeof d !== 'object' || d.type !== ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)}) return;\n var frame = document.getElementById('chat');\n if (!frame || evt.source !== frame.contentWindow) return;\n blobBootOk = true;\n if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }\n});\n\nfunction armBlobBootProbe(frame, url, gen) {\n blobBootOk = false;\n blobBootTimer = setTimeout(function () {\n blobBootTimer = null;\n if (gen !== mountGen || blobBootOk) return;\n console.warn('[session-chat] blob document never booted (host CSP?), falling back to a direct frame');\n revokeBlob();\n frame.remove();\n mountDirect(url);\n }, 5000);\n}\n\nfunction showNotInstalled() {\n var old = document.getElementById('chat');\n if (old) old.remove();\n revokeBlob();\n setMode('');\n mountedUrl = null;\n document.getElementById('card').style.display = 'none';\n document.getElementById('notice').className = 'show';\n document.getElementById('link').textContent = 'app not installed';\n}\n\n// The body of an ext-apps tool-result notification, whichever wrapping\n// shape the host uses (params IS the CallToolResult, or nests it under\n// params.result). null for error results / non-JSON text.\nfunction parseToolResultBody(params) {\n if (!params || typeof params !== 'object') return null;\n var res = (params.result && typeof params.result === 'object') ? params.result : params;\n if (res.isError) return null;\n var content = Array.isArray(res.content) ? res.content : [];\n var item = content[0];\n if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;\n try { return JSON.parse(item.text); } catch (_) { return null; }\n}\n\n// INLINED COPY of live-session/logic.ts extractToolResultSessionId — keep\n// in sync. \\`sessionId\\` (an agentproto_session_chat / live_session result)\n// wins over \\`id\\` (an agent_start result: the spawned session descriptor).\nfunction extractToolResultSessionId(params) {\n var body = parseToolResultBody(params);\n if (!body) return null;\n if (typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;\n if (typeof body.id === 'string' && body.id) return body.id;\n return null;\n}\n\n// ── Embed-token refresh ──────────────────────────────────────────────────\n// The token baked into this page by registerMcpApps dies with the daemon\n// boot that minted it (runtime embed-tokens.ts), but hosts cache the\n// rendered widget: Claude Desktop keeps a conversation's srcdoc, so after\n// any daemon restart the cached page replays a dead token and every daemon\n// call 403s — the blob fetch fails, the direct frame is refused, and the\n// user is left on the launcher card forever. Every tool result carries a\n// live token (SessionChatOutput.embedToken), so adopt it before mounting.\n//\n// Only a render that WAS baked refreshes. A standalone tab or VS Code\n// HTTP-iframe panel still holds the literal placeholder and passes the\n// daemon's origin checks on its own; handing it a token would switch it\n// onto the blob path for no reason.\nfunction adoptEmbedToken(body) {\n var cur = window.__AGENPROTO_EMBED_TOKEN__;\n if (!cur || cur === '__AGENPROTO_EMBED_TOKEN__') return;\n if (!body || typeof body.embedToken !== 'string' || !body.embedToken) return;\n window.__AGENPROTO_EMBED_TOKEN__ = body.embedToken;\n}\n\n// Turn a session id into the installed app's deep link over the bridge.\nfunction resolveSession(sessionId) {\n noteSession(sessionId);\n return callTool('agentproto_session_chat', sessionId ? { sessionId: sessionId } : {})\n .then(function(out) {\n adoptEmbedToken(out);\n if (out && out.installed && typeof out.url === 'string' && out.url) mount(out.url);\n else showNotInstalled();\n })\n .catch(function(err) {\n document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));\n });\n}\n\n// ── Launcher card (the frame-blocked fallback UI) ────────────────────────\n// The iframe mounts OVER the card. When the host's own CSP refuses the\n// frame (Claude Desktop / Codex widget frames run frame-src 'self' blob:\n// data: and do not yet merge csp.frameDomains), the frame stays on a\n// same-origin about:blank — readable, unlike the loaded cross-origin chat —\n// and is removed after a settle window so the card becomes the interactive\n// surface.\n//\n// The anchor carries target=\"_blank\" rel=\"noreferrer\" as the fallback path,\n// but that is a plain no-op in hosts that sandbox the widget iframe without\n// allow-popups (observed in Claude Desktop's side chat) — a normal click\n// on such a host does nothing. Where the host advertises the capability\n// (McpUiInitializeResult.hostCapabilities.openLinks in the ext-apps spec,\n// captured by the bridge's getHostCapabilities()), route through the\n// host-mediated ui/open-link request instead, which is not subject to the\n// iframe's popup sandboxing.\ncardOpenEl.addEventListener('click', function (evt) {\n if (!cardUrl) { evt.preventDefault(); return; }\n if (getHostCapabilities() && getHostCapabilities().openLinks) {\n evt.preventDefault();\n openLink(cardUrl).catch(function () { window.open(cardUrl, '_blank'); });\n }\n});\n\nfunction noteSession(id) {\n if (id && cardSessionEl) cardSessionEl.textContent = id;\n}\n\nfunction setCardUrl(u) {\n if (!u) return;\n cardUrl = u;\n cardOpenEl.setAttribute('href', u);\n}\n\n// A blocked frame never leaves about:blank (same-origin with this page —\n// readable); the loaded chat is cross-origin (the probe throws). A readable\n// frame that is STILL blank when the settle window closes = blocked.\nfunction isBlank(frame) {\n try { return frame.contentWindow.location.href === 'about:blank'; } catch (_) { return false; }\n}\n\nfunction armBlockProbe(frame) {\n var settle = null;\n function probe() {\n if (settle) { clearTimeout(settle); settle = null; }\n if (!isBlank(frame)) return;\n // Still on about:blank right now — arm the settle window, but\n // re-check when it closes: a slow (not blocked) load may have landed\n // in the meantime, and only a frame STILL blank after the window gets\n // removed. load firing in between clears this timeout above.\n settle = setTimeout(function () {\n settle = null;\n if (isBlank(frame)) frame.remove();\n }, 2000);\n }\n setTimeout(function () {\n frame.addEventListener('load', probe);\n probe();\n }, 100);\n}\n\n// A url baked at render time (a host that re-renders per call with a real\n// initData) mounts on boot — through the same blob-first path as every\n// other mount, since the same opaque widget context frames it. The\n// pre-rendered #chat frame is replaced by mount()'s own.\nif (mountedUrl) {\n var bootUrl = mountedUrl;\n mountedUrl = null;\n adoptEmbedToken(window.__APP_INIT__);\n mount(bootUrl);\n}\n\n// The host pushes the triggering tool call's result (ext-apps\n// ui/notifications/tool-result). For agent_start that result IS the spawned\n// session's descriptor — pin the widget to THAT session.\nonHostNotification(function(method, params) {\n if (method !== 'ui/notifications/tool-result') return;\n var body = parseToolResultBody(params);\n if (!body) return;\n if (typeof body.url === 'string' && body.url && body.installed !== false) {\n // Adopt now, not at mount time: the token doesn't depend on the bridge.\n adoptEmbedToken(body);\n if (bridged) mount(body.url); else pendingUrl = body.url;\n return;\n }\n if (body.installed === false) {\n if (bridged) showNotInstalled(); else pendingNotInstalled = true;\n return;\n }\n var id = extractToolResultSessionId(params);\n if (!id || id === pinnedSessionId) return;\n pinnedSessionId = id;\n noteSession(id);\n if (bridged) resolveSession(id);\n});\n\ninitBridge().then(function() {\n bridged = true;\n if (pendingUrl) { mount(pendingUrl); pendingUrl = null; return; }\n if (pendingNotInstalled) { showNotInstalled(); return; }\n if (mountedUrl) return; // rendered with a real url\n var init = window.__APP_INIT__ || {};\n if (init.installed === false) { showNotInstalled(); return; }\n // No url baked in (static ui:// render): resolve over the bridge — the\n // pinned session when the host named one, else the app's picker.\n return resolveSession(pinnedSessionId);\n}).catch(function(err) {\n document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));\n});\n</script>\n</body>\n</html>`\n}\n\nexport const SESSION_CHAT_FALLBACK_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Session Chat — not installed</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3}\n#wrap{max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}\nh1{font-size:18px;margin-bottom:12px}\ncode{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}\np{margin:8px 0;color:#8b949e;font-size:13px}\n</style>\n</head>\n<body>\n<div id=\"wrap\">\n <h1>Session Chat is not installed</h1>\n <p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app —\n it does not bundle the chat UI itself.</p>\n <p>To use it, install the app into this daemon:</p>\n <p><code>agentproto app install @agentik/session-chat</code></p>\n <p>or open <a href=\"/store?install=%40agentik%2Fsession-chat\">the App Store</a>\n (the daemon's <code>/store</code> panel, with the entry pre-selected).</p>\n <p>Once installed, call <code>agentproto_session_chat</code> again (optionally with a\n <code>sessionId</code>) and the panel will embed the app's chat UI deep-linked to that\n session.</p>\n</div>\n</body>\n</html>`\n"]}
|