@agentproto/apps 0.13.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * McpApp definition for the agentproto review verdict panel — a list +
3
+ * detail view over the daemon's review ledger
4
+ * (`packages/runtime/src/review-ledger.ts`) and its in-flight runs
5
+ * (`review-runner.ts`'s `ReviewRunner.list()`).
6
+ *
7
+ * Uses the AgnoMcpApp contract (a local McpApp-compatible shape, no
8
+ * @agstudio/mcp-apps / Mastra dependency), same as sessions-panel /
9
+ * work-board — the host (@agentproto/runtime) wires this factory's output
10
+ * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the
11
+ * mounted list assembled in runtime's builtin-apps.ts.
12
+ *
13
+ * Protocol flow:
14
+ * 1. Tool `agentproto_reviews` is called by the host with the optional
15
+ * `{cwd, repoRemote, runId, requesterSessionId}` scoping input.
16
+ * 2. execute() answers an initial `review_ledger`-shaped snapshot
17
+ * (`includeRunning: true`) via `ops.listReviews` — the SAME body
18
+ * `review_ledger` itself runs (`reviewLedgerView`, review-tools.ts),
19
+ * so a non-interactive caller sees exactly what the tool would return.
20
+ * 3. The HTML panel — a static work-board-style Vite build, so it does
21
+ * NOT thread this execute() input through — opens its own bridge and
22
+ * polls `review_ledger({includeRunning: true})` unscoped (mirrors
23
+ * work-board's own boardId-from-execute() omission), then
24
+ * `review_status(runId)` for whichever row is selected. Actions go
25
+ * through `review_cancel`/`review_run`/`review_pr`/`review_export` —
26
+ * real tool names only, no second write path.
27
+ *
28
+ * This module also exports `reviewPanelApp`, a real `defineApp()`
29
+ * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.
30
+ * It's separate from `makeReviewPanelApp` above, which the daemon mounts
31
+ * directly at boot: that factory closes over the LIVE ledger/runner read,
32
+ * something a static emitted `ui.html` snapshot can't carry.
33
+ */
34
+ import { z } from "zod";
35
+ import { type AppHandle } from "@agentproto/app-kit";
36
+ import type { AgnoMcpApp } from "../mcp-app-types.js";
37
+ export declare const reviewPanelInputSchema: z.ZodObject<{
38
+ cwd: z.ZodOptional<z.ZodString>;
39
+ repoRemote: z.ZodOptional<z.ZodString>;
40
+ runId: z.ZodOptional<z.ZodString>;
41
+ requesterSessionId: z.ZodOptional<z.ZodString>;
42
+ }, z.core.$strip>;
43
+ export type ReviewPanelInput = z.infer<typeof reviewPanelInputSchema>;
44
+ export interface ReviewPanelOutput {
45
+ total: number;
46
+ attestations: Record<string, unknown>[];
47
+ /** Echoed back from the input — the row this snapshot's caller already
48
+ * had in mind, when known. Not consumed by the panel's own client JS
49
+ * (see this file's docblock on why the static build doesn't thread it). */
50
+ focusRunId?: string;
51
+ }
52
+ /**
53
+ * Generic host wiring for `makeReviewPanelApp` — the daemon supplies
54
+ * `listReviews` bound to `reviewLedgerView(reviewRunner, ...)`
55
+ * (review-tools.ts), the exact function `review_ledger` itself calls, so
56
+ * the panel's initial snapshot and the tool's own output can never drift
57
+ * apart into two implementations.
58
+ */
59
+ export interface ReviewPanelOps {
60
+ listReviews(input: ReviewPanelInput): Promise<ReviewPanelOutput> | ReviewPanelOutput;
61
+ }
62
+ export type { AgnoMcpApp };
63
+ /**
64
+ * Factory: close over the ledger/runner read so execute() doesn't need an
65
+ * AppContext (agentproto has no userId/guildId concept) — mirrors
66
+ * makeWorkBoardApp.
67
+ */
68
+ export declare function makeReviewPanelApp(ops: ReviewPanelOps): AgnoMcpApp<ReviewPanelInput, ReviewPanelOutput>;
69
+ export declare const reviewPanelApp: AppHandle;
70
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/review-panel/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAErD,eAAO,MAAM,sBAAsB;;;;;iBAQjC,CAAA;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AAErE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IACvC;;gFAE4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,GAAG,iBAAiB,CAAA;CACrF;AAED,YAAY,EAAE,UAAU,EAAE,CAAA;AAE1B;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,UAAU,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CAiBvG;AAED,eAAO,MAAM,cAAc,EAAE,SAY3B,CAAA"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * HTML bundle for the agentproto review-panel — the `REVIEW_PANEL_HTML`
3
+ * served as the `ui://agentproto_reviews/view` resource by `registerMcpApps`
4
+ * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via
5
+ * the `agentproto_reviews` tool defined in ./index.ts.
6
+ *
7
+ * A verdict panel over the daemon's review ledger
8
+ * (`packages/runtime/src/review-ledger.ts` + `review-runner.ts`'s in-flight
9
+ * runs) — list view (running first, then newest) and a per-run detail view
10
+ * (lanes, findings, agent-lane provenance).
11
+ *
12
+ * `REVIEW_PANEL_HTML` is a real single-file Vite build, not a hand-authored
13
+ * template string — same technique as work-board (see that package's
14
+ * ./panel.ts docblock): the source lives in ./ui/** (index.html/main.ts/
15
+ * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with
16
+ * vite-plugin-singlefile) and is committed, pre-built, into
17
+ * ./panel.generated.ts by `pnpm --filter @agentproto/apps run build:ui:reviews`
18
+ * (scripts/build-review-panel-ui.mjs) — so this package still builds with
19
+ * plain `pnpm build` on a fresh clone, no Vite step in the critical path.
20
+ * `build:ui:reviews:check` (wired into CI) fails if that generated file
21
+ * drifts from what the Vite sources actually produce.
22
+ *
23
+ * Protocol: MCP Apps ext spec 2026-01-26
24
+ * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts)
25
+ * – Handshake: ui/initialize → host result → ui/notifications/initialized
26
+ * – Data: tools/call → review_ledger (`includeRunning: true`) on a poll
27
+ * that speeds up to ~4s while any row is running, ~15s otherwise; a
28
+ * selected row's detail comes from review_status (runId)
29
+ * – Actions: tools/call → review_cancel / review_run (nocache + wait:false
30
+ * + supersede, "Re-run fresh") / review_pr / review_export — real tool
31
+ * names only, no second write path
32
+ * – Deep link: an agent lane's reviewer-session id opens (openLink, or
33
+ * window.open as a fallback) `/apps/@agentproto/live-session/ui
34
+ * ?sessionId=<id>` — runtime's `handleAppUiPage` validates and bakes
35
+ * that query param into the live-session widget's initData
36
+ * (builtin-apps.ts's `isValidDeepLinkSessionId`), so it boots already
37
+ * pinned to the exact reviewer session instead of self-discovering the
38
+ * newest running one. See ./ui/render.ts's `liveSessionUrl`.
39
+ *
40
+ * The verdict/status chip (never render a running/cancelled run as if it
41
+ * had a verdict): pass/block/incomplete/running/cancelled/failed, one
42
+ * visual language everywhere it appears — see ./ui/render.ts's `verdictChip`.
43
+ */
44
+ /** The builtin's app id — how `app_catalog` reports the panel. Lives in this
45
+ * html-only entry (not ./index.ts) so a consumer that must NOT pull the
46
+ * app-kit dependency graph can still name the panel from source (mirrors
47
+ * work-board/panel.ts's `WORK_BOARD_APP_ID`). */
48
+ export declare const REVIEW_PANEL_APP_ID = "@agentproto/review-panel";
49
+ /** The MCP tool the panel is registered under (./index.ts
50
+ * `makeReviewPanelApp`), hence its resource uri `ui://agentproto_reviews/view`. */
51
+ export declare const REVIEW_PANEL_TOOL_ID = "agentproto_reviews";
52
+ /** The daemon tools this panel's html calls, in order of first use. Consumed
53
+ * as `ui.tools` by ./index.ts and as the client-side allowlist by any host
54
+ * that dispatches the panel's `tools/call` directly. The reviewer-session
55
+ * deep link (./ui/render.ts's `liveSessionUrl`) is a plain `openLink`/
56
+ * `window.open` navigation, not a tools/call, so it needs no entry here. */
57
+ export declare const REVIEW_PANEL_UI_TOOLS: readonly ["review_ledger", "review_status", "review_cancel", "review_run", "review_pr", "review_export"];
58
+ export declare const REVIEW_PANEL_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<title>agentproto reviews</title>\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\n vite.config.ts's injectPanelBridge plugin \u2014 a classic (non-module)\n script so its `initBridge`/`callTool` land as globals before the\n deferred module script below runs. -->\n<script>// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500\n// JSON-RPC 2.0 over window.parent.postMessage \u00B7 spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nvar _hostCaps = null;\nvar _standaloneApp = null;\nfunction _isStandalone(){\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\n}\n// \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Panels that iframe the daemon's standalone app host carry this proof so\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\n// registerMcpApps replaces the placeholder with a real per-boot token when\n// serving the panel as a resource; every other render (tests, standalone\n// tab, VS Code HTTP-iframe panel \u2014 the latter two already pass\n// iframeEmbedAllowed's origin checks) keeps the literal, and\n// withEmbedToken() then degrades to an identity function.\nwindow.__AGENPROTO_EMBED_TOKEN__ = \"__AGENPROTO_EMBED_TOKEN__\";\nfunction withEmbedToken(url){\n var t = window.__AGENPROTO_EMBED_TOKEN__;\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\n}\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\nfunction getHostContext(){ return _hostContext; }\nfunction getHostCapabilities(){ return _hostCaps; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== 'object') return;\n // ui/notifications/host-context-changed carries only the changed keys \u2014\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener('message', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === 'ui/notifications/host-context-changed'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n if (_isStandalone()){\n return window.McpApp.connect().then(function(conn){\n _standaloneApp = conn;\n // No host to advertise a hostContext \u2014 default to inline with no\n // other modes available, which keeps the display-mode toggle buttons\n // hidden (they only show for modes hostContext.availableDisplayModes\n // lists) instead of rendering two buttons with nothing to switch to.\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\n });\n }\n return rpcRequest('ui/initialize', {\n appInfo: {name: \"agentproto-review-panel\", version: '0.1.0'},\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\n protocolVersion: '2026-01-26'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) \u2014 capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 e.g. openLinks,\n // \"Host supports opening external URLs\". Captured once, read via\n // getHostCapabilities() by panels that need to branch on it.\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\n rpcNotify('ui/notifications/initialized', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest('ui/request-display-mode', {mode: mode});\n}\nfunction openLink(url){\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\n return rpcRequest('ui/open-link', {url: url});\n}\nfunction callTool(name, args){\n var raw = _standaloneApp\n ? _standaloneApp.callTool(name, args || {})\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\n return raw.then(function(result){\n if (result.isError){\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\n throw new Error(e);\n }\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\n return JSON.parse(text);\n });\n}\n\n// \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500\n// The installer is inlined verbatim (it is self-contained and idempotent),\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\n// panel stays inline until the user clicks, and the buttons only appear for\n// the modes the host advertises in hostContext.availableDisplayModes.\n(function () {\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\n window.AgentprotoUI = window.AgentprotoUI || {};\n\n var STYLE_ID = \"agentproto-display-mode-style\";\n var BAR_ID = \"agentproto-display-mode-bar\";\n // Gap between the host's safe area (or the viewport edge) and the button.\n var EDGE_GAP = 8;\n\n // Glyph + accessible label per mode. The button is an icon; the words go\n // to title/aria-label so the control stays compact without going mute to\n // a screen reader.\n var LABELS = {\n fullscreen: {\n on: \"\\u2921\", off: \"\\u2922\",\n onLabel: \"R\\u00e9duire\", offLabel: \"Agrandir\"\n },\n pip: {\n on: \"\\u2921\", off: \"\\uD83D\\uDCCC\",\n onLabel: \"D\\u00e9tacher\", offLabel: \"\\u00c9pingler sur le c\\u00f4t\\u00e9 (pip)\"\n }\n };\n\n // Every colour and both offsets resolve through an app-overridable custom\n // property first, so an app can restyle/reposition the button with a\n // stylesheet and never touch this script. The safe-area vars are what the\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\n function ensureStyle() {\n if (!document.head || document.getElementById(STYLE_ID)) return;\n var style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent =\n \"#\" + BAR_ID + \"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\" +\n \"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\" +\n \"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\" +\n \".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\" +\n \"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\" +\n \"cursor:pointer;-webkit-appearance:none;appearance:none;\" +\n \"font:600 14px/1 system-ui,-apple-system,sans-serif;\" +\n \"border:1px solid var(--agentproto-dm-border,#d0d0d0);\" +\n \"background:var(--agentproto-dm-bg,#fff);\" +\n \"color:var(--agentproto-dm-fg,#1a1a1a);\" +\n \"box-shadow:0 1px 4px rgba(0,0,0,.18)}\" +\n \".agentproto-dm-btn:hover{filter:brightness(.95)}\" +\n \".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\" +\n \".agentproto-dm-btn[data-theme=\\\"dark\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\" +\n \"@media (prefers-color-scheme:dark){\" +\n \".agentproto-dm-btn:not([data-theme=\\\"light\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\";\n document.head.appendChild(style);\n }\n\n function ready(fn) {\n if (document.body) fn();\n else document.addEventListener(\"DOMContentLoaded\", fn);\n }\n\n function metaToggle() {\n var el = document.querySelector('meta[name=\"agentproto-display-toggle\"]');\n if (!el) return \"\";\n return (el.getAttribute(\"content\") || \"\").toLowerCase().replace(/^\\s+|\\s+$/g, \"\");\n }\n\n function ensureBar() {\n var bar = document.getElementById(BAR_ID);\n if (bar) return bar;\n bar = document.createElement(\"div\");\n bar.id = BAR_ID;\n document.body.appendChild(bar);\n return bar;\n }\n\n function px(v) {\n return typeof v === \"number\" && isFinite(v) && v > 0 ? v : 0;\n }\n\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\n api = api || {};\n opts = opts || {};\n\n var ctx = {};\n var listeners = [];\n var buttons = [];\n // Modes the host has refused once. Checked before availability, so a\n // later host-context-changed can never resurrect a dead button.\n var refused = {};\n var loggedKeys = false;\n\n var setting = opts.toggle || metaToggle() || \"auto\";\n var optimistic = setting === \"optimistic\";\n var autoMount = setting !== \"none\";\n\n function get() {\n return ctx.displayMode || \"inline\";\n }\n\n function available() {\n var a = ctx.availableDisplayModes;\n return a && a.slice ? a.slice() : [];\n }\n\n function supports(mode) {\n if (refused[mode]) return false;\n var a = ctx.availableDisplayModes;\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\n // Optimistic: the host advertises nothing (Claude Desktop, some\n // embedders) but may still honour the request. Fullscreen only \u2014 pip\n // is niche enough that guessing at it is just noise.\n return optimistic && mode === \"fullscreen\";\n }\n\n // Host chrome to stay clear of. Published as custom properties rather\n // than inline styles so an app's own --agentproto-dm-top/right still\n // wins (an inline style would beat any stylesheet it could write).\n function applyInsets() {\n var insets = ctx.safeAreaInsets || {};\n var root = document.documentElement;\n if (!root || !root.style || !root.style.setProperty) return;\n root.style.setProperty(\"--agentproto-dm-safe-top\", (px(insets.top) + EDGE_GAP) + \"px\");\n root.style.setProperty(\"--agentproto-dm-safe-right\", (px(insets.right) + EDGE_GAP) + \"px\");\n }\n\n function syncButton(btn) {\n var mode = btn.getAttribute(\"data-agentproto-dm\") || \"fullscreen\";\n var labels = LABELS[mode] || LABELS.fullscreen;\n var active = get() === mode;\n btn.style.display = supports(mode) ? \"\" : \"none\";\n btn.textContent = active ? labels.on : labels.off;\n var label = active ? labels.onLabel : labels.offLabel;\n btn.title = label;\n btn.setAttribute(\"aria-label\", label);\n btn.setAttribute(\"aria-pressed\", active ? \"true\" : \"false\");\n if (ctx.theme === \"dark\" || ctx.theme === \"light\") btn.setAttribute(\"data-theme\", ctx.theme);\n else btn.removeAttribute(\"data-theme\");\n }\n\n function sync() {\n applyInsets();\n for (var i = buttons.length - 1; i >= 0; i--) {\n // An app that removed its inline toggle stops being our problem.\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\n syncButton(buttons[i]);\n }\n }\n\n function emit() {\n var mode = get();\n for (var i = 0; i < listeners.length; i++) {\n try { listeners[i](mode, ctx); } catch (_) {}\n }\n }\n\n // host-context-changed carries only the changed keys \u2014 merge, matching\n // the official ext-apps App behaviour.\n function setContext(next) {\n if (!next || typeof next !== \"object\") return;\n for (var k in next) {\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\n }\n sync();\n emit();\n }\n\n function retire(mode) {\n refused[mode] = true;\n sync();\n }\n\n function request(mode) {\n var pending;\n try {\n pending = api.requestDisplayMode\n ? api.requestDisplayMode(mode)\n : Promise.reject(new Error(\"display mode: no host\"));\n } catch (err) {\n pending = Promise.reject(err);\n }\n return Promise.resolve(pending).then(function (result) {\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\n // the spec lets it differ from the one asked for \u2014 so a host that\n // answers \"still inline\" is a refusal, not a success.\n var settled = (result && result.mode) || mode;\n if (optimistic && mode !== \"inline\" && settled !== mode) {\n retire(mode);\n throw new Error(\"host refused display mode: \" + mode);\n }\n // Hosts are not required to follow up with host-context-changed \u2014\n // trust the result so the glyph flips either way.\n setContext({ displayMode: settled });\n return settled;\n }, function (err) {\n if (optimistic && mode !== \"inline\") retire(mode);\n throw err;\n });\n }\n\n function makeButton(mode) {\n var btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.className = \"agentproto-dm-btn\";\n // Stable id for the floating pair (styling hooks, tests). An app that\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\n var id = mode === \"fullscreen\"\n ? \"agentproto-display-mode\"\n : \"agentproto-display-mode-\" + mode;\n if (!document.getElementById(id)) btn.id = id;\n btn.setAttribute(\"data-agentproto-dm\", mode);\n btn.style.display = \"none\";\n btn.addEventListener(\"click\", function () {\n var cur = get();\n // Same rule both copies had: anything that isn't inline collapses\n // back to inline, so the button is always an escape hatch.\n var inPanel = cur === \"fullscreen\" || cur === \"pip\";\n request(inPanel ? \"inline\" : mode)[\"catch\"](function () {});\n });\n return btn;\n }\n\n function mountToggle(el, mode) {\n ensureStyle();\n mode = mode || \"fullscreen\";\n var btn = makeButton(mode);\n if (el) el.appendChild(btn);\n else ensureBar().appendChild(btn);\n buttons.push(btn);\n syncButton(btn);\n return btn;\n }\n\n function onChange(cb) {\n if (typeof cb !== \"function\") return function () {};\n listeners.push(cb);\n try { cb(get(), ctx); } catch (_) {}\n return function () {\n var i = listeners.indexOf(cb);\n if (i >= 0) listeners.splice(i, 1);\n };\n }\n\n ready(function () {\n ensureStyle();\n // Only the first installer in a document owns the floating bar: a\n // built-in panel served standalone gets the injected window.McpApp\n // bridge's copy AND its own, and two stacked buttons is a bug.\n if (autoMount && !document.getElementById(BAR_ID)) {\n ensureBar();\n mountToggle(null, \"pip\");\n mountToggle(null, \"fullscreen\");\n }\n sync();\n });\n\n // The diagnostic both copies had, now on BOTH paths: it used to exist\n // only in the built-in panel bridge, so an installed app debugged in\n // Claude Desktop (which advertises no fullscreen, hence no button)\n // printed nothing at all and left the user with no way to tell \"the\n // host refuses\" from \"the toggle is broken\".\n function observe(next) {\n console.log('[mcp-app] displayMode=', next && next.displayMode,\n 'availableDisplayModes=', next && next.availableDisplayModes);\n setContext(next);\n // What ELSE does this host send? Keys only, once \u2014 a host context can\n // carry toolInfo (the instantiating tool call's arguments) and other\n // host-private fields, so the names are the diagnostic and the values\n // stay out of the console.\n if (!loggedKeys && next && typeof next === \"object\") {\n loggedKeys = true;\n var keys = [];\n for (var k in ctx) {\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\n }\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\n }\n }\n\n if (typeof api.onHostContext === \"function\") api.onHostContext(observe);\n else if (typeof api.getHostContext === \"function\") observe(api.getHostContext());\n\n return {\n get: get,\n available: available,\n request: request,\n onChange: onChange,\n mountToggle: mountToggle\n };\n };\n})();\nvar displayMode = window.AgentprotoUI.installDisplayMode({\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n requestDisplayMode: requestDisplayMode\n});</script>\n <script>(function() {\n \"use strict\";\n function esc(value) {\n return value.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n }\n function liveSessionUrl(origin, sessionId) {\n return `${origin}/apps/@agentproto/live-session/ui?sessionId=${encodeURIComponent(sessionId)}`;\n }\n function shortRange(baseSha, headSha) {\n if (!baseSha || !headSha) return \"\u2014\";\n return `${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`;\n }\n function ageOf(createdAt, now = Date.now()) {\n const ms = now - new Date(createdAt).getTime();\n if (!Number.isFinite(ms) || ms < 0) return \"just now\";\n const mins = Math.floor(ms / 6e4);\n if (mins < 1) return \"just now\";\n if (mins < 60) return `${mins}m`;\n const hours = Math.floor(mins / 60);\n if (hours < 24) return `${hours}h`;\n return `${Math.floor(hours / 24)}d`;\n }\n function verdictChip(status) {\n return `<span class=\"chip chip-verdict v-${esc(status)}\">${esc(status)}</span>`;\n }\n const LANE_STATUS_LABEL = {\n pass: \"pass\",\n fail: \"fail\",\n skipped: \"skipped\",\n timeout: \"timeout\"\n };\n function laneStatusChip(status) {\n return `<span class=\"chip chip-lane l-${esc(status)}\">${esc(LANE_STATUS_LABEL[status])}</span>`;\n }\n function severityTag(severity) {\n return `<span class=\"tag sev-${esc(severity)}\">${esc(severity)}</span>`;\n }\n function requesterLabel(row) {\n const r = row.requester;\n if (!r) return \"\u2014\";\n const id = r.sessionId ? esc(r.sessionId) : \"\";\n const author = r.gitAuthor ? esc(r.gitAuthor.name) : \"\";\n if (id && author) return `${id} <span class=\"muted\">(${author})</span>`;\n return id || author || \"\u2014\";\n }\n function prLabel(row) {\n if (!row.pr) return \"\u2014\";\n const state = row.prState ? ` <span class=\"tag pr-${esc(row.prState)}\">${esc(row.prState)}</span>` : \"\";\n return `<a href=\"${esc(row.pr.url)}\" target=\"_blank\" rel=\"noopener\">#${row.pr.number}</a>${state}`;\n }\n function renderRow(row) {\n const status = row.status ?? row.verdict ?? \"incomplete\";\n const flags = [];\n if (row.dirty) flags.push('<span class=\"tag t-dirty\">dirty</span>');\n if (row.cached) flags.push('<span class=\"tag t-cached\">cached</span>');\n if (row.status !== \"running\") {\n flags.push(row.signed ? '<span class=\"tag t-signed\">signed</span>' : '<span class=\"tag t-unsigned\">unsigned</span>');\n }\n return `<tr class=\"row\" data-runid=\"${esc(row.runId)}\" tabindex=\"0\"><td>${verdictChip(status)}</td><td class=\"mono\">${esc(shortRange(row.baseSha, row.headSha))}</td><td>${esc(row.binding ?? \"\u2014\")}</td><td class=\"mono\">${esc(row.repoRemote ?? \"\u2014\")}</td><td>${requesterLabel(row)}</td><td>${prLabel(row)}</td><td>${flags.join(\" \") || \"\u2014\"}</td><td>${esc(ageOf(row.createdAt))}</td></tr>`;\n }\n function renderList(rows2) {\n if (rows2.length === 0) return '<div class=\"empty\">No reviews recorded yet.</div>';\n const header = \"<tr><th>Verdict</th><th>Range</th><th>Binding</th><th>Repo</th><th>Requester</th><th>PR</th><th>Flags</th><th>Age</th></tr>\";\n return `<table class=\"list\"><thead>${header}</thead><tbody>${rows2.map(renderRow).join(\"\")}</tbody></table>`;\n }\n function findingLine(f) {\n const loc = f.file ? `${esc(f.file)}${f.line ? `:${f.line}` : \"\"}` : \"\";\n return `<details class=\"finding\"><summary>${severityTag(f.severity)} ${esc(f.title)}${loc ? ` <span class=\"mono muted\">${loc}</span>` : \"\"}</summary><div class=\"finding-detail\">${esc(f.detail)}</div></details>`;\n }\n function renderLaneDetail(lane, rubrics = []) {\n const parts = [];\n parts.push(`<div class=\"lane-hdr\">`);\n parts.push(`<span class=\"lane-id\">${esc(lane.id)}</span>`);\n parts.push(laneStatusChip(lane.status));\n parts.push(`<span class=\"tag ${lane.blocking ? \"t-blocking\" : \"t-advisory\"}\">${lane.blocking ? \"blocking\" : \"advisory\"}</span>`);\n if (lane.durationMs !== void 0) parts.push(`<span class=\"muted\">${(lane.durationMs / 1e3).toFixed(1)}s</span>`);\n parts.push(`</div>`);\n if (lane.error) parts.push(`<div class=\"lane-error\">${esc(lane.error)}</div>`);\n if (lane.sessionId || lane.preset || lane.model) {\n const rubric = rubrics.find((r) => r.check === lane.id);\n const bits = [];\n if (lane.model) bits.push(`model: ${esc(lane.model)}`);\n if (lane.preset) bits.push(`preset: ${esc(lane.preset)}`);\n if (rubric) bits.push(`rubric: <span class=\"mono\">${esc(rubric.sha256.slice(0, 12))}</span>`);\n if (lane.sessionId) {\n bits.push(\n `reviewer: <button class=\"sess-link\" data-session-id=\"${esc(lane.sessionId)}\">${esc(lane.sessionId)}</button>`\n );\n }\n parts.push(`<div class=\"lane-agent-meta\">${bits.join(\" \u00B7 \")}</div>`);\n }\n if (lane.findings.length > 0) parts.push(`<div class=\"findings\">${lane.findings.map(findingLine).join(\"\")}</div>`);\n return `<div class=\"lane\">${parts.join(\"\")}</div>`;\n }\n function renderDetail(detail2) {\n const status = detail2.status === \"done\" ? detail2.verdict ?? \"incomplete\" : detail2.status;\n const header = [`<div class=\"detail-hdr\">`, verdictChip(status), `<span class=\"mono\">${esc(detail2.runId)}</span>`];\n if (detail2.binding) header.push(`<span class=\"tag\">${esc(detail2.binding)}</span>`);\n if (detail2.cached) header.push('<span class=\"tag t-cached\">cached</span>');\n if (detail2.status === \"done\" && detail2.attestation) {\n const sig = detail2.attestation.attestor?.signature;\n header.push(\n sig ? `<span class=\"tag t-signed\" title=\"${esc(sig.keyFingerprint)}\">signed: ${esc(sig.principal)}</span>` : '<span class=\"tag t-unsigned\">unsigned</span>'\n );\n }\n header.push(`</div>`);\n if (detail2.error) header.push(`<div class=\"detail-error\">${esc(detail2.error)}</div>`);\n if (detail2.supersededBy) header.push(`<div class=\"muted\">superseded by ${esc(detail2.supersededBy)}</div>`);\n const fullLanes = detail2.attestation?.lanes;\n const rubrics = detail2.attestation?.rubrics ?? [];\n let body;\n if (fullLanes) {\n body = fullLanes.map((l) => renderLaneDetail(l, rubrics)).join(\"\");\n } else if (detail2.lanes) {\n body = detail2.lanes.map((l) => `<div class=\"lane\"><div class=\"lane-hdr\"><span class=\"lane-id\">${esc(l.id)}</span>${laneStatusChip(l.status)}</div></div>`).join(\"\");\n } else {\n body = \"\";\n }\n return `<div class=\"detail\">${header.join(\"\")}${body}</div>`;\n }\n const POLL_RUNNING_MS = 4e3;\n const POLL_IDLE_MS = 15e3;\n function getEl(id) {\n const el = document.getElementById(id);\n if (!el) throw new Error(`review panel: missing #${id}`);\n return el;\n }\n let rows = [];\n let selectedRunId = null;\n let detail = null;\n let pollTimer = null;\n let loadInFlight = false;\n function boot() {\n const listPaneEl = getEl(\"list-pane\");\n const detailPaneEl = getEl(\"detail-pane\");\n const detailBodyEl = getEl(\"detail-body\");\n const statusbarEl = getEl(\"statusbar\");\n const cancelBtn = getEl(\"cancel-btn\");\n const rerunBtn = getEl(\"rerun-btn\");\n const prBtn = getEl(\"pr-btn\");\n const exportBtn = getEl(\"export-btn\");\n function setStatus(msg) {\n statusbarEl.textContent = msg;\n }\n function findRow(runId) {\n return rows.find((r) => r.runId === runId);\n }\n function renderListPane() {\n listPaneEl.innerHTML = renderList(rows);\n if (!selectedRunId) return;\n listPaneEl.querySelectorAll(\"tr[data-runid]\").forEach((tr) => {\n if (tr.dataset.runid === selectedRunId) tr.classList.add(\"selected\");\n });\n }\n function renderDetailPane() {\n if (!selectedRunId || !detail) {\n detailPaneEl.classList.remove(\"open\");\n return;\n }\n detailPaneEl.classList.add(\"open\");\n detailBodyEl.innerHTML = renderDetail(detail);\n const running = detail.status === \"running\";\n cancelBtn.disabled = !running;\n cancelBtn.style.display = running ? \"\" : \"none\";\n detailBodyEl.querySelectorAll(\".sess-link\").forEach((btn) => {\n btn.addEventListener(\"click\", () => openSession(btn.dataset.sessionId ?? \"\"));\n });\n }\n function openSession(sessionId) {\n if (!sessionId) return;\n const origin = window.location.origin && window.location.origin !== \"null\" ? window.location.origin : \"\";\n const url = liveSessionUrl(origin, sessionId);\n const caps = getHostCapabilities();\n if (caps?.openLinks) {\n openLink(url).catch(() => window.open(url, \"_blank\"));\n } else {\n window.open(url, \"_blank\");\n }\n }\n function loadList() {\n if (loadInFlight) return Promise.resolve();\n loadInFlight = true;\n return callTool(\"review_ledger\", { includeRunning: true }).then((result) => {\n rows = result.attestations;\n renderListPane();\n const runningCount = rows.filter((r) => r.status === \"running\").length;\n setStatus(`${result.total} review${result.total === 1 ? \"\" : \"s\"} \u00B7 ${runningCount} running \u00B7 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\n if (selectedRunId && findRow(selectedRunId)) return loadDetail(selectedRunId);\n return void 0;\n }).catch((e) => setStatus(`Error: ${e.message}`)).finally(() => {\n loadInFlight = false;\n });\n }\n function loadDetail(runId) {\n return callTool(\"review_status\", { runId }).then((result) => {\n detail = result;\n renderDetailPane();\n }).catch((e) => setStatus(`Error loading detail: ${e.message}`));\n }\n function openDetail(runId) {\n selectedRunId = runId;\n renderListPane();\n void loadDetail(runId);\n }\n function closeDetail() {\n selectedRunId = null;\n detail = null;\n renderListPane();\n renderDetailPane();\n }\n listPaneEl.addEventListener(\"click\", (evt) => {\n if (!(evt.target instanceof Element)) return;\n const tr = evt.target.closest(\"tr[data-runid]\");\n if (!(tr instanceof HTMLElement)) return;\n const runId = tr.dataset.runid;\n if (runId) openDetail(runId);\n });\n listPaneEl.addEventListener(\"keydown\", (evt) => {\n if (evt.key !== \"Enter\" && evt.key !== \" \") return;\n if (!(evt.target instanceof Element)) return;\n const tr = evt.target.closest(\"tr[data-runid]\");\n if (!(tr instanceof HTMLElement)) return;\n evt.preventDefault();\n const runId = tr.dataset.runid;\n if (runId) openDetail(runId);\n });\n getEl(\"detail-close\").addEventListener(\"click\", closeDetail);\n getEl(\"refresh-btn\").addEventListener(\"click\", () => {\n void loadList();\n });\n cancelBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_cancel\", { runId: selectedRunId }).then(() => {\n setStatus(\"Cancelled.\");\n return loadList();\n }).catch((e) => setStatus(`Cancel failed: ${e.message}`));\n });\n rerunBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n const row = findRow(selectedRunId);\n const cwd = row?.cwd;\n if (!cwd) {\n setStatus(\"Re-run failed: this row has no known checkout path.\");\n return;\n }\n callTool(\"review_run\", {\n cwd,\n ...row?.binding ? { binding: row.binding } : {},\n nocache: true,\n wait: false,\n supersede: true\n }).then(() => {\n setStatus(\"Re-run started.\");\n return loadList();\n }).catch((e) => setStatus(`Re-run failed: ${e.message}`));\n });\n prBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_pr\", { runId: selectedRunId }).then(() => {\n setStatus(\"Fetched PR status.\");\n return loadList();\n }).catch((e) => setStatus(`Fetch PR status failed: ${e.message}`));\n });\n exportBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_export\", { runId: selectedRunId }).then((result) => {\n setStatus(result.path ? `Exported to ${result.path}` : \"Exported.\");\n }).catch((e) => setStatus(`Export failed: ${e.message}`));\n });\n function doPoll() {\n if (pollTimer) clearTimeout(pollTimer);\n loadList().finally(() => {\n const anyRunning = rows.some((r) => r.status === \"running\");\n pollTimer = setTimeout(doPoll, anyRunning ? POLL_RUNNING_MS : POLL_IDLE_MS);\n });\n }\n initBridge().then(() => loadList()).then(() => {\n pollTimer = setTimeout(doPoll, POLL_RUNNING_MS);\n }).catch((e) => setStatus(`Bridge error: ${e.message}`));\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", boot);\n } else {\n boot();\n }\n})();</script>\n <style rel=\"stylesheet\" crossorigin>/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Semantic tokens + runtime mapping sheets \u2014 same scheme as work-board's\n style.css (see that file's header doc for the two-mapping-sheet\n rationale): every rule below reads ONLY the --rp-* custom properties,\n each resolving through a live --vscode-* var() first, then the\n agentproto brand fallback.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n:root {\n --rp-bg: var(--vscode-editor-background, #f5f6f3);\n --rp-surface: var(--vscode-sideBar-background, #ffffff);\n --rp-ink: var(--vscode-editor-foreground, #171a1e);\n --rp-ink-muted: var(--vscode-descriptionForeground, #5c6470);\n --rp-border: var(--vscode-panel-border, #dde0da);\n --rp-accent: var(--vscode-button-background, #0f62fe);\n\n --rp-pass: var(--vscode-charts-green, #3edc81);\n --rp-block: var(--vscode-charts-red, #fa4d56);\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\n --rp-running: var(--vscode-charts-blue, #4589ff);\n --rp-cancelled: var(--vscode-descriptionForeground, #5c6470);\n --rp-failed: var(--vscode-charts-red, #fa4d56);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --rp-bg: var(--vscode-editor-background, #101214);\n --rp-surface: var(--vscode-sideBar-background, #16191d);\n --rp-ink: var(--vscode-editor-foreground, #e8eaed);\n --rp-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\n --rp-border: var(--vscode-panel-border, #272c33);\n --rp-accent: var(--vscode-button-background, #4589ff);\n\n --rp-pass: var(--vscode-charts-green, #3edc81);\n --rp-block: var(--vscode-charts-red, #fa4d56);\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\n --rp-running: var(--vscode-charts-blue, #4589ff);\n --rp-cancelled: var(--vscode-descriptionForeground, #99a2ad);\n --rp-failed: var(--vscode-charts-red, #fa4d56);\n }\n}\n\n/* \u2500\u2500 Base \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n* { box-sizing: border-box; margin: 0; padding: 0; }\n\nhtml, body {\n height: 100%;\n font-family: \"IBM Plex Sans\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n font-size: 13px;\n background: var(--rp-bg);\n color: var(--rp-ink);\n overflow: hidden;\n}\n\n.mono {\n font-family: \"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, Monaco, \"Courier New\", monospace;\n}\n.muted { color: var(--rp-ink-muted); }\n\n#app { display: flex; flex-direction: column; height: 100%; }\n\n#toolbar {\n padding: 8px 12px;\n border-bottom: 1px solid var(--rp-border);\n display: flex;\n align-items: center;\n gap: 8px;\n background: var(--rp-surface);\n flex-shrink: 0;\n}\n#toolbar-label { font-size: 11px; color: var(--rp-ink-muted); }\n.tbtn {\n background: var(--rp-bg);\n border: 1px solid var(--rp-border);\n color: var(--rp-ink);\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n}\n.tbtn:hover { background: var(--rp-border); }\n.tbtn:disabled { opacity: 0.5; cursor: default; }\n.tbtn.danger { border-color: var(--rp-block); color: var(--rp-block); }\n\n#main { flex: 1; overflow: hidden; display: flex; }\n#list-pane { flex: 1; overflow-y: auto; }\n#detail-pane {\n width: 420px;\n min-width: 320px;\n border-left: 1px solid var(--rp-border);\n overflow-y: auto;\n display: none;\n flex-direction: column;\n}\n#detail-pane.open { display: flex; }\n#detail-close { align-self: flex-end; margin: 6px; }\n\ntable.list { width: 100%; border-collapse: collapse; }\ntable.list th {\n text-align: left;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--rp-ink-muted);\n padding: 6px 8px;\n border-bottom: 1px solid var(--rp-border);\n position: sticky;\n top: 0;\n background: var(--rp-surface);\n}\ntable.list td { padding: 6px 8px; border-bottom: 1px solid var(--rp-border); font-size: 12px; vertical-align: middle; }\ntr.row { cursor: pointer; }\ntr.row:hover, tr.row:focus { background: color-mix(in srgb, var(--rp-accent) 8%, transparent); outline: none; }\ntr.row.selected { background: color-mix(in srgb, var(--rp-accent) 14%, transparent); }\n\n.empty { padding: 24px 12px; color: var(--rp-ink-muted); font-size: 12px; text-align: center; }\n\n.chip { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }\n.chip-verdict.v-pass { background: color-mix(in srgb, var(--rp-pass) 18%, transparent); color: var(--rp-pass); }\n.chip-verdict.v-block { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\n.chip-verdict.v-incomplete { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\n.chip-verdict.v-running { background: color-mix(in srgb, var(--rp-running) 18%, transparent); color: var(--rp-running); }\n.chip-verdict.v-cancelled { background: color-mix(in srgb, var(--rp-cancelled) 15%, transparent); color: var(--rp-cancelled); }\n.chip-verdict.v-failed { background: color-mix(in srgb, var(--rp-failed) 18%, transparent); color: var(--rp-failed); }\n\n.chip-lane.l-pass { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\n.chip-lane.l-fail { background: color-mix(in srgb, var(--rp-block) 15%, transparent); color: var(--rp-block); }\n.chip-lane.l-skipped { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.chip-lane.l-timeout { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\n\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\n.t-dirty { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\n.t-cached { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.t-signed { background: color-mix(in srgb, var(--rp-pass) 12%, transparent); color: var(--rp-pass); }\n.t-unsigned { background: color-mix(in srgb, var(--rp-ink-muted) 10%, transparent); color: var(--rp-ink-muted); }\n.t-blocking { background: color-mix(in srgb, var(--rp-block) 12%, transparent); color: var(--rp-block); }\n.t-advisory { background: color-mix(in srgb, var(--rp-ink-muted) 12%, transparent); color: var(--rp-ink-muted); }\n.pr-open { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\n.pr-merged { background: color-mix(in srgb, var(--rp-accent) 15%, transparent); color: var(--rp-accent); }\n.pr-closed { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.sev-high { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\n.sev-medium { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\n.sev-low { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n\n.detail { padding: 10px; display: flex; flex-direction: column; gap: 10px; }\n.detail-hdr { display: flex; align-items: center; gap: 8px; }\n.detail-error { color: var(--rp-block); font-size: 12px; word-break: break-word; }\n.detail-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 10px 10px; }\n\n.lane {\n border: 1px solid var(--rp-border);\n border-radius: 6px;\n padding: 8px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.lane-hdr { display: flex; align-items: center; gap: 6px; }\n.lane-id { font-weight: 600; font-size: 12px; }\n.lane-error { color: var(--rp-block); font-size: 11px; word-break: break-word; }\n.lane-agent-meta { font-size: 11px; color: var(--rp-ink-muted); }\n.sess-link {\n background: none;\n border: none;\n color: var(--rp-accent);\n cursor: pointer;\n font: inherit;\n padding: 0;\n text-decoration: underline;\n}\n\n.findings { display: flex; flex-direction: column; gap: 4px; }\n.finding summary { cursor: pointer; font-size: 11px; list-style: none; }\n.finding summary::-webkit-details-marker { display: none; }\n.finding-detail { font-size: 11px; color: var(--rp-ink-muted); padding: 4px 0 0 4px; white-space: pre-wrap; word-break: break-word; }\n\n#statusbar {\n padding: 4px 12px;\n font-size: 11px;\n color: var(--rp-ink-muted);\n border-top: 1px solid var(--rp-border);\n background: var(--rp-surface);\n flex-shrink: 0;\n}</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"toolbar\">\n <span id=\"toolbar-label\">Reviews</span>\n <button class=\"tbtn\" id=\"refresh-btn\" title=\"Refresh\">&#8635;</button>\n </div>\n <div id=\"main\">\n <div id=\"list-pane\"></div>\n <div id=\"detail-pane\">\n <button class=\"tbtn\" id=\"detail-close\">&times; Close</button>\n <div id=\"detail-body\"></div>\n <div class=\"detail-actions\">\n <button class=\"tbtn danger\" id=\"cancel-btn\">Cancel</button>\n <button class=\"tbtn\" id=\"rerun-btn\">Re-run fresh</button>\n <button class=\"tbtn\" id=\"pr-btn\">Fetch PR status</button>\n <button class=\"tbtn\" id=\"export-btn\">Export</button>\n </div>\n </div>\n </div>\n <div id=\"statusbar\">Connecting to bridge&#8230;</div>\n</div>\n</body>\n</html>\n";
59
+ //# sourceMappingURL=panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/review-panel/panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAIH;;;kDAGkD;AAClD,eAAO,MAAM,mBAAmB,6BAA6B,CAAA;AAE7D;oFACoF;AACpF,eAAO,MAAM,oBAAoB,uBAAuB,CAAA;AAExD;;;;6EAI6E;AAC7E,eAAO,MAAM,qBAAqB,0GAOxB,CAAA;AAEV,eAAO,MAAM,iBAAiB,u+xCAA8B,CAAA"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @generated — DO NOT EDIT BY HAND.
3
+ *
4
+ * Built from src/review-panel/ui/** (Vite + vite-plugin-singlefile) by
5
+ * scripts/build-review-panel-ui.mjs. Edit the Vite sources and regenerate:
6
+ *
7
+ * pnpm --filter @agentproto/apps run build:ui:reviews
8
+ *
9
+ * `pnpm --filter @agentproto/apps run build:ui:reviews:check` (wired into
10
+ * CI) fails if this file drifts from what the Vite sources actually produce.
11
+ */
12
+ export declare const REVIEW_PANEL_HTML_GENERATED = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<title>agentproto reviews</title>\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\n vite.config.ts's injectPanelBridge plugin \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 \u00B7 spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nvar _hostCaps = null;\nvar _standaloneApp = null;\nfunction _isStandalone(){\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\n}\n// \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Panels that iframe the daemon's standalone app host carry this proof so\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\n// registerMcpApps replaces the placeholder with a real per-boot token when\n// serving the panel as a resource; every other render (tests, standalone\n// tab, VS Code HTTP-iframe panel \u2014 the latter two already pass\n// iframeEmbedAllowed's origin checks) keeps the literal, and\n// withEmbedToken() then degrades to an identity function.\nwindow.__AGENPROTO_EMBED_TOKEN__ = \"__AGENPROTO_EMBED_TOKEN__\";\nfunction withEmbedToken(url){\n var t = window.__AGENPROTO_EMBED_TOKEN__;\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\n}\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\nfunction getHostContext(){ return _hostContext; }\nfunction getHostCapabilities(){ return _hostCaps; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== 'object') return;\n // ui/notifications/host-context-changed carries only the changed keys \u2014\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener('message', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === 'ui/notifications/host-context-changed'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n if (_isStandalone()){\n return window.McpApp.connect().then(function(conn){\n _standaloneApp = conn;\n // No host to advertise a hostContext \u2014 default to inline with no\n // other modes available, which keeps the display-mode toggle buttons\n // hidden (they only show for modes hostContext.availableDisplayModes\n // lists) instead of rendering two buttons with nothing to switch to.\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\n });\n }\n return rpcRequest('ui/initialize', {\n appInfo: {name: \"agentproto-review-panel\", version: '0.1.0'},\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\n protocolVersion: '2026-01-26'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) \u2014 capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 e.g. openLinks,\n // \"Host supports opening external URLs\". Captured once, read via\n // getHostCapabilities() by panels that need to branch on it.\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\n rpcNotify('ui/notifications/initialized', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest('ui/request-display-mode', {mode: mode});\n}\nfunction openLink(url){\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\n return rpcRequest('ui/open-link', {url: url});\n}\nfunction callTool(name, args){\n var raw = _standaloneApp\n ? _standaloneApp.callTool(name, args || {})\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\n return raw.then(function(result){\n if (result.isError){\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\n throw new Error(e);\n }\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\n return JSON.parse(text);\n });\n}\n\n// \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500\n// The installer is inlined verbatim (it is self-contained and idempotent),\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\n// panel stays inline until the user clicks, and the buttons only appear for\n// the modes the host advertises in hostContext.availableDisplayModes.\n(function () {\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\n window.AgentprotoUI = window.AgentprotoUI || {};\n\n var STYLE_ID = \"agentproto-display-mode-style\";\n var BAR_ID = \"agentproto-display-mode-bar\";\n // Gap between the host's safe area (or the viewport edge) and the button.\n var EDGE_GAP = 8;\n\n // Glyph + accessible label per mode. The button is an icon; the words go\n // to title/aria-label so the control stays compact without going mute to\n // a screen reader.\n var LABELS = {\n fullscreen: {\n on: \"\\u2921\", off: \"\\u2922\",\n onLabel: \"R\\u00e9duire\", offLabel: \"Agrandir\"\n },\n pip: {\n on: \"\\u2921\", off: \"\\uD83D\\uDCCC\",\n onLabel: \"D\\u00e9tacher\", offLabel: \"\\u00c9pingler sur le c\\u00f4t\\u00e9 (pip)\"\n }\n };\n\n // Every colour and both offsets resolve through an app-overridable custom\n // property first, so an app can restyle/reposition the button with a\n // stylesheet and never touch this script. The safe-area vars are what the\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\n function ensureStyle() {\n if (!document.head || document.getElementById(STYLE_ID)) return;\n var style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent =\n \"#\" + BAR_ID + \"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\" +\n \"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\" +\n \"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\" +\n \".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\" +\n \"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\" +\n \"cursor:pointer;-webkit-appearance:none;appearance:none;\" +\n \"font:600 14px/1 system-ui,-apple-system,sans-serif;\" +\n \"border:1px solid var(--agentproto-dm-border,#d0d0d0);\" +\n \"background:var(--agentproto-dm-bg,#fff);\" +\n \"color:var(--agentproto-dm-fg,#1a1a1a);\" +\n \"box-shadow:0 1px 4px rgba(0,0,0,.18)}\" +\n \".agentproto-dm-btn:hover{filter:brightness(.95)}\" +\n \".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\" +\n \".agentproto-dm-btn[data-theme=\\\"dark\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\" +\n \"@media (prefers-color-scheme:dark){\" +\n \".agentproto-dm-btn:not([data-theme=\\\"light\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\";\n document.head.appendChild(style);\n }\n\n function ready(fn) {\n if (document.body) fn();\n else document.addEventListener(\"DOMContentLoaded\", fn);\n }\n\n function metaToggle() {\n var el = document.querySelector('meta[name=\"agentproto-display-toggle\"]');\n if (!el) return \"\";\n return (el.getAttribute(\"content\") || \"\").toLowerCase().replace(/^\\s+|\\s+$/g, \"\");\n }\n\n function ensureBar() {\n var bar = document.getElementById(BAR_ID);\n if (bar) return bar;\n bar = document.createElement(\"div\");\n bar.id = BAR_ID;\n document.body.appendChild(bar);\n return bar;\n }\n\n function px(v) {\n return typeof v === \"number\" && isFinite(v) && v > 0 ? v : 0;\n }\n\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\n api = api || {};\n opts = opts || {};\n\n var ctx = {};\n var listeners = [];\n var buttons = [];\n // Modes the host has refused once. Checked before availability, so a\n // later host-context-changed can never resurrect a dead button.\n var refused = {};\n var loggedKeys = false;\n\n var setting = opts.toggle || metaToggle() || \"auto\";\n var optimistic = setting === \"optimistic\";\n var autoMount = setting !== \"none\";\n\n function get() {\n return ctx.displayMode || \"inline\";\n }\n\n function available() {\n var a = ctx.availableDisplayModes;\n return a && a.slice ? a.slice() : [];\n }\n\n function supports(mode) {\n if (refused[mode]) return false;\n var a = ctx.availableDisplayModes;\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\n // Optimistic: the host advertises nothing (Claude Desktop, some\n // embedders) but may still honour the request. Fullscreen only \u2014 pip\n // is niche enough that guessing at it is just noise.\n return optimistic && mode === \"fullscreen\";\n }\n\n // Host chrome to stay clear of. Published as custom properties rather\n // than inline styles so an app's own --agentproto-dm-top/right still\n // wins (an inline style would beat any stylesheet it could write).\n function applyInsets() {\n var insets = ctx.safeAreaInsets || {};\n var root = document.documentElement;\n if (!root || !root.style || !root.style.setProperty) return;\n root.style.setProperty(\"--agentproto-dm-safe-top\", (px(insets.top) + EDGE_GAP) + \"px\");\n root.style.setProperty(\"--agentproto-dm-safe-right\", (px(insets.right) + EDGE_GAP) + \"px\");\n }\n\n function syncButton(btn) {\n var mode = btn.getAttribute(\"data-agentproto-dm\") || \"fullscreen\";\n var labels = LABELS[mode] || LABELS.fullscreen;\n var active = get() === mode;\n btn.style.display = supports(mode) ? \"\" : \"none\";\n btn.textContent = active ? labels.on : labels.off;\n var label = active ? labels.onLabel : labels.offLabel;\n btn.title = label;\n btn.setAttribute(\"aria-label\", label);\n btn.setAttribute(\"aria-pressed\", active ? \"true\" : \"false\");\n if (ctx.theme === \"dark\" || ctx.theme === \"light\") btn.setAttribute(\"data-theme\", ctx.theme);\n else btn.removeAttribute(\"data-theme\");\n }\n\n function sync() {\n applyInsets();\n for (var i = buttons.length - 1; i >= 0; i--) {\n // An app that removed its inline toggle stops being our problem.\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\n syncButton(buttons[i]);\n }\n }\n\n function emit() {\n var mode = get();\n for (var i = 0; i < listeners.length; i++) {\n try { listeners[i](mode, ctx); } catch (_) {}\n }\n }\n\n // host-context-changed carries only the changed keys \u2014 merge, matching\n // the official ext-apps App behaviour.\n function setContext(next) {\n if (!next || typeof next !== \"object\") return;\n for (var k in next) {\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\n }\n sync();\n emit();\n }\n\n function retire(mode) {\n refused[mode] = true;\n sync();\n }\n\n function request(mode) {\n var pending;\n try {\n pending = api.requestDisplayMode\n ? api.requestDisplayMode(mode)\n : Promise.reject(new Error(\"display mode: no host\"));\n } catch (err) {\n pending = Promise.reject(err);\n }\n return Promise.resolve(pending).then(function (result) {\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\n // the spec lets it differ from the one asked for \u2014 so a host that\n // answers \"still inline\" is a refusal, not a success.\n var settled = (result && result.mode) || mode;\n if (optimistic && mode !== \"inline\" && settled !== mode) {\n retire(mode);\n throw new Error(\"host refused display mode: \" + mode);\n }\n // Hosts are not required to follow up with host-context-changed \u2014\n // trust the result so the glyph flips either way.\n setContext({ displayMode: settled });\n return settled;\n }, function (err) {\n if (optimistic && mode !== \"inline\") retire(mode);\n throw err;\n });\n }\n\n function makeButton(mode) {\n var btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.className = \"agentproto-dm-btn\";\n // Stable id for the floating pair (styling hooks, tests). An app that\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\n var id = mode === \"fullscreen\"\n ? \"agentproto-display-mode\"\n : \"agentproto-display-mode-\" + mode;\n if (!document.getElementById(id)) btn.id = id;\n btn.setAttribute(\"data-agentproto-dm\", mode);\n btn.style.display = \"none\";\n btn.addEventListener(\"click\", function () {\n var cur = get();\n // Same rule both copies had: anything that isn't inline collapses\n // back to inline, so the button is always an escape hatch.\n var inPanel = cur === \"fullscreen\" || cur === \"pip\";\n request(inPanel ? \"inline\" : mode)[\"catch\"](function () {});\n });\n return btn;\n }\n\n function mountToggle(el, mode) {\n ensureStyle();\n mode = mode || \"fullscreen\";\n var btn = makeButton(mode);\n if (el) el.appendChild(btn);\n else ensureBar().appendChild(btn);\n buttons.push(btn);\n syncButton(btn);\n return btn;\n }\n\n function onChange(cb) {\n if (typeof cb !== \"function\") return function () {};\n listeners.push(cb);\n try { cb(get(), ctx); } catch (_) {}\n return function () {\n var i = listeners.indexOf(cb);\n if (i >= 0) listeners.splice(i, 1);\n };\n }\n\n ready(function () {\n ensureStyle();\n // Only the first installer in a document owns the floating bar: a\n // built-in panel served standalone gets the injected window.McpApp\n // bridge's copy AND its own, and two stacked buttons is a bug.\n if (autoMount && !document.getElementById(BAR_ID)) {\n ensureBar();\n mountToggle(null, \"pip\");\n mountToggle(null, \"fullscreen\");\n }\n sync();\n });\n\n // The diagnostic both copies had, now on BOTH paths: it used to exist\n // only in the built-in panel bridge, so an installed app debugged in\n // Claude Desktop (which advertises no fullscreen, hence no button)\n // printed nothing at all and left the user with no way to tell \"the\n // host refuses\" from \"the toggle is broken\".\n function observe(next) {\n console.log('[mcp-app] displayMode=', next && next.displayMode,\n 'availableDisplayModes=', next && next.availableDisplayModes);\n setContext(next);\n // What ELSE does this host send? Keys only, once \u2014 a host context can\n // carry toolInfo (the instantiating tool call's arguments) and other\n // host-private fields, so the names are the diagnostic and the values\n // stay out of the console.\n if (!loggedKeys && next && typeof next === \"object\") {\n loggedKeys = true;\n var keys = [];\n for (var k in ctx) {\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\n }\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\n }\n }\n\n if (typeof api.onHostContext === \"function\") api.onHostContext(observe);\n else if (typeof api.getHostContext === \"function\") observe(api.getHostContext());\n\n return {\n get: get,\n available: available,\n request: request,\n onChange: onChange,\n mountToggle: mountToggle\n };\n };\n})();\nvar displayMode = window.AgentprotoUI.installDisplayMode({\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n requestDisplayMode: requestDisplayMode\n});</script>\n <script>(function() {\n \"use strict\";\n function esc(value) {\n return value.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n }\n function liveSessionUrl(origin, sessionId) {\n return `${origin}/apps/@agentproto/live-session/ui?sessionId=${encodeURIComponent(sessionId)}`;\n }\n function shortRange(baseSha, headSha) {\n if (!baseSha || !headSha) return \"\u2014\";\n return `${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`;\n }\n function ageOf(createdAt, now = Date.now()) {\n const ms = now - new Date(createdAt).getTime();\n if (!Number.isFinite(ms) || ms < 0) return \"just now\";\n const mins = Math.floor(ms / 6e4);\n if (mins < 1) return \"just now\";\n if (mins < 60) return `${mins}m`;\n const hours = Math.floor(mins / 60);\n if (hours < 24) return `${hours}h`;\n return `${Math.floor(hours / 24)}d`;\n }\n function verdictChip(status) {\n return `<span class=\"chip chip-verdict v-${esc(status)}\">${esc(status)}</span>`;\n }\n const LANE_STATUS_LABEL = {\n pass: \"pass\",\n fail: \"fail\",\n skipped: \"skipped\",\n timeout: \"timeout\"\n };\n function laneStatusChip(status) {\n return `<span class=\"chip chip-lane l-${esc(status)}\">${esc(LANE_STATUS_LABEL[status])}</span>`;\n }\n function severityTag(severity) {\n return `<span class=\"tag sev-${esc(severity)}\">${esc(severity)}</span>`;\n }\n function requesterLabel(row) {\n const r = row.requester;\n if (!r) return \"\u2014\";\n const id = r.sessionId ? esc(r.sessionId) : \"\";\n const author = r.gitAuthor ? esc(r.gitAuthor.name) : \"\";\n if (id && author) return `${id} <span class=\"muted\">(${author})</span>`;\n return id || author || \"\u2014\";\n }\n function prLabel(row) {\n if (!row.pr) return \"\u2014\";\n const state = row.prState ? ` <span class=\"tag pr-${esc(row.prState)}\">${esc(row.prState)}</span>` : \"\";\n return `<a href=\"${esc(row.pr.url)}\" target=\"_blank\" rel=\"noopener\">#${row.pr.number}</a>${state}`;\n }\n function renderRow(row) {\n const status = row.status ?? row.verdict ?? \"incomplete\";\n const flags = [];\n if (row.dirty) flags.push('<span class=\"tag t-dirty\">dirty</span>');\n if (row.cached) flags.push('<span class=\"tag t-cached\">cached</span>');\n if (row.status !== \"running\") {\n flags.push(row.signed ? '<span class=\"tag t-signed\">signed</span>' : '<span class=\"tag t-unsigned\">unsigned</span>');\n }\n return `<tr class=\"row\" data-runid=\"${esc(row.runId)}\" tabindex=\"0\"><td>${verdictChip(status)}</td><td class=\"mono\">${esc(shortRange(row.baseSha, row.headSha))}</td><td>${esc(row.binding ?? \"\u2014\")}</td><td class=\"mono\">${esc(row.repoRemote ?? \"\u2014\")}</td><td>${requesterLabel(row)}</td><td>${prLabel(row)}</td><td>${flags.join(\" \") || \"\u2014\"}</td><td>${esc(ageOf(row.createdAt))}</td></tr>`;\n }\n function renderList(rows2) {\n if (rows2.length === 0) return '<div class=\"empty\">No reviews recorded yet.</div>';\n const header = \"<tr><th>Verdict</th><th>Range</th><th>Binding</th><th>Repo</th><th>Requester</th><th>PR</th><th>Flags</th><th>Age</th></tr>\";\n return `<table class=\"list\"><thead>${header}</thead><tbody>${rows2.map(renderRow).join(\"\")}</tbody></table>`;\n }\n function findingLine(f) {\n const loc = f.file ? `${esc(f.file)}${f.line ? `:${f.line}` : \"\"}` : \"\";\n return `<details class=\"finding\"><summary>${severityTag(f.severity)} ${esc(f.title)}${loc ? ` <span class=\"mono muted\">${loc}</span>` : \"\"}</summary><div class=\"finding-detail\">${esc(f.detail)}</div></details>`;\n }\n function renderLaneDetail(lane, rubrics = []) {\n const parts = [];\n parts.push(`<div class=\"lane-hdr\">`);\n parts.push(`<span class=\"lane-id\">${esc(lane.id)}</span>`);\n parts.push(laneStatusChip(lane.status));\n parts.push(`<span class=\"tag ${lane.blocking ? \"t-blocking\" : \"t-advisory\"}\">${lane.blocking ? \"blocking\" : \"advisory\"}</span>`);\n if (lane.durationMs !== void 0) parts.push(`<span class=\"muted\">${(lane.durationMs / 1e3).toFixed(1)}s</span>`);\n parts.push(`</div>`);\n if (lane.error) parts.push(`<div class=\"lane-error\">${esc(lane.error)}</div>`);\n if (lane.sessionId || lane.preset || lane.model) {\n const rubric = rubrics.find((r) => r.check === lane.id);\n const bits = [];\n if (lane.model) bits.push(`model: ${esc(lane.model)}`);\n if (lane.preset) bits.push(`preset: ${esc(lane.preset)}`);\n if (rubric) bits.push(`rubric: <span class=\"mono\">${esc(rubric.sha256.slice(0, 12))}</span>`);\n if (lane.sessionId) {\n bits.push(\n `reviewer: <button class=\"sess-link\" data-session-id=\"${esc(lane.sessionId)}\">${esc(lane.sessionId)}</button>`\n );\n }\n parts.push(`<div class=\"lane-agent-meta\">${bits.join(\" \u00B7 \")}</div>`);\n }\n if (lane.findings.length > 0) parts.push(`<div class=\"findings\">${lane.findings.map(findingLine).join(\"\")}</div>`);\n return `<div class=\"lane\">${parts.join(\"\")}</div>`;\n }\n function renderDetail(detail2) {\n const status = detail2.status === \"done\" ? detail2.verdict ?? \"incomplete\" : detail2.status;\n const header = [`<div class=\"detail-hdr\">`, verdictChip(status), `<span class=\"mono\">${esc(detail2.runId)}</span>`];\n if (detail2.binding) header.push(`<span class=\"tag\">${esc(detail2.binding)}</span>`);\n if (detail2.cached) header.push('<span class=\"tag t-cached\">cached</span>');\n if (detail2.status === \"done\" && detail2.attestation) {\n const sig = detail2.attestation.attestor?.signature;\n header.push(\n sig ? `<span class=\"tag t-signed\" title=\"${esc(sig.keyFingerprint)}\">signed: ${esc(sig.principal)}</span>` : '<span class=\"tag t-unsigned\">unsigned</span>'\n );\n }\n header.push(`</div>`);\n if (detail2.error) header.push(`<div class=\"detail-error\">${esc(detail2.error)}</div>`);\n if (detail2.supersededBy) header.push(`<div class=\"muted\">superseded by ${esc(detail2.supersededBy)}</div>`);\n const fullLanes = detail2.attestation?.lanes;\n const rubrics = detail2.attestation?.rubrics ?? [];\n let body;\n if (fullLanes) {\n body = fullLanes.map((l) => renderLaneDetail(l, rubrics)).join(\"\");\n } else if (detail2.lanes) {\n body = detail2.lanes.map((l) => `<div class=\"lane\"><div class=\"lane-hdr\"><span class=\"lane-id\">${esc(l.id)}</span>${laneStatusChip(l.status)}</div></div>`).join(\"\");\n } else {\n body = \"\";\n }\n return `<div class=\"detail\">${header.join(\"\")}${body}</div>`;\n }\n const POLL_RUNNING_MS = 4e3;\n const POLL_IDLE_MS = 15e3;\n function getEl(id) {\n const el = document.getElementById(id);\n if (!el) throw new Error(`review panel: missing #${id}`);\n return el;\n }\n let rows = [];\n let selectedRunId = null;\n let detail = null;\n let pollTimer = null;\n let loadInFlight = false;\n function boot() {\n const listPaneEl = getEl(\"list-pane\");\n const detailPaneEl = getEl(\"detail-pane\");\n const detailBodyEl = getEl(\"detail-body\");\n const statusbarEl = getEl(\"statusbar\");\n const cancelBtn = getEl(\"cancel-btn\");\n const rerunBtn = getEl(\"rerun-btn\");\n const prBtn = getEl(\"pr-btn\");\n const exportBtn = getEl(\"export-btn\");\n function setStatus(msg) {\n statusbarEl.textContent = msg;\n }\n function findRow(runId) {\n return rows.find((r) => r.runId === runId);\n }\n function renderListPane() {\n listPaneEl.innerHTML = renderList(rows);\n if (!selectedRunId) return;\n listPaneEl.querySelectorAll(\"tr[data-runid]\").forEach((tr) => {\n if (tr.dataset.runid === selectedRunId) tr.classList.add(\"selected\");\n });\n }\n function renderDetailPane() {\n if (!selectedRunId || !detail) {\n detailPaneEl.classList.remove(\"open\");\n return;\n }\n detailPaneEl.classList.add(\"open\");\n detailBodyEl.innerHTML = renderDetail(detail);\n const running = detail.status === \"running\";\n cancelBtn.disabled = !running;\n cancelBtn.style.display = running ? \"\" : \"none\";\n detailBodyEl.querySelectorAll(\".sess-link\").forEach((btn) => {\n btn.addEventListener(\"click\", () => openSession(btn.dataset.sessionId ?? \"\"));\n });\n }\n function openSession(sessionId) {\n if (!sessionId) return;\n const origin = window.location.origin && window.location.origin !== \"null\" ? window.location.origin : \"\";\n const url = liveSessionUrl(origin, sessionId);\n const caps = getHostCapabilities();\n if (caps?.openLinks) {\n openLink(url).catch(() => window.open(url, \"_blank\"));\n } else {\n window.open(url, \"_blank\");\n }\n }\n function loadList() {\n if (loadInFlight) return Promise.resolve();\n loadInFlight = true;\n return callTool(\"review_ledger\", { includeRunning: true }).then((result) => {\n rows = result.attestations;\n renderListPane();\n const runningCount = rows.filter((r) => r.status === \"running\").length;\n setStatus(`${result.total} review${result.total === 1 ? \"\" : \"s\"} \u00B7 ${runningCount} running \u00B7 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\n if (selectedRunId && findRow(selectedRunId)) return loadDetail(selectedRunId);\n return void 0;\n }).catch((e) => setStatus(`Error: ${e.message}`)).finally(() => {\n loadInFlight = false;\n });\n }\n function loadDetail(runId) {\n return callTool(\"review_status\", { runId }).then((result) => {\n detail = result;\n renderDetailPane();\n }).catch((e) => setStatus(`Error loading detail: ${e.message}`));\n }\n function openDetail(runId) {\n selectedRunId = runId;\n renderListPane();\n void loadDetail(runId);\n }\n function closeDetail() {\n selectedRunId = null;\n detail = null;\n renderListPane();\n renderDetailPane();\n }\n listPaneEl.addEventListener(\"click\", (evt) => {\n if (!(evt.target instanceof Element)) return;\n const tr = evt.target.closest(\"tr[data-runid]\");\n if (!(tr instanceof HTMLElement)) return;\n const runId = tr.dataset.runid;\n if (runId) openDetail(runId);\n });\n listPaneEl.addEventListener(\"keydown\", (evt) => {\n if (evt.key !== \"Enter\" && evt.key !== \" \") return;\n if (!(evt.target instanceof Element)) return;\n const tr = evt.target.closest(\"tr[data-runid]\");\n if (!(tr instanceof HTMLElement)) return;\n evt.preventDefault();\n const runId = tr.dataset.runid;\n if (runId) openDetail(runId);\n });\n getEl(\"detail-close\").addEventListener(\"click\", closeDetail);\n getEl(\"refresh-btn\").addEventListener(\"click\", () => {\n void loadList();\n });\n cancelBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_cancel\", { runId: selectedRunId }).then(() => {\n setStatus(\"Cancelled.\");\n return loadList();\n }).catch((e) => setStatus(`Cancel failed: ${e.message}`));\n });\n rerunBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n const row = findRow(selectedRunId);\n const cwd = row?.cwd;\n if (!cwd) {\n setStatus(\"Re-run failed: this row has no known checkout path.\");\n return;\n }\n callTool(\"review_run\", {\n cwd,\n ...row?.binding ? { binding: row.binding } : {},\n nocache: true,\n wait: false,\n supersede: true\n }).then(() => {\n setStatus(\"Re-run started.\");\n return loadList();\n }).catch((e) => setStatus(`Re-run failed: ${e.message}`));\n });\n prBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_pr\", { runId: selectedRunId }).then(() => {\n setStatus(\"Fetched PR status.\");\n return loadList();\n }).catch((e) => setStatus(`Fetch PR status failed: ${e.message}`));\n });\n exportBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_export\", { runId: selectedRunId }).then((result) => {\n setStatus(result.path ? `Exported to ${result.path}` : \"Exported.\");\n }).catch((e) => setStatus(`Export failed: ${e.message}`));\n });\n function doPoll() {\n if (pollTimer) clearTimeout(pollTimer);\n loadList().finally(() => {\n const anyRunning = rows.some((r) => r.status === \"running\");\n pollTimer = setTimeout(doPoll, anyRunning ? POLL_RUNNING_MS : POLL_IDLE_MS);\n });\n }\n initBridge().then(() => loadList()).then(() => {\n pollTimer = setTimeout(doPoll, POLL_RUNNING_MS);\n }).catch((e) => setStatus(`Bridge error: ${e.message}`));\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", boot);\n } else {\n boot();\n }\n})();</script>\n <style rel=\"stylesheet\" crossorigin>/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Semantic tokens + runtime mapping sheets \u2014 same scheme as work-board's\n style.css (see that file's header doc for the two-mapping-sheet\n rationale): every rule below reads ONLY the --rp-* custom properties,\n each resolving through a live --vscode-* var() first, then the\n agentproto brand fallback.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n:root {\n --rp-bg: var(--vscode-editor-background, #f5f6f3);\n --rp-surface: var(--vscode-sideBar-background, #ffffff);\n --rp-ink: var(--vscode-editor-foreground, #171a1e);\n --rp-ink-muted: var(--vscode-descriptionForeground, #5c6470);\n --rp-border: var(--vscode-panel-border, #dde0da);\n --rp-accent: var(--vscode-button-background, #0f62fe);\n\n --rp-pass: var(--vscode-charts-green, #3edc81);\n --rp-block: var(--vscode-charts-red, #fa4d56);\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\n --rp-running: var(--vscode-charts-blue, #4589ff);\n --rp-cancelled: var(--vscode-descriptionForeground, #5c6470);\n --rp-failed: var(--vscode-charts-red, #fa4d56);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --rp-bg: var(--vscode-editor-background, #101214);\n --rp-surface: var(--vscode-sideBar-background, #16191d);\n --rp-ink: var(--vscode-editor-foreground, #e8eaed);\n --rp-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\n --rp-border: var(--vscode-panel-border, #272c33);\n --rp-accent: var(--vscode-button-background, #4589ff);\n\n --rp-pass: var(--vscode-charts-green, #3edc81);\n --rp-block: var(--vscode-charts-red, #fa4d56);\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\n --rp-running: var(--vscode-charts-blue, #4589ff);\n --rp-cancelled: var(--vscode-descriptionForeground, #99a2ad);\n --rp-failed: var(--vscode-charts-red, #fa4d56);\n }\n}\n\n/* \u2500\u2500 Base \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n* { box-sizing: border-box; margin: 0; padding: 0; }\n\nhtml, body {\n height: 100%;\n font-family: \"IBM Plex Sans\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n font-size: 13px;\n background: var(--rp-bg);\n color: var(--rp-ink);\n overflow: hidden;\n}\n\n.mono {\n font-family: \"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, Monaco, \"Courier New\", monospace;\n}\n.muted { color: var(--rp-ink-muted); }\n\n#app { display: flex; flex-direction: column; height: 100%; }\n\n#toolbar {\n padding: 8px 12px;\n border-bottom: 1px solid var(--rp-border);\n display: flex;\n align-items: center;\n gap: 8px;\n background: var(--rp-surface);\n flex-shrink: 0;\n}\n#toolbar-label { font-size: 11px; color: var(--rp-ink-muted); }\n.tbtn {\n background: var(--rp-bg);\n border: 1px solid var(--rp-border);\n color: var(--rp-ink);\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n}\n.tbtn:hover { background: var(--rp-border); }\n.tbtn:disabled { opacity: 0.5; cursor: default; }\n.tbtn.danger { border-color: var(--rp-block); color: var(--rp-block); }\n\n#main { flex: 1; overflow: hidden; display: flex; }\n#list-pane { flex: 1; overflow-y: auto; }\n#detail-pane {\n width: 420px;\n min-width: 320px;\n border-left: 1px solid var(--rp-border);\n overflow-y: auto;\n display: none;\n flex-direction: column;\n}\n#detail-pane.open { display: flex; }\n#detail-close { align-self: flex-end; margin: 6px; }\n\ntable.list { width: 100%; border-collapse: collapse; }\ntable.list th {\n text-align: left;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--rp-ink-muted);\n padding: 6px 8px;\n border-bottom: 1px solid var(--rp-border);\n position: sticky;\n top: 0;\n background: var(--rp-surface);\n}\ntable.list td { padding: 6px 8px; border-bottom: 1px solid var(--rp-border); font-size: 12px; vertical-align: middle; }\ntr.row { cursor: pointer; }\ntr.row:hover, tr.row:focus { background: color-mix(in srgb, var(--rp-accent) 8%, transparent); outline: none; }\ntr.row.selected { background: color-mix(in srgb, var(--rp-accent) 14%, transparent); }\n\n.empty { padding: 24px 12px; color: var(--rp-ink-muted); font-size: 12px; text-align: center; }\n\n.chip { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }\n.chip-verdict.v-pass { background: color-mix(in srgb, var(--rp-pass) 18%, transparent); color: var(--rp-pass); }\n.chip-verdict.v-block { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\n.chip-verdict.v-incomplete { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\n.chip-verdict.v-running { background: color-mix(in srgb, var(--rp-running) 18%, transparent); color: var(--rp-running); }\n.chip-verdict.v-cancelled { background: color-mix(in srgb, var(--rp-cancelled) 15%, transparent); color: var(--rp-cancelled); }\n.chip-verdict.v-failed { background: color-mix(in srgb, var(--rp-failed) 18%, transparent); color: var(--rp-failed); }\n\n.chip-lane.l-pass { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\n.chip-lane.l-fail { background: color-mix(in srgb, var(--rp-block) 15%, transparent); color: var(--rp-block); }\n.chip-lane.l-skipped { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.chip-lane.l-timeout { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\n\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\n.t-dirty { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\n.t-cached { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.t-signed { background: color-mix(in srgb, var(--rp-pass) 12%, transparent); color: var(--rp-pass); }\n.t-unsigned { background: color-mix(in srgb, var(--rp-ink-muted) 10%, transparent); color: var(--rp-ink-muted); }\n.t-blocking { background: color-mix(in srgb, var(--rp-block) 12%, transparent); color: var(--rp-block); }\n.t-advisory { background: color-mix(in srgb, var(--rp-ink-muted) 12%, transparent); color: var(--rp-ink-muted); }\n.pr-open { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\n.pr-merged { background: color-mix(in srgb, var(--rp-accent) 15%, transparent); color: var(--rp-accent); }\n.pr-closed { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.sev-high { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\n.sev-medium { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\n.sev-low { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n\n.detail { padding: 10px; display: flex; flex-direction: column; gap: 10px; }\n.detail-hdr { display: flex; align-items: center; gap: 8px; }\n.detail-error { color: var(--rp-block); font-size: 12px; word-break: break-word; }\n.detail-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 10px 10px; }\n\n.lane {\n border: 1px solid var(--rp-border);\n border-radius: 6px;\n padding: 8px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.lane-hdr { display: flex; align-items: center; gap: 6px; }\n.lane-id { font-weight: 600; font-size: 12px; }\n.lane-error { color: var(--rp-block); font-size: 11px; word-break: break-word; }\n.lane-agent-meta { font-size: 11px; color: var(--rp-ink-muted); }\n.sess-link {\n background: none;\n border: none;\n color: var(--rp-accent);\n cursor: pointer;\n font: inherit;\n padding: 0;\n text-decoration: underline;\n}\n\n.findings { display: flex; flex-direction: column; gap: 4px; }\n.finding summary { cursor: pointer; font-size: 11px; list-style: none; }\n.finding summary::-webkit-details-marker { display: none; }\n.finding-detail { font-size: 11px; color: var(--rp-ink-muted); padding: 4px 0 0 4px; white-space: pre-wrap; word-break: break-word; }\n\n#statusbar {\n padding: 4px 12px;\n font-size: 11px;\n color: var(--rp-ink-muted);\n border-top: 1px solid var(--rp-border);\n background: var(--rp-surface);\n flex-shrink: 0;\n}</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"toolbar\">\n <span id=\"toolbar-label\">Reviews</span>\n <button class=\"tbtn\" id=\"refresh-btn\" title=\"Refresh\">&#8635;</button>\n </div>\n <div id=\"main\">\n <div id=\"list-pane\"></div>\n <div id=\"detail-pane\">\n <button class=\"tbtn\" id=\"detail-close\">&times; Close</button>\n <div id=\"detail-body\"></div>\n <div class=\"detail-actions\">\n <button class=\"tbtn danger\" id=\"cancel-btn\">Cancel</button>\n <button class=\"tbtn\" id=\"rerun-btn\">Re-run fresh</button>\n <button class=\"tbtn\" id=\"pr-btn\">Fetch PR status</button>\n <button class=\"tbtn\" id=\"export-btn\">Export</button>\n </div>\n </div>\n </div>\n <div id=\"statusbar\">Connecting to bridge&#8230;</div>\n</div>\n</body>\n</html>\n";
13
+ //# sourceMappingURL=panel.generated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel.generated.d.ts","sourceRoot":"","sources":["../../src/review-panel/panel.generated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,2BAA2B,u+xCAA2vvC,CAAA"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @agentproto/apps v0.1.0
3
+ * Ready-made agentproto apps (teams of agents + workflows) built on app-kit.
4
+ */
5
+
6
+ // src/review-panel/panel.generated.ts
7
+ var REVIEW_PANEL_HTML_GENERATED = '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8" />\n<meta name="viewport" content="width=device-width, initial-scale=1.0" />\n<title>agentproto reviews</title>\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\n vite.config.ts\'s injectPanelBridge plugin \u2014 a classic (non-module)\n script so its `initBridge`/`callTool` land as globals before the\n deferred module script below runs. -->\n<script>// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500\n// JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nvar _hostCaps = null;\nvar _standaloneApp = null;\nfunction _isStandalone(){\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === \'function\';\n}\n// \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Panels that iframe the daemon\'s standalone app host carry this proof so\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\n// registerMcpApps replaces the placeholder with a real per-boot token when\n// serving the panel as a resource; every other render (tests, standalone\n// tab, VS Code HTTP-iframe panel \u2014 the latter two already pass\n// iframeEmbedAllowed\'s origin checks) keeps the literal, and\n// withEmbedToken() then degrades to an identity function.\nwindow.__AGENPROTO_EMBED_TOKEN__ = "__AGENPROTO_EMBED_TOKEN__";\nfunction withEmbedToken(url){\n var t = window.__AGENPROTO_EMBED_TOKEN__;\n if (!t || t === \'__AGENPROTO_EMBED_TOKEN__\') return url;\n return url + (url.indexOf(\'?\') >= 0 ? \'&\' : \'?\') + \'et=\' + encodeURIComponent(t);\n}\nfunction post(msg){ window.parent.postMessage(msg, \'*\'); }\nfunction getHostContext(){ return _hostContext; }\nfunction getHostCapabilities(){ return _hostCaps; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn\'t stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== \'object\') return;\n // ui/notifications/host-context-changed carries only the changed keys \u2014\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: \'2.0\', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: \'2.0\', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener(\'message\', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== \'object\' || msg.jsonrpc !== \'2.0\') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || (\'rpc error \' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === \'ui/notifications/host-context-changed\'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n if (_isStandalone()){\n return window.McpApp.connect().then(function(conn){\n _standaloneApp = conn;\n // No host to advertise a hostContext \u2014 default to inline with no\n // other modes available, which keeps the display-mode toggle buttons\n // hidden (they only show for modes hostContext.availableDisplayModes\n // lists) instead of rendering two buttons with nothing to switch to.\n _setHostContext({displayMode: \'inline\', availableDisplayModes: []});\n });\n }\n return rpcRequest(\'ui/initialize\', {\n appInfo: {name: "agentproto-review-panel", version: \'0.1.0\'},\n appCapabilities: {availableDisplayModes: [\'inline\', \'fullscreen\', \'pip\']},\n protocolVersion: \'2026-01-26\'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) \u2014 capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 e.g. openLinks,\n // "Host supports opening external URLs". Captured once, read via\n // getHostCapabilities() by panels that need to branch on it.\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\n rpcNotify(\'ui/notifications/initialized\', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest(\'ui/request-display-mode\', {mode: mode});\n}\nfunction openLink(url){\n if (_standaloneApp && typeof _standaloneApp.openLink === \'function\') return _standaloneApp.openLink(url);\n return rpcRequest(\'ui/open-link\', {url: url});\n}\nfunction callTool(name, args){\n var raw = _standaloneApp\n ? _standaloneApp.callTool(name, args || {})\n : rpcRequest(\'tools/call\', {name: name, arguments: args || {}});\n return raw.then(function(result){\n if (result.isError){\n var e = (result.content && result.content[0] && result.content[0].text) || \'tool error\';\n throw new Error(e);\n }\n var text = (result.content && result.content[0] && result.content[0].text) || \'{}\';\n return JSON.parse(text);\n });\n}\n\n// \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500\n// The installer is inlined verbatim (it is self-contained and idempotent),\n// then handed this panel\'s own JSON-RPC plumbing. NO auto-request: the\n// panel stays inline until the user clicks, and the buttons only appear for\n// the modes the host advertises in hostContext.availableDisplayModes.\n(function () {\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\n window.AgentprotoUI = window.AgentprotoUI || {};\n\n var STYLE_ID = "agentproto-display-mode-style";\n var BAR_ID = "agentproto-display-mode-bar";\n // Gap between the host\'s safe area (or the viewport edge) and the button.\n var EDGE_GAP = 8;\n\n // Glyph + accessible label per mode. The button is an icon; the words go\n // to title/aria-label so the control stays compact without going mute to\n // a screen reader.\n var LABELS = {\n fullscreen: {\n on: "\\u2921", off: "\\u2922",\n onLabel: "R\\u00e9duire", offLabel: "Agrandir"\n },\n pip: {\n on: "\\u2921", off: "\\uD83D\\uDCCC",\n onLabel: "D\\u00e9tacher", offLabel: "\\u00c9pingler sur le c\\u00f4t\\u00e9 (pip)"\n }\n };\n\n // Every colour and both offsets resolve through an app-overridable custom\n // property first, so an app can restyle/reposition the button with a\n // stylesheet and never touch this script. The safe-area vars are what the\n // host\'s own chrome feeds (see applyInsets), sitting UNDER the app\'s.\n function ensureStyle() {\n if (!document.head || document.getElementById(STYLE_ID)) return;\n var style = document.createElement("style");\n style.id = STYLE_ID;\n style.textContent =\n "#" + BAR_ID + "{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;" +\n "top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));" +\n "right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}" +\n ".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;" +\n "box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;" +\n "cursor:pointer;-webkit-appearance:none;appearance:none;" +\n "font:600 14px/1 system-ui,-apple-system,sans-serif;" +\n "border:1px solid var(--agentproto-dm-border,#d0d0d0);" +\n "background:var(--agentproto-dm-bg,#fff);" +\n "color:var(--agentproto-dm-fg,#1a1a1a);" +\n "box-shadow:0 1px 4px rgba(0,0,0,.18)}" +\n ".agentproto-dm-btn:hover{filter:brightness(.95)}" +\n ".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}" +\n ".agentproto-dm-btn[data-theme=\\"dark\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}" +\n "@media (prefers-color-scheme:dark){" +\n ".agentproto-dm-btn:not([data-theme=\\"light\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}";\n document.head.appendChild(style);\n }\n\n function ready(fn) {\n if (document.body) fn();\n else document.addEventListener("DOMContentLoaded", fn);\n }\n\n function metaToggle() {\n var el = document.querySelector(\'meta[name="agentproto-display-toggle"]\');\n if (!el) return "";\n return (el.getAttribute("content") || "").toLowerCase().replace(/^\\s+|\\s+$/g, "");\n }\n\n function ensureBar() {\n var bar = document.getElementById(BAR_ID);\n if (bar) return bar;\n bar = document.createElement("div");\n bar.id = BAR_ID;\n document.body.appendChild(bar);\n return bar;\n }\n\n function px(v) {\n return typeof v === "number" && isFinite(v) && v > 0 ? v : 0;\n }\n\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\n api = api || {};\n opts = opts || {};\n\n var ctx = {};\n var listeners = [];\n var buttons = [];\n // Modes the host has refused once. Checked before availability, so a\n // later host-context-changed can never resurrect a dead button.\n var refused = {};\n var loggedKeys = false;\n\n var setting = opts.toggle || metaToggle() || "auto";\n var optimistic = setting === "optimistic";\n var autoMount = setting !== "none";\n\n function get() {\n return ctx.displayMode || "inline";\n }\n\n function available() {\n var a = ctx.availableDisplayModes;\n return a && a.slice ? a.slice() : [];\n }\n\n function supports(mode) {\n if (refused[mode]) return false;\n var a = ctx.availableDisplayModes;\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\n // Optimistic: the host advertises nothing (Claude Desktop, some\n // embedders) but may still honour the request. Fullscreen only \u2014 pip\n // is niche enough that guessing at it is just noise.\n return optimistic && mode === "fullscreen";\n }\n\n // Host chrome to stay clear of. Published as custom properties rather\n // than inline styles so an app\'s own --agentproto-dm-top/right still\n // wins (an inline style would beat any stylesheet it could write).\n function applyInsets() {\n var insets = ctx.safeAreaInsets || {};\n var root = document.documentElement;\n if (!root || !root.style || !root.style.setProperty) return;\n root.style.setProperty("--agentproto-dm-safe-top", (px(insets.top) + EDGE_GAP) + "px");\n root.style.setProperty("--agentproto-dm-safe-right", (px(insets.right) + EDGE_GAP) + "px");\n }\n\n function syncButton(btn) {\n var mode = btn.getAttribute("data-agentproto-dm") || "fullscreen";\n var labels = LABELS[mode] || LABELS.fullscreen;\n var active = get() === mode;\n btn.style.display = supports(mode) ? "" : "none";\n btn.textContent = active ? labels.on : labels.off;\n var label = active ? labels.onLabel : labels.offLabel;\n btn.title = label;\n btn.setAttribute("aria-label", label);\n btn.setAttribute("aria-pressed", active ? "true" : "false");\n if (ctx.theme === "dark" || ctx.theme === "light") btn.setAttribute("data-theme", ctx.theme);\n else btn.removeAttribute("data-theme");\n }\n\n function sync() {\n applyInsets();\n for (var i = buttons.length - 1; i >= 0; i--) {\n // An app that removed its inline toggle stops being our problem.\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\n syncButton(buttons[i]);\n }\n }\n\n function emit() {\n var mode = get();\n for (var i = 0; i < listeners.length; i++) {\n try { listeners[i](mode, ctx); } catch (_) {}\n }\n }\n\n // host-context-changed carries only the changed keys \u2014 merge, matching\n // the official ext-apps App behaviour.\n function setContext(next) {\n if (!next || typeof next !== "object") return;\n for (var k in next) {\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\n }\n sync();\n emit();\n }\n\n function retire(mode) {\n refused[mode] = true;\n sync();\n }\n\n function request(mode) {\n var pending;\n try {\n pending = api.requestDisplayMode\n ? api.requestDisplayMode(mode)\n : Promise.reject(new Error("display mode: no host"));\n } catch (err) {\n pending = Promise.reject(err);\n }\n return Promise.resolve(pending).then(function (result) {\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\n // the spec lets it differ from the one asked for \u2014 so a host that\n // answers "still inline" is a refusal, not a success.\n var settled = (result && result.mode) || mode;\n if (optimistic && mode !== "inline" && settled !== mode) {\n retire(mode);\n throw new Error("host refused display mode: " + mode);\n }\n // Hosts are not required to follow up with host-context-changed \u2014\n // trust the result so the glyph flips either way.\n setContext({ displayMode: settled });\n return settled;\n }, function (err) {\n if (optimistic && mode !== "inline") retire(mode);\n throw err;\n });\n }\n\n function makeButton(mode) {\n var btn = document.createElement("button");\n btn.type = "button";\n btn.className = "agentproto-dm-btn";\n // Stable id for the floating pair (styling hooks, tests). An app that\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\n var id = mode === "fullscreen"\n ? "agentproto-display-mode"\n : "agentproto-display-mode-" + mode;\n if (!document.getElementById(id)) btn.id = id;\n btn.setAttribute("data-agentproto-dm", mode);\n btn.style.display = "none";\n btn.addEventListener("click", function () {\n var cur = get();\n // Same rule both copies had: anything that isn\'t inline collapses\n // back to inline, so the button is always an escape hatch.\n var inPanel = cur === "fullscreen" || cur === "pip";\n request(inPanel ? "inline" : mode)["catch"](function () {});\n });\n return btn;\n }\n\n function mountToggle(el, mode) {\n ensureStyle();\n mode = mode || "fullscreen";\n var btn = makeButton(mode);\n if (el) el.appendChild(btn);\n else ensureBar().appendChild(btn);\n buttons.push(btn);\n syncButton(btn);\n return btn;\n }\n\n function onChange(cb) {\n if (typeof cb !== "function") return function () {};\n listeners.push(cb);\n try { cb(get(), ctx); } catch (_) {}\n return function () {\n var i = listeners.indexOf(cb);\n if (i >= 0) listeners.splice(i, 1);\n };\n }\n\n ready(function () {\n ensureStyle();\n // Only the first installer in a document owns the floating bar: a\n // built-in panel served standalone gets the injected window.McpApp\n // bridge\'s copy AND its own, and two stacked buttons is a bug.\n if (autoMount && !document.getElementById(BAR_ID)) {\n ensureBar();\n mountToggle(null, "pip");\n mountToggle(null, "fullscreen");\n }\n sync();\n });\n\n // The diagnostic both copies had, now on BOTH paths: it used to exist\n // only in the built-in panel bridge, so an installed app debugged in\n // Claude Desktop (which advertises no fullscreen, hence no button)\n // printed nothing at all and left the user with no way to tell "the\n // host refuses" from "the toggle is broken".\n function observe(next) {\n console.log(\'[mcp-app] displayMode=\', next && next.displayMode,\n \'availableDisplayModes=\', next && next.availableDisplayModes);\n setContext(next);\n // What ELSE does this host send? Keys only, once \u2014 a host context can\n // carry toolInfo (the instantiating tool call\'s arguments) and other\n // host-private fields, so the names are the diagnostic and the values\n // stay out of the console.\n if (!loggedKeys && next && typeof next === "object") {\n loggedKeys = true;\n var keys = [];\n for (var k in ctx) {\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\n }\n console.log(\'[mcp-app] hostContext keys=\', keys.sort().join(\',\'));\n }\n }\n\n if (typeof api.onHostContext === "function") api.onHostContext(observe);\n else if (typeof api.getHostContext === "function") observe(api.getHostContext());\n\n return {\n get: get,\n available: available,\n request: request,\n onChange: onChange,\n mountToggle: mountToggle\n };\n };\n})();\nvar displayMode = window.AgentprotoUI.installDisplayMode({\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n requestDisplayMode: requestDisplayMode\n});</script>\n <script>(function() {\n "use strict";\n function esc(value) {\n return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");\n }\n function liveSessionUrl(origin, sessionId) {\n return `${origin}/apps/@agentproto/live-session/ui?sessionId=${encodeURIComponent(sessionId)}`;\n }\n function shortRange(baseSha, headSha) {\n if (!baseSha || !headSha) return "\u2014";\n return `${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`;\n }\n function ageOf(createdAt, now = Date.now()) {\n const ms = now - new Date(createdAt).getTime();\n if (!Number.isFinite(ms) || ms < 0) return "just now";\n const mins = Math.floor(ms / 6e4);\n if (mins < 1) return "just now";\n if (mins < 60) return `${mins}m`;\n const hours = Math.floor(mins / 60);\n if (hours < 24) return `${hours}h`;\n return `${Math.floor(hours / 24)}d`;\n }\n function verdictChip(status) {\n return `<span class="chip chip-verdict v-${esc(status)}">${esc(status)}</span>`;\n }\n const LANE_STATUS_LABEL = {\n pass: "pass",\n fail: "fail",\n skipped: "skipped",\n timeout: "timeout"\n };\n function laneStatusChip(status) {\n return `<span class="chip chip-lane l-${esc(status)}">${esc(LANE_STATUS_LABEL[status])}</span>`;\n }\n function severityTag(severity) {\n return `<span class="tag sev-${esc(severity)}">${esc(severity)}</span>`;\n }\n function requesterLabel(row) {\n const r = row.requester;\n if (!r) return "\u2014";\n const id = r.sessionId ? esc(r.sessionId) : "";\n const author = r.gitAuthor ? esc(r.gitAuthor.name) : "";\n if (id && author) return `${id} <span class="muted">(${author})</span>`;\n return id || author || "\u2014";\n }\n function prLabel(row) {\n if (!row.pr) return "\u2014";\n const state = row.prState ? ` <span class="tag pr-${esc(row.prState)}">${esc(row.prState)}</span>` : "";\n return `<a href="${esc(row.pr.url)}" target="_blank" rel="noopener">#${row.pr.number}</a>${state}`;\n }\n function renderRow(row) {\n const status = row.status ?? row.verdict ?? "incomplete";\n const flags = [];\n if (row.dirty) flags.push(\'<span class="tag t-dirty">dirty</span>\');\n if (row.cached) flags.push(\'<span class="tag t-cached">cached</span>\');\n if (row.status !== "running") {\n flags.push(row.signed ? \'<span class="tag t-signed">signed</span>\' : \'<span class="tag t-unsigned">unsigned</span>\');\n }\n return `<tr class="row" data-runid="${esc(row.runId)}" tabindex="0"><td>${verdictChip(status)}</td><td class="mono">${esc(shortRange(row.baseSha, row.headSha))}</td><td>${esc(row.binding ?? "\u2014")}</td><td class="mono">${esc(row.repoRemote ?? "\u2014")}</td><td>${requesterLabel(row)}</td><td>${prLabel(row)}</td><td>${flags.join(" ") || "\u2014"}</td><td>${esc(ageOf(row.createdAt))}</td></tr>`;\n }\n function renderList(rows2) {\n if (rows2.length === 0) return \'<div class="empty">No reviews recorded yet.</div>\';\n const header = "<tr><th>Verdict</th><th>Range</th><th>Binding</th><th>Repo</th><th>Requester</th><th>PR</th><th>Flags</th><th>Age</th></tr>";\n return `<table class="list"><thead>${header}</thead><tbody>${rows2.map(renderRow).join("")}</tbody></table>`;\n }\n function findingLine(f) {\n const loc = f.file ? `${esc(f.file)}${f.line ? `:${f.line}` : ""}` : "";\n return `<details class="finding"><summary>${severityTag(f.severity)} ${esc(f.title)}${loc ? ` <span class="mono muted">${loc}</span>` : ""}</summary><div class="finding-detail">${esc(f.detail)}</div></details>`;\n }\n function renderLaneDetail(lane, rubrics = []) {\n const parts = [];\n parts.push(`<div class="lane-hdr">`);\n parts.push(`<span class="lane-id">${esc(lane.id)}</span>`);\n parts.push(laneStatusChip(lane.status));\n parts.push(`<span class="tag ${lane.blocking ? "t-blocking" : "t-advisory"}">${lane.blocking ? "blocking" : "advisory"}</span>`);\n if (lane.durationMs !== void 0) parts.push(`<span class="muted">${(lane.durationMs / 1e3).toFixed(1)}s</span>`);\n parts.push(`</div>`);\n if (lane.error) parts.push(`<div class="lane-error">${esc(lane.error)}</div>`);\n if (lane.sessionId || lane.preset || lane.model) {\n const rubric = rubrics.find((r) => r.check === lane.id);\n const bits = [];\n if (lane.model) bits.push(`model: ${esc(lane.model)}`);\n if (lane.preset) bits.push(`preset: ${esc(lane.preset)}`);\n if (rubric) bits.push(`rubric: <span class="mono">${esc(rubric.sha256.slice(0, 12))}</span>`);\n if (lane.sessionId) {\n bits.push(\n `reviewer: <button class="sess-link" data-session-id="${esc(lane.sessionId)}">${esc(lane.sessionId)}</button>`\n );\n }\n parts.push(`<div class="lane-agent-meta">${bits.join(" \xB7 ")}</div>`);\n }\n if (lane.findings.length > 0) parts.push(`<div class="findings">${lane.findings.map(findingLine).join("")}</div>`);\n return `<div class="lane">${parts.join("")}</div>`;\n }\n function renderDetail(detail2) {\n const status = detail2.status === "done" ? detail2.verdict ?? "incomplete" : detail2.status;\n const header = [`<div class="detail-hdr">`, verdictChip(status), `<span class="mono">${esc(detail2.runId)}</span>`];\n if (detail2.binding) header.push(`<span class="tag">${esc(detail2.binding)}</span>`);\n if (detail2.cached) header.push(\'<span class="tag t-cached">cached</span>\');\n if (detail2.status === "done" && detail2.attestation) {\n const sig = detail2.attestation.attestor?.signature;\n header.push(\n sig ? `<span class="tag t-signed" title="${esc(sig.keyFingerprint)}">signed: ${esc(sig.principal)}</span>` : \'<span class="tag t-unsigned">unsigned</span>\'\n );\n }\n header.push(`</div>`);\n if (detail2.error) header.push(`<div class="detail-error">${esc(detail2.error)}</div>`);\n if (detail2.supersededBy) header.push(`<div class="muted">superseded by ${esc(detail2.supersededBy)}</div>`);\n const fullLanes = detail2.attestation?.lanes;\n const rubrics = detail2.attestation?.rubrics ?? [];\n let body;\n if (fullLanes) {\n body = fullLanes.map((l) => renderLaneDetail(l, rubrics)).join("");\n } else if (detail2.lanes) {\n body = detail2.lanes.map((l) => `<div class="lane"><div class="lane-hdr"><span class="lane-id">${esc(l.id)}</span>${laneStatusChip(l.status)}</div></div>`).join("");\n } else {\n body = "";\n }\n return `<div class="detail">${header.join("")}${body}</div>`;\n }\n const POLL_RUNNING_MS = 4e3;\n const POLL_IDLE_MS = 15e3;\n function getEl(id) {\n const el = document.getElementById(id);\n if (!el) throw new Error(`review panel: missing #${id}`);\n return el;\n }\n let rows = [];\n let selectedRunId = null;\n let detail = null;\n let pollTimer = null;\n let loadInFlight = false;\n function boot() {\n const listPaneEl = getEl("list-pane");\n const detailPaneEl = getEl("detail-pane");\n const detailBodyEl = getEl("detail-body");\n const statusbarEl = getEl("statusbar");\n const cancelBtn = getEl("cancel-btn");\n const rerunBtn = getEl("rerun-btn");\n const prBtn = getEl("pr-btn");\n const exportBtn = getEl("export-btn");\n function setStatus(msg) {\n statusbarEl.textContent = msg;\n }\n function findRow(runId) {\n return rows.find((r) => r.runId === runId);\n }\n function renderListPane() {\n listPaneEl.innerHTML = renderList(rows);\n if (!selectedRunId) return;\n listPaneEl.querySelectorAll("tr[data-runid]").forEach((tr) => {\n if (tr.dataset.runid === selectedRunId) tr.classList.add("selected");\n });\n }\n function renderDetailPane() {\n if (!selectedRunId || !detail) {\n detailPaneEl.classList.remove("open");\n return;\n }\n detailPaneEl.classList.add("open");\n detailBodyEl.innerHTML = renderDetail(detail);\n const running = detail.status === "running";\n cancelBtn.disabled = !running;\n cancelBtn.style.display = running ? "" : "none";\n detailBodyEl.querySelectorAll(".sess-link").forEach((btn) => {\n btn.addEventListener("click", () => openSession(btn.dataset.sessionId ?? ""));\n });\n }\n function openSession(sessionId) {\n if (!sessionId) return;\n const origin = window.location.origin && window.location.origin !== "null" ? window.location.origin : "";\n const url = liveSessionUrl(origin, sessionId);\n const caps = getHostCapabilities();\n if (caps?.openLinks) {\n openLink(url).catch(() => window.open(url, "_blank"));\n } else {\n window.open(url, "_blank");\n }\n }\n function loadList() {\n if (loadInFlight) return Promise.resolve();\n loadInFlight = true;\n return callTool("review_ledger", { includeRunning: true }).then((result) => {\n rows = result.attestations;\n renderListPane();\n const runningCount = rows.filter((r) => r.status === "running").length;\n setStatus(`${result.total} review${result.total === 1 ? "" : "s"} \xB7 ${runningCount} running \xB7 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\n if (selectedRunId && findRow(selectedRunId)) return loadDetail(selectedRunId);\n return void 0;\n }).catch((e) => setStatus(`Error: ${e.message}`)).finally(() => {\n loadInFlight = false;\n });\n }\n function loadDetail(runId) {\n return callTool("review_status", { runId }).then((result) => {\n detail = result;\n renderDetailPane();\n }).catch((e) => setStatus(`Error loading detail: ${e.message}`));\n }\n function openDetail(runId) {\n selectedRunId = runId;\n renderListPane();\n void loadDetail(runId);\n }\n function closeDetail() {\n selectedRunId = null;\n detail = null;\n renderListPane();\n renderDetailPane();\n }\n listPaneEl.addEventListener("click", (evt) => {\n if (!(evt.target instanceof Element)) return;\n const tr = evt.target.closest("tr[data-runid]");\n if (!(tr instanceof HTMLElement)) return;\n const runId = tr.dataset.runid;\n if (runId) openDetail(runId);\n });\n listPaneEl.addEventListener("keydown", (evt) => {\n if (evt.key !== "Enter" && evt.key !== " ") return;\n if (!(evt.target instanceof Element)) return;\n const tr = evt.target.closest("tr[data-runid]");\n if (!(tr instanceof HTMLElement)) return;\n evt.preventDefault();\n const runId = tr.dataset.runid;\n if (runId) openDetail(runId);\n });\n getEl("detail-close").addEventListener("click", closeDetail);\n getEl("refresh-btn").addEventListener("click", () => {\n void loadList();\n });\n cancelBtn.addEventListener("click", () => {\n if (!selectedRunId) return;\n callTool("review_cancel", { runId: selectedRunId }).then(() => {\n setStatus("Cancelled.");\n return loadList();\n }).catch((e) => setStatus(`Cancel failed: ${e.message}`));\n });\n rerunBtn.addEventListener("click", () => {\n if (!selectedRunId) return;\n const row = findRow(selectedRunId);\n const cwd = row?.cwd;\n if (!cwd) {\n setStatus("Re-run failed: this row has no known checkout path.");\n return;\n }\n callTool("review_run", {\n cwd,\n ...row?.binding ? { binding: row.binding } : {},\n nocache: true,\n wait: false,\n supersede: true\n }).then(() => {\n setStatus("Re-run started.");\n return loadList();\n }).catch((e) => setStatus(`Re-run failed: ${e.message}`));\n });\n prBtn.addEventListener("click", () => {\n if (!selectedRunId) return;\n callTool("review_pr", { runId: selectedRunId }).then(() => {\n setStatus("Fetched PR status.");\n return loadList();\n }).catch((e) => setStatus(`Fetch PR status failed: ${e.message}`));\n });\n exportBtn.addEventListener("click", () => {\n if (!selectedRunId) return;\n callTool("review_export", { runId: selectedRunId }).then((result) => {\n setStatus(result.path ? `Exported to ${result.path}` : "Exported.");\n }).catch((e) => setStatus(`Export failed: ${e.message}`));\n });\n function doPoll() {\n if (pollTimer) clearTimeout(pollTimer);\n loadList().finally(() => {\n const anyRunning = rows.some((r) => r.status === "running");\n pollTimer = setTimeout(doPoll, anyRunning ? POLL_RUNNING_MS : POLL_IDLE_MS);\n });\n }\n initBridge().then(() => loadList()).then(() => {\n pollTimer = setTimeout(doPoll, POLL_RUNNING_MS);\n }).catch((e) => setStatus(`Bridge error: ${e.message}`));\n }\n if (document.readyState === "loading") {\n document.addEventListener("DOMContentLoaded", boot);\n } else {\n boot();\n }\n})();</script>\n <style rel="stylesheet" crossorigin>/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Semantic tokens + runtime mapping sheets \u2014 same scheme as work-board\'s\n style.css (see that file\'s header doc for the two-mapping-sheet\n rationale): every rule below reads ONLY the --rp-* custom properties,\n each resolving through a live --vscode-* var() first, then the\n agentproto brand fallback.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n:root {\n --rp-bg: var(--vscode-editor-background, #f5f6f3);\n --rp-surface: var(--vscode-sideBar-background, #ffffff);\n --rp-ink: var(--vscode-editor-foreground, #171a1e);\n --rp-ink-muted: var(--vscode-descriptionForeground, #5c6470);\n --rp-border: var(--vscode-panel-border, #dde0da);\n --rp-accent: var(--vscode-button-background, #0f62fe);\n\n --rp-pass: var(--vscode-charts-green, #3edc81);\n --rp-block: var(--vscode-charts-red, #fa4d56);\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\n --rp-running: var(--vscode-charts-blue, #4589ff);\n --rp-cancelled: var(--vscode-descriptionForeground, #5c6470);\n --rp-failed: var(--vscode-charts-red, #fa4d56);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --rp-bg: var(--vscode-editor-background, #101214);\n --rp-surface: var(--vscode-sideBar-background, #16191d);\n --rp-ink: var(--vscode-editor-foreground, #e8eaed);\n --rp-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\n --rp-border: var(--vscode-panel-border, #272c33);\n --rp-accent: var(--vscode-button-background, #4589ff);\n\n --rp-pass: var(--vscode-charts-green, #3edc81);\n --rp-block: var(--vscode-charts-red, #fa4d56);\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\n --rp-running: var(--vscode-charts-blue, #4589ff);\n --rp-cancelled: var(--vscode-descriptionForeground, #99a2ad);\n --rp-failed: var(--vscode-charts-red, #fa4d56);\n }\n}\n\n/* \u2500\u2500 Base \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n* { box-sizing: border-box; margin: 0; padding: 0; }\n\nhtml, body {\n height: 100%;\n font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;\n font-size: 13px;\n background: var(--rp-bg);\n color: var(--rp-ink);\n overflow: hidden;\n}\n\n.mono {\n font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace;\n}\n.muted { color: var(--rp-ink-muted); }\n\n#app { display: flex; flex-direction: column; height: 100%; }\n\n#toolbar {\n padding: 8px 12px;\n border-bottom: 1px solid var(--rp-border);\n display: flex;\n align-items: center;\n gap: 8px;\n background: var(--rp-surface);\n flex-shrink: 0;\n}\n#toolbar-label { font-size: 11px; color: var(--rp-ink-muted); }\n.tbtn {\n background: var(--rp-bg);\n border: 1px solid var(--rp-border);\n color: var(--rp-ink);\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n}\n.tbtn:hover { background: var(--rp-border); }\n.tbtn:disabled { opacity: 0.5; cursor: default; }\n.tbtn.danger { border-color: var(--rp-block); color: var(--rp-block); }\n\n#main { flex: 1; overflow: hidden; display: flex; }\n#list-pane { flex: 1; overflow-y: auto; }\n#detail-pane {\n width: 420px;\n min-width: 320px;\n border-left: 1px solid var(--rp-border);\n overflow-y: auto;\n display: none;\n flex-direction: column;\n}\n#detail-pane.open { display: flex; }\n#detail-close { align-self: flex-end; margin: 6px; }\n\ntable.list { width: 100%; border-collapse: collapse; }\ntable.list th {\n text-align: left;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--rp-ink-muted);\n padding: 6px 8px;\n border-bottom: 1px solid var(--rp-border);\n position: sticky;\n top: 0;\n background: var(--rp-surface);\n}\ntable.list td { padding: 6px 8px; border-bottom: 1px solid var(--rp-border); font-size: 12px; vertical-align: middle; }\ntr.row { cursor: pointer; }\ntr.row:hover, tr.row:focus { background: color-mix(in srgb, var(--rp-accent) 8%, transparent); outline: none; }\ntr.row.selected { background: color-mix(in srgb, var(--rp-accent) 14%, transparent); }\n\n.empty { padding: 24px 12px; color: var(--rp-ink-muted); font-size: 12px; text-align: center; }\n\n.chip { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }\n.chip-verdict.v-pass { background: color-mix(in srgb, var(--rp-pass) 18%, transparent); color: var(--rp-pass); }\n.chip-verdict.v-block { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\n.chip-verdict.v-incomplete { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\n.chip-verdict.v-running { background: color-mix(in srgb, var(--rp-running) 18%, transparent); color: var(--rp-running); }\n.chip-verdict.v-cancelled { background: color-mix(in srgb, var(--rp-cancelled) 15%, transparent); color: var(--rp-cancelled); }\n.chip-verdict.v-failed { background: color-mix(in srgb, var(--rp-failed) 18%, transparent); color: var(--rp-failed); }\n\n.chip-lane.l-pass { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\n.chip-lane.l-fail { background: color-mix(in srgb, var(--rp-block) 15%, transparent); color: var(--rp-block); }\n.chip-lane.l-skipped { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.chip-lane.l-timeout { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\n\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\n.t-dirty { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\n.t-cached { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.t-signed { background: color-mix(in srgb, var(--rp-pass) 12%, transparent); color: var(--rp-pass); }\n.t-unsigned { background: color-mix(in srgb, var(--rp-ink-muted) 10%, transparent); color: var(--rp-ink-muted); }\n.t-blocking { background: color-mix(in srgb, var(--rp-block) 12%, transparent); color: var(--rp-block); }\n.t-advisory { background: color-mix(in srgb, var(--rp-ink-muted) 12%, transparent); color: var(--rp-ink-muted); }\n.pr-open { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\n.pr-merged { background: color-mix(in srgb, var(--rp-accent) 15%, transparent); color: var(--rp-accent); }\n.pr-closed { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.sev-high { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\n.sev-medium { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\n.sev-low { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n\n.detail { padding: 10px; display: flex; flex-direction: column; gap: 10px; }\n.detail-hdr { display: flex; align-items: center; gap: 8px; }\n.detail-error { color: var(--rp-block); font-size: 12px; word-break: break-word; }\n.detail-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 10px 10px; }\n\n.lane {\n border: 1px solid var(--rp-border);\n border-radius: 6px;\n padding: 8px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.lane-hdr { display: flex; align-items: center; gap: 6px; }\n.lane-id { font-weight: 600; font-size: 12px; }\n.lane-error { color: var(--rp-block); font-size: 11px; word-break: break-word; }\n.lane-agent-meta { font-size: 11px; color: var(--rp-ink-muted); }\n.sess-link {\n background: none;\n border: none;\n color: var(--rp-accent);\n cursor: pointer;\n font: inherit;\n padding: 0;\n text-decoration: underline;\n}\n\n.findings { display: flex; flex-direction: column; gap: 4px; }\n.finding summary { cursor: pointer; font-size: 11px; list-style: none; }\n.finding summary::-webkit-details-marker { display: none; }\n.finding-detail { font-size: 11px; color: var(--rp-ink-muted); padding: 4px 0 0 4px; white-space: pre-wrap; word-break: break-word; }\n\n#statusbar {\n padding: 4px 12px;\n font-size: 11px;\n color: var(--rp-ink-muted);\n border-top: 1px solid var(--rp-border);\n background: var(--rp-surface);\n flex-shrink: 0;\n}</style>\n</head>\n<body>\n<div id="app">\n <div id="toolbar">\n <span id="toolbar-label">Reviews</span>\n <button class="tbtn" id="refresh-btn" title="Refresh">&#8635;</button>\n </div>\n <div id="main">\n <div id="list-pane"></div>\n <div id="detail-pane">\n <button class="tbtn" id="detail-close">&times; Close</button>\n <div id="detail-body"></div>\n <div class="detail-actions">\n <button class="tbtn danger" id="cancel-btn">Cancel</button>\n <button class="tbtn" id="rerun-btn">Re-run fresh</button>\n <button class="tbtn" id="pr-btn">Fetch PR status</button>\n <button class="tbtn" id="export-btn">Export</button>\n </div>\n </div>\n </div>\n <div id="statusbar">Connecting to bridge&#8230;</div>\n</div>\n</body>\n</html>\n';
8
+
9
+ // src/review-panel/panel.ts
10
+ var REVIEW_PANEL_APP_ID = "@agentproto/review-panel";
11
+ var REVIEW_PANEL_TOOL_ID = "agentproto_reviews";
12
+ var REVIEW_PANEL_UI_TOOLS = [
13
+ "review_ledger",
14
+ "review_status",
15
+ "review_cancel",
16
+ "review_run",
17
+ "review_pr",
18
+ "review_export"
19
+ ];
20
+ var REVIEW_PANEL_HTML = REVIEW_PANEL_HTML_GENERATED;
21
+
22
+ export { REVIEW_PANEL_APP_ID, REVIEW_PANEL_HTML, REVIEW_PANEL_TOOL_ID, REVIEW_PANEL_UI_TOOLS };
23
+ //# sourceMappingURL=panel.mjs.map
24
+ //# sourceMappingURL=panel.mjs.map