@agentproto/apps 0.19.0 → 0.20.1

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/index.d.ts CHANGED
@@ -52,6 +52,8 @@ export { makeSessionChatApp, sessionChatApp, sessionChatAppUrl, SESSION_CHAT_APP
52
52
  export type { SessionChatOps, SessionChatInput, SessionChatOutput, } from "./session-chat/index.js";
53
53
  export { makeWorkBoardApp, workBoardApp } from "./work-board/index.js";
54
54
  export type { WorkBoardOps, WorkBoardInput, WorkBoardOutput } from "./work-board/index.js";
55
+ export { makeStoreApp, storeApp } from "./store/index.js";
56
+ export type { StoreOps, StoreInput, StoreOutput } from "./store/index.js";
55
57
  export { makeReviewPanelApp, reviewPanelApp } from "./review-panel/index.js";
56
58
  export type { ReviewPanelOps, ReviewPanelInput, ReviewPanelOutput } from "./review-panel/index.js";
57
59
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD,YAAY,EACV,UAAU,EACV,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,UAAU,GACX,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAClF,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAChG,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AACrF,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AAC9G,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AACrF,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AAC9G,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AACpF,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AACtG,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAC5E,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAClG,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,yBAAyB,CAAA;AAChC,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACtE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC1F,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAC5E,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD,YAAY,EACV,UAAU,EACV,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,UAAU,GACX,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAClF,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAChG,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AACrF,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AAC9G,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AACrF,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AAC9G,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AACpF,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AACtG,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAC5E,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAClG,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,yBAAyB,CAAA;AAChC,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACtE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC1F,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AACzD,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACzE,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAC5E,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA"}
package/dist/index.mjs CHANGED
@@ -6616,7 +6616,7 @@ var SESSION_STORY_PANEL_HTML = `<!doctype html>
6616
6616
  <div class="h2" id="heroSub"></div>
6617
6617
  </div>
6618
6618
  <div class="modewrap"><button id="modeSimple" class="on" type="button">Simple</button><button id="modeTech" type="button">Tech</button></div>
6619
- <a class="sim" id="fullPanelLink" href="#" target="_blank" rel="noopener" title="Ouvrir le panneau complet (Terminal/Chat/JSON/TTY)">&#8599; panneau complet</a>
6619
+ <a class="sim hidden" id="fullPanelLink" href="#" target="_blank" rel="noopener" title="Ouvrir la session en direct (live-session du daemon)">&#8599; panneau complet</a>
6620
6620
  <button class="sim" id="switchBtn" type="button">&#8646; changer</button>
6621
6621
  </div>
6622
6622
  <div class="plan" id="plan"></div>
@@ -6976,7 +6976,13 @@ function openSession(id){
6976
6976
  lastSeenOutputAt=null;
6977
6977
  $('pickerScreen').classList.add('hidden');
6978
6978
  $('storyScreen').classList.remove('hidden');
6979
- $('fullPanelLink').href='https://cli.agentproto.sh/panel?session='+encodeURIComponent(id);
6979
+ // Deep-link to the daemon's own live-session panel (GET /apps/:appId/ui
6980
+ // pins it via ?sessionId=). Only resolvable when this panel is itself
6981
+ // served standalone by the daemon; inside an MCP host iframe the origin
6982
+ // isn't the daemon's, so the link stays hidden rather than dead.
6983
+ var daemonOrigin=(location.pathname.indexOf('/apps/')===0&&location.origin&&location.origin!=='null')?location.origin:'';
6984
+ $('fullPanelLink').href=daemonOrigin?daemonOrigin+'/apps/@agentproto/live-session/ui?sessionId='+encodeURIComponent(id):'#';
6985
+ $('fullPanelLink').classList.toggle('hidden',!daemonOrigin);
6980
6986
  closePanel();
6981
6987
  loadStory().then(renderAll);
6982
6988
  }
@@ -8233,6 +8239,7 @@ function escapeAttr(text) {
8233
8239
  function sessionChatEmbedHtml(initData) {
8234
8240
  const url = typeof initData.url === "string" && initData.url ? initData.url : null;
8235
8241
  const notInstalled = initData.installed === false;
8242
+ const storeUrl = typeof initData.storeUrl === "string" && initData.storeUrl ? initData.storeUrl : "/store?install=%40agentik%2Fsession-chat";
8236
8243
  const link = url != null ? `<a href="${escapeAttr(url)}" target="_blank" rel="noreferrer">open in a tab</a>` : notInstalled ? "app not installed" : "connecting\u2026";
8237
8244
  const iframe = url != null ? `<iframe id="chat" title="Session Chat"></iframe>` : "";
8238
8245
  return `<!DOCTYPE html>
@@ -8275,6 +8282,8 @@ html,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:
8275
8282
  it does not bundle the chat UI itself.</p>
8276
8283
  <p>To use it, install the app into this daemon:</p>
8277
8284
  <p><code>agentproto app install @agentik/session-chat</code></p>
8285
+ <p>or open <a href="${escapeAttr(storeUrl)}" target="_blank" rel="noreferrer">the App Store</a>
8286
+ (the daemon's <code>/store</code> panel, with the entry pre-selected).</p>
8278
8287
  </div></div>
8279
8288
  <script>
8280
8289
  window.__APP_INIT__ = ${JSON.stringify(initData)};
@@ -8619,6 +8628,8 @@ p{margin:8px 0;color:#8b949e;font-size:13px}
8619
8628
  it does not bundle the chat UI itself.</p>
8620
8629
  <p>To use it, install the app into this daemon:</p>
8621
8630
  <p><code>agentproto app install @agentik/session-chat</code></p>
8631
+ <p>or open <a href="/store?install=%40agentik%2Fsession-chat">the App Store</a>
8632
+ (the daemon's <code>/store</code> panel, with the entry pre-selected).</p>
8622
8633
  <p>Once installed, call <code>agentproto_session_chat</code> again (optionally with a
8623
8634
  <code>sessionId</code>) and the panel will embed the app's chat UI deep-linked to that
8624
8635
  session.</p>
@@ -8640,6 +8651,9 @@ function sessionChatAppUrl(httpBaseUrl, sessionId) {
8640
8651
  params.set("embed", "1");
8641
8652
  return `${base}?${params.toString()}`;
8642
8653
  }
8654
+ function sessionChatStoreUrl(httpBaseUrl) {
8655
+ return `${httpBaseUrl.replace(/\/+$/, "")}/store?install=${encodeURIComponent(SESSION_CHAT_APP_ID)}`;
8656
+ }
8643
8657
  function daemonOrigin(httpBaseUrl) {
8644
8658
  return new URL(httpBaseUrl).origin;
8645
8659
  }
@@ -8651,7 +8665,7 @@ function makeSessionChatApp(ops) {
8651
8665
  description: "Open the Session Chat app \u2014 a chat-thread view of any agentproto daemon session: full transcript replay, live follow-over SSE, and a composer that sends prompts into the same session. Requires the `@agentik/session-chat` app to be installed; without it the panel shows install instructions. Pass `sessionId` to deep-link straight into a known session.",
8652
8666
  inputSchema: sessionChatInputSchema,
8653
8667
  execute: async (input) => {
8654
- if (!installed) return { installed, url: null };
8668
+ if (!installed) return { installed, url: null, storeUrl: sessionChatStoreUrl(ops.httpBaseUrl) };
8655
8669
  const token = ops.mintEmbedToken?.();
8656
8670
  return {
8657
8671
  installed,
@@ -9620,6 +9634,65 @@ var workBoardApp = defineApp({
9620
9634
  }
9621
9635
  });
9622
9636
 
9637
+ // src/store/panel.generated.ts
9638
+ var STORE_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 app store</title>\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\n vite.config.ts\'s injectPanelBridge plugin \u2014 a classic (non-module)\n script so its `initBridge`/`callTool` land as globals before the\n deferred module script below runs. -->\n<script>// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500\n// JSON-RPC 2.0 over window.parent.postMessage \xB7 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// \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \u2014 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 \u2014\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 \u2014 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-store-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) \u2014 capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 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// \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500\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 \u2014 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 \u2014 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 \u2014 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 \u2014\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 \u2014 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/\'/g, "&#39;");\n }\n function text(value, fallback) {\n return value && value.trim() !== "" ? esc(value) : esc(fallback);\n }\n function chips(row) {\n const parts = [];\n if (row.tier) parts.push(`<span class="chip">${esc(row.tier)}</span>`);\n if (row.placement) parts.push(`<span class="chip">placement: ${esc(row.placement)}</span>`);\n if (row.requires?.agentprotoVersion) {\n parts.push(`<span class="chip">requires ${esc(row.requires.agentprotoVersion)}</span>`);\n }\n if (row.requires?.apps && row.requires.apps.length > 0) {\n parts.push(`<span class="chip">requires apps: ${esc(row.requires.apps.join(", "))}</span>`);\n }\n if (row.requires?.agents && row.requires.agents.length > 0) {\n parts.push(`<span class="chip">requires agents: ${esc(row.requires.agents.join(", "))}</span>`);\n }\n return parts.join("");\n }\n function installButton(catalogRow) {\n return `<button class="sbtn store-install-btn" data-appid="${esc(catalogRow.appId)}" data-decision="install">Install</button>`;\n }\n function renderCatalogFeatured(rows) {\n const featured = rows.filter((r) => r.featured === true);\n if (featured.length === 0) return "";\n return featured.map(\n (r) => `<div class="card store-card-featured" id="store-entry-${esc(r.appId)}">\n <div class="card-title">${text(r.name, r.appId)}</div>\n <div class="card-desc">${text(r.description, "")}</div>\n <div class="card-chips">${chips(r)}</div>\n <div class="card-actions">${installButton(r)}</div>\n </div>`\n ).join("");\n }\n function renderCatalogAvailable(rows) {\n const byCategory = /* @__PURE__ */ new Map();\n for (const r of rows) {\n if (r.installed) continue;\n if (r.featured === true) continue;\n const key = r.category && r.category !== "" ? r.category : "Other";\n const list = byCategory.get(key);\n if (list) list.push(r);\n else byCategory.set(key, [r]);\n }\n let out = "";\n for (const [category, list] of byCategory) {\n out = out + `<h3 class="store-category-title">${esc(category)}</h3>`;\n for (const r of list) {\n out = out + `<div class="card store-card-available" id="store-entry-${esc(r.appId)}">\n <div class="card-title">${text(r.name, r.appId)}</div>\n <div class="card-desc">${text(r.description, "")}</div>\n <div class="card-chips">${chips(r)}</div>\n <div class="card-actions">${installButton(r)}</div>\n </div>`;\n }\n }\n return out;\n }\n function renderInstalled(installed, catalogRows, updates) {\n const updateIds = new Set(updates.map((u) => u.appId));\n const byId = new Map(catalogRows.map((c) => [c.appId, c]));\n if (installed.length === 0) return "";\n return installed.map((app) => {\n const version = app.version ?? byId.get(app.appId)?.version ?? "";\n const source = app.source?.kind ?? "local";\n const hasUi = byId.get(app.appId)?.hasUi === true || app.source?.kind === "local" && app.dir !== void 0;\n const actions = [\n `<button class="sbtn store-resync-btn" data-appid="${esc(app.appId)}" data-decision="resync">Update</button>`,\n `<button class="sbtn store-uninstall-btn" data-appid="${esc(app.appId)}" data-decision="uninstall">Uninstall</button>`,\n ...hasUi ? [\n `<button class="sbtn store-open-btn" data-appid="${esc(app.appId)}" data-decision="open">Open</button>`\n ] : []\n ];\n return `<div class="card store-card-installed" id="store-entry-${esc(app.appId)}">\n <div class="card-title">${text(app.name, app.appId)}</div>\n <div class="card-actions-meta"><span class="card-version">${text(version, "")}</span><span class="card-source">${esc(source)}</span>${updateIds.has(app.appId) ? `<span class="badge store-update-badge">Update available</span>` : ""}</div>\n <div class="card-actions">${actions.join("")}</div>\n </div>`;\n }).join("");\n }\n function renderSourcesHeader(snapshot2) {\n if (!snapshot2.warnings || snapshot2.warnings.length === 0) return "";\n return `<div class="store-warnings">${snapshot2.warnings.map((w) => `<p class="store-warning">${esc(w)}</p>`).join("")}</div>`;\n }\n function renderBuiltins(builtinNames) {\n if (builtinNames.length === 0) return "";\n const details = `<details class="store-builtins"><summary>Builtin panels</summary><ul class="store-builtin-list">${builtinNames.map((n) => `<li class="store-builtin-item">${esc(n)}</li>`).join("")}</ul></details>`;\n return details;\n }\n function isAgentappUrl(url) {\n return url.replace(/\\?.*$/, "").replace(/#.*$/, "").toLowerCase().endsWith(".agentapp");\n }\n function storeEmpty(catalogRows, installed) {\n if (catalogRows.length > 0 || installed.length > 0) return "";\n return `<div class="store-empty">\n <p class="store-empty-title">No apps installed, and the app catalog is empty.</p>\n <p class="store-empty-hint">Add a <code>catalog.sources</code> entry to your daemon config, drop an <code>app-catalog.json</code> next to it, or install an app directly from a URL in the form below.</p>\n </div>`;\n }\n function render(snapshot2) {\n const installed = renderInstalled(snapshot2.installed, snapshot2.catalog, snapshot2.updates);\n const featured = renderCatalogFeatured(snapshot2.catalog);\n const available = renderCatalogAvailable(snapshot2.catalog);\n const builtins = renderBuiltins(["sessions-panel", "agents-overview", "bureau-sessions", "session-story", "live-session", "work-board"]);\n const sections = [];\n sections.push(renderSourcesHeader(snapshot2));\n if (installed !== "") {\n sections.push(`<h2 class="store-section-title">Installed</h2>`, `<div class="grill">${installed}</div>`);\n }\n if (featured !== "") {\n sections.push(`<h2 class="store-section-title">Featured</h2>`, featured);\n }\n if (available !== "") {\n sections.push(`<h2 class="store-section-title">Available</h2>`, `<div class="grill">${available}</div>`);\n }\n if (installed === "" && featured === "" && available === "") {\n sections.push(storeEmpty(snapshot2.catalog, snapshot2.installed));\n }\n sections.push(builtins);\n return sections.join("\\n");\n }\n function getEl(id) {\n const el = document.getElementById(id);\n if (!el) throw new Error(`app store: missing #${id}`);\n return el;\n }\n let snapshot = { catalog: [], installed: [], updates: [] };\n function setStatus(msg) {\n getEl("statusbar").textContent = msg;\n }\n function sendInstall(payload) {\n return callTool(\n "app_install",\n payload\n );\n }\n function runInstallConfirm(payload, ask) {\n sendInstall(payload).then((result) => {\n if ("needsConfirmation" in result && result.needsConfirmation === true) {\n if (!ask(result)) return;\n callTool("app_install", {\n ...payload,\n confirm: result.confirm\n }).then(() => {\n setStatus(`Installed ${String(payload.url)}`);\n return refresh();\n }).catch((e) => setStatus(`Install failed: ${e.message}`));\n } else {\n setStatus(`Installed ${result.appId}`);\n return refresh();\n }\n }).catch((e) => setStatus(`Install failed: ${e.message}`));\n }\n function buildInstallPayload(entry) {\n const source = entry.source;\n if (!source || typeof source !== "object") return void 0;\n const s = source;\n const payload = { url: s.url };\n if (s.kind === "agentapp") {\n payload.sha256 = s.sha256;\n } else {\n if (s.ref) payload.ref = s.ref;\n if (s.subdir) payload.subdir = s.subdir;\n if (s.sha) payload.sha = s.sha;\n payload.allowBuild = false;\n }\n if (entry.catalogUrl) payload.catalogUrl = entry.catalogUrl;\n return payload;\n }\n function describePreview(preview) {\n const sha = preview.kind === "agentapp" ? preview.sha256 ? `sha256 ${preview.sha256}` : "no pinned digest" : preview.sha ? `commit ${preview.sha}` : "no pinned commit";\n const buildLine = preview.runsBuildCommand ? "It WILL run the app\'s ui.build command." : "It will not run any build command.";\n return `Install ${preview.url}? (${preview.kind}; ${sha})\n\n${buildLine}\n\nConfirm to proceed.`;\n }\n function armInstall(entry) {\n const payload = buildInstallPayload(entry);\n if (!payload) {\n setStatus("Catalog entry has no remote source \u2014 cannot install.");\n return;\n }\n runInstallConfirm(payload, (preview) => window.confirm(describePreview(preview)));\n }\n function installFromUrl(url, sha256, ref, subdir) {\n const payload = { url };\n if (isAgentappUrl(url)) {\n if (sha256) payload.sha256 = sha256;\n } else {\n if (ref) payload.ref = ref;\n if (subdir) payload.subdir = subdir;\n payload.allowBuild = false;\n }\n runInstallConfirm(payload, (preview) => window.confirm(describePreview(preview)));\n }\n function resync(appId) {\n callTool("app_resync", { appId }).then((result) => {\n setStatus(result.changed ? `${appId} updated` : `${appId} already up to date`);\n return refresh();\n }).catch((e) => setStatus(`Update failed: ${e.message}`));\n }\n function uninstall(appId) {\n if (!window.confirm(`Uninstall ${appId}? Its data directory is kept.`)) return;\n callTool("app_uninstall", { appId }).then(() => {\n setStatus(`Uninstalled ${appId}`);\n return refresh();\n }).catch((e) => setStatus(`Uninstall failed: ${e.message}`));\n }\n function openUi(appId) {\n const encoded = appId.split("/").map(encodeURIComponent).join("/").replace(/%40/g, "@");\n window.open(`/apps/${encoded}/ui`, "_blank", "noopener");\n }\n let deepLinked = false;\n function deepLinkInstall() {\n if (deepLinked) return;\n const target = new URLSearchParams(window.location.search).get("install");\n if (!target) return;\n deepLinked = true;\n const entry = snapshot.catalog.find((c) => c.appId === target && !c.installed);\n if (!entry) return;\n const el = document.getElementById(`store-entry-${CSS.escape(target)}`);\n if (el && typeof el.scrollIntoView === "function") el.scrollIntoView({ block: "center" });\n armInstall(entry);\n }\n function refresh() {\n return callTool("app_catalog", {}).then((catalogRaw) => {\n const snapshotRaw = catalogRaw ?? {};\n const rows = Array.isArray(snapshotRaw.catalog) ? snapshotRaw.catalog : Array.isArray(catalogRaw) ? catalogRaw : [];\n snapshot.catalog = rows.filter((r) => r && r.appId);\n return callTool("app_list", {});\n }).then((appsRaw) => {\n const snapshotRaw = appsRaw ?? {};\n const rows = Array.isArray(snapshotRaw.installed) ? snapshotRaw.installed : Array.isArray(appsRaw) ? appsRaw : [];\n snapshot.installed = rows.filter((a) => a && a.appId);\n return callTool("app_updates", {});\n }).then((updatesRaw) => {\n const sr = updatesRaw ?? {};\n const rows = Array.isArray(sr.updates) ? sr.updates : Array.isArray(updatesRaw) ? updatesRaw : [];\n snapshot.updates = rows.filter((u) => u && u.appId);\n renderBody();\n deepLinkInstall();\n }).catch((e) => setStatus(`Error: ${e.message}`));\n }\n function renderBody() {\n const content = getEl("content");\n content.innerHTML = render(snapshot);\n }\n function boot() {\n const content = getEl("content");\n const urlBtn = document.getElementById("store-install-from-url-btn");\n const urlInput = document.getElementById("store-url");\n const shaInput = document.getElementById("store-sha256");\n const refInput = document.getElementById("store-ref");\n const subdirInput = document.getElementById("store-subdir");\n if (urlBtn && urlInput) {\n urlBtn.addEventListener("click", () => {\n const url = urlInput.value.trim();\n if (url === "") return;\n installFromUrl(\n url,\n shaInput && shaInput.value.trim() !== "" ? shaInput.value.trim() : void 0,\n refInput && refInput.value.trim() !== "" ? refInput.value.trim() : void 0,\n subdirInput && subdirInput.value.trim() !== "" ? subdirInput.value.trim() : void 0\n );\n });\n }\n content.addEventListener("click", (evt) => {\n if (!(evt.target instanceof Element)) return;\n const btn = evt.target.closest("button[data-decision]");\n if (!(btn instanceof HTMLButtonElement)) return;\n const appId = btn.getAttribute("data-appid");\n if (!appId) return;\n const decision = btn.getAttribute("data-decision");\n if (decision === "install") {\n const entry = snapshot.catalog.find((c) => c.appId === appId);\n if (entry) armInstall(entry);\n } else if (decision === "resync") {\n resync(appId);\n } else if (decision === "uninstall") {\n uninstall(appId);\n } else if (decision === "open") {\n openUi(appId);\n }\n });\n void initBridge().then(() => refresh()).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>/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n The store panel\'s stylesheet \u2014 same two-sheet token mapping as\n work-board\'s (--vscode-* wins when a host injects them, the agentproto\n brand fallbacks otherwise; light/dark split via prefers-color-scheme\n for a standalone tab). See that file\'s header for the full rationale\n \u2014 the --st-* names here are this panel\'s own namespace.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n:root {\n --st-bg: var(--vscode-editor-background, #f5f6f3);\n --st-surface: var(--vscode-sideBar-background, #ffffff);\n --st-ink: var(--vscode-editor-foreground, #171a1e);\n --st-ink-muted: var(--vscode-descriptionForeground, #5c6470);\n --st-border: var(--vscode-panel-border, #dde0da);\n --st-accent: var(--vscode-button-background, #0f62fe);\n --st-ink-on-accent: var(--vscode-button-foreground, #ffffff);\n --st-warn: var(--vscode-charts-yellow, #f0b429);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --st-bg: var(--vscode-editor-background, #101214);\n --st-surface: var(--vscode-sideBar-background, #16191d);\n --st-ink: var(--vscode-editor-foreground, #e8eaed);\n --st-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\n --st-border: var(--vscode-panel-border, #272c33);\n --st-accent: var(--vscode-button-background, #4589ff);\n --st-ink-on-accent: var(--vscode-button-foreground, #ffffff);\n --st-warn: var(--vscode-charts-yellow, #f0b429);\n }\n}\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(--st-bg);\n color: var(--st-ink);\n overflow: hidden;\n}\n\n#app { display: flex; flex-direction: column; height: 100%; }\n\n#store-head {\n padding: 8px 12px;\n border-bottom: 1px solid var(--st-border);\n display: flex;\n align-items: center;\n gap: 8px;\n background: var(--st-surface);\n flex-shrink: 0;\n font-weight: 600;\n}\n\n#content { flex: 1; overflow-y: auto; padding: 12px; }\n\n.store-section-title {\n font-size: 11px;\n font-weight: 600;\n color: var(--st-ink-muted);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 14px 0 8px;\n}\n.store-section-title:first-child { margin-top: 0; }\n\n.store-category-title { font-size: 12px; font-weight: 600; margin: 12px 0 6px; }\n\n.card {\n background: var(--st-surface);\n border: 1px solid var(--st-border);\n border-radius: 6px;\n padding: 8px 10px;\n margin-bottom: 8px;\n}\n.card-title { font-size: 12px; font-weight: 500; margin-bottom: 4px; word-break: break-word; }\n.card-desc { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 6px; }\n.card-version, .card-source {\n font-size: 10px;\n color: var(--st-ink-muted);\n font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace;\n margin-right: 6px;\n}\n.card-chips { margin-bottom: 6px; }\n.card-actions { display: flex; gap: 4px; flex-wrap: wrap; }\n\n.chip {\n display: inline-block;\n padding: 1px 6px;\n border-radius: 10px;\n font-size: 10px;\n font-weight: 600;\n white-space: nowrap;\n background: color-mix(in srgb, var(--st-ink-muted) 15%, transparent);\n color: var(--st-ink-muted);\n margin-right: 4px;\n}\n\n.badge {\n display: inline-block;\n padding: 1px 6px;\n border-radius: 10px;\n font-size: 10px;\n font-weight: 700;\n white-space: nowrap;\n background: color-mix(in srgb, var(--st-warn) 18%, transparent);\n color: var(--st-warn);\n margin-right: 4px;\n}\n\n.sbtn {\n background: var(--st-bg);\n border: 1px solid var(--st-border);\n color: var(--st-ink);\n padding: 3px 8px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 11px;\n font-family: inherit;\n}\n.sbtn:hover { background: var(--st-border); }\n.sbtn.primary { background: var(--st-accent); color: var(--st-ink-on-accent); border-color: var(--st-accent); }\n.sbtn.danger { border-color: var(--st-warn); color: var(--st-warn); }\n\n.store-url-install { display: flex; gap: 8px; margin: 6px 0 12px; }\n.store-url-install input {\n flex: 1;\n min-width: 120px;\n background: var(--st-bg);\n border: 1px solid var(--st-border);\n color: var(--st-ink);\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 12px;\n font-family: inherit;\n}\n\n#confirm-panel {\n border: 1px solid var(--st-accent);\n border-radius: 6px;\n padding: 10px;\n margin-bottom: 12px;\n background: var(--st-surface);\n}\n#confirm-panel h2 { font-size: 13px; margin-bottom: 8px; }\n#confirm-meta { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 8px; }\n#confirm-meta .mono, .mono {\n font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace;\n}\n.build-flag { color: var(--st-warn); font-weight: 600; }\n\n.store-warnings { margin-bottom: 10px; }\n.store-warning {\n font-size: 11px;\n color: var(--st-warn);\n padding: 4px 8px;\n border: 1px solid color-mix(in srgb, var(--st-warn) 40%, transparent);\n border-radius: 4px;\n margin-bottom: 4px;\n word-break: break-word;\n}\n\n.store-empty {\n border: 1px dashed var(--st-border);\n border-radius: 6px;\n padding: 16px;\n margin-bottom: 12px;\n}\n.store-empty-title { font-size: 13px; font-weight: 600; margin-bottom: 6px; }\n.store-empty-hint { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 10px; line-height: 1.5; }\n.store-empty code {\n font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace;\n background: color-mix(in srgb, var(--st-ink-muted) 12%, transparent);\n padding: 0 3px;\n border-radius: 3px;\n}\n\n.store-builtins { border-top: 1px solid var(--st-border); padding-top: 8px; margin-top: 12px; }\n.store-builtins summary {\n font-size: 11px;\n font-weight: 600;\n color: var(--st-ink-muted);\n cursor: pointer;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n.store-builtin-list { margin: 8px 0 0 16px; font-size: 11px; color: var(--st-ink-muted); }\n\n#statusbar {\n padding: 4px 12px;\n font-size: 11px;\n color: var(--st-ink-muted);\n border-top: 1px solid var(--st-border);\n background: var(--st-surface);\n flex-shrink: 0;\n}</style>\n</head>\n<body>\n<div id="app">\n <div id="store-head"><span>App Store</span></div>\n <div id="confirm-panel" hidden></div>\n <div id="content"></div>\n <div id="url-install-static">\n <div class="store-url-install">\n <input id="store-url" type="text" placeholder="https://example.com/my-app.agentapp or https://github.com/user/repo" />\n <input id="store-sha256" type="text" placeholder="sha256 (.agentapp, optional)" />\n <input id="store-ref" type="text" placeholder="git ref (optional)" />\n <input id="store-subdir" type="text" placeholder="git subdir (optional)" />\n <button class="sbtn" id="store-install-from-url-btn">Install from URL</button>\n </div>\n </div>\n <div id="statusbar">Connecting to bridge&#8230;</div>\n</div>\n</body>\n</html>\n';
9639
+
9640
+ // src/store/panel.ts
9641
+ var STORE_APP_ID = "@agentproto/store";
9642
+ var STORE_TOOL_ID = "agentproto_store";
9643
+ var STORE_UI_TOOLS = [
9644
+ "app_catalog",
9645
+ "app_list",
9646
+ "app_install",
9647
+ "app_resync",
9648
+ "app_updates",
9649
+ "app_uninstall",
9650
+ "app_status"
9651
+ ];
9652
+ var STORE_HTML = STORE_HTML_GENERATED;
9653
+
9654
+ // src/store/index.ts
9655
+ var storeInputSchema = z.object({
9656
+ catalogUrl: z.string().optional().describe(
9657
+ "Restrict the snapshot to ONE catalog source (`catalog.sources[i].url`). Omit for the merged listing app_catalog itself answers."
9658
+ )
9659
+ });
9660
+ function makeStoreApp(ops) {
9661
+ return {
9662
+ id: STORE_TOOL_ID,
9663
+ title: "App Store",
9664
+ description: "Open the agentproto App Store \u2014 browse/install apps from the daemon's app_catalog, resync and uninstall installed ones. Sections: Installed (with an Update-available badge) / Featured+Available from the catalog / Install-from-URL / Sources / Builtin panels (the `?install=<appId>` deep link pre-opens an entry's install confirmation).",
9665
+ inputSchema: storeInputSchema,
9666
+ execute: async (input) => {
9667
+ const rows = await ops.listCatalog(input.catalogUrl);
9668
+ const warnings = [];
9669
+ const catalog = rows.filter((row) => {
9670
+ const w = row.warnings;
9671
+ if (w) warnings.push(...w);
9672
+ return true;
9673
+ });
9674
+ return {
9675
+ catalog,
9676
+ installed: ops.listInstalled(),
9677
+ updates: ops.listUpdates(),
9678
+ ...warnings.length > 0 ? { warnings } : {}
9679
+ };
9680
+ },
9681
+ html: STORE_HTML
9682
+ };
9683
+ }
9684
+ var storeApp = defineApp({
9685
+ id: STORE_APP_ID,
9686
+ name: "App Store",
9687
+ description: "Open the agentproto App Store \u2014 installed apps, catalog shelves (featured/available), install-from-URL with an explicit confirmation, sources and builtin panels.",
9688
+ agents: [],
9689
+ ui: {
9690
+ html: STORE_HTML,
9691
+ title: "App Store",
9692
+ tools: [...STORE_UI_TOOLS]
9693
+ }
9694
+ });
9695
+
9623
9696
  // src/review-panel/panel.generated.ts
9624
9697
  var 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 \u2014 a classic (non-module)\n script so its `initBridge`/`callTool` land as globals before the\n deferred module script below runs. -->\n<script>// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500\n// JSON-RPC 2.0 over window.parent.postMessage \xB7 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// \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \u2014 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 \u2014\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 \u2014 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) \u2014 capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 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// \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500\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 \u2014 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 \u2014 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 \u2014 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 \u2014\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 \u2014 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");\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 "\u2014";\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 "\u2014";\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 || "\u2014";\n }\n function prLabel(row) {\n if (!row.pr) return "\u2014";\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 ?? "\u2014")}</td><td class="mono">${esc(row.repoRemote ?? "\u2014")}</td><td>${requesterLabel(row)}</td><td>${prLabel(row)}</td><td>${flags.join(" ") || "\u2014"}</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(" \xB7 ")}</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"} \xB7 ${runningCount} running \xB7 ${(/* @__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>/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Semantic tokens + runtime mapping sheets \u2014 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 \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\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/* \u2500\u2500 Base \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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">&#8635;</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">&times; 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&#8230;</div>\n</div>\n</body>\n</html>\n';
9625
9698
 
@@ -9668,6 +9741,6 @@ var reviewPanelApp = defineApp({
9668
9741
  }
9669
9742
  });
9670
9743
 
9671
- export { SESSION_CHAT_APP_ID, SESSION_CHAT_FALLBACK_HTML, agentsOverviewApp, bureauSessionsApp, codeTeam, configApp, contentTeam, liveSessionApp, mailTriage, makeAgentsOverviewApp, makeBureauSessionsApp, makeLiveSessionApp, makeReviewPanelApp, makeSessionChatApp, makeSessionStoryPanelApp, makeSessionsPanelApp, makeWorkBoardApp, mediaViewer, opsPanel, panelBridgeScript, reviewPanelApp, sessionChatApp, sessionChatAppUrl, sessionChatEmbedHtml, sessionStoryApp, sessionViewer, sessionsPanelApp, workBoardApp };
9744
+ export { SESSION_CHAT_APP_ID, SESSION_CHAT_FALLBACK_HTML, agentsOverviewApp, bureauSessionsApp, codeTeam, configApp, contentTeam, liveSessionApp, mailTriage, makeAgentsOverviewApp, makeBureauSessionsApp, makeLiveSessionApp, makeReviewPanelApp, makeSessionChatApp, makeSessionStoryPanelApp, makeSessionsPanelApp, makeStoreApp, makeWorkBoardApp, mediaViewer, opsPanel, panelBridgeScript, reviewPanelApp, sessionChatApp, sessionChatAppUrl, sessionChatEmbedHtml, sessionStoryApp, sessionViewer, sessionsPanelApp, storeApp, workBoardApp };
9672
9745
  //# sourceMappingURL=index.mjs.map
9673
9746
  //# sourceMappingURL=index.mjs.map