@agentproto/apps 0.20.3 → 0.21.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.
Files changed (65) hide show
  1. package/dist/agents-overview/panel.mjs +21 -8
  2. package/dist/agents-overview/panel.mjs.map +1 -1
  3. package/dist/agents-overview.mjs +21 -8
  4. package/dist/agents-overview.mjs.map +1 -1
  5. package/dist/bin/sync.mjs +19 -2
  6. package/dist/bin/sync.mjs.map +1 -1
  7. package/dist/bureau-sessions/panel.mjs +21 -8
  8. package/dist/bureau-sessions/panel.mjs.map +1 -1
  9. package/dist/bureau-sessions.mjs +21 -8
  10. package/dist/bureau-sessions.mjs.map +1 -1
  11. package/dist/index.mjs +63 -20
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/live-session/panel.mjs +21 -8
  14. package/dist/live-session/panel.mjs.map +1 -1
  15. package/dist/live-session.mjs +21 -8
  16. package/dist/live-session.mjs.map +1 -1
  17. package/dist/ops-panel/ui.d.ts +1 -1
  18. package/dist/ops-panel/ui.d.ts.map +1 -1
  19. package/dist/ops-panel.mjs +19 -2
  20. package/dist/ops-panel.mjs.map +1 -1
  21. package/dist/panel-bridge.d.ts.map +1 -1
  22. package/dist/review-panel/panel.d.ts +1 -1
  23. package/dist/review-panel/panel.d.ts.map +1 -1
  24. package/dist/review-panel/panel.generated.d.ts +1 -1
  25. package/dist/review-panel/panel.generated.d.ts.map +1 -1
  26. package/dist/review-panel/panel.mjs +1 -1
  27. package/dist/review-panel/panel.mjs.map +1 -1
  28. package/dist/review-panel.mjs +1 -1
  29. package/dist/review-panel.mjs.map +1 -1
  30. package/dist/session-chat/panel.mjs +21 -8
  31. package/dist/session-chat/panel.mjs.map +1 -1
  32. package/dist/session-chat.mjs +21 -8
  33. package/dist/session-chat.mjs.map +1 -1
  34. package/dist/session-story/panel.mjs +21 -8
  35. package/dist/session-story/panel.mjs.map +1 -1
  36. package/dist/session-story.mjs +21 -8
  37. package/dist/session-story.mjs.map +1 -1
  38. package/dist/sessions-panel/panel.mjs +21 -8
  39. package/dist/sessions-panel/panel.mjs.map +1 -1
  40. package/dist/sessions-panel.mjs +21 -8
  41. package/dist/sessions-panel.mjs.map +1 -1
  42. package/dist/store/panel.d.ts +1 -1
  43. package/dist/store/panel.d.ts.map +1 -1
  44. package/dist/store/panel.generated.d.ts +1 -1
  45. package/dist/store/panel.generated.d.ts.map +1 -1
  46. package/dist/store/panel.mjs +1 -1
  47. package/dist/store/panel.mjs.map +1 -1
  48. package/dist/store.mjs +1 -1
  49. package/dist/store.mjs.map +1 -1
  50. package/dist/work-board/panel.d.ts +1 -1
  51. package/dist/work-board/panel.d.ts.map +1 -1
  52. package/dist/work-board/panel.generated.d.ts +1 -1
  53. package/dist/work-board/panel.generated.d.ts.map +1 -1
  54. package/dist/work-board/panel.mjs +21 -8
  55. package/dist/work-board/panel.mjs.map +1 -1
  56. package/dist/work-board.mjs +21 -8
  57. package/dist/work-board.mjs.map +1 -1
  58. package/package.json +3 -3
  59. package/repo-maintenance/.agentproto/workflows/maintain/WORKFLOW.md +17 -1
  60. package/repo-maintenance/.agentproto/workflows/maintain/entry.mjs +88 -5
  61. package/session-steward/.agentproto/workflows/session-steward/WORKFLOW.md +38 -6
  62. package/session-steward/.agentproto/workflows/session-steward/entry.mjs +44 -6
  63. package/session-steward/.agentproto/workflows/session-steward/origin-policy.mjs +2 -2
  64. package/session-steward/README.md +12 -4
  65. package/session-steward/routines/session-steward-hourly/ROUTINE.md +6 -3
