@agentproto/apps 0.20.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/agents-overview/panel.mjs +21 -8
  2. package/dist/agents-overview/panel.mjs.map +1 -1
  3. package/dist/agents-overview.mjs +21 -8
  4. package/dist/agents-overview.mjs.map +1 -1
  5. package/dist/bin/sync.mjs +19 -2
  6. package/dist/bin/sync.mjs.map +1 -1
  7. package/dist/bureau-sessions/panel.mjs +21 -8
  8. package/dist/bureau-sessions/panel.mjs.map +1 -1
  9. package/dist/bureau-sessions.mjs +21 -8
  10. package/dist/bureau-sessions.mjs.map +1 -1
  11. package/dist/index.mjs +63 -20
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/live-session/panel.mjs +21 -8
  14. package/dist/live-session/panel.mjs.map +1 -1
  15. package/dist/live-session.mjs +21 -8
  16. package/dist/live-session.mjs.map +1 -1
  17. package/dist/ops-panel/ui.d.ts +1 -1
  18. package/dist/ops-panel/ui.d.ts.map +1 -1
  19. package/dist/ops-panel.mjs +19 -2
  20. package/dist/ops-panel.mjs.map +1 -1
  21. package/dist/panel-bridge.d.ts.map +1 -1
  22. package/dist/review-panel/panel.d.ts +1 -1
  23. package/dist/review-panel/panel.d.ts.map +1 -1
  24. package/dist/review-panel/panel.generated.d.ts +1 -1
  25. package/dist/review-panel/panel.generated.d.ts.map +1 -1
  26. package/dist/review-panel/panel.mjs +1 -1
  27. package/dist/review-panel/panel.mjs.map +1 -1
  28. package/dist/review-panel.mjs +1 -1
  29. package/dist/review-panel.mjs.map +1 -1
  30. package/dist/session-chat/panel.mjs +21 -8
  31. package/dist/session-chat/panel.mjs.map +1 -1
  32. package/dist/session-chat.mjs +21 -8
  33. package/dist/session-chat.mjs.map +1 -1
  34. package/dist/session-story/panel.mjs +21 -8
  35. package/dist/session-story/panel.mjs.map +1 -1
  36. package/dist/session-story.mjs +21 -8
  37. package/dist/session-story.mjs.map +1 -1
  38. package/dist/sessions-panel/panel.mjs +21 -8
  39. package/dist/sessions-panel/panel.mjs.map +1 -1
  40. package/dist/sessions-panel.mjs +21 -8
  41. package/dist/sessions-panel.mjs.map +1 -1
  42. package/dist/store/panel.d.ts +1 -1
  43. package/dist/store/panel.d.ts.map +1 -1
  44. package/dist/store/panel.generated.d.ts +1 -1
  45. package/dist/store/panel.generated.d.ts.map +1 -1
  46. package/dist/store/panel.mjs +1 -1
  47. package/dist/store/panel.mjs.map +1 -1
  48. package/dist/store.mjs +1 -1
  49. package/dist/store.mjs.map +1 -1
  50. package/dist/work-board/panel.d.ts +1 -1
  51. package/dist/work-board/panel.d.ts.map +1 -1
  52. package/dist/work-board/panel.generated.d.ts +1 -1
  53. package/dist/work-board/panel.generated.d.ts.map +1 -1
  54. package/dist/work-board/panel.mjs +21 -8
  55. package/dist/work-board/panel.mjs.map +1 -1
  56. package/dist/work-board.mjs +21 -8
  57. package/dist/work-board.mjs.map +1 -1
  58. package/package.json +2 -2
  59. package/repo-maintenance/.agentproto/workflows/maintain/WORKFLOW.md +23 -1
  60. package/repo-maintenance/.agentproto/workflows/maintain/entry.mjs +101 -9
  61. package/session-steward/.agentproto/workflows/session-steward/WORKFLOW.md +14 -10
  62. package/session-steward/.agentproto/workflows/session-steward/cron-rules.mjs +62 -19
  63. package/session-steward/.agentproto/workflows/session-steward/entry.mjs +40 -11
  64. package/session-steward/.agentproto/workflows/session-steward/origin-policy.mjs +2 -2
  65. package/session-steward/README.md +1 -1
  66. package/session-steward/routines/session-steward-hourly/ROUTINE.md +3 -3
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/work-board/panel.generated.ts","../src/work-board/panel.ts","../src/work-board/index.ts"],"names":[],"mappings":";;;;;;;;;;AAYO,IAAM,yBAAA,GAA4B,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;;;ACoDlC,IAAM,iBAAA,GAAoB,wBAAA;AAI1B,IAAM,kBAAA,GAAqB,uBAAA;AAM3B,IAAM,mBAAA,GAAsB;AAAA,EACjC,WAAA;AAAA,EACA,YAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF,CAAA;AAEO,IAAM,eAAA,GAAkB,yBAAA;;;ACvCxB,IAAM,oBAAA,GAAuB,EAAE,MAAA,CAAO;AAAA,EAC3C,OAAA,EAAS,CAAA,CACN,MAAA,EAAO,CACP,UAAS,CACT,QAAA;AAAA,IACC;AAAA;AAIN,CAAC;AAiCM,SAAS,iBACd,GAAA,EACoD;AACpD,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,kBAAA;AAAA,IACJ,KAAA,EAAO,YAAA;AAAA,IACP,WAAA,EACE,0ZAAA;AAAA,IAMF,WAAA,EAAa,oBAAA;AAAA,IACb,SAAS,OAAM,KAAA,KAAS,GAAA,CAAI,SAAA,CAAU,MAAM,OAAO,CAAA;AAAA,IACnD,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,eAA0B,SAAA,CAAU;AAAA,EAC/C,EAAA,EAAI,iBAAA;AAAA,EACJ,IAAA,EAAM,YAAA;AAAA,EACN,WAAA,EACE,oLAAA;AAAA,EAEF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,eAAA;AAAA,IACN,KAAA,EAAO,YAAA;AAAA,IACP,KAAA,EAAO,CAAC,GAAG,mBAAmB;AAAA;AAElC,CAAC","file":"work-board.mjs","sourcesContent":["/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/work-board/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-work-board-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui\n *\n * `pnpm --filter @agentproto/apps run build:ui:check` (wired into CI) fails\n * if this file drifts from what the Vite sources actually produce.\n */\n\nexport const WORK_BOARD_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 work board</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-work-board-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(function(result){\\n if (result.isError){\\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\\n throw new Error(e);\\n }\\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\\n return JSON.parse(text);\\n });\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n const COLUMN_STATUSES = [\\\"pending\\\", \\\"in_progress\\\", \\\"done\\\", \\\"failed\\\"];\\n const COLUMN_LABELS = {\\n pending: \\\"Pending\\\",\\n in_progress: \\\"In Progress\\\",\\n done: \\\"Done\\\",\\n failed: \\\"Failed\\\"\\n };\\n function esc(value) {\\n return value.replace(/&/g, \\\"&amp;\\\").replace(/</g, \\\"&lt;\\\").replace(/>/g, \\\"&gt;\\\").replace(/\\\"/g, \\\"&quot;\\\");\\n }\\n function columnOf(status) {\\n return status === \\\"cancelled\\\" ? \\\"failed\\\" : status;\\n }\\n function verificationTag(task) {\\n const v = task.verification;\\n if (v) {\\n if (v.kind === \\\"gate\\\") return '<span class=\\\"tag t-gate\\\">&#10003; gate</span>';\\n if (v.kind === \\\"self-report\\\") return '<span class=\\\"tag t-self\\\">self-report</span>';\\n if (v.kind === \\\"human\\\") return '<span class=\\\"tag t-human\\\">human</span>';\\n }\\n if (task.status === \\\"in_progress\\\" && task.verify) {\\n return '<span class=\\\"tag t-gated\\\">gated</span>';\\n }\\n return \\\"\\\";\\n }\\n function ownerTag(task) {\\n if (!task.owner) return '<span class=\\\"tag t-unclaimed\\\">Unclaimed</span>';\\n return `<span class=\\\"tag t-owner\\\">${esc(task.owner)}</span>`;\\n }\\n function actionsFor(task) {\\n if (task.status === \\\"pending\\\") {\\n return [\\n task.owner ? { label: \\\"Start\\\", action: \\\"start\\\", cls: \\\"primary\\\" } : { label: \\\"Claim\\\", action: \\\"claim\\\", cls: \\\"primary\\\" },\\n { label: \\\"Cancel\\\", action: \\\"cancel\\\", cls: \\\"danger\\\" }\\n ];\\n }\\n if (task.status === \\\"in_progress\\\") {\\n return [\\n { label: \\\"Done\\\", action: \\\"done\\\", cls: \\\"primary\\\" },\\n { label: \\\"Fail\\\", action: \\\"fail\\\", cls: \\\"danger\\\" },\\n { label: \\\"Release\\\", action: \\\"release\\\", cls: \\\"\\\" },\\n { label: \\\"Cancel\\\", action: \\\"cancel\\\", cls: \\\"danger\\\" }\\n ];\\n }\\n if (task.status === \\\"done\\\") {\\n return [{ label: \\\"Reopen\\\", action: \\\"reopen\\\", cls: \\\"\\\" }];\\n }\\n return [];\\n }\\n function renderCard(task) {\\n let html = `<div class=\\\"card\\\" data-taskid=\\\"${esc(task.taskId)}\\\">`;\\n html += `<div class=\\\"c-title\\\">${esc(task.title)}</div>`;\\n html += `<div class=\\\"c-id\\\">#${esc(task.taskId)}</div>`;\\n html += `<div class=\\\"c-row\\\">${ownerTag(task)}${verificationTag(task)}`;\\n if (task.status === \\\"cancelled\\\") html += '<span class=\\\"tag t-cancelled\\\">cancelled</span>';\\n html += \\\"</div>\\\";\\n if (task.lastVerifyError) {\\n html += `<div class=\\\"c-err\\\">verify failed: ${esc(task.lastVerifyError)}</div>`;\\n }\\n const actions = actionsFor(task);\\n if (actions.length > 0) {\\n html += '<div class=\\\"c-actions\\\">';\\n for (const a of actions) {\\n html += `<button class=\\\"abtn ${a.cls}\\\" data-taskid=\\\"${esc(task.taskId)}\\\" data-action=\\\"${a.action}\\\">${a.label}</button>`;\\n }\\n html += \\\"</div>\\\";\\n }\\n html += \\\"</div>\\\";\\n return html;\\n }\\n function renderColumns(tasks2) {\\n let html = \\\"\\\";\\n for (const status of COLUMN_STATUSES) {\\n const rows = tasks2.filter((t) => columnOf(t.status) === status);\\n html += `<div class=\\\"col\\\"><div class=\\\"col-hdr\\\"><span>${COLUMN_LABELS[status]}</span><span>${rows.length}</span></div><div class=\\\"col-body\\\">`;\\n html += rows.length === 0 ? '<div class=\\\"empty\\\">No tasks</div>' : rows.map(renderCard).join(\\\"\\\");\\n html += \\\"</div></div>\\\";\\n }\\n return html;\\n }\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`work board: missing #${id}`);\\n return el;\\n }\\n function getInput(id) {\\n const el = document.getElementById(id);\\n if (!(el instanceof HTMLInputElement)) throw new Error(`work board: missing input #${id}`);\\n return el;\\n }\\n function isCardAction(value) {\\n switch (value) {\\n case \\\"claim\\\":\\n case \\\"start\\\":\\n case \\\"done\\\":\\n case \\\"fail\\\":\\n case \\\"release\\\":\\n case \\\"cancel\\\":\\n case \\\"reopen\\\":\\n return true;\\n default:\\n return false;\\n }\\n }\\n let boardId = null;\\n let tasks = [];\\n let pollActive = false;\\n function boot() {\\n const columnsEl = getEl(\\\"columns\\\");\\n const statusbarEl = getEl(\\\"statusbar\\\");\\n const boardIdEl = getEl(\\\"board-id\\\");\\n const boardInputEl = getInput(\\\"board-input\\\");\\n const newTitleEl = getInput(\\\"new-title\\\");\\n function setStatus(msg) {\\n statusbarEl.textContent = msg;\\n }\\n function render() {\\n boardIdEl.textContent = boardId ?? \\\"—\\\";\\n if (!boardInputEl.value) boardInputEl.value = boardId ?? \\\"\\\";\\n columnsEl.innerHTML = renderColumns(tasks);\\n }\\n function findTask(taskId) {\\n return tasks.find((t) => t.taskId === taskId);\\n }\\n function loadBoard() {\\n const args = {\\n includeClosed: true,\\n full: true,\\n ...boardId ? { boardId } : {}\\n };\\n return callTool(\\\"task_list\\\", args).then((data) => {\\n boardId = data.boardId ?? boardId;\\n tasks = data.tasks ?? [];\\n render();\\n setStatus(`${tasks.length} task${tasks.length === 1 ? \\\"\\\" : \\\"s\\\"} · ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\\n }).catch((e) => {\\n setStatus(`Error: ${e.message}`);\\n });\\n }\\n function applyAction(taskId, action) {\\n const task = findTask(taskId);\\n if (!task) return;\\n const base = { taskId: task.taskId, rev: task.rev };\\n let promise;\\n if (action === \\\"claim\\\") {\\n promise = callTool(\\\"task_claim\\\", base);\\n } else if (action === \\\"start\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"in_progress\\\" });\\n } else if (action === \\\"done\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"done\\\" });\\n } else if (action === \\\"fail\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"failed\\\" });\\n } else if (action === \\\"release\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, owner: null });\\n } else if (action === \\\"cancel\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"cancelled\\\" });\\n } else {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"pending\\\" });\\n }\\n promise.then((result) => {\\n if (\\\"conflict\\\" in result) {\\n setStatus(`Someone else moved \\\"${task.title}\\\" first — refreshed.`);\\n } else if (\\\"error\\\" in result) {\\n setStatus(`Action failed: ${result.error}`);\\n } else {\\n setStatus(`Updated \\\"${task.title}\\\".`);\\n }\\n return loadBoard();\\n }).catch((e) => {\\n setStatus(`Action failed: ${e.message}`);\\n });\\n }\\n function doPoll() {\\n if (pollActive) return;\\n pollActive = true;\\n loadBoard().finally(() => {\\n pollActive = false;\\n setTimeout(doPoll, 4e3);\\n });\\n }\\n columnsEl.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const btn = evt.target.closest(\\\"button[data-action]\\\");\\n if (!(btn instanceof HTMLButtonElement)) return;\\n const taskId = btn.getAttribute(\\\"data-taskid\\\");\\n const action = btn.getAttribute(\\\"data-action\\\");\\n if (!taskId || !action || !isCardAction(action)) return;\\n applyAction(taskId, action);\\n });\\n getEl(\\\"refresh-btn\\\").addEventListener(\\\"click\\\", () => {\\n void loadBoard();\\n });\\n getEl(\\\"go-btn\\\").addEventListener(\\\"click\\\", () => {\\n const value = boardInputEl.value.trim();\\n if (!value) return;\\n boardId = value;\\n void loadBoard();\\n });\\n getEl(\\\"add-btn\\\").addEventListener(\\\"click\\\", () => {\\n const title = newTitleEl.value.trim();\\n if (!title) return;\\n const args = boardId ? { title, boardId } : { title };\\n callTool(\\\"task_create\\\", args).then((result) => {\\n if (result.error) {\\n setStatus(`Create failed: ${result.error}`);\\n return void 0;\\n }\\n newTitleEl.value = \\\"\\\";\\n setStatus(`Created \\\"${title}\\\".`);\\n return loadBoard();\\n }).catch((e) => {\\n setStatus(`Create failed: ${e.message}`);\\n });\\n });\\n initBridge().then(() => loadBoard()).then(() => {\\n setTimeout(doPoll, 4e3);\\n }).catch((e) => {\\n setStatus(`Bridge error: ${e.message}`);\\n });\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n Semantic tokens + runtime mapping sheets.\\n\\n Every rule below the next section reads ONLY the --wb-* custom\\n properties. Each one resolves through TWO mapping sheets, chosen at\\n PAINT time via plain CSS var() fallback — no build step, no JS, so a\\n host that swaps its theme live (VS Code) is never frozen at whatever\\n theme happened to be active when this bundle was built:\\n\\n 1. embedded — VS Code injects a live --vscode-* custom-property set\\n into the webview and swaps it on theme change. Whenever the\\n relevant --vscode-* variable exists, it wins — in a VS Code\\n `srcdoc` relay panel AND (structurally identically) any other host\\n that happened to expose the same names, though none is known to.\\n 2. standalone — the agentproto brand tokens (see\\n projects/agentproto/site/src/app/brand.css), used whenever no\\n --vscode-* variable is set — a plain MCP-Apps host or the daemon's\\n own `/apps/.../ui` tab. `--phos`/`--amber`/`--crimson` are\\n THEME-INVARIANT in brand.css (the phosphor terminal palette), so\\n they're identical in both blocks below; the rest follow brand.css's\\n light/dark split via `prefers-color-scheme` — a standalone tab has\\n no host to pick a theme for it, unlike VS Code's own.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --wb-bg: var(--vscode-editor-background, #f5f6f3);\\n --wb-surface: var(--vscode-sideBar-background, #ffffff);\\n --wb-ink: var(--vscode-editor-foreground, #171a1e);\\n --wb-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --wb-border: var(--vscode-panel-border, #dde0da);\\n --wb-accent: var(--vscode-button-background, #0f62fe);\\n\\n /* Status colors — phos/amber/crimson are the theme-invariant terminal\\n palette in brand.css, so light and dark standalone fallbacks match. */\\n --wb-status-pending: var(--vscode-descriptionForeground, #5c6470);\\n --wb-status-in-progress: var(--vscode-charts-yellow, #f0b429);\\n --wb-status-done: var(--vscode-charts-green, #3edc81);\\n --wb-status-failed: var(--vscode-charts-red, #fa4d56);\\n --wb-status-cancelled: var(--vscode-descriptionForeground, #5c6470);\\n --wb-status-cancelled-weak: color-mix(in srgb, var(--wb-status-cancelled) 55%, transparent);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --wb-bg: var(--vscode-editor-background, #101214);\\n --wb-surface: var(--vscode-sideBar-background, #16191d);\\n --wb-ink: var(--vscode-editor-foreground, #e8eaed);\\n --wb-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-border: var(--vscode-panel-border, #272c33);\\n --wb-accent: var(--vscode-button-background, #4589ff);\\n\\n --wb-status-pending: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-status-in-progress: var(--vscode-charts-yellow, #f0b429);\\n --wb-status-done: var(--vscode-charts-green, #3edc81);\\n --wb-status-failed: var(--vscode-charts-red, #fa4d56);\\n --wb-status-cancelled: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-status-cancelled-weak: color-mix(in srgb, var(--wb-status-cancelled) 55%, transparent);\\n }\\n}\\n\\n/* ── Base ─────────────────────────────────────────────────────────── */\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--wb-bg);\\n color: var(--wb-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\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#toolbar {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--wb-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--wb-surface);\\n flex-shrink: 0;\\n}\\n#board-label { font-size: 11px; color: var(--wb-ink-muted); }\\n#board-id { font-weight: 600; color: var(--wb-ink); font-size: 12px; font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace; }\\n#board-input {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n width: 220px;\\n}\\n.tbtn {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-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(--wb-border); }\\n#new-title {\\n flex: 1;\\n min-width: 120px;\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n\\n#columns { flex: 1; display: flex; overflow-x: auto; overflow-y: hidden; }\\n.col {\\n width: 260px;\\n min-width: 220px;\\n flex-shrink: 0;\\n display: flex;\\n flex-direction: column;\\n border-right: 1px solid var(--wb-border);\\n}\\n.col-hdr {\\n padding: 8px 10px;\\n border-bottom: 1px solid var(--wb-border);\\n background: var(--wb-surface);\\n font-size: 11px;\\n font-weight: 600;\\n color: var(--wb-ink-muted);\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n display: flex;\\n justify-content: space-between;\\n}\\n.col-body { flex: 1; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px; }\\n\\n.card {\\n background: var(--wb-surface);\\n border: 1px solid var(--wb-border);\\n border-radius: 6px;\\n padding: 8px 10px;\\n}\\n.c-title { font-size: 12px; font-weight: 500; margin-bottom: 4px; word-break: break-word; }\\n.c-id {\\n font-size: 10px;\\n color: var(--wb-ink-muted);\\n margin-bottom: 6px;\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.c-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin-bottom: 6px; }\\n\\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\\n.t-unclaimed { background: color-mix(in srgb, var(--wb-ink-muted) 15%, transparent); color: var(--wb-ink-muted); border: 1px dashed var(--wb-ink-muted); }\\n.t-owner { background: color-mix(in srgb, var(--wb-accent) 15%, transparent); color: var(--wb-accent); }\\n\\n/* Verification tell: gate solid green, self-report amber outline, human\\n blue, a declared verify with no verification yet grey \\\"gated\\\" — never\\n render an unverified done as gate-passed (see render.ts's verificationTag). */\\n.t-gate { background: color-mix(in srgb, var(--wb-status-done) 18%, transparent); color: var(--wb-status-done); }\\n.t-self { background: transparent; color: var(--wb-status-in-progress); border: 1px solid var(--wb-status-in-progress); }\\n.t-human { background: color-mix(in srgb, var(--wb-accent) 18%, transparent); color: var(--wb-accent); }\\n.t-gated { background: color-mix(in srgb, var(--wb-ink-muted) 12%, transparent); color: var(--wb-ink-muted); }\\n.t-cancelled {\\n background: color-mix(in srgb, var(--wb-status-cancelled) 10%, transparent);\\n color: var(--wb-status-cancelled-weak);\\n border: 1px dashed color-mix(in srgb, var(--wb-status-cancelled) 40%, transparent);\\n}\\n\\n.c-err { font-size: 10px; color: var(--wb-status-failed); margin-bottom: 6px; word-break: break-word; }\\n.c-actions { display: flex; flex-wrap: wrap; gap: 4px; }\\n.abtn {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 3px 8px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 11px;\\n font-family: inherit;\\n}\\n.abtn:hover { background: var(--wb-border); }\\n.abtn.danger { border-color: var(--wb-status-failed); color: var(--wb-status-failed); }\\n.abtn.primary { border-color: var(--wb-status-done); color: var(--wb-status-done); }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--wb-ink-muted);\\n border-top: 1px solid var(--wb-border);\\n background: var(--wb-surface);\\n flex-shrink: 0;\\n}\\n.empty { padding: 16px 8px; color: var(--wb-ink-muted); font-size: 11px; text-align: center; }</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"toolbar\\\">\\n <span id=\\\"board-label\\\">Board:</span>\\n <span id=\\\"board-id\\\">—</span>\\n <input id=\\\"board-input\\\" type=\\\"text\\\" placeholder=\\\"tree:&lt;sessionId&gt; or ws:&lt;slug&gt;\\\">\\n <button class=\\\"tbtn\\\" id=\\\"go-btn\\\">Go</button>\\n <button class=\\\"tbtn\\\" id=\\\"refresh-btn\\\" title=\\\"Refresh\\\">&#8635;</button>\\n <input id=\\\"new-title\\\" type=\\\"text\\\" placeholder=\\\"New task title&#8230;\\\">\\n <button class=\\\"tbtn\\\" id=\\\"add-btn\\\">+ Add</button>\\n </div>\\n <div id=\\\"columns\\\"></div>\\n <div id=\\\"statusbar\\\">Connecting to bridge&#8230;</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto work-board panel — the `WORK_BOARD_HTML`\n * served as the `ui://agentproto_work_board/view` resource by\n * `registerMcpApps` (see ../mcp-app-types.ts and runtime's\n * mcp-apps-adapter.ts). Mounted via the `agentproto_work_board` tool defined\n * in ./index.ts.\n *\n * A kanban over the daemon's Task ledger (`packages/runtime/src/\n * task-ledger.ts`) — the one writable multi-party entity the daemon exposes\n * (Activity is a read-only projection; a session has a lifecycle, not an\n * intention). Columns are the ledger's own status enum, folding `cancelled`\n * into the `failed` column (tagged distinctly — see `columnOf` in\n * ./ui/render.ts) rather than adding a fifth column for a status v1 treats\n * as terminal scrap. The board/swimlane selector is the ledger's own board\n * id (`tree:<rootSessionId>` or `ws:<slug>`) — always shown verbatim, never\n * hidden behind a friendly name, per the ledger's scoping model.\n *\n * `WORK_BOARD_HTML` is a real single-file Vite build, not a hand-authored\n * template string: the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run build:ui`\n * (scripts/build-work-board-ui.mjs) — so this package still builds with\n * plain `pnpm build` on a fresh clone, no Vite step in the critical path.\n * `build:ui:check` (wired into CI) fails if that generated file drifts from\n * what the Vite sources actually produce. See ./ui/vite.config.ts for how\n * the shared `panelBridgeScript` (../panel-bridge.ts) gets inlined as a\n * classic `<script>` ahead of the bundled module script.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts,\n * unmodified by this change — this app only calls its `initBridge()`/\n * `callTool()` surface). Standalone (no host at all, e.g. a top-level\n * `GET /apps/:appId/ui` tab) does NOT connect yet: panel-bridge.ts's\n * handshake has no reply to wait for in that case and hangs on\n * \"Connecting to bridge…\" — a known gap being fixed in panel-bridge.ts\n * itself by a separate change. This app will pick that fix up automatically\n * (rebuild + rebase) once it lands; nothing here needs to special-case it.\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → task_list (`full: true` — the compact projection\n * drops `verification`, which the verification tell needs) on a ~4 s poll\n * – Actions: tools/call → task_claim / task_update / task_create\n *\n * Read-only-safe first: no drag-and-drop. Each card gets the explicit status\n * actions valid from its current state (Claim/Start/Done/Fail/Release/\n * Cancel/Reopen) — a correct read-only-plus-explicit-actions board beats a\n * half-wired drag, per the brief.\n *\n * The verification tell (never render an unverified done as gate-passed):\n * - `verification.kind === \"gate\"` → solid green \"✓ gate\"\n * - `verification.kind === \"self-report\"` → amber outline \"self-report\"\n * - `verification.kind === \"human\"` → blue \"human\"\n * - a declared `verify` with no verification yet → grey \"gated\" tag\n * An owner-less task always reads \"Unclaimed\", never \"pending\" (that word is\n * reserved for the Activity vocabulary's \"blocked\" meaning elsewhere).\n */\n\nimport { WORK_BOARD_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph — the VS Code extension bundles with esbuild and\n * chokes on its transitive native `.node` deps — can still name the panel\n * from source. */\nexport const WORK_BOARD_APP_ID = \"@agentproto/work-board\"\n\n/** The MCP tool the panel is registered under (./index.ts `makeWorkBoardApp`),\n * hence its resource uri `ui://agentproto_work_board/view`. */\nexport const WORK_BOARD_TOOL_ID = \"agentproto_work_board\"\n\n/** The daemon tools this panel's html calls, in order of use. Consumed as\n * `ui.tools` by ./index.ts and as the client-side allowlist by any host that\n * dispatches the panel's `tools/call` directly — a builtin is not an\n * installed app, so `app_tool_call` cannot route for it. */\nexport const WORK_BOARD_UI_TOOLS = [\n \"task_list\",\n \"task_claim\",\n \"task_update\",\n \"task_create\",\n] as const\n\nexport const WORK_BOARD_HTML = WORK_BOARD_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto work-board panel — a kanban over the\n * daemon's Task ledger (`packages/runtime/src/task-ledger.ts`), the ONLY\n * writable multi-party entity the daemon exposes (Activity is a read-only\n * projection recomputed on every `list()`; a session has a lifecycle, not an\n * intention).\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra), same as sessions-panel /\n * session-chat — the host (@agentproto/runtime) wires this factory's output\n * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the\n * mounted list assembled in runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_work_board` is called by the host with an optional\n * `boardId`.\n * 2. execute() answers an initial `{boardId, tasks}` snapshot off the\n * ledger — resolved to the caller's default board when `boardId` is\n * omitted (a session's `tree:<root>`, the operator's `ws:<slug>`).\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `task_list` (`full: true` — the compact projection drops\n * `verification`) every ~4 s; writes go through `task_claim` /\n * `task_update` / `task_create`, the same verbs any other caller uses —\n * no second write path.\n *\n * This module also exports `workBoardApp`, a real `defineApp()` `AppHandle`\n * (`agents: []`, UI-only) — the catalog/emit/`app_install` path. It's\n * separate from `makeWorkBoardApp` above, which the daemon mounts directly\n * at boot: that factory closes over the LIVE ledger read, something a\n * static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport {\n WORK_BOARD_APP_ID,\n WORK_BOARD_HTML,\n WORK_BOARD_TOOL_ID,\n WORK_BOARD_UI_TOOLS,\n} from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const workBoardInputSchema = z.object({\n boardId: z\n .string()\n .optional()\n .describe(\n \"Board to open — `tree:<rootSessionId>` (a supervisor and its \" +\n \"executors) or `ws:<slug>` (an operator workspace). Omit → the \" +\n \"caller's default board.\",\n ),\n})\n\nexport type WorkBoardInput = z.infer<typeof workBoardInputSchema>\n\nexport interface WorkBoardOutput<TTask = unknown> {\n /** Always echoed back — the ledger's board ids are the swimlane\n * selector, and must be visible rather than hidden behind a friendly\n * name (it's how the operator knows which mission they're looking at). */\n boardId: string\n tasks: TTask[]\n}\n\n/**\n * Generic over the host's own task-record shape — this package doesn't own\n * that type (it lives in @agentproto/runtime's task ledger), so the factory\n * stays agnostic to it and the host supplies the concrete type as a type\n * argument when it wires the app up (mirrors SessionsPanelOps<TSession>).\n */\nexport interface WorkBoardOps<TTask = unknown> {\n /** Full (unprojected) records for a board — NOT `task_list`'s default\n * compact projection, since the verification tell needs `verification`\n * (self-report vs gate vs human), which compact mode omits. Omit\n * `boardId` to resolve the caller's default board. */\n listTasks(boardId?: string): WorkBoardOutput<TTask>\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the ledger read so execute() doesn't need an\n * AppContext (agentproto has no userId/guildId concept) — mirrors\n * makeSessionsPanelApp.\n */\nexport function makeWorkBoardApp<TTask = unknown>(\n ops: WorkBoardOps<TTask>,\n): AgnoMcpApp<WorkBoardInput, WorkBoardOutput<TTask>> {\n return {\n id: WORK_BOARD_TOOL_ID,\n title: \"Work Board\",\n description:\n \"Open the agentproto work board — a kanban over the daemon's Task \" +\n \"ledger (columns: pending/in_progress/done/failed, cancelled folded \" +\n \"into failed). Swimlane is the ledger's own board id (`tree:<root>` \" +\n \"or `ws:<slug>`). Each card shows its owner (or \\\"Unclaimed\\\") and the \" +\n \"verification tell — a gate-passed done reads visually distinct from \" +\n \"a self-reported one. Pass `boardId` to open a specific board.\",\n inputSchema: workBoardInputSchema,\n execute: async input => ops.listTasks(input.boardId),\n html: WORK_BOARD_HTML,\n }\n}\n\nexport const workBoardApp: AppHandle = defineApp({\n id: WORK_BOARD_APP_ID,\n name: \"Work Board\",\n description:\n \"Open the agentproto work board — a kanban over the daemon's Task ledger, scoped by board id \" +\n \"(`tree:<rootSessionId>` or `ws:<slug>`), with an explicit status action per card.\",\n agents: [],\n ui: {\n html: WORK_BOARD_HTML,\n title: \"Work Board\",\n tools: [...WORK_BOARD_UI_TOOLS],\n },\n})\n"]}
1
+ {"version":3,"sources":["../src/work-board/panel.generated.ts","../src/work-board/panel.ts","../src/work-board/index.ts"],"names":[],"mappings":";;;;;;;;;;AAYO,IAAM,yBAAA,GAA4B,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;;;ACoDlC,IAAM,iBAAA,GAAoB,wBAAA;AAI1B,IAAM,kBAAA,GAAqB,uBAAA;AAM3B,IAAM,mBAAA,GAAsB;AAAA,EACjC,WAAA;AAAA,EACA,YAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF,CAAA;AAEO,IAAM,eAAA,GAAkB,yBAAA;;;ACvCxB,IAAM,oBAAA,GAAuB,EAAE,MAAA,CAAO;AAAA,EAC3C,OAAA,EAAS,CAAA,CACN,MAAA,EAAO,CACP,UAAS,CACT,QAAA;AAAA,IACC;AAAA;AAIN,CAAC;AAiCM,SAAS,iBACd,GAAA,EACoD;AACpD,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,kBAAA;AAAA,IACJ,KAAA,EAAO,YAAA;AAAA,IACP,WAAA,EACE,0ZAAA;AAAA,IAMF,WAAA,EAAa,oBAAA;AAAA,IACb,SAAS,OAAM,KAAA,KAAS,GAAA,CAAI,SAAA,CAAU,MAAM,OAAO,CAAA;AAAA,IACnD,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,eAA0B,SAAA,CAAU;AAAA,EAC/C,EAAA,EAAI,iBAAA;AAAA,EACJ,IAAA,EAAM,YAAA;AAAA,EACN,WAAA,EACE,oLAAA;AAAA,EAEF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,eAAA;AAAA,IACN,KAAA,EAAO,YAAA;AAAA,IACP,KAAA,EAAO,CAAC,GAAG,mBAAmB;AAAA;AAElC,CAAC","file":"work-board.mjs","sourcesContent":["/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/work-board/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-work-board-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui\n *\n * `pnpm --filter @agentproto/apps run build:ui:check` (wired into CI) fails\n * if this file drifts from what the Vite sources actually produce.\n */\n\nexport const WORK_BOARD_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 work board</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-work-board-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\n// An MCP tool-result envelope: {content: [{type: 'text', text}, ...]}.\\nfunction _isEnvelope(v){\\n return !!v && typeof v === 'object' && Array.isArray(v.content) &&\\n !!v.content[0] && v.content[0].type === 'text' && typeof v.content[0].text === 'string';\\n}\\n// Peels every envelope layer. The standalone path (POST /apps/:id/tool-call,\\n// runtime app-tools.ts dispatchAllowlistedAppTool) wraps an already-wrapped\\n// builtin result a second time, and an inner isError rides inside that outer\\n// success — so isError is honoured at each layer, not just the first.\\nfunction _unwrapToolResult(result){\\n var env = result;\\n while (true){\\n var first = env && env.content && env.content[0];\\n var text = (first && first.text) || '';\\n if (env && env.isError) throw new Error(text || 'tool error');\\n try { var value = JSON.parse(text || '{}'); } catch(_) { return text; }\\n if (!_isEnvelope(value)) return value;\\n env = value;\\n }\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(_unwrapToolResult);\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n const COLUMN_STATUSES = [\\\"pending\\\", \\\"in_progress\\\", \\\"done\\\", \\\"failed\\\"];\\n const COLUMN_LABELS = {\\n pending: \\\"Pending\\\",\\n in_progress: \\\"In Progress\\\",\\n done: \\\"Done\\\",\\n failed: \\\"Failed\\\"\\n };\\n function esc(value) {\\n return value.replace(/&/g, \\\"&amp;\\\").replace(/</g, \\\"&lt;\\\").replace(/>/g, \\\"&gt;\\\").replace(/\\\"/g, \\\"&quot;\\\");\\n }\\n function columnOf(status) {\\n return status === \\\"cancelled\\\" ? \\\"failed\\\" : status;\\n }\\n function verificationTag(task) {\\n const v = task.verification;\\n if (v) {\\n if (v.kind === \\\"gate\\\") return '<span class=\\\"tag t-gate\\\">&#10003; gate</span>';\\n if (v.kind === \\\"self-report\\\") return '<span class=\\\"tag t-self\\\">self-report</span>';\\n if (v.kind === \\\"human\\\") return '<span class=\\\"tag t-human\\\">human</span>';\\n }\\n if (task.status === \\\"in_progress\\\" && task.verify) {\\n return '<span class=\\\"tag t-gated\\\">gated</span>';\\n }\\n return \\\"\\\";\\n }\\n function ownerTag(task) {\\n if (!task.owner) return '<span class=\\\"tag t-unclaimed\\\">Unclaimed</span>';\\n return `<span class=\\\"tag t-owner\\\">${esc(task.owner)}</span>`;\\n }\\n function actionsFor(task) {\\n if (task.status === \\\"pending\\\") {\\n return [\\n task.owner ? { label: \\\"Start\\\", action: \\\"start\\\", cls: \\\"primary\\\" } : { label: \\\"Claim\\\", action: \\\"claim\\\", cls: \\\"primary\\\" },\\n { label: \\\"Cancel\\\", action: \\\"cancel\\\", cls: \\\"danger\\\" }\\n ];\\n }\\n if (task.status === \\\"in_progress\\\") {\\n return [\\n { label: \\\"Done\\\", action: \\\"done\\\", cls: \\\"primary\\\" },\\n { label: \\\"Fail\\\", action: \\\"fail\\\", cls: \\\"danger\\\" },\\n { label: \\\"Release\\\", action: \\\"release\\\", cls: \\\"\\\" },\\n { label: \\\"Cancel\\\", action: \\\"cancel\\\", cls: \\\"danger\\\" }\\n ];\\n }\\n if (task.status === \\\"done\\\") {\\n return [{ label: \\\"Reopen\\\", action: \\\"reopen\\\", cls: \\\"\\\" }];\\n }\\n return [];\\n }\\n function renderCard(task) {\\n let html = `<div class=\\\"card\\\" data-taskid=\\\"${esc(task.taskId)}\\\">`;\\n html += `<div class=\\\"c-title\\\">${esc(task.title)}</div>`;\\n html += `<div class=\\\"c-id\\\">#${esc(task.taskId)}</div>`;\\n html += `<div class=\\\"c-row\\\">${ownerTag(task)}${verificationTag(task)}`;\\n if (task.status === \\\"cancelled\\\") html += '<span class=\\\"tag t-cancelled\\\">cancelled</span>';\\n html += \\\"</div>\\\";\\n if (task.lastVerifyError) {\\n html += `<div class=\\\"c-err\\\">verify failed: ${esc(task.lastVerifyError)}</div>`;\\n }\\n const actions = actionsFor(task);\\n if (actions.length > 0) {\\n html += '<div class=\\\"c-actions\\\">';\\n for (const a of actions) {\\n html += `<button class=\\\"abtn ${a.cls}\\\" data-taskid=\\\"${esc(task.taskId)}\\\" data-action=\\\"${a.action}\\\">${a.label}</button>`;\\n }\\n html += \\\"</div>\\\";\\n }\\n html += \\\"</div>\\\";\\n return html;\\n }\\n function renderColumns(tasks2) {\\n let html = \\\"\\\";\\n for (const status of COLUMN_STATUSES) {\\n const rows = tasks2.filter((t) => columnOf(t.status) === status);\\n html += `<div class=\\\"col\\\"><div class=\\\"col-hdr\\\"><span>${COLUMN_LABELS[status]}</span><span>${rows.length}</span></div><div class=\\\"col-body\\\">`;\\n html += rows.length === 0 ? '<div class=\\\"empty\\\">No tasks</div>' : rows.map(renderCard).join(\\\"\\\");\\n html += \\\"</div></div>\\\";\\n }\\n return html;\\n }\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`work board: missing #${id}`);\\n return el;\\n }\\n function getInput(id) {\\n const el = document.getElementById(id);\\n if (!(el instanceof HTMLInputElement)) throw new Error(`work board: missing input #${id}`);\\n return el;\\n }\\n function isCardAction(value) {\\n switch (value) {\\n case \\\"claim\\\":\\n case \\\"start\\\":\\n case \\\"done\\\":\\n case \\\"fail\\\":\\n case \\\"release\\\":\\n case \\\"cancel\\\":\\n case \\\"reopen\\\":\\n return true;\\n default:\\n return false;\\n }\\n }\\n let boardId = null;\\n let tasks = [];\\n let pollActive = false;\\n function boot() {\\n const columnsEl = getEl(\\\"columns\\\");\\n const statusbarEl = getEl(\\\"statusbar\\\");\\n const boardIdEl = getEl(\\\"board-id\\\");\\n const boardInputEl = getInput(\\\"board-input\\\");\\n const newTitleEl = getInput(\\\"new-title\\\");\\n function setStatus(msg) {\\n statusbarEl.textContent = msg;\\n }\\n function render() {\\n boardIdEl.textContent = boardId ?? \\\"—\\\";\\n if (!boardInputEl.value) boardInputEl.value = boardId ?? \\\"\\\";\\n columnsEl.innerHTML = renderColumns(tasks);\\n }\\n function findTask(taskId) {\\n return tasks.find((t) => t.taskId === taskId);\\n }\\n function loadBoard() {\\n const args = {\\n includeClosed: true,\\n full: true,\\n ...boardId ? { boardId } : {}\\n };\\n return callTool(\\\"task_list\\\", args).then((data) => {\\n boardId = data.boardId ?? boardId;\\n tasks = data.tasks ?? [];\\n render();\\n setStatus(`${tasks.length} task${tasks.length === 1 ? \\\"\\\" : \\\"s\\\"} · ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\\n }).catch((e) => {\\n setStatus(`Error: ${e.message}`);\\n });\\n }\\n function applyAction(taskId, action) {\\n const task = findTask(taskId);\\n if (!task) return;\\n const base = { taskId: task.taskId, rev: task.rev };\\n let promise;\\n if (action === \\\"claim\\\") {\\n promise = callTool(\\\"task_claim\\\", base);\\n } else if (action === \\\"start\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"in_progress\\\" });\\n } else if (action === \\\"done\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"done\\\" });\\n } else if (action === \\\"fail\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"failed\\\" });\\n } else if (action === \\\"release\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, owner: null });\\n } else if (action === \\\"cancel\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"cancelled\\\" });\\n } else {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"pending\\\" });\\n }\\n promise.then((result) => {\\n if (\\\"conflict\\\" in result) {\\n setStatus(`Someone else moved \\\"${task.title}\\\" first — refreshed.`);\\n } else if (\\\"error\\\" in result) {\\n setStatus(`Action failed: ${result.error}`);\\n } else {\\n setStatus(`Updated \\\"${task.title}\\\".`);\\n }\\n return loadBoard();\\n }).catch((e) => {\\n setStatus(`Action failed: ${e.message}`);\\n });\\n }\\n function doPoll() {\\n if (pollActive) return;\\n pollActive = true;\\n loadBoard().finally(() => {\\n pollActive = false;\\n setTimeout(doPoll, 4e3);\\n });\\n }\\n columnsEl.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const btn = evt.target.closest(\\\"button[data-action]\\\");\\n if (!(btn instanceof HTMLButtonElement)) return;\\n const taskId = btn.getAttribute(\\\"data-taskid\\\");\\n const action = btn.getAttribute(\\\"data-action\\\");\\n if (!taskId || !action || !isCardAction(action)) return;\\n applyAction(taskId, action);\\n });\\n getEl(\\\"refresh-btn\\\").addEventListener(\\\"click\\\", () => {\\n void loadBoard();\\n });\\n getEl(\\\"go-btn\\\").addEventListener(\\\"click\\\", () => {\\n const value = boardInputEl.value.trim();\\n if (!value) return;\\n boardId = value;\\n void loadBoard();\\n });\\n getEl(\\\"add-btn\\\").addEventListener(\\\"click\\\", () => {\\n const title = newTitleEl.value.trim();\\n if (!title) return;\\n const args = boardId ? { title, boardId } : { title };\\n callTool(\\\"task_create\\\", args).then((result) => {\\n if (result.error) {\\n setStatus(`Create failed: ${result.error}`);\\n return void 0;\\n }\\n newTitleEl.value = \\\"\\\";\\n setStatus(`Created \\\"${title}\\\".`);\\n return loadBoard();\\n }).catch((e) => {\\n setStatus(`Create failed: ${e.message}`);\\n });\\n });\\n initBridge().then(() => loadBoard()).then(() => {\\n setTimeout(doPoll, 4e3);\\n }).catch((e) => {\\n setStatus(`Bridge error: ${e.message}`);\\n });\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n Semantic tokens + runtime mapping sheets.\\n\\n Every rule below the next section reads ONLY the --wb-* custom\\n properties. Each one resolves through TWO mapping sheets, chosen at\\n PAINT time via plain CSS var() fallback — no build step, no JS, so a\\n host that swaps its theme live (VS Code) is never frozen at whatever\\n theme happened to be active when this bundle was built:\\n\\n 1. embedded — VS Code injects a live --vscode-* custom-property set\\n into the webview and swaps it on theme change. Whenever the\\n relevant --vscode-* variable exists, it wins — in a VS Code\\n `srcdoc` relay panel AND (structurally identically) any other host\\n that happened to expose the same names, though none is known to.\\n 2. standalone — the agentproto brand tokens (see\\n projects/agentproto/site/src/app/brand.css), used whenever no\\n --vscode-* variable is set — a plain MCP-Apps host or the daemon's\\n own `/apps/.../ui` tab. `--phos`/`--amber`/`--crimson` are\\n THEME-INVARIANT in brand.css (the phosphor terminal palette), so\\n they're identical in both blocks below; the rest follow brand.css's\\n light/dark split via `prefers-color-scheme` — a standalone tab has\\n no host to pick a theme for it, unlike VS Code's own.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --wb-bg: var(--vscode-editor-background, #f5f6f3);\\n --wb-surface: var(--vscode-sideBar-background, #ffffff);\\n --wb-ink: var(--vscode-editor-foreground, #171a1e);\\n --wb-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --wb-border: var(--vscode-panel-border, #dde0da);\\n --wb-accent: var(--vscode-button-background, #0f62fe);\\n\\n /* Status colors — phos/amber/crimson are the theme-invariant terminal\\n palette in brand.css, so light and dark standalone fallbacks match. */\\n --wb-status-pending: var(--vscode-descriptionForeground, #5c6470);\\n --wb-status-in-progress: var(--vscode-charts-yellow, #f0b429);\\n --wb-status-done: var(--vscode-charts-green, #3edc81);\\n --wb-status-failed: var(--vscode-charts-red, #fa4d56);\\n --wb-status-cancelled: var(--vscode-descriptionForeground, #5c6470);\\n --wb-status-cancelled-weak: color-mix(in srgb, var(--wb-status-cancelled) 55%, transparent);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --wb-bg: var(--vscode-editor-background, #101214);\\n --wb-surface: var(--vscode-sideBar-background, #16191d);\\n --wb-ink: var(--vscode-editor-foreground, #e8eaed);\\n --wb-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-border: var(--vscode-panel-border, #272c33);\\n --wb-accent: var(--vscode-button-background, #4589ff);\\n\\n --wb-status-pending: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-status-in-progress: var(--vscode-charts-yellow, #f0b429);\\n --wb-status-done: var(--vscode-charts-green, #3edc81);\\n --wb-status-failed: var(--vscode-charts-red, #fa4d56);\\n --wb-status-cancelled: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-status-cancelled-weak: color-mix(in srgb, var(--wb-status-cancelled) 55%, transparent);\\n }\\n}\\n\\n/* ── Base ─────────────────────────────────────────────────────────── */\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--wb-bg);\\n color: var(--wb-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\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#toolbar {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--wb-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--wb-surface);\\n flex-shrink: 0;\\n}\\n#board-label { font-size: 11px; color: var(--wb-ink-muted); }\\n#board-id { font-weight: 600; color: var(--wb-ink); font-size: 12px; font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace; }\\n#board-input {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n width: 220px;\\n}\\n.tbtn {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-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(--wb-border); }\\n#new-title {\\n flex: 1;\\n min-width: 120px;\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n\\n#columns { flex: 1; display: flex; overflow-x: auto; overflow-y: hidden; }\\n.col {\\n width: 260px;\\n min-width: 220px;\\n flex-shrink: 0;\\n display: flex;\\n flex-direction: column;\\n border-right: 1px solid var(--wb-border);\\n}\\n.col-hdr {\\n padding: 8px 10px;\\n border-bottom: 1px solid var(--wb-border);\\n background: var(--wb-surface);\\n font-size: 11px;\\n font-weight: 600;\\n color: var(--wb-ink-muted);\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n display: flex;\\n justify-content: space-between;\\n}\\n.col-body { flex: 1; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px; }\\n\\n.card {\\n background: var(--wb-surface);\\n border: 1px solid var(--wb-border);\\n border-radius: 6px;\\n padding: 8px 10px;\\n}\\n.c-title { font-size: 12px; font-weight: 500; margin-bottom: 4px; word-break: break-word; }\\n.c-id {\\n font-size: 10px;\\n color: var(--wb-ink-muted);\\n margin-bottom: 6px;\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.c-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin-bottom: 6px; }\\n\\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\\n.t-unclaimed { background: color-mix(in srgb, var(--wb-ink-muted) 15%, transparent); color: var(--wb-ink-muted); border: 1px dashed var(--wb-ink-muted); }\\n.t-owner { background: color-mix(in srgb, var(--wb-accent) 15%, transparent); color: var(--wb-accent); }\\n\\n/* Verification tell: gate solid green, self-report amber outline, human\\n blue, a declared verify with no verification yet grey \\\"gated\\\" — never\\n render an unverified done as gate-passed (see render.ts's verificationTag). */\\n.t-gate { background: color-mix(in srgb, var(--wb-status-done) 18%, transparent); color: var(--wb-status-done); }\\n.t-self { background: transparent; color: var(--wb-status-in-progress); border: 1px solid var(--wb-status-in-progress); }\\n.t-human { background: color-mix(in srgb, var(--wb-accent) 18%, transparent); color: var(--wb-accent); }\\n.t-gated { background: color-mix(in srgb, var(--wb-ink-muted) 12%, transparent); color: var(--wb-ink-muted); }\\n.t-cancelled {\\n background: color-mix(in srgb, var(--wb-status-cancelled) 10%, transparent);\\n color: var(--wb-status-cancelled-weak);\\n border: 1px dashed color-mix(in srgb, var(--wb-status-cancelled) 40%, transparent);\\n}\\n\\n.c-err { font-size: 10px; color: var(--wb-status-failed); margin-bottom: 6px; word-break: break-word; }\\n.c-actions { display: flex; flex-wrap: wrap; gap: 4px; }\\n.abtn {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 3px 8px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 11px;\\n font-family: inherit;\\n}\\n.abtn:hover { background: var(--wb-border); }\\n.abtn.danger { border-color: var(--wb-status-failed); color: var(--wb-status-failed); }\\n.abtn.primary { border-color: var(--wb-status-done); color: var(--wb-status-done); }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--wb-ink-muted);\\n border-top: 1px solid var(--wb-border);\\n background: var(--wb-surface);\\n flex-shrink: 0;\\n}\\n.empty { padding: 16px 8px; color: var(--wb-ink-muted); font-size: 11px; text-align: center; }</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"toolbar\\\">\\n <span id=\\\"board-label\\\">Board:</span>\\n <span id=\\\"board-id\\\">—</span>\\n <input id=\\\"board-input\\\" type=\\\"text\\\" placeholder=\\\"tree:&lt;sessionId&gt; or ws:&lt;slug&gt;\\\">\\n <button class=\\\"tbtn\\\" id=\\\"go-btn\\\">Go</button>\\n <button class=\\\"tbtn\\\" id=\\\"refresh-btn\\\" title=\\\"Refresh\\\">&#8635;</button>\\n <input id=\\\"new-title\\\" type=\\\"text\\\" placeholder=\\\"New task title&#8230;\\\">\\n <button class=\\\"tbtn\\\" id=\\\"add-btn\\\">+ Add</button>\\n </div>\\n <div id=\\\"columns\\\"></div>\\n <div id=\\\"statusbar\\\">Connecting to bridge&#8230;</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto work-board panel — the `WORK_BOARD_HTML`\n * served as the `ui://agentproto_work_board/view` resource by\n * `registerMcpApps` (see ../mcp-app-types.ts and runtime's\n * mcp-apps-adapter.ts). Mounted via the `agentproto_work_board` tool defined\n * in ./index.ts.\n *\n * A kanban over the daemon's Task ledger (`packages/runtime/src/\n * task-ledger.ts`) — the one writable multi-party entity the daemon exposes\n * (Activity is a read-only projection; a session has a lifecycle, not an\n * intention). Columns are the ledger's own status enum, folding `cancelled`\n * into the `failed` column (tagged distinctly — see `columnOf` in\n * ./ui/render.ts) rather than adding a fifth column for a status v1 treats\n * as terminal scrap. The board/swimlane selector is the ledger's own board\n * id (`tree:<rootSessionId>` or `ws:<slug>`) — always shown verbatim, never\n * hidden behind a friendly name, per the ledger's scoping model.\n *\n * `WORK_BOARD_HTML` is a real single-file Vite build, not a hand-authored\n * template string: the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run build:ui`\n * (scripts/build-work-board-ui.mjs) — so this package still builds with\n * plain `pnpm build` on a fresh clone, no Vite step in the critical path.\n * `build:ui:check` (wired into CI) fails if that generated file drifts from\n * what the Vite sources actually produce. See ./ui/vite.config.ts for how\n * the shared `panelBridgeScript` (../panel-bridge.ts) gets inlined as a\n * classic `<script>` ahead of the bundled module script.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts,\n * unmodified by this change — this app only calls its `initBridge()`/\n * `callTool()` surface). Standalone (no host at all, e.g. a top-level\n * `GET /apps/:appId/ui` tab) does NOT connect yet: panel-bridge.ts's\n * handshake has no reply to wait for in that case and hangs on\n * \"Connecting to bridge…\" — a known gap being fixed in panel-bridge.ts\n * itself by a separate change. This app will pick that fix up automatically\n * (rebuild + rebase) once it lands; nothing here needs to special-case it.\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → task_list (`full: true` — the compact projection\n * drops `verification`, which the verification tell needs) on a ~4 s poll\n * – Actions: tools/call → task_claim / task_update / task_create\n *\n * Read-only-safe first: no drag-and-drop. Each card gets the explicit status\n * actions valid from its current state (Claim/Start/Done/Fail/Release/\n * Cancel/Reopen) — a correct read-only-plus-explicit-actions board beats a\n * half-wired drag, per the brief.\n *\n * The verification tell (never render an unverified done as gate-passed):\n * - `verification.kind === \"gate\"` → solid green \"✓ gate\"\n * - `verification.kind === \"self-report\"` → amber outline \"self-report\"\n * - `verification.kind === \"human\"` → blue \"human\"\n * - a declared `verify` with no verification yet → grey \"gated\" tag\n * An owner-less task always reads \"Unclaimed\", never \"pending\" (that word is\n * reserved for the Activity vocabulary's \"blocked\" meaning elsewhere).\n */\n\nimport { WORK_BOARD_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph — the VS Code extension bundles with esbuild and\n * chokes on its transitive native `.node` deps — can still name the panel\n * from source. */\nexport const WORK_BOARD_APP_ID = \"@agentproto/work-board\"\n\n/** The MCP tool the panel is registered under (./index.ts `makeWorkBoardApp`),\n * hence its resource uri `ui://agentproto_work_board/view`. */\nexport const WORK_BOARD_TOOL_ID = \"agentproto_work_board\"\n\n/** The daemon tools this panel's html calls, in order of use. Consumed as\n * `ui.tools` by ./index.ts and as the client-side allowlist by any host that\n * dispatches the panel's `tools/call` directly — a builtin is not an\n * installed app, so `app_tool_call` cannot route for it. */\nexport const WORK_BOARD_UI_TOOLS = [\n \"task_list\",\n \"task_claim\",\n \"task_update\",\n \"task_create\",\n] as const\n\nexport const WORK_BOARD_HTML = WORK_BOARD_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto work-board panel — a kanban over the\n * daemon's Task ledger (`packages/runtime/src/task-ledger.ts`), the ONLY\n * writable multi-party entity the daemon exposes (Activity is a read-only\n * projection recomputed on every `list()`; a session has a lifecycle, not an\n * intention).\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra), same as sessions-panel /\n * session-chat — the host (@agentproto/runtime) wires this factory's output\n * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the\n * mounted list assembled in runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_work_board` is called by the host with an optional\n * `boardId`.\n * 2. execute() answers an initial `{boardId, tasks}` snapshot off the\n * ledger — resolved to the caller's default board when `boardId` is\n * omitted (a session's `tree:<root>`, the operator's `ws:<slug>`).\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `task_list` (`full: true` — the compact projection drops\n * `verification`) every ~4 s; writes go through `task_claim` /\n * `task_update` / `task_create`, the same verbs any other caller uses —\n * no second write path.\n *\n * This module also exports `workBoardApp`, a real `defineApp()` `AppHandle`\n * (`agents: []`, UI-only) — the catalog/emit/`app_install` path. It's\n * separate from `makeWorkBoardApp` above, which the daemon mounts directly\n * at boot: that factory closes over the LIVE ledger read, something a\n * static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport {\n WORK_BOARD_APP_ID,\n WORK_BOARD_HTML,\n WORK_BOARD_TOOL_ID,\n WORK_BOARD_UI_TOOLS,\n} from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const workBoardInputSchema = z.object({\n boardId: z\n .string()\n .optional()\n .describe(\n \"Board to open — `tree:<rootSessionId>` (a supervisor and its \" +\n \"executors) or `ws:<slug>` (an operator workspace). Omit → the \" +\n \"caller's default board.\",\n ),\n})\n\nexport type WorkBoardInput = z.infer<typeof workBoardInputSchema>\n\nexport interface WorkBoardOutput<TTask = unknown> {\n /** Always echoed back — the ledger's board ids are the swimlane\n * selector, and must be visible rather than hidden behind a friendly\n * name (it's how the operator knows which mission they're looking at). */\n boardId: string\n tasks: TTask[]\n}\n\n/**\n * Generic over the host's own task-record shape — this package doesn't own\n * that type (it lives in @agentproto/runtime's task ledger), so the factory\n * stays agnostic to it and the host supplies the concrete type as a type\n * argument when it wires the app up (mirrors SessionsPanelOps<TSession>).\n */\nexport interface WorkBoardOps<TTask = unknown> {\n /** Full (unprojected) records for a board — NOT `task_list`'s default\n * compact projection, since the verification tell needs `verification`\n * (self-report vs gate vs human), which compact mode omits. Omit\n * `boardId` to resolve the caller's default board. */\n listTasks(boardId?: string): WorkBoardOutput<TTask>\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the ledger read so execute() doesn't need an\n * AppContext (agentproto has no userId/guildId concept) — mirrors\n * makeSessionsPanelApp.\n */\nexport function makeWorkBoardApp<TTask = unknown>(\n ops: WorkBoardOps<TTask>,\n): AgnoMcpApp<WorkBoardInput, WorkBoardOutput<TTask>> {\n return {\n id: WORK_BOARD_TOOL_ID,\n title: \"Work Board\",\n description:\n \"Open the agentproto work board — a kanban over the daemon's Task \" +\n \"ledger (columns: pending/in_progress/done/failed, cancelled folded \" +\n \"into failed). Swimlane is the ledger's own board id (`tree:<root>` \" +\n \"or `ws:<slug>`). Each card shows its owner (or \\\"Unclaimed\\\") and the \" +\n \"verification tell — a gate-passed done reads visually distinct from \" +\n \"a self-reported one. Pass `boardId` to open a specific board.\",\n inputSchema: workBoardInputSchema,\n execute: async input => ops.listTasks(input.boardId),\n html: WORK_BOARD_HTML,\n }\n}\n\nexport const workBoardApp: AppHandle = defineApp({\n id: WORK_BOARD_APP_ID,\n name: \"Work Board\",\n description:\n \"Open the agentproto work board — a kanban over the daemon's Task ledger, scoped by board id \" +\n \"(`tree:<rootSessionId>` or `ws:<slug>`), with an explicit status action per card.\",\n agents: [],\n ui: {\n html: WORK_BOARD_HTML,\n title: \"Work Board\",\n tools: [...WORK_BOARD_UI_TOOLS],\n },\n})\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/apps",
3
- "version": "0.20.2",
3
+ "version": "0.21.0",
4
4
  "description": "@agentproto/apps — a home for ready-made agentproto apps (teams of agents + their workflows), declared with @agentproto/app-kit. Import a team (code-team, content-team, …) and use any of its agents/workflows from your own host: in-process via toMastraAgents(), or emit its manifests to disk.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -175,7 +175,7 @@
175
175
  "zod": "^4.6.5",
176
176
  "@agentproto/agent": "0.2.5",
177
177
  "@agentproto/app-client": "0.4.1",
178
- "@agentproto/app-kit": "1.6.1",
178
+ "@agentproto/app-kit": "1.7.0",
179
179
  "@agentproto/workflow": "0.7.1"
180
180
  },
181
181
  "peerDependencies": {
@@ -44,6 +44,14 @@ inputs:
44
44
  with a stored verdict are never re-reviewed, so daily runs walk the
45
45
  backlog.
46
46
  default: 40
47
+ reviewHeldWorktrees:
48
+ type: boolean
49
+ description: >-
50
+ Also review branches held only because they're checked out in an idle,
51
+ clean linked worktree with no open PR (tip at least 3 days old). The
52
+ report lists each verdict and the removal command when the reviewer
53
+ agreed; nothing is removed automatically.
54
+ default: true
47
55
  notify:
48
56
  type: object
49
57
  description: >-
@@ -68,11 +76,15 @@ steps:
68
76
  - id: worktreeGcPlan
69
77
  kind: tool
70
78
  name: Plan worktree gc
79
+ description: >-
80
+ `wait: true` — without it worktree_gc falls back to a background job
81
+ after 25 s and returns a bare jobId instead of the plan.
71
82
  tool: worktree_gc
72
83
  inputs:
73
84
  repoRoot: $input.repoRoot
74
85
  workspaceSlug: $input.workspaceSlug
75
86
  apply: false
87
+ wait: true
76
88
 
77
89
  - id: branchGcPlan
78
90
  kind: tool
@@ -89,7 +101,10 @@ steps:
89
101
  name: Queue unreviewed review candidates, one per branch name
90
102
  description: >-
91
103
  Entry-based — no string expression language for `compute` in the
92
- declarative manifest. See entry.mjs's buildReviewQueue: dedupes by tip
104
+ declarative manifest. With `reviewHeldWorktrees` (default true) it also
105
+ queues branches branch_gc held only for `worktree` when worktreeGcPlan
106
+ shows that worktree idle, clean and without an open PR (entry.mjs's
107
+ reviewableWorktrees). See entry.mjs's buildReviewQueue: dedupes by tip
93
108
  sha, then merges candidates whose refs share one branch name (a local
94
109
  branch and its remote twin review together even when their tips
95
110
  diverged — the newest tip is primary, the older ones ride along as
@@ -175,6 +190,7 @@ steps:
175
190
  workspaceSlug: $input.workspaceSlug
176
191
  apply: $input.applyMerged
177
192
  salvageDirty: false
193
+ wait: true
178
194
 
179
195
  - id: branchGcApply
180
196
  kind: tool
@@ -265,6 +281,12 @@ it when the item ends; `reviewCleanup` removes them all after the map, and
265
281
  entry.mjs's `finally` block removes them once more on failure or cancel — a
266
282
  `finally` step always runs.
267
283
 
284
+ A review worktree sits outside every app zone, so a reviewer spawned there
285
+ gets two extra READ-ONLY zones for its own session: the worktree and the
286
+ repo's git dir (`git log/show/diff` read the object store). Both are checked
287
+ against git's own worktree back-pointer first, and both are read-only, so a
288
+ reviewer still cannot stash, checkout or commit in the shared repo.
289
+
268
290
  ## Out of scope, on purpose
269
291
 
270
292
  Recording a verdict via `branch_gc_verdict` never reclaims a branch by
@@ -41,6 +41,9 @@ const DEFAULT_MAX_REVIEWS = 40
41
41
  /** Longest name list the markdown report inlines; the full lists are in the
42
42
  * run output (`gaps`, …). */
43
43
  const REPORT_LIST_CAP = 20
44
+ /** A worktree-held branch is reviewed only once its tip is this old — the
45
+ * same floor as branch_gc's `minAgeDays` default for `review`. */
46
+ const HELD_WORKTREE_MIN_AGE_DAYS = 3
44
47
 
45
48
  const REVIEWER_REF = "@agentproto/repo-maintenance-reviewer"
46
49
 
@@ -84,6 +87,11 @@ const REVIEW_PROMPT =
84
87
  "(the tree base would have if this branch merged cleanly, or null when the merge conflicts): " +
85
88
  "{{#item.mergedTree}}{{item.mergedTree}}{{/item.mergedTree}}{{^item.mergedTree}}null{{/item.mergedTree}}. Ahead {{item.ahead}}, behind {{item.behind}}. Push state: {{item.pushed}}. " +
86
89
  "Refs of this branch (across its tips): {{item.refs}}." +
90
+ "{{#item.worktree}}\n\nThis branch is checked out in the linked worktree {{item.worktree}} — " +
91
+ "idle (no live session), clean tree, no open PR — which is the only reason branch gc held it. " +
92
+ "Your verdict decides whether that worktree (and the branch) can be removed: agree only when " +
93
+ "everything committed on it is in base or not worth keeping. Never touch that worktree; read " +
94
+ "the branch through its sha like any other.{{/item.worktree}}" +
87
95
  "{{#item.otherTips}}\n\nThis branch name has OLDER tip(s) besides {{item.sha}} — " +
88
96
  "{{item.otherTips}} (JSON array of { sha, refs }; each sha may carry work the newer tip does " +
89
97
  "not). The coverage above describes ONLY the primary tip {{item.sha}}. Inspect EVERY sha " +
@@ -147,6 +155,49 @@ const VERDICT_NEEDS = (id, checkId) => ({
147
155
  },
148
156
  })
149
157
 
158
+ /** Linked worktrees whose branch is worth an LLM review: idle (no live
159
+ * session), clean tree, and no open PR — `path → branch`. branch_gc holds
160
+ * every branch checked out in a worktree (`holdReason: "worktree"`) and
161
+ * worktree_gc holds every worktree whose branch isn't provably merged, so
162
+ * without this an abandoned-but-unmerged worktree is held by both forever.
163
+ * `worktree_gc`'s dry run never lists the main checkout, so its branch is
164
+ * never a candidate. A dirty tree is left out: its WIP isn't on the tip a
165
+ * reviewer reads. */
166
+ export function reviewableWorktrees(worktreeGcPlanResult) {
167
+ const out = new Map()
168
+ const entries = Array.isArray(worktreeGcPlanResult?.plan) ? worktreeGcPlanResult.plan : []
169
+ for (const w of entries) {
170
+ if (w?.class !== "hold" || !w.branch || !w.path) continue
171
+ if (w.liveness?.state !== "idle" || w.tree?.state !== "clean") continue
172
+ if (w.integration?.state === "open") continue
173
+ out.set(w.path, w.branch)
174
+ }
175
+ return out
176
+ }
177
+
178
+ /** The worktree path a branch_gc entry is held by, when that hold is the only
179
+ * thing between it and `review`: held for `worktree`, unmerged, old enough,
180
+ * and the worktree is one {@link reviewableWorktrees} kept. Else null. */
181
+ function heldWorktreeOf(e, heldWorktrees) {
182
+ if (e?.class !== "hold" || e.holdReason !== "worktree" || e.status !== "unmerged") return null
183
+ if ((e.ageDays ?? 0) < HELD_WORKTREE_MIN_AGE_DAYS) return null
184
+ return heldWorktrees.get(e.holdDetail) === e.name ? e.holdDetail : null
185
+ }
186
+
187
+ /** One line per reviewed held worktree (from the verify plan's stored
188
+ * verdicts): what the reviewer said, and — when it agreed nothing of value
189
+ * is lost — the command that removes it. Removal stays a human step. */
190
+ export function heldWorktreeVerdicts(branchGcVerifyResult, worktreeGcPlanResult) {
191
+ const held = reviewableWorktrees(worktreeGcPlanResult)
192
+ const byPath = new Map()
193
+ for (const e of branchGcVerifyResult?.plan?.entries ?? []) {
194
+ const path = heldWorktreeOf(e, held)
195
+ if (!path || !e.verdict || byPath.has(path)) continue
196
+ byPath.set(path, { name: e.name, path, sha: e.sha, triage: e.verdict.triage, agree: e.verdict.agree ?? null })
197
+ }
198
+ return [...byPath.values()]
199
+ }
200
+
150
201
  /** One `review`-class branch_gc plan entry per BRANCH NAME — a local branch
151
202
  * and its remote twin share one review instead of costing the reviewer two
152
203
  * turns, whether they share a tip (grouped by sha, as before) or DIVERGED
@@ -167,13 +218,15 @@ const VERDICT_NEEDS = (id, checkId) => ({
167
218
  * queued with only its unreviewed tips. Returns the queue (oldest first,
168
219
  * then the larger residual) plus how many names were skipped as already
169
220
  * reviewed. */
170
- export function buildReviewQueue(branchGcPlanResult) {
221
+ export function buildReviewQueue(branchGcPlanResult, worktreeGcPlanResult, opts = {}) {
171
222
  const plan = branchGcPlanResult?.plan
172
223
  const entries = Array.isArray(plan?.entries) ? plan.entries : []
224
+ const heldWorktrees = opts.reviewHeldWorktrees === false ? new Map() : reviewableWorktrees(worktreeGcPlanResult)
225
+ const reviewable = e => e.class === "review" || heldWorktreeOf(e, heldWorktrees) !== null
173
226
  const bySha = new Map()
174
227
  const dates = new Map()
175
228
  for (const e of entries) {
176
- if (e.class !== "review") continue
229
+ if (!reviewable(e)) continue
177
230
  dates.set(e.sha, e.date ?? "")
178
231
  if (e.verdict) continue
179
232
  const seen = bySha.get(e.sha)
@@ -205,6 +258,7 @@ export function buildReviewQueue(branchGcPlanResult) {
205
258
  residualFiles: e.residualFiles ?? [],
206
259
  residualFileCount: e.residualFileCount ?? 0,
207
260
  refs: [e.ref],
261
+ ...(heldWorktreeOf(e, heldWorktrees) ? { worktree: heldWorktreeOf(e, heldWorktrees) } : {}),
208
262
  })
209
263
  }
210
264
  // Merge candidates whose refs share one branch NAME: `foo` and `origin/foo`
@@ -241,7 +295,7 @@ export function buildReviewQueue(branchGcPlanResult) {
241
295
  const pendingNames = new Set(queue.map(c => c.name))
242
296
  const fullyReviewed = new Set()
243
297
  for (const e of entries) {
244
- if (e.class === "review" && e.verdict && !pendingNames.has(e.name)) fullyReviewed.add(e.name)
298
+ if (reviewable(e) && e.verdict && !pendingNames.has(e.name)) fullyReviewed.add(e.name)
245
299
  }
246
300
  return { queue, alreadyReviewed: fullyReviewed.size }
247
301
  }
@@ -258,13 +312,30 @@ export function formatNameList(names, cap = REPORT_LIST_CAP) {
258
312
  return names.length > cap ? `${shown}, … and ${names.length - cap} more` : shown
259
313
  }
260
314
 
261
- /** Distinct error messages of a tolerant fan-out's rejected items, most
315
+ /** `error` with the per-candidate parts — its review worktree path, branch
316
+ * name and tip sha, then any other full sha — replaced by placeholders, so
317
+ * one systemic failure hitting N candidates groups as ONE reason instead of
318
+ * N "distinct" ones that differ only by `…/repo-<sha>`. */
319
+ export function normalizeReviewError(error, candidate) {
320
+ let out = String(error ?? "")
321
+ const swap = (needle, placeholder) => {
322
+ if (typeof needle === "string" && needle.length > 0) out = out.split(needle).join(placeholder)
323
+ }
324
+ swap(candidate?.reviewWorktree, "<review worktree>")
325
+ swap(candidate?.sha, "<sha>")
326
+ swap(candidate?.name, "<branch>")
327
+ return out.replace(/\b[0-9a-f]{40}\b/g, "<sha>")
328
+ }
329
+
330
+ /** Distinct error messages of a tolerant fan-out's rejected items (normalized
331
+ * by {@link normalizeReviewError} against `reviewCandidates[index]`), most
262
332
  * frequent first: `[{ error, count }]`. */
263
- export function tallyReviewErrors(review) {
333
+ export function tallyReviewErrors(review, reviewCandidates) {
264
334
  const counts = new Map()
265
335
  for (const r of Array.isArray(review?.results) ? review.results : []) {
266
336
  if (r?.status !== "rejected") continue
267
- counts.set(r.error, (counts.get(r.error) ?? 0) + 1)
337
+ const error = normalizeReviewError(r.error, r.item ?? reviewCandidates?.[r.index])
338
+ counts.set(error, (counts.get(error) ?? 0) + 1)
268
339
  }
269
340
  return [...counts].map(([error, count]) => ({ error, count })).sort((a, b) => b.count - a.count)
270
341
  }
@@ -363,7 +434,7 @@ export function buildReport(b) {
363
434
  const reviewOutcome = Array.isArray(review)
364
435
  ? { succeeded: review.length, failed: 0, skipped: 0 }
365
436
  : { succeeded: 0, failed: 0, skipped: 0, ...(review ?? {}) }
366
- const reviewErrors = tallyReviewErrors(review)
437
+ const reviewErrors = tallyReviewErrors(review, reviewCandidates)
367
438
  const gaps = b.steps.gaps ?? []
368
439
  const verdicts = collectCandidateVerdicts(reviewCandidates, b.steps.branchGcVerify)
369
440
  const applyMerged = b.input?.applyMerged === true
@@ -424,6 +495,19 @@ export function buildReport(b) {
424
495
  } else if (reviewCandidates.length > 0 && reviewOutcome.skipped === 0) {
425
496
  lines.push("- every review candidate has a recorded verdict")
426
497
  }
498
+ const heldVerdicts = b.input?.reviewHeldWorktrees === false ? [] : heldWorktreeVerdicts(b.steps.branchGcVerify, wtPlan)
499
+ if (heldVerdicts.length > 0) {
500
+ lines.push("")
501
+ lines.push(`## Held worktrees reviewed (${heldVerdicts.length})`)
502
+ lines.push("Idle, clean worktrees whose branch isn't provably merged — gc holds them; the reviewer's verdict says whether they can go. Removal is manual.")
503
+ for (const v of heldVerdicts.slice(0, REPORT_LIST_CAP)) {
504
+ lines.push(
505
+ `- \`${v.name}\` — ${v.triage}` +
506
+ (v.agree === true ? ` — nothing of value lost: \`agentproto worktree rm ${v.path}\`` : ` — keep (${v.path})`),
507
+ )
508
+ }
509
+ if (heldVerdicts.length > REPORT_LIST_CAP) lines.push(`- … and ${heldVerdicts.length - REPORT_LIST_CAP} more`)
510
+ }
427
511
  lines.push("")
428
512
  lines.push("## Apply")
429
513
  if (!applyMerged) {
@@ -468,6 +552,7 @@ export default {
468
552
  reviewModelSmall: { type: "string", description: `Model for a review candidate with residualFileCount <= 3. Default: the \`${ROLE_REVIEW_SMALL}\` model role (repo agentproto.json \`models\` > daemon config \`models\` > built-in).` },
469
553
  reviewModelLarge: { type: "string", description: `Model for a review candidate with residualFileCount > 3, and the retry reviewer. Default: the \`${ROLE_REVIEW_LARGE}\` model role (repo agentproto.json \`models\` > daemon config \`models\` > built-in).` },
470
554
  maxReviews: { type: "number", description: `Most review candidates to review this run — newest tip first, then the larger residual. The rest are reported as not reviewed this run; tips with a stored verdict are never re-reviewed, so daily runs walk the backlog. Default ${DEFAULT_MAX_REVIEWS}.`, default: DEFAULT_MAX_REVIEWS },
555
+ reviewHeldWorktrees: { type: "boolean", description: `Also review branches held only because they're checked out in an idle, clean linked worktree with no open PR (tip ≥ ${HELD_WORKTREE_MIN_AGE_DAYS}d old). The report lists each verdict and the removal command when the reviewer agreed; nothing is removed automatically. Default true.`, default: true },
471
556
  notify: {
472
557
  type: "object",
473
558
  description: "Optional agentpush `to` target ({ channel, address }) to notify with the report when set. Omit for no notification.",
@@ -493,7 +578,11 @@ export default {
493
578
  id: "worktreeGcPlan",
494
579
  kind: "tool",
495
580
  tool: "worktree_gc",
496
- inputs: { repoRoot: "$input.repoRoot", workspaceSlug: "$input.workspaceSlug", apply: false },
581
+ // `wait: true` — without it worktree_gc returns `{ jobId, status:
582
+ // "running" }` after its 25 s default `waitMs`, and on a repo with
583
+ // dozens of worktrees the report would count 0 classified. branch_gc
584
+ // already blocks by default.
585
+ inputs: { repoRoot: "$input.repoRoot", workspaceSlug: "$input.workspaceSlug", apply: false, wait: true },
497
586
  },
498
587
  {
499
588
  id: "branchGcPlan",
@@ -504,7 +593,7 @@ export default {
504
593
  {
505
594
  id: "reviewQueue",
506
595
  kind: "transform",
507
- compute: b => buildReviewQueue(b.steps.branchGcPlan),
596
+ compute: b => buildReviewQueue(b.steps.branchGcPlan, b.steps.worktreeGcPlan, { reviewHeldWorktrees: b.input?.reviewHeldWorktrees !== false }),
508
597
  },
509
598
  {
510
599
  id: "reviewCandidates",
@@ -612,6 +701,9 @@ export default {
612
701
  workspaceSlug: "$input.workspaceSlug",
613
702
  apply: "$input.applyMerged",
614
703
  salvageDirty: false,
704
+ // Same as worktreeGcPlan: block for the real outcomes, or an apply
705
+ // would run unreported in the background after the workflow ends.
706
+ wait: true,
615
707
  },
616
708
  },
617
709
  {