package/dist/store.mjs CHANGED
@@ -8,7 +8,7 @@ import { defineApp } from '@agentproto/app-kit';
8
8
 
9
9
 
10
10
  // src/store/panel.generated.ts
11
- 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';
11
+ 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}\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 \u2014 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// \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 const STORE_APP_ID = "@agentproto/store";\n function isBuiltin(row) {\n return row.category === "builtin";\n }\n const EMPTY_VIEW = { app: "", q: "", cat: "" };\n function parseView(search) {\n const params = new URLSearchParams(search);\n return { app: params.get("app") || "", q: params.get("q") || "", cat: params.get("cat") || "" };\n }\n function viewToSearch(view2) {\n const parts = [];\n if (view2.app !== "") parts.push("app=" + encodeURIComponent(view2.app));\n if (view2.q !== "") parts.push("q=" + encodeURIComponent(view2.q));\n if (view2.cat !== "") parts.push("cat=" + encodeURIComponent(view2.cat));\n return parts.length > 0 ? "?" + parts.join("&") : "";\n }\n function viewHref(view2) {\n const search = viewToSearch(view2);\n return search === "" ? "?" : search;\n }\n function safeIconUrl(icon) {\n if (!icon) return "";\n const trimmed = icon.trim();\n if (/^https?:\\/\\//i.test(trimmed) || /^data:image\\//i.test(trimmed)) return trimmed;\n return "";\n }\n function initialOf(row) {\n const base = (row.name && row.name.trim() !== "" ? row.name : row.appId).replace(/^[^A-Za-z0-9]+/, "");\n return base.charAt(0).toUpperCase() || "?";\n }\n function iconHtml(row, size) {\n const initial = esc(initialOf(row));\n const url = safeIconUrl(row.icon);\n if (url === "") {\n return `<span class="store-icon store-icon-${size} store-icon-fallback" data-initial="${initial}" aria-hidden="true">${initial}</span>`;\n }\n return `<span class="store-icon store-icon-${size}" data-initial="${initial}" aria-hidden="true"><img src="${esc(url)}" alt="" loading="lazy" referrerpolicy="no-referrer"></span>`;\n }\n function appLink(appId, inner, cls) {\n return `<a class="${cls}" href="${esc(viewHref({ ...EMPTY_VIEW, app: appId }))}" data-open-app="${esc(appId)}">${inner}</a>`;\n }\n function categoryOf(row) {\n return row.category && row.category !== "" ? row.category : "Other";\n }\n function matchesView(view2, category, haystack) {\n if (view2.cat !== "" && category !== view2.cat) return false;\n const q = view2.q.trim().toLowerCase();\n if (q === "") return true;\n for (const part of haystack) {\n if (part && part.toLowerCase().indexOf(q) !== -1) return true;\n }\n return false;\n }\n function catalogMatches(view2, r) {\n return matchesView(view2, categoryOf(r), [r.name, r.appId, r.description, r.publisher]);\n }\n function filterActive(view2) {\n return view2.q.trim() !== "" || view2.cat !== "";\n }\n function renderCategoryChips(snapshot2, view2) {\n const seen = /* @__PURE__ */ new Set();\n for (const r of snapshot2.catalog) seen.add(categoryOf(r));\n const known = new Set(snapshot2.catalog.map((c) => c.appId));\n for (const a of snapshot2.installed) if (!known.has(a.appId)) seen.add("Other");\n const cats = Array.from(seen).sort();\n const chip = (value, label) => `<button class="store-cat-chip${view2.cat === value ? " active" : ""}" data-cat="${esc(value)}" aria-pressed="${view2.cat === value ? "true" : "false"}">${esc(label)}</button>`;\n return [chip("", "All")].concat(cats.map((c) => chip(c, c))).join("");\n }\n function remoteCard(r, cls) {\n return `<div class="card ${cls}" id="store-entry-${esc(r.appId)}">\n <div class="card-head">${appLink(r.appId, iconHtml(r, "sm"), "store-icon-link")}<div class="card-body">\n <div class="card-title">${appLink(r.appId, text(r.name, r.appId), "store-app-link")}</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></div>\n </div>`;\n }\n function renderCatalogFeatured(rows, view2 = EMPTY_VIEW) {\n const featured = rows.filter((r) => r.featured === true && !isBuiltin(r) && catalogMatches(view2, r));\n if (featured.length === 0) return "";\n return featured.map((r) => remoteCard(r, "store-card-featured")).join("");\n }\n function renderCatalogAvailable(rows, view2 = EMPTY_VIEW) {\n const byCategory = /* @__PURE__ */ new Map();\n for (const r of rows) {\n if (r.installed) continue;\n if (r.featured === true) continue;\n if (isBuiltin(r)) continue;\n if (!catalogMatches(view2, r)) continue;\n const key = categoryOf(r);\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) out = out + remoteCard(r, "store-card-available");\n }\n return out;\n }\n function installedHasUi(app, byId) {\n return byId.get(app.appId)?.hasUi === true || app.source?.kind === "local" && app.dir !== void 0;\n }\n function renderInstalled(installed, catalogRows, updates, view2 = EMPTY_VIEW) {\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.filter((app) => {\n const c = byId.get(app.appId);\n return matchesView(view2, categoryOf(c ?? {}), [app.name, c?.name, app.appId, app.description, c?.description, c?.publisher]);\n }).map((app) => {\n const c = byId.get(app.appId);\n const version = app.version ?? c?.version ?? "";\n const source = app.source?.kind ?? "local";\n const hasUi = installedHasUi(app, byId);\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 const iconRow = { name: app.name ?? c?.name, appId: app.appId, icon: c?.icon };\n return `<div class="card store-card-installed" id="store-entry-${esc(app.appId)}">\n <div class="card-head">${appLink(app.appId, iconHtml(iconRow, "sm"), "store-icon-link")}<div class="card-body">\n <div class="card-title">${appLink(app.appId, text(app.name, app.appId), "store-app-link")}</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></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(rows, view2 = EMPTY_VIEW) {\n const builtins = rows.filter((r) => isBuiltin(r) && r.appId !== STORE_APP_ID && catalogMatches(view2, r));\n if (builtins.length === 0) return "";\n const items = builtins.map(\n (r) => `<li class="store-builtin-item" id="store-entry-${esc(r.appId)}">${appLink(r.appId, iconHtml(r, "sm"), "store-icon-link")}<span class="store-builtin-name">${appLink(r.appId, text(r.name, r.appId), "store-app-link")}</span> <span class="store-builtin-desc">${text(r.description, "")}</span> <button class="sbtn store-open-btn" data-appid="${esc(r.appId)}" data-decision="open">Open</button></li>`\n ).join("");\n return `<details class="store-builtins" open><summary>Builtin panels</summary><ul class="store-builtin-list">${items}</ul></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 renderNoResults(view2) {\n const what = view2.q.trim() !== "" ? `\u201C${esc(view2.q.trim())}\u201D` : "this filter";\n const cat = view2.cat !== "" ? ` in <strong>${esc(view2.cat)}</strong>` : "";\n return `<div class="store-no-results"><p class="store-empty-title">No apps match ${what}${cat}.</p><p class="store-empty-hint">Clear the search or pick another category.</p></div>`;\n }\n function render(snapshot2, view2 = EMPTY_VIEW) {\n const installed = renderInstalled(snapshot2.installed, snapshot2.catalog, snapshot2.updates, view2);\n const featured = renderCatalogFeatured(snapshot2.catalog, view2);\n const available = renderCatalogAvailable(snapshot2.catalog, view2);\n const builtins = renderBuiltins(snapshot2.catalog, view2);\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 if (filterActive(view2) && (snapshot2.installed.length > 0 || snapshot2.catalog.some((r) => !isBuiltin(r)))) {\n sections.push(renderNoResults(view2));\n } else if (!filterActive(view2) || builtins === "") {\n sections.push(storeEmpty(snapshot2.catalog.filter((r) => !isBuiltin(r)), snapshot2.installed));\n }\n }\n sections.push(builtins);\n return sections.join("\\n");\n }\n function buildInstallPayload(entry) {\n const s = entry.source;\n if (!s || typeof s !== "object") return void 0;\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 shellQuote(value) {\n if (/^[A-Za-z0-9_@%+=:,.\\/-]+$/.test(value)) return value;\n return "\'" + value.replace(/\'/g, "\'\\\\\'\'") + "\'";\n }\n function cliInstallCommand(source) {\n if (!source || !source.url) return "";\n let cmd = "agentproto app install " + shellQuote(source.url);\n if (source.kind === "agentapp") {\n if (source.sha256) cmd += " --sha256 " + shellQuote(source.sha256);\n } else {\n if (source.ref) cmd += " --ref " + shellQuote(source.ref);\n if (source.subdir) cmd += " --subdir " + shellQuote(source.subdir);\n if (source.sha) cmd += " --sha " + shellQuote(source.sha);\n }\n return cmd;\n }\n function isRemoteSource(source) {\n return !!source && (source.kind === "git" || source.kind === "agentapp");\n }\n function mcpCall(tool, args) {\n return JSON.stringify({ name: tool, arguments: args }, null, 2);\n }\n function copyItems(entry, inst, appId) {\n const items = [];\n if (entry && isBuiltin(entry)) {\n if (entry.toolId) items.push({ label: "MCP tool id", text: entry.toolId });\n if (entry.resourceUri) items.push({ label: "MCP resource URI", text: entry.resourceUri });\n return items;\n }\n if (!inst && entry && entry.source) {\n items.push({ label: "CLI \u2014 install from catalog", text: "agentproto app install " + shellQuote(appId) });\n const explicit = cliInstallCommand(entry.source);\n if (explicit !== "") items.push({ label: "CLI \u2014 install from pinned source", text: explicit });\n const payload = buildInstallPayload(entry);\n if (payload) items.push({ label: "MCP \u2014 app_install", text: mcpCall("app_install", payload) });\n }\n if (inst) {\n if (isRemoteSource(inst.source)) {\n items.push({ label: "CLI \u2014 resync", text: "agentproto app resync " + shellQuote(appId) });\n items.push({ label: "MCP \u2014 app_resync", text: mcpCall("app_resync", { appId }) });\n }\n items.push({ label: "CLI \u2014 uninstall", text: "agentproto app uninstall " + shellQuote(appId) });\n items.push({ label: "MCP \u2014 app_uninstall", text: mcpCall("app_uninstall", { appId }) });\n }\n return items;\n }\n function renderCopyBlock(items) {\n if (items.length === 0) return "";\n const rows = items.map(\n (it, i) => `<div class="store-copy-item"><div class="store-copy-head"><span class="store-copy-label">${esc(it.label)}</span><button class="sbtn store-copy-btn" data-decision="copy" data-copy-id="store-copy-${i}">Copy</button></div><pre class="store-copy-text mono" id="store-copy-${i}">${esc(it.text)}</pre></div>`\n ).join("");\n return `<h3 class="store-detail-section">Copy</h3><div class="store-copy">${rows}</div>`;\n }\n function formatBytes(n) {\n if (typeof n !== "number" || !isFinite(n) || n < 0) return "";\n if (n < 1024) return n + " B";\n if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";\n if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + " MB";\n return (n / 1024 / 1024 / 1024).toFixed(1) + " GB";\n }\n function formatTime(iso) {\n if (!iso) return "\u2014";\n const ms = Date.parse(iso);\n if (isNaN(ms)) return iso;\n return new Date(ms).toISOString().slice(0, 16).replace("T", " ") + "Z";\n }\n function formatDuration(startedAt, endedAt) {\n if (!startedAt || !endedAt) return "\u2014";\n const ms = Date.parse(endedAt) - Date.parse(startedAt);\n if (isNaN(ms) || ms < 0) return "\u2014";\n const total = Math.floor(ms / 1e3);\n if (total < 60) return total + "s";\n const mins = Math.floor(total / 60);\n if (mins < 60) return mins + "m " + total % 60 + "s";\n return Math.floor(mins / 60) + "h " + mins % 60 + "m";\n }\n function dlRow(label, valueHtml) {\n return valueHtml === "" ? "" : `<dt>${esc(label)}</dt><dd>${valueHtml}</dd>`;\n }\n function mono(value) {\n return value && value !== "" ? `<span class="mono">${esc(value)}</span>` : "";\n }\n function sourceRows(source) {\n if (!source) return "";\n return dlRow("Kind", esc(source.kind)) + dlRow("URL", mono(source.url)) + dlRow("sha256", mono(source.sha256)) + dlRow("Commit (sha)", mono(source.sha)) + dlRow("Ref", mono(source.ref)) + dlRow("Subdir", mono(source.subdir)) + dlRow("Source version", mono(source.version)) + dlRow("Size", esc(formatBytes(source.size)));\n }\n function idList(ids) {\n if (!ids || ids.length === 0) return "";\n return `<ul class="store-detail-list">${ids.map((id) => `<li class="mono">${esc(id)}</li>`).join("")}</ul>`;\n }\n function renderRuns(runs) {\n if (!runs || runs.length === 0) return "";\n const sorted = runs.slice().sort((a, b) => (b.startedAt || "") < (a.startedAt || "") ? -1 : (b.startedAt || "") > (a.startedAt || "") ? 1 : 0);\n const shown = sorted.slice(0, 10);\n const rows = shown.map((r) => {\n const where = [r.harness || r.adapter, r.model].filter((x) => !!x).join(" / ");\n return `<tr><td>${text(r.status, "\u2014")}</td><td>${esc(formatTime(r.startedAt))}</td><td>${esc(formatDuration(r.startedAt, r.endedAt))}</td><td>${text(where, "\u2014")}</td></tr>`;\n }).join("");\n const more = runs.length > shown.length ? ` <span class="store-detail-more">(last ${shown.length} of ${runs.length})</span>` : "";\n return `<h3 class="store-detail-section">Recent runs${more}</h3><table class="store-runs"><thead><tr><th>Status</th><th>Started</th><th>Duration</th><th>Harness / model</th></tr></thead><tbody>${rows}</tbody></table>`;\n }\n function uniq(values) {\n const out = [];\n for (const v of values) if (out.indexOf(v) === -1) out.push(v);\n return out;\n }\n function renderDetail(snapshot2, view2) {\n const appId = view2.app;\n const back = `<a class="store-back" href="${esc(viewHref({ ...view2, app: "" }))}" data-nav="back">\u2190 Back to store</a>`;\n const entry = snapshot2.catalog.find((c) => c.appId === appId);\n const inst = snapshot2.installed.find((a) => a.appId === appId);\n if (!entry && !inst) {\n return `<div class="store-detail">${back}<div class="store-no-results"><p class="store-empty-title">No app \u201C${esc(appId)}\u201D in the catalog or installed.</p></div></div>`;\n }\n const builtin = !!entry && isBuiltin(entry);\n const update = snapshot2.updates.some((u) => u.appId === appId) || entry?.updateAvailable === true;\n const name = (inst?.name ?? entry?.name) || appId;\n const description = (entry?.description ?? inst?.description) || "";\n const version = inst?.version ?? entry?.installedVersion ?? entry?.version;\n const installed = !builtin && (!!inst || entry?.installed === true);\n const hasUi = builtin ? appId !== STORE_APP_ID : entry?.hasUi === true || !!inst && installedHasUi(inst, new Map(snapshot2.catalog.map((c) => [c.appId, c])));\n const catalogVersionLine = update && entry?.version && entry.version !== version ? `${esc(version || "")} \u2192 ${esc(entry.version)} available` : esc(version || "");\n const badges = [];\n if (version) badges.push(`<span class="chip">v${esc(version)}</span>`);\n if (update) badges.push(`<span class="badge store-update-badge">Update available</span>`);\n if (builtin) badges.push(`<span class="chip">builtin</span>`);\n else if (installed) badges.push(`<span class="chip">installed</span>`);\n if (entry?.stale) badges.push(`<span class="badge store-stale-badge">stale catalog</span>`);\n if (entry) badges.push(chips(entry));\n const actions = [];\n const idAttr = `data-appid="${esc(appId)}"`;\n if (!installed && !builtin && entry && entry.source) {\n actions.push(`<button class="sbtn primary store-install-btn" ${idAttr} data-decision="install">Install</button>`);\n }\n if (installed && (isRemoteSource(inst?.source) || update)) {\n actions.push(`<button class="sbtn store-resync-btn" ${idAttr} data-decision="resync">Update</button>`);\n }\n if (installed) {\n actions.push(`<button class="sbtn danger store-uninstall-btn" ${idAttr} data-decision="uninstall">Uninstall</button>`);\n }\n if (hasUi && (installed || builtin)) {\n actions.push(`<button class="sbtn store-open-btn" ${idAttr} data-decision="open">Open</button>`);\n }\n const requiresApps = uniq((entry?.requires?.apps ?? []).concat(inst?.requires ?? []));\n const requires = dlRow("agentproto version", esc(entry?.requires?.agentprotoVersion ?? "")) + dlRow("Apps", requiresApps.length > 0 ? esc(requiresApps.join(", ")) : "") + dlRow("Agents", entry?.requires?.agents && entry.requires.agents.length > 0 ? esc(entry.requires.agents.join(", ")) : "");\n const license = entry?.license ? esc(entry.license.kind ?? "") + (entry.license.url ? ` ${mono(entry.license.url)}` : "") : "";\n const info = dlRow("App id", mono(appId)) + dlRow("Publisher", esc(entry?.publisher ?? "")) + dlRow("Version", catalogVersionLine) + dlRow("License", license.trim()) + dlRow("Tier", esc(entry?.tier ?? "")) + dlRow("Category", esc(entry?.category ?? "")) + dlRow("Placement", esc(entry?.placement ?? "")) + dlRow("Size", esc(formatBytes(entry?.source?.size ?? inst?.source?.size))) + dlRow("Origin", esc(entry?.origin ?? "")) + dlRow("Catalog", mono(entry?.catalogUrl) + (entry?.stale ? ` <span class="badge store-stale-badge">stale</span>` : "")) + dlRow("Tool id", mono(entry?.toolId)) + dlRow("Resource URI", mono(entry?.resourceUri));\n const location = inst ? dlRow("Install dir", mono(inst.dir)) + dlRow("Data dir", mono(inst.dataDir)) + (inst.dirMissing ? `<dt>Warning</dt><dd><span class="badge store-dir-missing">dir missing \u2014 the install dir no longer exists on disk; reinstall to restore it</span></dd>` : "") : "";\n const catalogSource = sourceRows(entry?.source);\n const installedSource = inst?.source && inst.source.kind !== "local" ? sourceRows(inst.source) : "";\n const sections = [];\n sections.push(\n `<div class="store-detail-head">${iconHtml({ name, appId, icon: entry?.icon }, "lg")}<div class="store-detail-title"><h2 class="store-detail-name">${esc(name)}</h2><div class="store-detail-id mono">${esc(appId)}</div><div class="card-chips">${badges.join("")}</div></div></div>`\n );\n if (actions.length > 0) sections.push(`<div class="card-actions store-detail-actions">${actions.join("")}</div>`);\n if (description !== "") sections.push(`<p class="store-detail-desc">${esc(description)}</p>`);\n sections.push(`<dl class="store-kv">${info}</dl>`);\n if (requires !== "") sections.push(`<h3 class="store-detail-section">Requires</h3><dl class="store-kv">${requires}</dl>`);\n if (catalogSource !== "") sections.push(`<h3 class="store-detail-section">Source${inst ? " (catalog)" : ""}</h3><dl class="store-kv">${catalogSource}</dl>`);\n if (installedSource !== "") sections.push(`<h3 class="store-detail-section">Installed from</h3><dl class="store-kv">${installedSource}</dl>`);\n if (location !== "") sections.push(`<h3 class="store-detail-section">Installation</h3><dl class="store-kv">${location}</dl>`);\n const agents = idList(inst?.agents);\n if (agents !== "") sections.push(`<h3 class="store-detail-section">Agents</h3>${agents}`);\n const workflows = idList(inst?.workflows);\n if (workflows !== "") sections.push(`<h3 class="store-detail-section">Workflows</h3>${workflows}`);\n const runs = renderRuns(inst?.runs);\n if (runs !== "") sections.push(runs);\n sections.push(renderCopyBlock(copyItems(entry, inst, appId)));\n return `<div class="store-detail" data-appid="${esc(appId)}">${back}${sections.join("")}</div>`;\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 let view = parseView(window.location.search);\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 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 const path = `/apps/${encoded}/ui`;\n try {\n window.open(path, "_blank", "noopener");\n } catch (e) {\n setStatus(`Cannot open a tab from this host \u2014 visit ${path} on the daemon.`);\n }\n }\n function urlFor(v) {\n const params = new URLSearchParams(window.location.search);\n const set = (key, value) => {\n if (value === "") params.delete(key);\n else params.set(key, value);\n };\n set("app", v.app);\n set("q", v.q);\n set("cat", v.cat);\n const search = params.toString();\n return window.location.pathname + (search === "" ? "" : "?" + search);\n }\n function navigate(next, mode) {\n const appChanged = next.app !== view.app;\n view = next;\n try {\n if (mode === "push") window.history.pushState(null, "", urlFor(next));\n else window.history.replaceState(null, "", urlFor(next));\n } catch (e) {\n }\n renderBody();\n if (appChanged) getEl("content").scrollTop = 0;\n }\n function copyText(id) {\n const pre = document.getElementById(id);\n if (!pre) return;\n const value = pre.textContent || "";\n const selectFallback = () => {\n const selection = window.getSelection();\n if (selection) {\n const range = document.createRange();\n range.selectNodeContents(pre);\n selection.removeAllRanges();\n selection.addRange(range);\n }\n let copied = false;\n try {\n copied = typeof document.execCommand === "function" && document.execCommand("copy");\n } catch (e) {\n copied = false;\n }\n setStatus(copied ? "Copied" : "Selected \u2014 press Ctrl/Cmd+C to copy");\n };\n if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {\n navigator.clipboard.writeText(value).then(() => setStatus("Copied"), selectFallback);\n } else {\n selectFallback();\n }\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 setStatus(summarize(snapshot));\n deepLinkInstall();\n }).catch((e) => setStatus(`Error: ${e.message}`));\n }\n function summarize(snap) {\n const available = snap.catalog.filter((r) => !r.installed && !isBuiltin(r)).length;\n const builtin = snap.catalog.filter(isBuiltin).length;\n const warnings = snap.warnings?.length ?? 0;\n return `${snap.installed.length} installed \xB7 ${available} available \xB7 ${builtin} builtin` + (warnings > 0 ? ` \xB7 ${warnings} warning${warnings === 1 ? "" : "s"}` : "");\n }\n function renderBody() {\n const detail = view.app !== "";\n getEl("store-filter").hidden = detail;\n getEl("url-install-static").hidden = detail;\n if (!detail) {\n const search = getEl("store-search");\n if (search.value !== view.q) search.value = view.q;\n getEl("store-cats").innerHTML = renderCategoryChips(snapshot, view);\n }\n getEl("content").innerHTML = detail ? renderDetail(snapshot, view) : render(snapshot, view);\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 const search = document.getElementById("store-search");\n if (search) {\n search.addEventListener("input", () => navigate({ ...view, q: search.value }, "replace"));\n }\n getEl("store-cats").addEventListener("click", (evt) => {\n if (!(evt.target instanceof Element)) return;\n const chip = evt.target.closest("button[data-cat]");\n if (chip) navigate({ ...view, cat: chip.getAttribute("data-cat") || "" }, "replace");\n });\n content.addEventListener("click", (evt) => {\n if (!(evt.target instanceof Element)) return;\n const mouse = evt;\n const link = evt.target.closest("a[data-open-app], a[data-nav]");\n if (link && !(mouse.ctrlKey || mouse.metaKey || mouse.shiftKey || mouse.button > 0)) {\n evt.preventDefault();\n const openApp = link.getAttribute("data-open-app");\n navigate({ ...view, app: openApp || "" }, "push");\n return;\n }\n const btn = evt.target.closest("button[data-decision]");\n if (!(btn instanceof HTMLButtonElement)) return;\n const decision = btn.getAttribute("data-decision");\n if (decision === "copy") {\n const copyId = btn.getAttribute("data-copy-id");\n if (copyId) copyText(copyId);\n return;\n }\n const appId = btn.getAttribute("data-appid");\n if (!appId) return;\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 document.addEventListener(\n "error",\n (evt) => {\n const img = evt.target;\n if (!(img instanceof HTMLImageElement)) return;\n const tile = img.closest(".store-icon");\n if (!tile) return;\n tile.classList.add("store-icon-fallback");\n tile.textContent = tile.getAttribute("data-initial") || "?";\n },\n true\n );\n window.addEventListener("popstate", () => {\n view = parseView(window.location.search);\n renderBody();\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 0; padding: 0; list-style: none; font-size: 11px; color: var(--st-ink-muted); }\n.store-builtin-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; }\n.store-builtin-name { color: var(--st-ink); font-weight: 600; }\n.store-builtin-desc { flex: 1; min-width: 0; }\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}\n\n[hidden] { display: none !important; }\n\n/* \u2500\u2500 search + category chips \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#store-filter {\n padding: 8px 12px 0;\n flex-shrink: 0;\n}\n#store-search {\n width: 100%;\n background: var(--st-bg);\n border: 1px solid var(--st-border);\n color: var(--st-ink);\n padding: 5px 8px;\n border-radius: 4px;\n font-size: 12px;\n font-family: inherit;\n}\n#store-cats { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }\n.store-cat-chip {\n background: var(--st-bg);\n border: 1px solid var(--st-border);\n color: var(--st-ink-muted);\n padding: 1px 8px;\n border-radius: 10px;\n font-size: 10px;\n font-weight: 600;\n cursor: pointer;\n font-family: inherit;\n}\n.store-cat-chip:hover { background: var(--st-border); }\n.store-cat-chip.active { background: var(--st-accent); border-color: var(--st-accent); color: var(--st-ink-on-accent); }\n.store-no-results {\n border: 1px dashed var(--st-border);\n border-radius: 6px;\n padding: 16px;\n margin-bottom: 12px;\n}\n\n/* \u2500\u2500 icons \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.store-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n overflow: hidden;\n border-radius: 6px;\n background: color-mix(in srgb, var(--st-ink-muted) 15%, transparent);\n color: var(--st-ink-muted);\n font-weight: 700;\n}\n.store-icon-sm { width: 32px; height: 32px; font-size: 14px; }\n.store-icon-lg { width: 64px; height: 64px; font-size: 28px; border-radius: 10px; }\n.store-icon img { width: 100%; height: 100%; object-fit: cover; display: block; }\n.store-icon-link, .store-app-link { color: inherit; text-decoration: none; }\n.store-app-link:hover { text-decoration: underline; color: var(--st-accent); }\n.card-head { display: flex; gap: 10px; align-items: flex-start; }\n.card-body { flex: 1; min-width: 0; }\n.store-builtin-item .store-icon { margin-right: 4px; }\n\n/* \u2500\u2500 detail view \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.store-back { display: inline-block; font-size: 12px; color: var(--st-accent); text-decoration: none; margin-bottom: 10px; }\n.store-back:hover { text-decoration: underline; }\n.store-detail-head { display: flex; gap: 14px; align-items: center; margin-bottom: 10px; }\n.store-detail-title { min-width: 0; }\n.store-detail-name { font-size: 18px; font-weight: 600; word-break: break-word; }\n.store-detail-id { font-size: 11px; color: var(--st-ink-muted); margin: 2px 0 6px; word-break: break-all; }\n.store-detail-actions { margin-bottom: 12px; }\n.store-detail-desc { font-size: 12px; line-height: 1.5; white-space: pre-wrap; margin-bottom: 12px; }\n.store-detail-section {\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: 16px 0 6px;\n}\n.store-detail-more { text-transform: none; letter-spacing: 0; font-weight: 400; }\n.store-kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: 12px; }\n.store-kv dt { color: var(--st-ink-muted); }\n.store-kv dd { min-width: 0; word-break: break-all; }\n.store-detail-list { list-style: none; font-size: 12px; }\n.store-detail-list li { padding: 2px 0; }\n.store-dir-missing { background: color-mix(in srgb, var(--st-warn) 25%, transparent); white-space: normal; }\n.store-runs { width: 100%; border-collapse: collapse; font-size: 11px; }\n.store-runs th, .store-runs td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--st-border); }\n.store-runs th { color: var(--st-ink-muted); font-weight: 600; }\n.store-copy-item { margin-bottom: 8px; }\n.store-copy-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; }\n.store-copy-label { font-size: 11px; color: var(--st-ink-muted); }\n.store-copy-text {\n background: var(--st-surface);\n border: 1px solid var(--st-border);\n border-radius: 4px;\n padding: 6px 8px;\n font-size: 11px;\n white-space: pre-wrap;\n word-break: break-all;\n user-select: all;\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="store-filter">\n <input id="store-search" type="search" placeholder="Search apps by name, id, description or publisher" aria-label="Search apps" autocomplete="off" />\n <div id="store-cats"></div>\n </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';
12
12
 
13
13
  // src/store/panel.ts
14
14
  var STORE_APP_ID = "@agentproto/store";