@agentproto/apps 0.20.3 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/agents-overview/panel.mjs +21 -8
  2. package/dist/agents-overview/panel.mjs.map +1 -1
  3. package/dist/agents-overview.mjs +21 -8
  4. package/dist/agents-overview.mjs.map +1 -1
  5. package/dist/bin/sync.mjs +19 -2
  6. package/dist/bin/sync.mjs.map +1 -1
  7. package/dist/bureau-sessions/panel.mjs +21 -8
  8. package/dist/bureau-sessions/panel.mjs.map +1 -1
  9. package/dist/bureau-sessions.mjs +21 -8
  10. package/dist/bureau-sessions.mjs.map +1 -1
  11. package/dist/index.mjs +63 -20
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/live-session/panel.mjs +21 -8
  14. package/dist/live-session/panel.mjs.map +1 -1
  15. package/dist/live-session.mjs +21 -8
  16. package/dist/live-session.mjs.map +1 -1
  17. package/dist/ops-panel/ui.d.ts +1 -1
  18. package/dist/ops-panel/ui.d.ts.map +1 -1
  19. package/dist/ops-panel.mjs +19 -2
  20. package/dist/ops-panel.mjs.map +1 -1
  21. package/dist/panel-bridge.d.ts.map +1 -1
  22. package/dist/review-panel/panel.d.ts +1 -1
  23. package/dist/review-panel/panel.d.ts.map +1 -1
  24. package/dist/review-panel/panel.generated.d.ts +1 -1
  25. package/dist/review-panel/panel.generated.d.ts.map +1 -1
  26. package/dist/review-panel/panel.mjs +1 -1
  27. package/dist/review-panel/panel.mjs.map +1 -1
  28. package/dist/review-panel.mjs +1 -1
  29. package/dist/review-panel.mjs.map +1 -1
  30. package/dist/session-chat/panel.mjs +21 -8
  31. package/dist/session-chat/panel.mjs.map +1 -1
  32. package/dist/session-chat.mjs +21 -8
  33. package/dist/session-chat.mjs.map +1 -1
  34. package/dist/session-story/panel.mjs +21 -8
  35. package/dist/session-story/panel.mjs.map +1 -1
  36. package/dist/session-story.mjs +21 -8
  37. package/dist/session-story.mjs.map +1 -1
  38. package/dist/sessions-panel/panel.mjs +21 -8
  39. package/dist/sessions-panel/panel.mjs.map +1 -1
  40. package/dist/sessions-panel.mjs +21 -8
  41. package/dist/sessions-panel.mjs.map +1 -1
  42. package/dist/store/panel.d.ts +1 -1
  43. package/dist/store/panel.d.ts.map +1 -1
  44. package/dist/store/panel.generated.d.ts +1 -1
  45. package/dist/store/panel.generated.d.ts.map +1 -1
  46. package/dist/store/panel.mjs +1 -1
  47. package/dist/store/panel.mjs.map +1 -1
  48. package/dist/store.mjs +1 -1
  49. package/dist/store.mjs.map +1 -1
  50. package/dist/work-board/panel.d.ts +1 -1
  51. package/dist/work-board/panel.d.ts.map +1 -1
  52. package/dist/work-board/panel.generated.d.ts +1 -1
  53. package/dist/work-board/panel.generated.d.ts.map +1 -1
  54. package/dist/work-board/panel.mjs +21 -8
  55. package/dist/work-board/panel.mjs.map +1 -1
  56. package/dist/work-board.mjs +21 -8
  57. package/dist/work-board.mjs.map +1 -1
  58. package/package.json +3 -3
  59. package/repo-maintenance/.agentproto/workflows/maintain/WORKFLOW.md +17 -1
  60. package/repo-maintenance/.agentproto/workflows/maintain/entry.mjs +88 -5
  61. package/session-steward/.agentproto/workflows/session-steward/WORKFLOW.md +38 -6
  62. package/session-steward/.agentproto/workflows/session-steward/entry.mjs +44 -6
  63. package/session-steward/.agentproto/workflows/session-steward/origin-policy.mjs +2 -2
  64. package/session-steward/README.md +12 -4
  65. package/session-steward/routines/session-steward-hourly/ROUTINE.md +6 -3
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ops-panel/agents/manager.ts","../src/ops-panel/agents/watchdog.ts","../src/ops-panel/ui.ts","../src/ops-panel/index.ts"],"names":["defineAgent"],"mappings":";;;;;;;;AAgBO,IAAM,aAAA,GAAgB;AAEtB,IAAM,OAAA,GAAsB;AAAA,EACjC,OAAO,WAAA,CAAY;AAAA,IACjB,MAAA,EAAQ,UAAA;AAAA,IACR,EAAA,EAAI,8BAAA;AAAA,IACJ,WAAA,EACE,+JAAA;AAAA,IACF,KAAA,EAAO,iBAAA;AAAA,IACP,UAAA,EAAY;AAAA,MACV,oFAAA;AAAA,MACA,mFAAA;AAAA,MACA;AAAA,KACF;AAAA,IACA,KAAA,EAAO;AAAA,MACL,cAAA;AAAA,MACA,mBAAA;AAAA,MACA,cAAA;AAAA,MACA,iBAAA;AAAA,MACA,aAAA;AAAA,MACA;AAAA;AACF,GACD,CAAA;AAAA,EACD,IAAA,EAAM;AAAA,IACJ,gGAAA;AAAA,IACA,2GAAA;AAAA,IACA,6EAAA;AAAA,IACA,EAAA;AAAA,IACA,cAAA;AAAA,IACA,6FAAA;AAAA,IACA,sGAAA;AAAA,IACA,wFAAA;AAAA,IACA,qFAAA;AAAA,IACA,yFAAA;AAAA,IACA,8FAAA;AAAA,IACA,EAAA;AAAA,IACA,aAAA;AAAA,IACA,2EAAA;AAAA,IACA,kGAAA;AAAA,IACA,uBAAA;AAAA,IACA,gGAAA;AAAA,IACA;AAAA,GACF,CAAE,KAAK,IAAI;AACb;AC1CO,IAAM,cAAA,GAAiB;AAGvB,IAAM,mBAAA,GAAsB;AAG5B,IAAM,sBAAA,GAAyB;AAG/B,IAAM,oBAAA,GAAuB;AAE7B,IAAM,QAAA,GAAuB;AAAA,EAClC,OAAOA,WAAAA,CAAY;AAAA,IACjB,MAAA,EAAQ,UAAA;AAAA,IACR,EAAA,EAAI,8BAAA;AAAA,IACJ,WAAA,EACE,qKAAA;AAAA,IACF,KAAA,EAAO,iBAAA;AAAA,IACP,UAAA,EAAY;AAAA,MACV,oGAAA;AAAA,MACA,qGAAA;AAAA,MACA;AAAA,KACF;AAAA,IACA,KAAA,EAAO;AAAA,MACL,cAAA;AAAA,MACA,qBAAA;AAAA,MACA,qBAAA;AAAA,MACA,iBAAA;AAAA,MACA;AAAA;AACF,GACD,CAAA;AAAA,EACD,IAAA,EAAM;AAAA,IACJ,2FAAA;AAAA,IACA,mGAAA;AAAA,IACA,oGAAA;AAAA,IACA,EAAA;AAAA,IACA,YAAA;AAAA,IACA,8FAAA;AAAA,IACA,oDAAA;AAAA,IACA,iGAAA;AAAA,IACA,qCAAA;AAAA,IACA,cAAA;AAAA,IACA,gGAAA;AAAA,IACA,8FAAA;AAAA,IACA,8FAAA;AAAA,IACA,0EAAA;AAAA,IACA,+EAAA;AAAA,IACA,iGAAA;AAAA,IACA,kGAAA;AAAA,IACA,mFAAA;AAAA,IACA,kGAAA;AAAA,IACA,kGAAA;AAAA,IACA,+FAAA;AAAA,IACA,qGAAA;AAAA,IACA,6CAAA;AAAA,IACA,qGAAA;AAAA,IACA,CAAA,2CAAA,CAAA;AAAA,IACA,EAAA;AAAA,IACA;AAAA,GACF,CAAE,KAAK,IAAI;AACb;;;AChDO,IAAM,eAAA,GAAkB;AAAA,EAC7B,eAAA;AAAA,EACA,cAAA;AAAA,EACA,aAAA;AAAA,EACA,cAAA;AAAA,EACA,YAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,YAAA;AAAA,EACA,WAAA;AAAA,EACA,aAAA;AAAA,EACA,aAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA;AAEA,IAAM,MAAA,GAAS,uBAAA;AAGf,IAAM,QAAA,GAAW;AAAA,EACf;AAAA,IACE,GAAA,EAAK,UAAA;AAAA,IACL,KAAA,EAAO,cAAA;AAAA,IACP,OAAA,EAAS,+DAAA;AAAA,IACT,MAAA,EAAQ,SAAS,IAAA,IAAQ,EAAA;AAAA,IACzB,IAAA,EAAM;AAAA,MACJ,KAAA,EAAO,mBAAA;AAAA,MACP,QAAA,EAAU,sBAAA;AAAA,MACV,MAAA,EAAQ;AAAA;AACV,GACF;AAAA,EACA;AAAA,IACE,GAAA,EAAK,SAAA;AAAA,IACL,KAAA,EAAO,aAAA;AAAA,IACP,OAAA,EAAS,+CAAA;AAAA,IACT,MAAA,EAAQ,QAAQ,IAAA,IAAQ,EAAA;AAAA,IACxB,IAAA,EAAM;AAAA;AAEV,CAAA;AAEO,IAAM,cAAA,GAAiB,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,EA0Gf,IAAA,CAAK,SAAA,CAAU,MAAM,CAAC,CAAA;AAAA,eAAA,EACpB,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAC,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,OAAA,CAAA;;;AC/JlC,IAAM,WAAsB,SAAA,CAAU;AAAA,EAC3C,EAAA,EAAI,uBAAA;AAAA,EACJ,IAAA,EAAM,WAAA;AAAA,EACN,OAAA,EAAS,OAAA;AAAA,EACT,WAAA,EAAa,mHAAA;AAAA,EACb,MAAA,EAAQ,CAAC,QAAA,EAAU,OAAO,CAAA;AAAA,EAC1B,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,cAAA;AAAA,IACN,KAAA,EAAO,WAAA;AAAA,IACP,KAAA,EAAO,CAAC,GAAG,eAAe;AAAA;AAE9B,CAAC","file":"ops-panel.mjs","sourcesContent":["import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/**\n * The Sessions Manager — the durable \"master of the sessions\" coordinator:\n * the human talks to IT, it talks to the other sessions. Distilled from the\n * hand-run plateau-coordinator session this app productizes.\n *\n * Where the watchdog only OBSERVES (and never touches a live session), the\n * manager DRIVES: it re-prompts idle sessions onto their task, restarts dead\n * ones, and answers \"where is X at?\" by reading transcripts. The Ops Panel\n * spawns it via `agent_start` with {@link MANAGER_LABEL} and `keepAlive`, so\n * the idle-reaper never retires a legitimately-parked coordinator.\n */\n\n/** Session label the panel spawns under — how a live manager is recognized. */\nexport const MANAGER_LABEL = \"Sessions Manager\"\n\nexport const manager: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/sessions-manager\",\n description:\n \"Durable coordinator over the daemon's sessions — routes questions to the right session, re-prompts stalled work, restarts dead sessions, reports status.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Never kill a session unless the human explicitly asks for that session by id/label\",\n \"Never merge PRs or bypass a repo's declared review flow from a supervised session\",\n \"Prefer re-prompting an existing session over spawning a duplicate for the same task\",\n ],\n tools: [\n \"session_list\",\n \"conversation_read\",\n \"agent_prompt\",\n \"session_restart\",\n \"agent_start\",\n \"session_events_poll\",\n ],\n }),\n body: [\n \"You are the manager of this agentproto daemon's sessions: the human talks to you, and you talk\",\n \"to the other sessions. You are durable — the same session across many turns — so keep a working\",\n \"memory of who is doing what and don't re-derive it from scratch every turn.\",\n \"\",\n \"What you do:\",\n \"- Locate: given a task or question, find the relevant session (`session_list`, labels, cwd,\",\n \" recency) and read its transcript (`conversation_read`) before answering — never guess status.\",\n \"- Drive: re-prompt an idle/parked session onto its task with `agent_prompt`; restart a\",\n \" killed/exited session with `session_restart` when its work is clearly unfinished.\",\n \"- Spawn: only when no existing session owns the task — check for duplicates first.\",\n \"- Report: answer with short, concrete status (who, doing what, since when, blocked on what).\",\n \"\",\n \"Boundaries:\",\n \"- Never kill a session unless the human explicitly asks for that session.\",\n \"- A session's own repo conventions (review flow, merge policy) always win — you coordinate,\",\n \" you don't override.\",\n \"- If a session awaits a human decision, surface the question to the human instead of answering\",\n \" in their place.\",\n ].join(\"\\n\"),\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/**\n * The Session Watchdog — a durable daemon-health session, re-prompted on a\n * cron tick (~every 5 minutes). Distilled from the hand-run watchdog session\n * this app productizes: same classification rules, same hard boundaries\n * (never kills a live session, never spawns).\n *\n * The Ops Panel spawns it via `agent_start` with {@link WATCHDOG_LABEL} and\n * wires a `prompt-session` cron ({@link WATCHDOG_TICK_LABEL} /\n * {@link WATCHDOG_TICK_SCHEDULE}) pointing at the spawned session — see\n * `ui.ts`. The agent is also usable standalone via `app_run` for a one-shot\n * health pass.\n */\n\n/** Session label the panel spawns under — how a live watchdog is recognized. */\nexport const WATCHDOG_LABEL = \"Session Watchdog\"\n\n/** Label of the cron job that re-prompts the watchdog session. */\nexport const WATCHDOG_TICK_LABEL = \"watchdog-tick\"\n\n/** Every 5 minutes. */\nexport const WATCHDOG_TICK_SCHEDULE = \"*/5 * * * *\"\n\n/** The prompt each cron tick sends to the durable session. */\nexport const WATCHDOG_TICK_PROMPT = \"Run your maintenance check now.\"\n\nexport const watchdog: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/session-watchdog\",\n description:\n \"Durable daemon-health watchdog — classifies sessions (needs-you, stalled, duplicated, abandonable), corrects flags, archives dead rows. Never kills or spawns.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"NEVER kill a live session, message/interrupt another session's conversation, or spawn new sessions\",\n \"Housekeeping only on terminal-status sessions (exited/killed/error) — never touches a live one\",\n \"Report only what's NEW since the last tick — no padding, no repeats unless something got worse\",\n ],\n tools: [\n \"session_list\",\n \"session_events_poll\",\n \"session_flag_status\",\n \"session_archive\",\n \"session_gc\",\n ],\n }),\n body: [\n \"You are the Session Watchdog for this agentproto daemon. You run on a recurring cron tick\",\n \"(~every 5 minutes) as the SAME durable session, re-prompted each time — you retain memory of\",\n \"what you already flagged, so don't repeat an alert unless something got meaningfully worse or new.\",\n \"\",\n \"Each tick:\",\n \"1. Call `session_list` to see current session state (status, awaitingInput/awaitingQuestion,\",\n \" stalledSinceMs, busy, blockedOn, childrenBusy).\",\n \"2. Call `session_events_poll` for events since your last tick (stalled, exited, awaiting-input)\",\n \" to catch anything between ticks.\",\n \"3. Classify:\",\n \" - Needs you: `awaitingInput: true` with a real unanswered question, older than ~10 minutes.\",\n \" - Stalled: busy but silent well past normal turn latency, no legitimate blockedOn excuse.\",\n \" - Possibly duplicated: two or more live sessions share workspace/cwd and a highly similar\",\n \" recent prompt/label — use judgment, not exact string matching.\",\n \" - Looks done/abandonable: idle/parked with no plausible remaining purpose.\",\n \"4. Correct misclassifications when confident: if `session_flag_status` is available and you can\",\n \" tell a session is actually waiting on a human (or isn't, despite being flagged), call it with\",\n \" a clear `reason`. If that tool is unknown on this daemon build, skip silently.\",\n \"5. Autonomous housekeeping (reversible, so no need to ask): `session_archive` or `session_gc` on\",\n \" sessions already terminal-status (exited/killed/error) and old. Never touches a live session.\",\n \"6. NEVER: kill a live session, message/interrupt another session's conversation, or spawn new\",\n \" sessions. You only have these five tools on purpose — if you'd want another one, report the\",\n \" suggestion instead of working around it.\",\n \"7. Report only what's NEW since last tick — one line per finding. If nothing new, reply with a\",\n \" short \\\"nothing new\\\" — don't pad it.\",\n \"\",\n \"You are not a general coding assistant across ticks — stay scoped to this watchdog role.\",\n ].join(\"\\n\"),\n}\n","/**\n * Ops Panel — a self-contained MCP App panel for `ops-panel`.\n *\n * The daemon operations cockpit: special durable sessions (Session Watchdog,\n * Sessions Manager) as launchable cards, the session list filterable by\n * workspace with lifecycle actions, housekeeping (session GC, worktree GC)\n * and the cron ledger. Talks to the daemon only through the\n * `window.McpApp.connect()` bridge — every action round-trips through\n * `app_tool_call { appId, tool, args }`, gated by {@link OPS_PANEL_TOOLS}\n * (same convention as `session-viewer`).\n *\n * Special-session recognition is by spawn label (`Session Watchdog` /\n * `Sessions Manager` — see `agents/`): the panel finds the newest session\n * wearing the label, shows its status, and offers Launch / Restart / Kill.\n * Launching the watchdog also (re)wires its `watchdog-tick` cron — the\n * `prompt-session` job that re-prompts the durable session every 5 minutes —\n * deleting any stale tick first so a dead session id never keeps a failing\n * cron behind.\n */\n\nimport { manager, MANAGER_LABEL } from \"./agents/manager.js\"\nimport {\n watchdog,\n WATCHDOG_LABEL,\n WATCHDOG_TICK_LABEL,\n WATCHDOG_TICK_PROMPT,\n WATCHDOG_TICK_SCHEDULE,\n} from \"./agents/watchdog.js\"\n\nexport const OPS_PANEL_TOOLS = [\n \"daemon_health\",\n \"session_list\",\n \"agent_start\",\n \"agent_prompt\",\n \"agent_kill\",\n \"session_restart\",\n \"session_archive\",\n \"session_gc\",\n \"cron_list\",\n \"cron_create\",\n \"cron_delete\",\n \"cron_run\",\n \"worktree_gc\",\n] as const\n\nconst APP_ID = \"@agentproto/ops-panel\"\n\n/** What the panel needs to launch each special session, injected below. */\nconst SPECIALS = [\n {\n key: \"watchdog\",\n label: WATCHDOG_LABEL,\n tagline: \"Health ticks every 5 min — flags, archives, never kills.\",\n prompt: watchdog.body ?? \"\",\n cron: {\n label: WATCHDOG_TICK_LABEL,\n schedule: WATCHDOG_TICK_SCHEDULE,\n prompt: WATCHDOG_TICK_PROMPT,\n },\n },\n {\n key: \"manager\",\n label: MANAGER_LABEL,\n tagline: \"You talk to it, it drives the other sessions.\",\n prompt: manager.body ?? \"\",\n cron: null,\n },\n] as const\n\nexport const OPS_PANEL_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Ops Panel</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}\n#app{display:flex;flex-direction:column;height:100%;overflow:hidden}\n\n#topbar{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}\n#topbar h1{font-size:13px;font-weight:700;letter-spacing:.02em}\n.chip{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:600;background:var(--bg3);border:1px solid var(--border);color:var(--text2);padding:3px 9px;border-radius:999px}\n.chip .d{width:7px;height:7px;border-radius:50%;background:var(--text3);flex:none}\n.chip.ok .d{background:var(--green)}\n.chip.bad .d{background:var(--red)}\n#ws-select{margin-left:auto;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:11.5px;font-family:inherit}\n#refresh-btn{background:none;border:1px solid var(--border);cursor:pointer;color:var(--text2);font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;font-family:inherit}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n\n#body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:16px}\n.sect h2{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px;display:flex;align-items:center;gap:8px}\n.sect h2 .count{font-weight:600;color:var(--text3);text-transform:none;letter-spacing:0}\n\n#specials{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}\n.card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:12px}\n.card .ct{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:700}\n.card .tagline{font-size:11px;color:var(--text2);margin-top:3px}\n.card .meta{font-size:10.5px;color:var(--text3);margin-top:7px;display:flex;flex-wrap:wrap;gap:5px 10px}\n.card .cronline{font-size:10.5px;margin-top:7px;display:flex;align-items:center;gap:6px;color:var(--text2)}\n.card .actions{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}\n\n.badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em}\n.b-run{background:rgba(63,185,80,.15);color:var(--green)}\n.b-busy{background:rgba(63,185,80,.25);color:var(--green)}\n.b-wait{background:rgba(88,166,255,.15);color:var(--blue)}\n.b-idle{background:rgba(139,148,158,.12);color:var(--text2)}\n.b-end{background:rgba(139,148,158,.1);color:var(--text3)}\n.b-err{background:rgba(248,81,73,.15);color:var(--red)}\n.b-warn{background:rgba(210,153,34,.15);color:var(--yellow)}\n\nbutton.act{background:var(--bg3);border:1px solid var(--border);color:var(--text);font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;cursor:pointer;font-family:inherit}\nbutton.act:hover{border-color:var(--text3)}\nbutton.act.primary{background:rgba(88,166,255,.15);border-color:rgba(88,166,255,.4);color:var(--blue)}\nbutton.act.danger{color:var(--red)}\nbutton.act.danger:hover{border-color:var(--red)}\nbutton.act.confirm{background:rgba(248,81,73,.2);border-color:var(--red);color:var(--red)}\nbutton.act:disabled{opacity:.45;cursor:default}\n\ntable{width:100%;border-collapse:collapse}\nth{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.04em;text-align:left;padding:4px 8px;border-bottom:1px solid var(--border)}\ntd{font-size:11.5px;padding:6px 8px;border-bottom:1px solid var(--bg3);vertical-align:middle}\ntr:hover td{background:var(--bg2)}\ntd .sn{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:260px;display:block}\ntd .sid{font-size:10px;color:var(--text3);font-family:Menlo,Monaco,monospace}\n.ws-tag{display:inline-block;font-size:10px;font-weight:600;background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:1px 6px;color:var(--text2)}\ntd.actions{white-space:nowrap;text-align:right}\ntd.actions button{margin-left:4px}\n\n.hk{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}\n.hk .card .nums{display:flex;gap:14px;margin-top:8px}\n.hk .num{display:flex;flex-direction:column}\n.hk .num b{font-size:17px;font-weight:700}\n.hk .num span{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.04em}\n\n#toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:8px 14px;font-size:11.5px;display:none;max-width:80%;z-index:10}\n#toast.err{border-color:var(--red);color:var(--red)}\n.empty{color:var(--text3);font-size:11.5px;padding:8px}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"topbar\">\n <h1>Agentproto Ops</h1>\n <span class=\"chip\" id=\"daemon-chip\"><span class=\"d\"></span><span id=\"daemon-txt\">daemon&hellip;</span></span>\n <select id=\"ws-select\"><option value=\"\">all workspaces</option></select>\n <button id=\"refresh-btn\" title=\"Refresh\" type=\"button\">&#8635; refresh</button>\n </div>\n <div id=\"body\">\n <div class=\"sect\">\n <h2>Special sessions</h2>\n <div id=\"specials\"></div>\n </div>\n <div class=\"sect\">\n <h2>Sessions <span class=\"count\" id=\"sess-count\"></span></h2>\n <div id=\"sessions\"></div>\n </div>\n <div class=\"sect\">\n <h2>Housekeeping</h2>\n <div class=\"hk\" id=\"housekeeping\"></div>\n </div>\n <div class=\"sect\">\n <h2>Crons <span class=\"count\" id=\"cron-count\"></span></h2>\n <div id=\"crons\"></div>\n </div>\n </div>\n</div>\n<div id=\"toast\"></div>\n<script>\nvar callTool = null;\nvar APP_ID = ${JSON.stringify(APP_ID)};\nvar SPECIALS = ${JSON.stringify(SPECIALS)};\n\n// app_tool_call wraps its dispatch result at least once (its own text-result\n// body is the JSON-stringified inner MCP tool result); peel back through\n// nested {content:[{type:\"text\",text:...}]} shells until the payload stops\n// looking like one — same convention as session-viewer.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);\n}\n\nfunction escHtml(s) {\n return String(s == null ? \"\" : s).replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n}\n\nfunction fmtRelative(iso) {\n if (!iso) return \"\";\n var ms = Date.now() - new Date(iso).getTime();\n if (!isFinite(ms)) return \"\";\n var s = Math.round(ms / 1000);\n if (s < 5) return \"just now\";\n if (s < 60) return s + \"s ago\";\n var m = Math.round(s / 60);\n if (m < 60) return m + \"m ago\";\n var h = Math.round(m / 60);\n if (h < 24) return h + \"h ago\";\n return Math.round(h / 24) + \"d ago\";\n}\n\nvar toastTimer = null;\nfunction toast(msg, isErr) {\n var el = document.getElementById(\"toast\");\n el.textContent = msg;\n el.className = isErr ? \"err\" : \"\";\n el.style.display = \"block\";\n if (toastTimer) clearTimeout(toastTimer);\n toastTimer = setTimeout(function () { el.style.display = \"none\"; }, isErr ? 6000 : 3000);\n}\n\n// Two-click confirm: first click arms the button, second within 4s fires.\nvar armed = {};\nfunction confirmClick(key, btn, fn) {\n if (armed[key]) {\n clearTimeout(armed[key].t);\n delete armed[key];\n fn();\n return;\n }\n var prev = btn.textContent;\n btn.classList.add(\"confirm\");\n btn.textContent = \"sure?\";\n armed[key] = { t: setTimeout(function () {\n delete armed[key];\n btn.classList.remove(\"confirm\");\n btn.textContent = prev;\n }, 4000) };\n}\n\n// ============================================================\n// state\n// ============================================================\n\nvar sessions = [];\nvar crons = [];\nvar wsFilter = \"\";\nvar pollTimer = null;\nvar busyActions = false;\n\nfunction isLive(s) { return !!s && (s.status === \"running\" || s.status === \"starting\"); }\n\nfunction displayBadge(s) {\n if (s.status === \"running\") {\n if (s.busy) return { label: \"working\", cls: \"b-busy\" };\n if (s.awaitingInput) return { label: \"waiting\", cls: \"b-wait\" };\n return { label: \"idle\", cls: \"b-idle\" };\n }\n if (s.status === \"starting\") return { label: \"starting\", cls: \"b-wait\" };\n if (s.status === \"error\" || s.status === \"killed\") return { label: s.status, cls: \"b-err\" };\n return { label: s.status || \"exited\", cls: \"b-end\" };\n}\n\nfunction labelOf(s) {\n return s.label || s.title || s.name || (s.command ? String(s.command).split(\"/\").pop() : null) || s.id;\n}\n\nfunction specialSessionFor(sp) {\n var best = null;\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n if ((s.label || s.title) !== sp.label) continue;\n if (!best) { best = s; continue; }\n // Prefer a live one; among equals prefer the newest.\n if (isLive(s) && !isLive(best)) { best = s; continue; }\n if (isLive(s) === isLive(best) && String(s.startedAt) > String(best.startedAt)) best = s;\n }\n return best;\n}\n\nfunction cronByLabel(label) {\n var hits = [];\n for (var i = 0; i < crons.length; i++) if (crons[i].label === label) hits.push(crons[i]);\n return hits;\n}\n\n// ============================================================\n// data loading\n// ============================================================\n\nfunction loadAll() {\n return Promise.all([\n callApp(\"session_list\", { kind: \"all\" }).then(function (d) {\n sessions = (d && d.sessions) || (Array.isArray(d) ? d : []);\n }),\n callApp(\"cron_list\", {}).then(function (d) {\n crons = Array.isArray(d) ? d : (d && d.jobs) || [];\n }).catch(function () { crons = []; }),\n ]).then(function () {\n renderWorkspaces();\n renderSpecials();\n renderSessions();\n renderCrons();\n updateHousekeepingNums();\n });\n}\n\nfunction loadHealth() {\n return callApp(\"daemon_health\", {}).then(function (d) {\n var chip = document.getElementById(\"daemon-chip\");\n chip.className = \"chip ok\";\n var v = (d && (d.version || (d.build && d.build.sha) || d.status)) || \"ok\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon \" + String(v).slice(0, 16);\n }).catch(function (e) {\n var chip = document.getElementById(\"daemon-chip\");\n chip.className = \"chip bad\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon unreachable\";\n });\n}\n\n// Housekeeping card is built ONCE (so the worktree plan text and armed\n// confirm buttons survive the 5s poll); only the counters re-render — see\n// updateHousekeepingNums. The worktree plan is a separate dry-run call, on\n// demand only (it walks git state — not something to hammer on every poll).\nfunction buildHousekeeping() {\n var el = document.getElementById(\"housekeeping\");\n el.innerHTML =\n '<div class=\"card\"><div class=\"ct\">Sessions GC</div>'\n + '<div class=\"nums\" id=\"gc-nums\"></div>'\n + '<div class=\"actions\">'\n + '<button class=\"act\" id=\"gc-archive\">Archive &gt;7d</button>'\n + '<button class=\"act danger\" id=\"gc-forget\">Forget &gt;30d</button>'\n + '</div></div>'\n + '<div class=\"card\"><div class=\"ct\">Worktrees</div>'\n + '<div class=\"meta\" id=\"wt-plan\">No plan yet — run a dry run.</div>'\n + '<div class=\"actions\">'\n + '<button class=\"act\" id=\"wt-dry\">Dry run</button>'\n + '<button class=\"act danger\" id=\"wt-apply\" disabled>Reclaim now</button>'\n + '</div></div>';\n\n document.getElementById(\"gc-archive\").addEventListener(\"click\", function () {\n runAction(\"session_gc\", { olderThanDays: 7 }, \"GC: archived old terminal sessions\");\n });\n var forgetBtn = document.getElementById(\"gc-forget\");\n forgetBtn.addEventListener(\"click\", function () {\n confirmClick(\"gc-forget\", forgetBtn, function () {\n runAction(\"session_gc\", { olderThanDays: 30, forget: true }, \"GC: forgot >30d terminal sessions\");\n });\n });\n document.getElementById(\"wt-dry\").addEventListener(\"click\", function () { worktreePlan(false); });\n var applyBtn = document.getElementById(\"wt-apply\");\n applyBtn.addEventListener(\"click\", function () {\n confirmClick(\"wt-apply\", applyBtn, function () { worktreePlan(true); });\n });\n updateHousekeepingNums();\n}\n\nfunction updateHousekeepingNums() {\n var el = document.getElementById(\"gc-nums\");\n if (!el) return;\n var terminal = 0, live = 0;\n for (var i = 0; i < sessions.length; i++) {\n if (isLive(sessions[i])) live++;\n else terminal++;\n }\n el.innerHTML =\n '<div class=\"num\"><b>' + live + '</b><span>live</span></div>'\n + '<div class=\"num\"><b>' + terminal + '</b><span>terminal</span></div>';\n}\n\nfunction worktreePlan(apply) {\n var el = document.getElementById(\"wt-plan\");\n el.textContent = apply ? \"reclaiming\\\\u2026\" : \"planning\\\\u2026\";\n callApp(\"worktree_gc\", apply ? { apply: true } : {}).then(function (d) {\n var entries = (d && (d.plan || d.entries || d.worktrees)) || [];\n var counts = { reclaim: 0, salvage: 0, hold: 0 };\n for (var i = 0; i < entries.length; i++) {\n var c = entries[i].classification || entries[i].class || entries[i].action;\n if (counts[c] != null) counts[c]++;\n }\n el.textContent = (apply ? \"applied — \" : \"dry run — \")\n + counts.reclaim + \" reclaim / \" + counts.salvage + \" salvage / \" + counts.hold + \" hold\"\n + (entries.length === 0 ? \" (no linked worktrees)\" : \"\");\n document.getElementById(\"wt-apply\").disabled = counts.reclaim === 0 || apply;\n if (apply) toast(\"Worktree GC applied\");\n }).catch(function (e) {\n el.textContent = \"worktree_gc: \" + e.message;\n });\n}\n\n// ============================================================\n// rendering\n// ============================================================\n\nfunction renderWorkspaces() {\n var sel = document.getElementById(\"ws-select\");\n var seen = {};\n var slugs = [];\n for (var i = 0; i < sessions.length; i++) {\n var w = sessions[i].workspaceSlug;\n if (w && !seen[w]) { seen[w] = true; slugs.push(w); }\n }\n slugs.sort();\n var cur = wsFilter;\n var html = '<option value=\"\">all workspaces</option>';\n for (var j = 0; j < slugs.length; j++) {\n html += '<option value=\"' + escHtml(slugs[j]) + '\"' + (slugs[j] === cur ? \" selected\" : \"\") + '>' + escHtml(slugs[j]) + \"</option>\";\n }\n sel.innerHTML = html;\n}\n\nfunction renderSpecials() {\n var el = document.getElementById(\"specials\");\n var html = \"\";\n for (var i = 0; i < SPECIALS.length; i++) {\n var sp = SPECIALS[i];\n var s = specialSessionFor(sp);\n var badge = s ? displayBadge(s) : { label: \"not running\", cls: \"b-end\" };\n var meta = \"\";\n if (s) {\n meta += \"<span>\" + escHtml(s.id) + \"</span>\";\n if (s.startedAt) meta += \"<span>started \" + escHtml(fmtRelative(s.startedAt)) + \"</span>\";\n if (s.costUsd != null) meta += \"<span>$\" + Number(s.costUsd).toFixed(2) + \"</span>\";\n }\n var cronline = \"\";\n if (sp.cron) {\n var jobs = cronByLabel(sp.cron.label);\n if (jobs.length === 0) {\n cronline = '<span class=\"badge b-end\">no cron</span> tick not scheduled';\n } else {\n var job = jobs[0];\n var ok = !job.lastResult || job.lastResult.ok;\n var pointsAt = job.action && job.action.sessionId;\n var stale = s && isLive(s) && pointsAt && pointsAt !== s.id;\n cronline = '<span class=\"badge ' + (job.active ? (ok && !stale ? \"b-run\" : \"b-warn\") : \"b-end\") + '\">'\n + (job.active ? (ok && !stale ? \"tick ok\" : (stale ? \"tick stale\" : \"tick failing\")) : \"tick off\") + \"</span> \"\n + escHtml(job.schedule) + (pointsAt ? \" &rarr; \" + escHtml(pointsAt) : \"\")\n + (!ok && job.lastResult && job.lastResult.summary ? \" &middot; \" + escHtml(String(job.lastResult.summary).slice(0, 60)) : \"\");\n }\n }\n var actions = \"\";\n if (!s || !isLive(s)) {\n actions += '<button class=\"act primary\" data-sp=\"' + sp.key + '\" data-do=\"launch\">Launch</button>';\n if (s) actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"restart\">Restart last</button>';\n } else {\n if (sp.cron) {\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"tick\">Tick now</button>';\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"fixcron\">Re-wire cron</button>';\n } else {\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"tick\">Ping</button>';\n }\n actions += '<button class=\"act danger\" data-sp=\"' + sp.key + '\" data-do=\"kill\">Kill</button>';\n }\n html += '<div class=\"card\">'\n + '<div class=\"ct\">' + escHtml(sp.label) + ' <span class=\"badge ' + badge.cls + '\">' + escHtml(badge.label) + \"</span></div>\"\n + '<div class=\"tagline\">' + escHtml(sp.tagline) + \"</div>\"\n + (meta ? '<div class=\"meta\">' + meta + \"</div>\" : \"\")\n + (cronline ? '<div class=\"cronline\">' + cronline + \"</div>\" : \"\")\n + '<div class=\"actions\">' + actions + \"</div>\"\n + \"</div>\";\n }\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-sp]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { specialAction(btn.getAttribute(\"data-sp\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\nfunction renderSessions() {\n var el = document.getElementById(\"sessions\");\n var rows = [];\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n if (wsFilter && s.workspaceSlug !== wsFilter) continue;\n rows.push(s);\n }\n rows.sort(function (a, b) {\n var la = isLive(a) ? 1 : 0, lb = isLive(b) ? 1 : 0;\n if (la !== lb) return lb - la;\n return String(b.startedAt || \"\").localeCompare(String(a.startedAt || \"\"));\n });\n var liveCount = 0;\n for (var c = 0; c < rows.length; c++) if (isLive(rows[c])) liveCount++;\n document.getElementById(\"sess-count\").textContent = liveCount + \" live / \" + rows.length + \" shown\";\n if (rows.length === 0) {\n el.innerHTML = '<div class=\"empty\">No sessions' + (wsFilter ? \" in \" + escHtml(wsFilter) : \"\") + \".</div>\";\n return;\n }\n var html = \"<table><thead><tr><th>Session</th><th>Workspace</th><th>Status</th><th>Started</th><th>Cost</th><th></th></tr></thead><tbody>\";\n var shown = rows.slice(0, 80);\n for (var j = 0; j < shown.length; j++) {\n var s2 = shown[j];\n var db = displayBadge(s2);\n var acts = \"\";\n if (isLive(s2)) {\n acts += '<button class=\"act danger\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"kill\">Kill</button>';\n } else {\n acts += '<button class=\"act\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"restart\">Restart</button>';\n if (!s2.archived) acts += '<button class=\"act\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"archive\">Archive</button>';\n }\n html += \"<tr>\"\n + '<td><span class=\"sn\" title=\"' + escHtml(labelOf(s2)) + '\">' + escHtml(labelOf(s2)) + '</span><span class=\"sid\">' + escHtml(s2.id) + \"</span></td>\"\n + \"<td>\" + (s2.workspaceSlug ? '<span class=\"ws-tag\">' + escHtml(s2.workspaceSlug) + \"</span>\" : \"\") + \"</td>\"\n + '<td><span class=\"badge ' + db.cls + '\">' + escHtml(db.label) + \"</span></td>\"\n + \"<td>\" + escHtml(fmtRelative(s2.startedAt)) + \"</td>\"\n + \"<td>\" + (s2.costUsd != null ? \"$\" + Number(s2.costUsd).toFixed(2) : \"\") + \"</td>\"\n + '<td class=\"actions\">' + acts + \"</td>\"\n + \"</tr>\";\n }\n html += \"</tbody></table>\";\n if (rows.length > shown.length) html += '<div class=\"empty\">&hellip; ' + (rows.length - shown.length) + \" more (narrow with the workspace filter)</div>\";\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-id]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { sessionAction(btn.getAttribute(\"data-id\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\nfunction renderCrons() {\n var el = document.getElementById(\"crons\");\n document.getElementById(\"cron-count\").textContent = crons.length ? crons.length + \" jobs\" : \"\";\n if (crons.length === 0) {\n el.innerHTML = '<div class=\"empty\">No cron jobs.</div>';\n return;\n }\n var html = \"<table><thead><tr><th>Label</th><th>Schedule</th><th>State</th><th>Last</th><th>Next</th><th></th></tr></thead><tbody>\";\n for (var i = 0; i < crons.length; i++) {\n var c = crons[i];\n var last = c.lastResult == null ? \"\" : (c.lastResult.ok ? \"ok\" : \"failed\");\n var lastCls = c.lastResult == null ? \"b-end\" : (c.lastResult.ok ? \"b-run\" : \"b-err\");\n html += \"<tr>\"\n + '<td><span class=\"sn\">' + escHtml(c.label || c.id) + \"</span></td>\"\n + \"<td>\" + escHtml(c.schedule) + \"</td>\"\n + '<td><span class=\"badge ' + (c.active ? \"b-run\" : \"b-end\") + '\">' + (c.active ? \"active\" : \"off\") + \"</span></td>\"\n + \"<td>\" + (last ? '<span class=\"badge ' + lastCls + '\" title=\"' + escHtml((c.lastResult && c.lastResult.summary) || \"\") + '\">' + last + \"</span>\" : \"\") + \"</td>\"\n + \"<td>\" + escHtml(c.nextRunAt ? fmtRelative(c.nextRunAt).replace(\" ago\", \"\") : \"\") + \"</td>\"\n + '<td class=\"actions\">'\n + '<button class=\"act\" data-cron=\"' + escHtml(c.id) + '\" data-do=\"run\">Run</button>'\n + '<button class=\"act danger\" data-cron=\"' + escHtml(c.id) + '\" data-do=\"delete\">Delete</button>'\n + \"</td></tr>\";\n }\n html += \"</tbody></table>\";\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-cron]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { cronAction(btn.getAttribute(\"data-cron\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\n// ============================================================\n// actions\n// ============================================================\n\nfunction runAction(tool, args, okMsg) {\n if (busyActions) return Promise.resolve();\n busyActions = true;\n return callApp(tool, args).then(function (d) {\n busyActions = false;\n if (okMsg) toast(okMsg);\n return loadAll().then(function () { return d; });\n }).catch(function (e) {\n busyActions = false;\n toast(tool + \": \" + e.message, true);\n throw e;\n });\n}\n\nfunction findSpecial(key) {\n for (var i = 0; i < SPECIALS.length; i++) if (SPECIALS[i].key === key) return SPECIALS[i];\n return null;\n}\n\nfunction rewireTickCron(sp, sessionId) {\n if (!sp.cron) return Promise.resolve();\n var stale = cronByLabel(sp.cron.label);\n var chain = Promise.resolve();\n for (var i = 0; i < stale.length; i++) {\n (function (jobId) {\n chain = chain.then(function () { return callApp(\"cron_delete\", { jobId: jobId }); });\n })(stale[i].id);\n }\n return chain.then(function () {\n return callApp(\"cron_create\", {\n schedule: sp.cron.schedule,\n recurring: true,\n label: sp.cron.label,\n action: { kind: \"prompt-session\", sessionId: sessionId, prompt: sp.cron.prompt },\n });\n });\n}\n\nfunction specialAction(key, what, btn) {\n var sp = findSpecial(key);\n if (!sp) return;\n var s = specialSessionFor(sp);\n if (what === \"launch\") {\n btn.disabled = true;\n callApp(\"agent_start\", {\n adapter: \"claude-code\",\n label: sp.label,\n prompt: sp.prompt,\n keepAlive: true,\n }).then(function (d) {\n var id = d && d.id;\n toast(sp.label + \" launched\" + (id ? \" (\" + id + \")\" : \"\"));\n return id && sp.cron ? rewireTickCron(sp, id) : null;\n }).then(function () { return loadAll(); })\n .catch(function (e) { toast(\"launch: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"restart\" && s) {\n btn.disabled = true;\n callApp(\"session_restart\", { idOrName: s.id }).then(function (d) {\n var id = (d && d.id) || s.id;\n toast(sp.label + \" restarted\");\n return sp.cron ? rewireTickCron(sp, id) : null;\n }).then(function () { return loadAll(); })\n .catch(function (e) { toast(\"restart: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"tick\" && s) {\n var prompt = sp.cron ? sp.cron.prompt : \"Status check: report where things stand, briefly.\";\n runAction(\"agent_prompt\", { sessionId: s.id, prompt: prompt }, sp.label + \" prompted\");\n return;\n }\n if (what === \"fixcron\" && s) {\n btn.disabled = true;\n rewireTickCron(sp, s.id).then(function () {\n toast(\"Cron re-wired to \" + s.id);\n return loadAll();\n }).catch(function (e) { toast(\"cron: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"kill\" && s) {\n confirmClick(\"kill-\" + s.id, btn, function () {\n runAction(\"agent_kill\", { sessionId: s.id }, sp.label + \" killed\");\n });\n }\n}\n\nfunction sessionAction(id, what, btn) {\n if (what === \"kill\") {\n confirmClick(\"kill-\" + id, btn, function () {\n runAction(\"agent_kill\", { sessionId: id }, \"Killed \" + id);\n });\n return;\n }\n if (what === \"restart\") { runAction(\"session_restart\", { idOrName: id }, \"Restarted \" + id); return; }\n if (what === \"archive\") { runAction(\"session_archive\", { idOrName: id }, \"Archived \" + id); }\n}\n\nfunction cronAction(jobId, what, btn) {\n if (what === \"run\") { runAction(\"cron_run\", { jobId: jobId }, \"Cron fired\"); return; }\n if (what === \"delete\") {\n confirmClick(\"cron-\" + jobId, btn, function () {\n runAction(\"cron_delete\", { jobId: jobId }, \"Cron deleted\");\n });\n }\n}\n\n// ============================================================\n// boot\n// ============================================================\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () { loadHealth(); loadAll(); });\ndocument.getElementById(\"ws-select\").addEventListener(\"change\", function (e) {\n wsFilter = e.target.value;\n renderSessions();\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n buildHousekeeping();\n loadHealth();\n return loadAll();\n })\n .then(function () {\n pollTimer = setInterval(loadAll, 5000);\n })\n .catch(function (err) {\n document.getElementById(\"body\").innerHTML =\n '<div class=\"empty\">Standalone mode \\\\u2014 no host bridge (' + escHtml(err.message) + \")</div>\";\n });\n</script>\n</body>\n</html>`\n","/**\n * `ops-panel` — the agentproto daemon operations cockpit as an app.\n *\n * Two durable \"special sessions\" ship as agents — the Session Watchdog\n * (cron-ticked health checks: classify, flag, archive; never kills) and the\n * Sessions Manager (the coordinator the human talks to, which drives the\n * other sessions). The panel (`ui.ts`) launches/restarts them by label,\n * wires the watchdog's `watchdog-tick` cron, lists every session filterable\n * by workspace with lifecycle actions, and fronts housekeeping: session GC,\n * worktree GC (dry-run first), and the cron ledger.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { manager } from \"./agents/manager.js\"\nimport { watchdog } from \"./agents/watchdog.js\"\nimport { OPS_PANEL_HTML, OPS_PANEL_TOOLS } from \"./ui.js\"\n\nexport const opsPanel: AppHandle = defineApp({\n id: \"@agentproto/ops-panel\",\n name: \"Ops Panel\",\n version: \"0.1.0\",\n description: \"Daemon ops cockpit — watchdog & manager sessions, workspace-filtered session list, GC and cron housekeeping.\",\n agents: [watchdog, manager],\n ui: {\n html: OPS_PANEL_HTML,\n title: \"Ops Panel\",\n tools: [...OPS_PANEL_TOOLS],\n },\n})\n\nexport { watchdog, manager }\nexport { WATCHDOG_LABEL, WATCHDOG_TICK_LABEL, WATCHDOG_TICK_PROMPT, WATCHDOG_TICK_SCHEDULE } from \"./agents/watchdog.js\"\nexport { MANAGER_LABEL } from \"./agents/manager.js\"\n"]}
1
+ {"version":3,"sources":["../src/ops-panel/agents/manager.ts","../src/ops-panel/agents/watchdog.ts","../src/ops-panel/ui.ts","../src/ops-panel/index.ts"],"names":["defineAgent"],"mappings":"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A;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;;;AChKlC,IAAM,WAAsB,SAAA,CAAU;AAAA,EAC3C,EAAA,EAAI,uBAAA;AAAA,EACJ,IAAA,EAAM,WAAA;AAAA,EACN,OAAA,EAAS,OAAA;AAAA,EACT,WAAA,EAAa,mHAAA;AAAA,EACb,MAAA,EAAQ,CAAC,QAAA,EAAU,OAAO,CAAA;AAAA,EAC1B,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,cAAA;AAAA,IACN,KAAA,EAAO,WAAA;AAAA,IACP,KAAA,EAAO,CAAC,GAAG,eAAe;AAAA;AAE9B,CAAC","file":"ops-panel.mjs","sourcesContent":["import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/**\n * The Sessions Manager — the durable \"master of the sessions\" coordinator:\n * the human talks to IT, it talks to the other sessions. Distilled from the\n * hand-run plateau-coordinator session this app productizes.\n *\n * Where the watchdog only OBSERVES (and never touches a live session), the\n * manager DRIVES: it re-prompts idle sessions onto their task, restarts dead\n * ones, and answers \"where is X at?\" by reading transcripts. The Ops Panel\n * spawns it via `agent_start` with {@link MANAGER_LABEL} and `keepAlive`, so\n * the idle-reaper never retires a legitimately-parked coordinator.\n */\n\n/** Session label the panel spawns under — how a live manager is recognized. */\nexport const MANAGER_LABEL = \"Sessions Manager\"\n\nexport const manager: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/sessions-manager\",\n description:\n \"Durable coordinator over the daemon's sessions — routes questions to the right session, re-prompts stalled work, restarts dead sessions, reports status.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Never kill a session unless the human explicitly asks for that session by id/label\",\n \"Never merge PRs or bypass a repo's declared review flow from a supervised session\",\n \"Prefer re-prompting an existing session over spawning a duplicate for the same task\",\n ],\n tools: [\n \"session_list\",\n \"conversation_read\",\n \"agent_prompt\",\n \"session_restart\",\n \"agent_start\",\n \"session_events_poll\",\n ],\n }),\n body: [\n \"You are the manager of this agentproto daemon's sessions: the human talks to you, and you talk\",\n \"to the other sessions. You are durable — the same session across many turns — so keep a working\",\n \"memory of who is doing what and don't re-derive it from scratch every turn.\",\n \"\",\n \"What you do:\",\n \"- Locate: given a task or question, find the relevant session (`session_list`, labels, cwd,\",\n \" recency) and read its transcript (`conversation_read`) before answering — never guess status.\",\n \"- Drive: re-prompt an idle/parked session onto its task with `agent_prompt`; restart a\",\n \" killed/exited session with `session_restart` when its work is clearly unfinished.\",\n \"- Spawn: only when no existing session owns the task — check for duplicates first.\",\n \"- Report: answer with short, concrete status (who, doing what, since when, blocked on what).\",\n \"\",\n \"Boundaries:\",\n \"- Never kill a session unless the human explicitly asks for that session.\",\n \"- A session's own repo conventions (review flow, merge policy) always win — you coordinate,\",\n \" you don't override.\",\n \"- If a session awaits a human decision, surface the question to the human instead of answering\",\n \" in their place.\",\n ].join(\"\\n\"),\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/**\n * The Session Watchdog — a durable daemon-health session, re-prompted on a\n * cron tick (~every 5 minutes). Distilled from the hand-run watchdog session\n * this app productizes: same classification rules, same hard boundaries\n * (never kills a live session, never spawns).\n *\n * The Ops Panel spawns it via `agent_start` with {@link WATCHDOG_LABEL} and\n * wires a `prompt-session` cron ({@link WATCHDOG_TICK_LABEL} /\n * {@link WATCHDOG_TICK_SCHEDULE}) pointing at the spawned session — see\n * `ui.ts`. The agent is also usable standalone via `app_run` for a one-shot\n * health pass.\n */\n\n/** Session label the panel spawns under — how a live watchdog is recognized. */\nexport const WATCHDOG_LABEL = \"Session Watchdog\"\n\n/** Label of the cron job that re-prompts the watchdog session. */\nexport const WATCHDOG_TICK_LABEL = \"watchdog-tick\"\n\n/** Every 5 minutes. */\nexport const WATCHDOG_TICK_SCHEDULE = \"*/5 * * * *\"\n\n/** The prompt each cron tick sends to the durable session. */\nexport const WATCHDOG_TICK_PROMPT = \"Run your maintenance check now.\"\n\nexport const watchdog: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/session-watchdog\",\n description:\n \"Durable daemon-health watchdog — classifies sessions (needs-you, stalled, duplicated, abandonable), corrects flags, archives dead rows. Never kills or spawns.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"NEVER kill a live session, message/interrupt another session's conversation, or spawn new sessions\",\n \"Housekeeping only on terminal-status sessions (exited/killed/error) — never touches a live one\",\n \"Report only what's NEW since the last tick — no padding, no repeats unless something got worse\",\n ],\n tools: [\n \"session_list\",\n \"session_events_poll\",\n \"session_flag_status\",\n \"session_archive\",\n \"session_gc\",\n ],\n }),\n body: [\n \"You are the Session Watchdog for this agentproto daemon. You run on a recurring cron tick\",\n \"(~every 5 minutes) as the SAME durable session, re-prompted each time — you retain memory of\",\n \"what you already flagged, so don't repeat an alert unless something got meaningfully worse or new.\",\n \"\",\n \"Each tick:\",\n \"1. Call `session_list` to see current session state (status, awaitingInput/awaitingQuestion,\",\n \" stalledSinceMs, busy, blockedOn, childrenBusy).\",\n \"2. Call `session_events_poll` for events since your last tick (stalled, exited, awaiting-input)\",\n \" to catch anything between ticks.\",\n \"3. Classify:\",\n \" - Needs you: `awaitingInput: true` with a real unanswered question, older than ~10 minutes.\",\n \" - Stalled: busy but silent well past normal turn latency, no legitimate blockedOn excuse.\",\n \" - Possibly duplicated: two or more live sessions share workspace/cwd and a highly similar\",\n \" recent prompt/label — use judgment, not exact string matching.\",\n \" - Looks done/abandonable: idle/parked with no plausible remaining purpose.\",\n \"4. Correct misclassifications when confident: if `session_flag_status` is available and you can\",\n \" tell a session is actually waiting on a human (or isn't, despite being flagged), call it with\",\n \" a clear `reason`. If that tool is unknown on this daemon build, skip silently.\",\n \"5. Autonomous housekeeping (reversible, so no need to ask): `session_archive` or `session_gc` on\",\n \" sessions already terminal-status (exited/killed/error) and old. Never touches a live session.\",\n \"6. NEVER: kill a live session, message/interrupt another session's conversation, or spawn new\",\n \" sessions. You only have these five tools on purpose — if you'd want another one, report the\",\n \" suggestion instead of working around it.\",\n \"7. Report only what's NEW since last tick — one line per finding. If nothing new, reply with a\",\n \" short \\\"nothing new\\\" — don't pad it.\",\n \"\",\n \"You are not a general coding assistant across ticks — stay scoped to this watchdog role.\",\n ].join(\"\\n\"),\n}\n","/**\n * Ops Panel — a self-contained MCP App panel for `ops-panel`.\n *\n * The daemon operations cockpit: special durable sessions (Session Watchdog,\n * Sessions Manager) as launchable cards, the session list filterable by\n * workspace with lifecycle actions, housekeeping (session GC, worktree GC)\n * and the cron ledger. Talks to the daemon only through the\n * `window.McpApp.connect()` bridge — every action round-trips through\n * `app_tool_call { appId, tool, args }`, gated by {@link OPS_PANEL_TOOLS}\n * (same convention as `session-viewer`).\n *\n * Special-session recognition is by spawn label (`Session Watchdog` /\n * `Sessions Manager` — see `agents/`): the panel finds the newest session\n * wearing the label, shows its status, and offers Launch / Restart / Kill.\n * Launching the watchdog also (re)wires its `watchdog-tick` cron — the\n * `prompt-session` job that re-prompts the durable session every 5 minutes —\n * deleting any stale tick first so a dead session id never keeps a failing\n * cron behind.\n */\n\nimport { manager, MANAGER_LABEL } from \"./agents/manager.js\"\nimport {\n watchdog,\n WATCHDOG_LABEL,\n WATCHDOG_TICK_LABEL,\n WATCHDOG_TICK_PROMPT,\n WATCHDOG_TICK_SCHEDULE,\n} from \"./agents/watchdog.js\"\n\nexport const OPS_PANEL_TOOLS = [\n \"daemon_health\",\n \"session_list\",\n \"agent_start\",\n \"agent_prompt\",\n \"agent_kill\",\n \"session_restart\",\n \"session_archive\",\n \"session_gc\",\n \"cron_list\",\n \"cron_create\",\n \"cron_delete\",\n \"cron_run\",\n \"worktree_gc\",\n \"worktree_gc_status\",\n] as const\n\nconst APP_ID = \"@agentproto/ops-panel\"\n\n/** What the panel needs to launch each special session, injected below. */\nconst SPECIALS = [\n {\n key: \"watchdog\",\n label: WATCHDOG_LABEL,\n tagline: \"Health ticks every 5 min — flags, archives, never kills.\",\n prompt: watchdog.body ?? \"\",\n cron: {\n label: WATCHDOG_TICK_LABEL,\n schedule: WATCHDOG_TICK_SCHEDULE,\n prompt: WATCHDOG_TICK_PROMPT,\n },\n },\n {\n key: \"manager\",\n label: MANAGER_LABEL,\n tagline: \"You talk to it, it drives the other sessions.\",\n prompt: manager.body ?? \"\",\n cron: null,\n },\n] as const\n\nexport const OPS_PANEL_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Ops Panel</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}\n#app{display:flex;flex-direction:column;height:100%;overflow:hidden}\n\n#topbar{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}\n#topbar h1{font-size:13px;font-weight:700;letter-spacing:.02em}\n.chip{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:600;background:var(--bg3);border:1px solid var(--border);color:var(--text2);padding:3px 9px;border-radius:999px}\n.chip .d{width:7px;height:7px;border-radius:50%;background:var(--text3);flex:none}\n.chip.ok .d{background:var(--green)}\n.chip.bad .d{background:var(--red)}\n#ws-select{margin-left:auto;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:11.5px;font-family:inherit}\n#refresh-btn{background:none;border:1px solid var(--border);cursor:pointer;color:var(--text2);font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;font-family:inherit}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n\n#body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:16px}\n.sect h2{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px;display:flex;align-items:center;gap:8px}\n.sect h2 .count{font-weight:600;color:var(--text3);text-transform:none;letter-spacing:0}\n\n#specials{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}\n.card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:12px}\n.card .ct{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:700}\n.card .tagline{font-size:11px;color:var(--text2);margin-top:3px}\n.card .meta{font-size:10.5px;color:var(--text3);margin-top:7px;display:flex;flex-wrap:wrap;gap:5px 10px}\n.card .cronline{font-size:10.5px;margin-top:7px;display:flex;align-items:center;gap:6px;color:var(--text2)}\n.card .actions{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}\n\n.badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em}\n.b-run{background:rgba(63,185,80,.15);color:var(--green)}\n.b-busy{background:rgba(63,185,80,.25);color:var(--green)}\n.b-wait{background:rgba(88,166,255,.15);color:var(--blue)}\n.b-idle{background:rgba(139,148,158,.12);color:var(--text2)}\n.b-end{background:rgba(139,148,158,.1);color:var(--text3)}\n.b-err{background:rgba(248,81,73,.15);color:var(--red)}\n.b-warn{background:rgba(210,153,34,.15);color:var(--yellow)}\n\nbutton.act{background:var(--bg3);border:1px solid var(--border);color:var(--text);font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;cursor:pointer;font-family:inherit}\nbutton.act:hover{border-color:var(--text3)}\nbutton.act.primary{background:rgba(88,166,255,.15);border-color:rgba(88,166,255,.4);color:var(--blue)}\nbutton.act.danger{color:var(--red)}\nbutton.act.danger:hover{border-color:var(--red)}\nbutton.act.confirm{background:rgba(248,81,73,.2);border-color:var(--red);color:var(--red)}\nbutton.act:disabled{opacity:.45;cursor:default}\n\ntable{width:100%;border-collapse:collapse}\nth{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.04em;text-align:left;padding:4px 8px;border-bottom:1px solid var(--border)}\ntd{font-size:11.5px;padding:6px 8px;border-bottom:1px solid var(--bg3);vertical-align:middle}\ntr:hover td{background:var(--bg2)}\ntd .sn{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:260px;display:block}\ntd .sid{font-size:10px;color:var(--text3);font-family:Menlo,Monaco,monospace}\n.ws-tag{display:inline-block;font-size:10px;font-weight:600;background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:1px 6px;color:var(--text2)}\ntd.actions{white-space:nowrap;text-align:right}\ntd.actions button{margin-left:4px}\n\n.hk{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}\n.hk .card .nums{display:flex;gap:14px;margin-top:8px}\n.hk .num{display:flex;flex-direction:column}\n.hk .num b{font-size:17px;font-weight:700}\n.hk .num span{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.04em}\n\n#toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:8px 14px;font-size:11.5px;display:none;max-width:80%;z-index:10}\n#toast.err{border-color:var(--red);color:var(--red)}\n.empty{color:var(--text3);font-size:11.5px;padding:8px}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"topbar\">\n <h1>Agentproto Ops</h1>\n <span class=\"chip\" id=\"daemon-chip\"><span class=\"d\"></span><span id=\"daemon-txt\">daemon&hellip;</span></span>\n <select id=\"ws-select\"><option value=\"\">all workspaces</option></select>\n <button id=\"refresh-btn\" title=\"Refresh\" type=\"button\">&#8635; refresh</button>\n </div>\n <div id=\"body\">\n <div class=\"sect\">\n <h2>Special sessions</h2>\n <div id=\"specials\"></div>\n </div>\n <div class=\"sect\">\n <h2>Sessions <span class=\"count\" id=\"sess-count\"></span></h2>\n <div id=\"sessions\"></div>\n </div>\n <div class=\"sect\">\n <h2>Housekeeping</h2>\n <div class=\"hk\" id=\"housekeeping\"></div>\n </div>\n <div class=\"sect\">\n <h2>Crons <span class=\"count\" id=\"cron-count\"></span></h2>\n <div id=\"crons\"></div>\n </div>\n </div>\n</div>\n<div id=\"toast\"></div>\n<script>\nvar callTool = null;\nvar APP_ID = ${JSON.stringify(APP_ID)};\nvar SPECIALS = ${JSON.stringify(SPECIALS)};\n\n// app_tool_call wraps its dispatch result at least once (its own text-result\n// body is the JSON-stringified inner MCP tool result); peel back through\n// nested {content:[{type:\"text\",text:...}]} shells until the payload stops\n// looking like one — same convention as session-viewer.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);\n}\n\nfunction escHtml(s) {\n return String(s == null ? \"\" : s).replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n}\n\nfunction fmtRelative(iso) {\n if (!iso) return \"\";\n var ms = Date.now() - new Date(iso).getTime();\n if (!isFinite(ms)) return \"\";\n var s = Math.round(ms / 1000);\n if (s < 5) return \"just now\";\n if (s < 60) return s + \"s ago\";\n var m = Math.round(s / 60);\n if (m < 60) return m + \"m ago\";\n var h = Math.round(m / 60);\n if (h < 24) return h + \"h ago\";\n return Math.round(h / 24) + \"d ago\";\n}\n\nvar toastTimer = null;\nfunction toast(msg, isErr) {\n var el = document.getElementById(\"toast\");\n el.textContent = msg;\n el.className = isErr ? \"err\" : \"\";\n el.style.display = \"block\";\n if (toastTimer) clearTimeout(toastTimer);\n toastTimer = setTimeout(function () { el.style.display = \"none\"; }, isErr ? 6000 : 3000);\n}\n\n// Two-click confirm: first click arms the button, second within 4s fires.\nvar armed = {};\nfunction confirmClick(key, btn, fn) {\n if (armed[key]) {\n clearTimeout(armed[key].t);\n delete armed[key];\n fn();\n return;\n }\n var prev = btn.textContent;\n btn.classList.add(\"confirm\");\n btn.textContent = \"sure?\";\n armed[key] = { t: setTimeout(function () {\n delete armed[key];\n btn.classList.remove(\"confirm\");\n btn.textContent = prev;\n }, 4000) };\n}\n\n// ============================================================\n// state\n// ============================================================\n\nvar sessions = [];\nvar crons = [];\nvar wsFilter = \"\";\nvar pollTimer = null;\nvar busyActions = false;\n\nfunction isLive(s) { return !!s && (s.status === \"running\" || s.status === \"starting\"); }\n\nfunction displayBadge(s) {\n if (s.status === \"running\") {\n if (s.busy) return { label: \"working\", cls: \"b-busy\" };\n if (s.awaitingInput) return { label: \"waiting\", cls: \"b-wait\" };\n return { label: \"idle\", cls: \"b-idle\" };\n }\n if (s.status === \"starting\") return { label: \"starting\", cls: \"b-wait\" };\n if (s.status === \"error\" || s.status === \"killed\") return { label: s.status, cls: \"b-err\" };\n return { label: s.status || \"exited\", cls: \"b-end\" };\n}\n\nfunction labelOf(s) {\n return s.label || s.title || s.name || (s.command ? String(s.command).split(\"/\").pop() : null) || s.id;\n}\n\nfunction specialSessionFor(sp) {\n var best = null;\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n if ((s.label || s.title) !== sp.label) continue;\n if (!best) { best = s; continue; }\n // Prefer a live one; among equals prefer the newest.\n if (isLive(s) && !isLive(best)) { best = s; continue; }\n if (isLive(s) === isLive(best) && String(s.startedAt) > String(best.startedAt)) best = s;\n }\n return best;\n}\n\nfunction cronByLabel(label) {\n var hits = [];\n for (var i = 0; i < crons.length; i++) if (crons[i].label === label) hits.push(crons[i]);\n return hits;\n}\n\n// ============================================================\n// data loading\n// ============================================================\n\nfunction loadAll() {\n return Promise.all([\n callApp(\"session_list\", { kind: \"all\" }).then(function (d) {\n sessions = (d && d.sessions) || (Array.isArray(d) ? d : []);\n }),\n callApp(\"cron_list\", {}).then(function (d) {\n crons = Array.isArray(d) ? d : (d && d.jobs) || [];\n }).catch(function () { crons = []; }),\n ]).then(function () {\n renderWorkspaces();\n renderSpecials();\n renderSessions();\n renderCrons();\n updateHousekeepingNums();\n });\n}\n\nfunction loadHealth() {\n return callApp(\"daemon_health\", {}).then(function (d) {\n var chip = document.getElementById(\"daemon-chip\");\n chip.className = \"chip ok\";\n var v = (d && (d.version || (d.build && d.build.sha) || d.status)) || \"ok\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon \" + String(v).slice(0, 16);\n }).catch(function (e) {\n var chip = document.getElementById(\"daemon-chip\");\n chip.className = \"chip bad\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon unreachable\";\n });\n}\n\n// Housekeeping card is built ONCE (so the worktree plan text and armed\n// confirm buttons survive the 5s poll); only the counters re-render — see\n// updateHousekeepingNums. The worktree plan is a separate dry-run call, on\n// demand only (it walks git state — not something to hammer on every poll).\nfunction buildHousekeeping() {\n var el = document.getElementById(\"housekeeping\");\n el.innerHTML =\n '<div class=\"card\"><div class=\"ct\">Sessions GC</div>'\n + '<div class=\"nums\" id=\"gc-nums\"></div>'\n + '<div class=\"actions\">'\n + '<button class=\"act\" id=\"gc-archive\">Archive &gt;7d</button>'\n + '<button class=\"act danger\" id=\"gc-forget\">Forget &gt;30d</button>'\n + '</div></div>'\n + '<div class=\"card\"><div class=\"ct\">Worktrees</div>'\n + '<div class=\"meta\" id=\"wt-plan\">No plan yet — run a dry run.</div>'\n + '<div class=\"actions\">'\n + '<button class=\"act\" id=\"wt-dry\">Dry run</button>'\n + '<button class=\"act danger\" id=\"wt-apply\" disabled>Reclaim now</button>'\n + '</div></div>';\n\n document.getElementById(\"gc-archive\").addEventListener(\"click\", function () {\n runAction(\"session_gc\", { olderThanDays: 7 }, \"GC: archived old terminal sessions\");\n });\n var forgetBtn = document.getElementById(\"gc-forget\");\n forgetBtn.addEventListener(\"click\", function () {\n confirmClick(\"gc-forget\", forgetBtn, function () {\n runAction(\"session_gc\", { olderThanDays: 30, forget: true }, \"GC: forgot >30d terminal sessions\");\n });\n });\n document.getElementById(\"wt-dry\").addEventListener(\"click\", function () { worktreePlan(false); });\n var applyBtn = document.getElementById(\"wt-apply\");\n applyBtn.addEventListener(\"click\", function () {\n confirmClick(\"wt-apply\", applyBtn, function () { worktreePlan(true); });\n });\n updateHousekeepingNums();\n}\n\nfunction updateHousekeepingNums() {\n var el = document.getElementById(\"gc-nums\");\n if (!el) return;\n var terminal = 0, live = 0;\n for (var i = 0; i < sessions.length; i++) {\n if (isLive(sessions[i])) live++;\n else terminal++;\n }\n el.innerHTML =\n '<div class=\"num\"><b>' + live + '</b><span>live</span></div>'\n + '<div class=\"num\"><b>' + terminal + '</b><span>terminal</span></div>';\n}\n\n// worktree_gc falls back to a background job after its 25 s default waitMs\n// and returns { jobId, status: \"running\" } — poll worktree_gc_status until\n// the real plan / outcomes land instead of reading the jobId as a result.\nfunction worktreeGc(args) {\n return callApp(\"worktree_gc\", args).then(function poll(d) {\n if (typeof d === \"string\") throw new Error(d);\n if (!d || !d.jobId) return d;\n if (d.status === \"done\") return d.result;\n if (d.status === \"failed\") throw new Error(d.error || \"worktree_gc job failed\");\n var after = (d.followUp && d.followUp.pollAfterMs) || 3000;\n return new Promise(function (r) { setTimeout(r, after); })\n .then(function () { return callApp(\"worktree_gc_status\", { jobId: d.jobId }); })\n .then(poll);\n });\n}\n\nfunction worktreePlan(apply) {\n var el = document.getElementById(\"wt-plan\");\n el.textContent = apply ? \"reclaiming\\\\u2026\" : \"planning\\\\u2026\";\n worktreeGc(apply ? { apply: true } : {}).then(function (d) {\n var entries = (d && (d.plan || d.entries || d.worktrees)) || [];\n var counts = { reclaim: 0, salvage: 0, hold: 0 };\n for (var i = 0; i < entries.length; i++) {\n var c = entries[i].classification || entries[i].class || entries[i].action;\n if (counts[c] != null) counts[c]++;\n }\n el.textContent = (apply ? \"applied — \" : \"dry run — \")\n + counts.reclaim + \" reclaim / \" + counts.salvage + \" salvage / \" + counts.hold + \" hold\"\n + (entries.length === 0 ? \" (no linked worktrees)\" : \"\");\n document.getElementById(\"wt-apply\").disabled = counts.reclaim === 0 || apply;\n if (apply) toast(\"Worktree GC applied\");\n }).catch(function (e) {\n el.textContent = \"worktree_gc: \" + e.message;\n });\n}\n\n// ============================================================\n// rendering\n// ============================================================\n\nfunction renderWorkspaces() {\n var sel = document.getElementById(\"ws-select\");\n var seen = {};\n var slugs = [];\n for (var i = 0; i < sessions.length; i++) {\n var w = sessions[i].workspaceSlug;\n if (w && !seen[w]) { seen[w] = true; slugs.push(w); }\n }\n slugs.sort();\n var cur = wsFilter;\n var html = '<option value=\"\">all workspaces</option>';\n for (var j = 0; j < slugs.length; j++) {\n html += '<option value=\"' + escHtml(slugs[j]) + '\"' + (slugs[j] === cur ? \" selected\" : \"\") + '>' + escHtml(slugs[j]) + \"</option>\";\n }\n sel.innerHTML = html;\n}\n\nfunction renderSpecials() {\n var el = document.getElementById(\"specials\");\n var html = \"\";\n for (var i = 0; i < SPECIALS.length; i++) {\n var sp = SPECIALS[i];\n var s = specialSessionFor(sp);\n var badge = s ? displayBadge(s) : { label: \"not running\", cls: \"b-end\" };\n var meta = \"\";\n if (s) {\n meta += \"<span>\" + escHtml(s.id) + \"</span>\";\n if (s.startedAt) meta += \"<span>started \" + escHtml(fmtRelative(s.startedAt)) + \"</span>\";\n if (s.costUsd != null) meta += \"<span>$\" + Number(s.costUsd).toFixed(2) + \"</span>\";\n }\n var cronline = \"\";\n if (sp.cron) {\n var jobs = cronByLabel(sp.cron.label);\n if (jobs.length === 0) {\n cronline = '<span class=\"badge b-end\">no cron</span> tick not scheduled';\n } else {\n var job = jobs[0];\n var ok = !job.lastResult || job.lastResult.ok;\n var pointsAt = job.action && job.action.sessionId;\n var stale = s && isLive(s) && pointsAt && pointsAt !== s.id;\n cronline = '<span class=\"badge ' + (job.active ? (ok && !stale ? \"b-run\" : \"b-warn\") : \"b-end\") + '\">'\n + (job.active ? (ok && !stale ? \"tick ok\" : (stale ? \"tick stale\" : \"tick failing\")) : \"tick off\") + \"</span> \"\n + escHtml(job.schedule) + (pointsAt ? \" &rarr; \" + escHtml(pointsAt) : \"\")\n + (!ok && job.lastResult && job.lastResult.summary ? \" &middot; \" + escHtml(String(job.lastResult.summary).slice(0, 60)) : \"\");\n }\n }\n var actions = \"\";\n if (!s || !isLive(s)) {\n actions += '<button class=\"act primary\" data-sp=\"' + sp.key + '\" data-do=\"launch\">Launch</button>';\n if (s) actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"restart\">Restart last</button>';\n } else {\n if (sp.cron) {\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"tick\">Tick now</button>';\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"fixcron\">Re-wire cron</button>';\n } else {\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"tick\">Ping</button>';\n }\n actions += '<button class=\"act danger\" data-sp=\"' + sp.key + '\" data-do=\"kill\">Kill</button>';\n }\n html += '<div class=\"card\">'\n + '<div class=\"ct\">' + escHtml(sp.label) + ' <span class=\"badge ' + badge.cls + '\">' + escHtml(badge.label) + \"</span></div>\"\n + '<div class=\"tagline\">' + escHtml(sp.tagline) + \"</div>\"\n + (meta ? '<div class=\"meta\">' + meta + \"</div>\" : \"\")\n + (cronline ? '<div class=\"cronline\">' + cronline + \"</div>\" : \"\")\n + '<div class=\"actions\">' + actions + \"</div>\"\n + \"</div>\";\n }\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-sp]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { specialAction(btn.getAttribute(\"data-sp\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\nfunction renderSessions() {\n var el = document.getElementById(\"sessions\");\n var rows = [];\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n if (wsFilter && s.workspaceSlug !== wsFilter) continue;\n rows.push(s);\n }\n rows.sort(function (a, b) {\n var la = isLive(a) ? 1 : 0, lb = isLive(b) ? 1 : 0;\n if (la !== lb) return lb - la;\n return String(b.startedAt || \"\").localeCompare(String(a.startedAt || \"\"));\n });\n var liveCount = 0;\n for (var c = 0; c < rows.length; c++) if (isLive(rows[c])) liveCount++;\n document.getElementById(\"sess-count\").textContent = liveCount + \" live / \" + rows.length + \" shown\";\n if (rows.length === 0) {\n el.innerHTML = '<div class=\"empty\">No sessions' + (wsFilter ? \" in \" + escHtml(wsFilter) : \"\") + \".</div>\";\n return;\n }\n var html = \"<table><thead><tr><th>Session</th><th>Workspace</th><th>Status</th><th>Started</th><th>Cost</th><th></th></tr></thead><tbody>\";\n var shown = rows.slice(0, 80);\n for (var j = 0; j < shown.length; j++) {\n var s2 = shown[j];\n var db = displayBadge(s2);\n var acts = \"\";\n if (isLive(s2)) {\n acts += '<button class=\"act danger\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"kill\">Kill</button>';\n } else {\n acts += '<button class=\"act\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"restart\">Restart</button>';\n if (!s2.archived) acts += '<button class=\"act\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"archive\">Archive</button>';\n }\n html += \"<tr>\"\n + '<td><span class=\"sn\" title=\"' + escHtml(labelOf(s2)) + '\">' + escHtml(labelOf(s2)) + '</span><span class=\"sid\">' + escHtml(s2.id) + \"</span></td>\"\n + \"<td>\" + (s2.workspaceSlug ? '<span class=\"ws-tag\">' + escHtml(s2.workspaceSlug) + \"</span>\" : \"\") + \"</td>\"\n + '<td><span class=\"badge ' + db.cls + '\">' + escHtml(db.label) + \"</span></td>\"\n + \"<td>\" + escHtml(fmtRelative(s2.startedAt)) + \"</td>\"\n + \"<td>\" + (s2.costUsd != null ? \"$\" + Number(s2.costUsd).toFixed(2) : \"\") + \"</td>\"\n + '<td class=\"actions\">' + acts + \"</td>\"\n + \"</tr>\";\n }\n html += \"</tbody></table>\";\n if (rows.length > shown.length) html += '<div class=\"empty\">&hellip; ' + (rows.length - shown.length) + \" more (narrow with the workspace filter)</div>\";\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-id]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { sessionAction(btn.getAttribute(\"data-id\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\nfunction renderCrons() {\n var el = document.getElementById(\"crons\");\n document.getElementById(\"cron-count\").textContent = crons.length ? crons.length + \" jobs\" : \"\";\n if (crons.length === 0) {\n el.innerHTML = '<div class=\"empty\">No cron jobs.</div>';\n return;\n }\n var html = \"<table><thead><tr><th>Label</th><th>Schedule</th><th>State</th><th>Last</th><th>Next</th><th></th></tr></thead><tbody>\";\n for (var i = 0; i < crons.length; i++) {\n var c = crons[i];\n var last = c.lastResult == null ? \"\" : (c.lastResult.ok ? \"ok\" : \"failed\");\n var lastCls = c.lastResult == null ? \"b-end\" : (c.lastResult.ok ? \"b-run\" : \"b-err\");\n html += \"<tr>\"\n + '<td><span class=\"sn\">' + escHtml(c.label || c.id) + \"</span></td>\"\n + \"<td>\" + escHtml(c.schedule) + \"</td>\"\n + '<td><span class=\"badge ' + (c.active ? \"b-run\" : \"b-end\") + '\">' + (c.active ? \"active\" : \"off\") + \"</span></td>\"\n + \"<td>\" + (last ? '<span class=\"badge ' + lastCls + '\" title=\"' + escHtml((c.lastResult && c.lastResult.summary) || \"\") + '\">' + last + \"</span>\" : \"\") + \"</td>\"\n + \"<td>\" + escHtml(c.nextRunAt ? fmtRelative(c.nextRunAt).replace(\" ago\", \"\") : \"\") + \"</td>\"\n + '<td class=\"actions\">'\n + '<button class=\"act\" data-cron=\"' + escHtml(c.id) + '\" data-do=\"run\">Run</button>'\n + '<button class=\"act danger\" data-cron=\"' + escHtml(c.id) + '\" data-do=\"delete\">Delete</button>'\n + \"</td></tr>\";\n }\n html += \"</tbody></table>\";\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-cron]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { cronAction(btn.getAttribute(\"data-cron\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\n// ============================================================\n// actions\n// ============================================================\n\nfunction runAction(tool, args, okMsg) {\n if (busyActions) return Promise.resolve();\n busyActions = true;\n return callApp(tool, args).then(function (d) {\n busyActions = false;\n if (okMsg) toast(okMsg);\n return loadAll().then(function () { return d; });\n }).catch(function (e) {\n busyActions = false;\n toast(tool + \": \" + e.message, true);\n throw e;\n });\n}\n\nfunction findSpecial(key) {\n for (var i = 0; i < SPECIALS.length; i++) if (SPECIALS[i].key === key) return SPECIALS[i];\n return null;\n}\n\nfunction rewireTickCron(sp, sessionId) {\n if (!sp.cron) return Promise.resolve();\n var stale = cronByLabel(sp.cron.label);\n var chain = Promise.resolve();\n for (var i = 0; i < stale.length; i++) {\n (function (jobId) {\n chain = chain.then(function () { return callApp(\"cron_delete\", { jobId: jobId }); });\n })(stale[i].id);\n }\n return chain.then(function () {\n return callApp(\"cron_create\", {\n schedule: sp.cron.schedule,\n recurring: true,\n label: sp.cron.label,\n action: { kind: \"prompt-session\", sessionId: sessionId, prompt: sp.cron.prompt },\n });\n });\n}\n\nfunction specialAction(key, what, btn) {\n var sp = findSpecial(key);\n if (!sp) return;\n var s = specialSessionFor(sp);\n if (what === \"launch\") {\n btn.disabled = true;\n callApp(\"agent_start\", {\n adapter: \"claude-code\",\n label: sp.label,\n prompt: sp.prompt,\n keepAlive: true,\n }).then(function (d) {\n var id = d && d.id;\n toast(sp.label + \" launched\" + (id ? \" (\" + id + \")\" : \"\"));\n return id && sp.cron ? rewireTickCron(sp, id) : null;\n }).then(function () { return loadAll(); })\n .catch(function (e) { toast(\"launch: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"restart\" && s) {\n btn.disabled = true;\n callApp(\"session_restart\", { idOrName: s.id }).then(function (d) {\n var id = (d && d.id) || s.id;\n toast(sp.label + \" restarted\");\n return sp.cron ? rewireTickCron(sp, id) : null;\n }).then(function () { return loadAll(); })\n .catch(function (e) { toast(\"restart: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"tick\" && s) {\n var prompt = sp.cron ? sp.cron.prompt : \"Status check: report where things stand, briefly.\";\n runAction(\"agent_prompt\", { sessionId: s.id, prompt: prompt }, sp.label + \" prompted\");\n return;\n }\n if (what === \"fixcron\" && s) {\n btn.disabled = true;\n rewireTickCron(sp, s.id).then(function () {\n toast(\"Cron re-wired to \" + s.id);\n return loadAll();\n }).catch(function (e) { toast(\"cron: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"kill\" && s) {\n confirmClick(\"kill-\" + s.id, btn, function () {\n runAction(\"agent_kill\", { sessionId: s.id }, sp.label + \" killed\");\n });\n }\n}\n\nfunction sessionAction(id, what, btn) {\n if (what === \"kill\") {\n confirmClick(\"kill-\" + id, btn, function () {\n runAction(\"agent_kill\", { sessionId: id }, \"Killed \" + id);\n });\n return;\n }\n if (what === \"restart\") { runAction(\"session_restart\", { idOrName: id }, \"Restarted \" + id); return; }\n if (what === \"archive\") { runAction(\"session_archive\", { idOrName: id }, \"Archived \" + id); }\n}\n\nfunction cronAction(jobId, what, btn) {\n if (what === \"run\") { runAction(\"cron_run\", { jobId: jobId }, \"Cron fired\"); return; }\n if (what === \"delete\") {\n confirmClick(\"cron-\" + jobId, btn, function () {\n runAction(\"cron_delete\", { jobId: jobId }, \"Cron deleted\");\n });\n }\n}\n\n// ============================================================\n// boot\n// ============================================================\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () { loadHealth(); loadAll(); });\ndocument.getElementById(\"ws-select\").addEventListener(\"change\", function (e) {\n wsFilter = e.target.value;\n renderSessions();\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n buildHousekeeping();\n loadHealth();\n return loadAll();\n })\n .then(function () {\n pollTimer = setInterval(loadAll, 5000);\n })\n .catch(function (err) {\n document.getElementById(\"body\").innerHTML =\n '<div class=\"empty\">Standalone mode \\\\u2014 no host bridge (' + escHtml(err.message) + \")</div>\";\n });\n</script>\n</body>\n</html>`\n","/**\n * `ops-panel` — the agentproto daemon operations cockpit as an app.\n *\n * Two durable \"special sessions\" ship as agents — the Session Watchdog\n * (cron-ticked health checks: classify, flag, archive; never kills) and the\n * Sessions Manager (the coordinator the human talks to, which drives the\n * other sessions). The panel (`ui.ts`) launches/restarts them by label,\n * wires the watchdog's `watchdog-tick` cron, lists every session filterable\n * by workspace with lifecycle actions, and fronts housekeeping: session GC,\n * worktree GC (dry-run first), and the cron ledger.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { manager } from \"./agents/manager.js\"\nimport { watchdog } from \"./agents/watchdog.js\"\nimport { OPS_PANEL_HTML, OPS_PANEL_TOOLS } from \"./ui.js\"\n\nexport const opsPanel: AppHandle = defineApp({\n id: \"@agentproto/ops-panel\",\n name: \"Ops Panel\",\n version: \"0.1.0\",\n description: \"Daemon ops cockpit — watchdog & manager sessions, workspace-filtered session list, GC and cron housekeeping.\",\n agents: [watchdog, manager],\n ui: {\n html: OPS_PANEL_HTML,\n title: \"Ops Panel\",\n tools: [...OPS_PANEL_TOOLS],\n },\n})\n\nexport { watchdog, manager }\nexport { WATCHDOG_LABEL, WATCHDOG_TICK_LABEL, WATCHDOG_TICK_PROMPT, WATCHDOG_TICK_SCHEDULE } from \"./agents/watchdog.js\"\nexport { MANAGER_LABEL } from \"./agents/manager.js\"\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"panel-bridge.d.ts","sourceRoot":"","sources":["../src/panel-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAIH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAiIzD"}
1
+ {"version":3,"file":"panel-bridge.d.ts","sourceRoot":"","sources":["../src/panel-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAIH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA8IzD"}
@@ -55,5 +55,5 @@ export declare const REVIEW_PANEL_TOOL_ID = "agentproto_reviews";
55
55
  * deep link (./ui/render.ts's `liveSessionUrl`) is a plain `openLink`/
56
56
  * `window.open` navigation, not a tools/call, so it needs no entry here. */
57
57
  export declare const REVIEW_PANEL_UI_TOOLS: readonly ["review_ledger", "review_status", "review_cancel", "review_run", "review_pr", "review_export"];
58
- export declare const REVIEW_PANEL_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<title>agentproto reviews</title>\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\n vite.config.ts's injectPanelBridge plugin \u2014 a classic (non-module)\n script so its `initBridge`/`callTool` land as globals before the\n deferred module script below runs. -->\n<script>// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500\n// JSON-RPC 2.0 over window.parent.postMessage \u00B7 spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nvar _hostCaps = null;\nvar _standaloneApp = null;\nfunction _isStandalone(){\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\n}\n// \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Panels that iframe the daemon's standalone app host carry this proof so\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\n// registerMcpApps replaces the placeholder with a real per-boot token when\n// serving the panel as a resource; every other render (tests, standalone\n// tab, VS Code HTTP-iframe panel \u2014 the latter two already pass\n// iframeEmbedAllowed's origin checks) keeps the literal, and\n// withEmbedToken() then degrades to an identity function.\nwindow.__AGENPROTO_EMBED_TOKEN__ = \"__AGENPROTO_EMBED_TOKEN__\";\nfunction withEmbedToken(url){\n var t = window.__AGENPROTO_EMBED_TOKEN__;\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\n}\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\nfunction getHostContext(){ return _hostContext; }\nfunction getHostCapabilities(){ return _hostCaps; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== 'object') return;\n // ui/notifications/host-context-changed carries only the changed keys \u2014\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener('message', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === 'ui/notifications/host-context-changed'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n if (_isStandalone()){\n return window.McpApp.connect().then(function(conn){\n _standaloneApp = conn;\n // No host to advertise a hostContext \u2014 default to inline with no\n // other modes available, which keeps the display-mode toggle buttons\n // hidden (they only show for modes hostContext.availableDisplayModes\n // lists) instead of rendering two buttons with nothing to switch to.\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\n });\n }\n return rpcRequest('ui/initialize', {\n appInfo: {name: \"agentproto-review-panel\", version: '0.1.0'},\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\n protocolVersion: '2026-01-26'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) \u2014 capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 e.g. openLinks,\n // \"Host supports opening external URLs\". Captured once, read via\n // getHostCapabilities() by panels that need to branch on it.\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\n rpcNotify('ui/notifications/initialized', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest('ui/request-display-mode', {mode: mode});\n}\nfunction openLink(url){\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\n return rpcRequest('ui/open-link', {url: url});\n}\nfunction callTool(name, args){\n var raw = _standaloneApp\n ? _standaloneApp.callTool(name, args || {})\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\n return raw.then(function(result){\n if (result.isError){\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\n throw new Error(e);\n }\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\n return JSON.parse(text);\n });\n}\n\n// \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500\n// The installer is inlined verbatim (it is self-contained and idempotent),\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\n// panel stays inline until the user clicks, and the buttons only appear for\n// the modes the host advertises in hostContext.availableDisplayModes.\n(function () {\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\n window.AgentprotoUI = window.AgentprotoUI || {};\n\n var STYLE_ID = \"agentproto-display-mode-style\";\n var BAR_ID = \"agentproto-display-mode-bar\";\n // Gap between the host's safe area (or the viewport edge) and the button.\n var EDGE_GAP = 8;\n\n // Glyph + accessible label per mode. The button is an icon; the words go\n // to title/aria-label so the control stays compact without going mute to\n // a screen reader.\n var LABELS = {\n fullscreen: {\n on: \"\\u2921\", off: \"\\u2922\",\n onLabel: \"R\\u00e9duire\", offLabel: \"Agrandir\"\n },\n pip: {\n on: \"\\u2921\", off: \"\\uD83D\\uDCCC\",\n onLabel: \"D\\u00e9tacher\", offLabel: \"\\u00c9pingler sur le c\\u00f4t\\u00e9 (pip)\"\n }\n };\n\n // Every colour and both offsets resolve through an app-overridable custom\n // property first, so an app can restyle/reposition the button with a\n // stylesheet and never touch this script. The safe-area vars are what the\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\n function ensureStyle() {\n if (!document.head || document.getElementById(STYLE_ID)) return;\n var style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent =\n \"#\" + BAR_ID + \"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\" +\n \"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\" +\n \"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\" +\n \".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\" +\n \"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\" +\n \"cursor:pointer;-webkit-appearance:none;appearance:none;\" +\n \"font:600 14px/1 system-ui,-apple-system,sans-serif;\" +\n \"border:1px solid var(--agentproto-dm-border,#d0d0d0);\" +\n \"background:var(--agentproto-dm-bg,#fff);\" +\n \"color:var(--agentproto-dm-fg,#1a1a1a);\" +\n \"box-shadow:0 1px 4px rgba(0,0,0,.18)}\" +\n \".agentproto-dm-btn:hover{filter:brightness(.95)}\" +\n \".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\" +\n \".agentproto-dm-btn[data-theme=\\\"dark\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\" +\n \"@media (prefers-color-scheme:dark){\" +\n \".agentproto-dm-btn:not([data-theme=\\\"light\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\";\n document.head.appendChild(style);\n }\n\n function ready(fn) {\n if (document.body) fn();\n else document.addEventListener(\"DOMContentLoaded\", fn);\n }\n\n function metaToggle() {\n var el = document.querySelector('meta[name=\"agentproto-display-toggle\"]');\n if (!el) return \"\";\n return (el.getAttribute(\"content\") || \"\").toLowerCase().replace(/^\\s+|\\s+$/g, \"\");\n }\n\n function ensureBar() {\n var bar = document.getElementById(BAR_ID);\n if (bar) return bar;\n bar = document.createElement(\"div\");\n bar.id = BAR_ID;\n document.body.appendChild(bar);\n return bar;\n }\n\n function px(v) {\n return typeof v === \"number\" && isFinite(v) && v > 0 ? v : 0;\n }\n\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\n api = api || {};\n opts = opts || {};\n\n var ctx = {};\n var listeners = [];\n var buttons = [];\n // Modes the host has refused once. Checked before availability, so a\n // later host-context-changed can never resurrect a dead button.\n var refused = {};\n var loggedKeys = false;\n\n var setting = opts.toggle || metaToggle() || \"auto\";\n var optimistic = setting === \"optimistic\";\n var autoMount = setting !== \"none\";\n\n function get() {\n return ctx.displayMode || \"inline\";\n }\n\n function available() {\n var a = ctx.availableDisplayModes;\n return a && a.slice ? a.slice() : [];\n }\n\n function supports(mode) {\n if (refused[mode]) return false;\n var a = ctx.availableDisplayModes;\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\n // Optimistic: the host advertises nothing (Claude Desktop, some\n // embedders) but may still honour the request. Fullscreen only \u2014 pip\n // is niche enough that guessing at it is just noise.\n return optimistic && mode === \"fullscreen\";\n }\n\n // Host chrome to stay clear of. Published as custom properties rather\n // than inline styles so an app's own --agentproto-dm-top/right still\n // wins (an inline style would beat any stylesheet it could write).\n function applyInsets() {\n var insets = ctx.safeAreaInsets || {};\n var root = document.documentElement;\n if (!root || !root.style || !root.style.setProperty) return;\n root.style.setProperty(\"--agentproto-dm-safe-top\", (px(insets.top) + EDGE_GAP) + \"px\");\n root.style.setProperty(\"--agentproto-dm-safe-right\", (px(insets.right) + EDGE_GAP) + \"px\");\n }\n\n function syncButton(btn) {\n var mode = btn.getAttribute(\"data-agentproto-dm\") || \"fullscreen\";\n var labels = LABELS[mode] || LABELS.fullscreen;\n var active = get() === mode;\n btn.style.display = supports(mode) ? \"\" : \"none\";\n btn.textContent = active ? labels.on : labels.off;\n var label = active ? labels.onLabel : labels.offLabel;\n btn.title = label;\n btn.setAttribute(\"aria-label\", label);\n btn.setAttribute(\"aria-pressed\", active ? \"true\" : \"false\");\n if (ctx.theme === \"dark\" || ctx.theme === \"light\") btn.setAttribute(\"data-theme\", ctx.theme);\n else btn.removeAttribute(\"data-theme\");\n }\n\n function sync() {\n applyInsets();\n for (var i = buttons.length - 1; i >= 0; i--) {\n // An app that removed its inline toggle stops being our problem.\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\n syncButton(buttons[i]);\n }\n }\n\n function emit() {\n var mode = get();\n for (var i = 0; i < listeners.length; i++) {\n try { listeners[i](mode, ctx); } catch (_) {}\n }\n }\n\n // host-context-changed carries only the changed keys \u2014 merge, matching\n // the official ext-apps App behaviour.\n function setContext(next) {\n if (!next || typeof next !== \"object\") return;\n for (var k in next) {\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\n }\n sync();\n emit();\n }\n\n function retire(mode) {\n refused[mode] = true;\n sync();\n }\n\n function request(mode) {\n var pending;\n try {\n pending = api.requestDisplayMode\n ? api.requestDisplayMode(mode)\n : Promise.reject(new Error(\"display mode: no host\"));\n } catch (err) {\n pending = Promise.reject(err);\n }\n return Promise.resolve(pending).then(function (result) {\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\n // the spec lets it differ from the one asked for \u2014 so a host that\n // answers \"still inline\" is a refusal, not a success.\n var settled = (result && result.mode) || mode;\n if (optimistic && mode !== \"inline\" && settled !== mode) {\n retire(mode);\n throw new Error(\"host refused display mode: \" + mode);\n }\n // Hosts are not required to follow up with host-context-changed \u2014\n // trust the result so the glyph flips either way.\n setContext({ displayMode: settled });\n return settled;\n }, function (err) {\n if (optimistic && mode !== \"inline\") retire(mode);\n throw err;\n });\n }\n\n function makeButton(mode) {\n var btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.className = \"agentproto-dm-btn\";\n // Stable id for the floating pair (styling hooks, tests). An app that\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\n var id = mode === \"fullscreen\"\n ? \"agentproto-display-mode\"\n : \"agentproto-display-mode-\" + mode;\n if (!document.getElementById(id)) btn.id = id;\n btn.setAttribute(\"data-agentproto-dm\", mode);\n btn.style.display = \"none\";\n btn.addEventListener(\"click\", function () {\n var cur = get();\n // Same rule both copies had: anything that isn't inline collapses\n // back to inline, so the button is always an escape hatch.\n var inPanel = cur === \"fullscreen\" || cur === \"pip\";\n request(inPanel ? \"inline\" : mode)[\"catch\"](function () {});\n });\n return btn;\n }\n\n function mountToggle(el, mode) {\n ensureStyle();\n mode = mode || \"fullscreen\";\n var btn = makeButton(mode);\n if (el) el.appendChild(btn);\n else ensureBar().appendChild(btn);\n buttons.push(btn);\n syncButton(btn);\n return btn;\n }\n\n function onChange(cb) {\n if (typeof cb !== \"function\") return function () {};\n listeners.push(cb);\n try { cb(get(), ctx); } catch (_) {}\n return function () {\n var i = listeners.indexOf(cb);\n if (i >= 0) listeners.splice(i, 1);\n };\n }\n\n ready(function () {\n ensureStyle();\n // Only the first installer in a document owns the floating bar: a\n // built-in panel served standalone gets the injected window.McpApp\n // bridge's copy AND its own, and two stacked buttons is a bug.\n if (autoMount && !document.getElementById(BAR_ID)) {\n ensureBar();\n mountToggle(null, \"pip\");\n mountToggle(null, \"fullscreen\");\n }\n sync();\n });\n\n // The diagnostic both copies had, now on BOTH paths: it used to exist\n // only in the built-in panel bridge, so an installed app debugged in\n // Claude Desktop (which advertises no fullscreen, hence no button)\n // printed nothing at all and left the user with no way to tell \"the\n // host refuses\" from \"the toggle is broken\".\n function observe(next) {\n console.log('[mcp-app] displayMode=', next && next.displayMode,\n 'availableDisplayModes=', next && next.availableDisplayModes);\n setContext(next);\n // What ELSE does this host send? Keys only, once \u2014 a host context can\n // carry toolInfo (the instantiating tool call's arguments) and other\n // host-private fields, so the names are the diagnostic and the values\n // stay out of the console.\n if (!loggedKeys && next && typeof next === \"object\") {\n loggedKeys = true;\n var keys = [];\n for (var k in ctx) {\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\n }\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\n }\n }\n\n if (typeof api.onHostContext === \"function\") api.onHostContext(observe);\n else if (typeof api.getHostContext === \"function\") observe(api.getHostContext());\n\n return {\n get: get,\n available: available,\n request: request,\n onChange: onChange,\n mountToggle: mountToggle\n };\n };\n})();\nvar displayMode = window.AgentprotoUI.installDisplayMode({\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n requestDisplayMode: requestDisplayMode\n});</script>\n <script>(function() {\n \"use strict\";\n function esc(value) {\n return value.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n }\n function liveSessionUrl(origin, sessionId) {\n return `${origin}/apps/@agentproto/live-session/ui?sessionId=${encodeURIComponent(sessionId)}`;\n }\n function shortRange(baseSha, headSha) {\n if (!baseSha || !headSha) return \"\u2014\";\n return `${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`;\n }\n function ageOf(createdAt, now = Date.now()) {\n const ms = now - new Date(createdAt).getTime();\n if (!Number.isFinite(ms) || ms < 0) return \"just now\";\n const mins = Math.floor(ms / 6e4);\n if (mins < 1) return \"just now\";\n if (mins < 60) return `${mins}m`;\n const hours = Math.floor(mins / 60);\n if (hours < 24) return `${hours}h`;\n return `${Math.floor(hours / 24)}d`;\n }\n function verdictChip(status) {\n return `<span class=\"chip chip-verdict v-${esc(status)}\">${esc(status)}</span>`;\n }\n const LANE_STATUS_LABEL = {\n pass: \"pass\",\n fail: \"fail\",\n skipped: \"skipped\",\n timeout: \"timeout\"\n };\n function laneStatusChip(status) {\n return `<span class=\"chip chip-lane l-${esc(status)}\">${esc(LANE_STATUS_LABEL[status])}</span>`;\n }\n function severityTag(severity) {\n return `<span class=\"tag sev-${esc(severity)}\">${esc(severity)}</span>`;\n }\n function requesterLabel(row) {\n const r = row.requester;\n if (!r) return \"\u2014\";\n const id = r.sessionId ? esc(r.sessionId) : \"\";\n const author = r.gitAuthor ? esc(r.gitAuthor.name) : \"\";\n if (id && author) return `${id} <span class=\"muted\">(${author})</span>`;\n return id || author || \"\u2014\";\n }\n function prLabel(row) {\n if (!row.pr) return \"\u2014\";\n const state = row.prState ? ` <span class=\"tag pr-${esc(row.prState)}\">${esc(row.prState)}</span>` : \"\";\n return `<a href=\"${esc(row.pr.url)}\" target=\"_blank\" rel=\"noopener\">#${row.pr.number}</a>${state}`;\n }\n function renderRow(row) {\n const status = row.status ?? row.verdict ?? \"incomplete\";\n const flags = [];\n if (row.dirty) flags.push('<span class=\"tag t-dirty\">dirty</span>');\n if (row.cached) flags.push('<span class=\"tag t-cached\">cached</span>');\n if (row.status !== \"running\") {\n flags.push(row.signed ? '<span class=\"tag t-signed\">signed</span>' : '<span class=\"tag t-unsigned\">unsigned</span>');\n }\n return `<tr class=\"row\" data-runid=\"${esc(row.runId)}\" tabindex=\"0\"><td>${verdictChip(status)}</td><td class=\"mono\">${esc(shortRange(row.baseSha, row.headSha))}</td><td>${esc(row.binding ?? \"\u2014\")}</td><td class=\"mono\">${esc(row.repoRemote ?? \"\u2014\")}</td><td>${requesterLabel(row)}</td><td>${prLabel(row)}</td><td>${flags.join(\" \") || \"\u2014\"}</td><td>${esc(ageOf(row.createdAt))}</td></tr>`;\n }\n function renderList(rows2) {\n if (rows2.length === 0) return '<div class=\"empty\">No reviews recorded yet.</div>';\n const header = \"<tr><th>Verdict</th><th>Range</th><th>Binding</th><th>Repo</th><th>Requester</th><th>PR</th><th>Flags</th><th>Age</th></tr>\";\n return `<table class=\"list\"><thead>${header}</thead><tbody>${rows2.map(renderRow).join(\"\")}</tbody></table>`;\n }\n function findingLine(f) {\n const loc = f.file ? `${esc(f.file)}${f.line ? `:${f.line}` : \"\"}` : \"\";\n return `<details class=\"finding\"><summary>${severityTag(f.severity)} ${esc(f.title)}${loc ? ` <span class=\"mono muted\">${loc}</span>` : \"\"}</summary><div class=\"finding-detail\">${esc(f.detail)}</div></details>`;\n }\n function renderLaneDetail(lane, rubrics = []) {\n const parts = [];\n parts.push(`<div class=\"lane-hdr\">`);\n parts.push(`<span class=\"lane-id\">${esc(lane.id)}</span>`);\n parts.push(laneStatusChip(lane.status));\n parts.push(`<span class=\"tag ${lane.blocking ? \"t-blocking\" : \"t-advisory\"}\">${lane.blocking ? \"blocking\" : \"advisory\"}</span>`);\n if (lane.durationMs !== void 0) parts.push(`<span class=\"muted\">${(lane.durationMs / 1e3).toFixed(1)}s</span>`);\n parts.push(`</div>`);\n if (lane.error) parts.push(`<div class=\"lane-error\">${esc(lane.error)}</div>`);\n if (lane.sessionId || lane.preset || lane.model || lane.fallbacks?.length) {\n const rubric = rubrics.find((r) => r.check === lane.id);\n const bits = [];\n if (lane.model) bits.push(`model: ${esc(lane.model)}`);\n if (lane.preset) bits.push(`preset: ${esc(lane.preset)}`);\n if (lane.fallbacks?.length) {\n bits.push(`after unavailable: ${lane.fallbacks.map((f) => `${esc(f.preset)} (${esc(f.error)})`).join(\", \")}`);\n }\n if (rubric) bits.push(`rubric: <span class=\"mono\">${esc(rubric.sha256.slice(0, 12))}</span>`);\n if (lane.sessionId) {\n bits.push(\n `reviewer: <button class=\"sess-link\" data-session-id=\"${esc(lane.sessionId)}\">${esc(lane.sessionId)}</button>`\n );\n }\n parts.push(`<div class=\"lane-agent-meta\">${bits.join(\" \u00B7 \")}</div>`);\n }\n if (lane.findings.length > 0) parts.push(`<div class=\"findings\">${lane.findings.map(findingLine).join(\"\")}</div>`);\n return `<div class=\"lane\">${parts.join(\"\")}</div>`;\n }\n function renderDetail(detail2) {\n const status = detail2.status === \"done\" ? detail2.verdict ?? \"incomplete\" : detail2.status;\n const header = [`<div class=\"detail-hdr\">`, verdictChip(status), `<span class=\"mono\">${esc(detail2.runId)}</span>`];\n if (detail2.binding) header.push(`<span class=\"tag\">${esc(detail2.binding)}</span>`);\n if (detail2.cached) header.push('<span class=\"tag t-cached\">cached</span>');\n if (detail2.status === \"done\" && detail2.attestation) {\n const sig = detail2.attestation.attestor?.signature;\n header.push(\n sig ? `<span class=\"tag t-signed\" title=\"${esc(sig.keyFingerprint)}\">signed: ${esc(sig.principal)}</span>` : '<span class=\"tag t-unsigned\">unsigned</span>'\n );\n }\n header.push(`</div>`);\n if (detail2.error) header.push(`<div class=\"detail-error\">${esc(detail2.error)}</div>`);\n if (detail2.supersededBy) header.push(`<div class=\"muted\">superseded by ${esc(detail2.supersededBy)}</div>`);\n const fullLanes = detail2.attestation?.lanes;\n const rubrics = detail2.attestation?.rubrics ?? [];\n let body;\n if (fullLanes) {\n body = fullLanes.map((l) => renderLaneDetail(l, rubrics)).join(\"\");\n } else if (detail2.lanes) {\n body = detail2.lanes.map((l) => `<div class=\"lane\"><div class=\"lane-hdr\"><span class=\"lane-id\">${esc(l.id)}</span>${laneStatusChip(l.status)}</div></div>`).join(\"\");\n } else {\n body = \"\";\n }\n return `<div class=\"detail\">${header.join(\"\")}${body}</div>`;\n }\n const POLL_RUNNING_MS = 4e3;\n const POLL_IDLE_MS = 15e3;\n function getEl(id) {\n const el = document.getElementById(id);\n if (!el) throw new Error(`review panel: missing #${id}`);\n return el;\n }\n let rows = [];\n let selectedRunId = null;\n let detail = null;\n let pollTimer = null;\n let loadInFlight = false;\n function boot() {\n const listPaneEl = getEl(\"list-pane\");\n const detailPaneEl = getEl(\"detail-pane\");\n const detailBodyEl = getEl(\"detail-body\");\n const statusbarEl = getEl(\"statusbar\");\n const cancelBtn = getEl(\"cancel-btn\");\n const rerunBtn = getEl(\"rerun-btn\");\n const prBtn = getEl(\"pr-btn\");\n const exportBtn = getEl(\"export-btn\");\n function setStatus(msg) {\n statusbarEl.textContent = msg;\n }\n function findRow(runId) {\n return rows.find((r) => r.runId === runId);\n }\n function renderListPane() {\n listPaneEl.innerHTML = renderList(rows);\n if (!selectedRunId) return;\n listPaneEl.querySelectorAll(\"tr[data-runid]\").forEach((tr) => {\n if (tr.dataset.runid === selectedRunId) tr.classList.add(\"selected\");\n });\n }\n function renderDetailPane() {\n if (!selectedRunId || !detail) {\n detailPaneEl.classList.remove(\"open\");\n return;\n }\n detailPaneEl.classList.add(\"open\");\n detailBodyEl.innerHTML = renderDetail(detail);\n const running = detail.status === \"running\";\n cancelBtn.disabled = !running;\n cancelBtn.style.display = running ? \"\" : \"none\";\n detailBodyEl.querySelectorAll(\".sess-link\").forEach((btn) => {\n btn.addEventListener(\"click\", () => openSession(btn.dataset.sessionId ?? \"\"));\n });\n }\n function openSession(sessionId) {\n if (!sessionId) return;\n const origin = window.location.origin && window.location.origin !== \"null\" ? window.location.origin : \"\";\n const url = liveSessionUrl(origin, sessionId);\n const caps = getHostCapabilities();\n if (caps?.openLinks) {\n openLink(url).catch(() => window.open(url, \"_blank\"));\n } else {\n window.open(url, \"_blank\");\n }\n }\n function loadList() {\n if (loadInFlight) return Promise.resolve();\n loadInFlight = true;\n return callTool(\"review_ledger\", { includeRunning: true }).then((result) => {\n rows = result.attestations;\n renderListPane();\n const runningCount = rows.filter((r) => r.status === \"running\").length;\n setStatus(`${result.total} review${result.total === 1 ? \"\" : \"s\"} \u00B7 ${runningCount} running \u00B7 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\n if (selectedRunId && findRow(selectedRunId)) return loadDetail(selectedRunId);\n return void 0;\n }).catch((e) => setStatus(`Error: ${e.message}`)).finally(() => {\n loadInFlight = false;\n });\n }\n function loadDetail(runId) {\n return callTool(\"review_status\", { runId }).then((result) => {\n detail = result;\n renderDetailPane();\n }).catch((e) => setStatus(`Error loading detail: ${e.message}`));\n }\n function openDetail(runId) {\n selectedRunId = runId;\n renderListPane();\n void loadDetail(runId);\n }\n function closeDetail() {\n selectedRunId = null;\n detail = null;\n renderListPane();\n renderDetailPane();\n }\n listPaneEl.addEventListener(\"click\", (evt) => {\n if (!(evt.target instanceof Element)) return;\n const tr = evt.target.closest(\"tr[data-runid]\");\n if (!(tr instanceof HTMLElement)) return;\n const runId = tr.dataset.runid;\n if (runId) openDetail(runId);\n });\n listPaneEl.addEventListener(\"keydown\", (evt) => {\n if (evt.key !== \"Enter\" && evt.key !== \" \") return;\n if (!(evt.target instanceof Element)) return;\n const tr = evt.target.closest(\"tr[data-runid]\");\n if (!(tr instanceof HTMLElement)) return;\n evt.preventDefault();\n const runId = tr.dataset.runid;\n if (runId) openDetail(runId);\n });\n getEl(\"detail-close\").addEventListener(\"click\", closeDetail);\n getEl(\"refresh-btn\").addEventListener(\"click\", () => {\n void loadList();\n });\n cancelBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_cancel\", { runId: selectedRunId }).then(() => {\n setStatus(\"Cancelled.\");\n return loadList();\n }).catch((e) => setStatus(`Cancel failed: ${e.message}`));\n });\n rerunBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n const row = findRow(selectedRunId);\n const cwd = row?.cwd;\n if (!cwd) {\n setStatus(\"Re-run failed: this row has no known checkout path.\");\n return;\n }\n callTool(\"review_run\", {\n cwd,\n ...row?.binding ? { binding: row.binding } : {},\n nocache: true,\n wait: false,\n supersede: true\n }).then(() => {\n setStatus(\"Re-run started.\");\n return loadList();\n }).catch((e) => setStatus(`Re-run failed: ${e.message}`));\n });\n prBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_pr\", { runId: selectedRunId }).then(() => {\n setStatus(\"Fetched PR status.\");\n return loadList();\n }).catch((e) => setStatus(`Fetch PR status failed: ${e.message}`));\n });\n exportBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_export\", { runId: selectedRunId }).then((result) => {\n setStatus(result.path ? `Exported to ${result.path}` : \"Exported.\");\n }).catch((e) => setStatus(`Export failed: ${e.message}`));\n });\n function doPoll() {\n if (pollTimer) clearTimeout(pollTimer);\n loadList().finally(() => {\n const anyRunning = rows.some((r) => r.status === \"running\");\n pollTimer = setTimeout(doPoll, anyRunning ? POLL_RUNNING_MS : POLL_IDLE_MS);\n });\n }\n initBridge().then(() => loadList()).then(() => {\n pollTimer = setTimeout(doPoll, POLL_RUNNING_MS);\n }).catch((e) => setStatus(`Bridge error: ${e.message}`));\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", boot);\n } else {\n boot();\n }\n})();</script>\n <style rel=\"stylesheet\" crossorigin>/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Semantic tokens + runtime mapping sheets \u2014 same scheme as work-board's\n style.css (see that file's header doc for the two-mapping-sheet\n rationale): every rule below reads ONLY the --rp-* custom properties,\n each resolving through a live --vscode-* var() first, then the\n agentproto brand fallback.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n:root {\n --rp-bg: var(--vscode-editor-background, #f5f6f3);\n --rp-surface: var(--vscode-sideBar-background, #ffffff);\n --rp-ink: var(--vscode-editor-foreground, #171a1e);\n --rp-ink-muted: var(--vscode-descriptionForeground, #5c6470);\n --rp-border: var(--vscode-panel-border, #dde0da);\n --rp-accent: var(--vscode-button-background, #0f62fe);\n\n --rp-pass: var(--vscode-charts-green, #3edc81);\n --rp-block: var(--vscode-charts-red, #fa4d56);\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\n --rp-running: var(--vscode-charts-blue, #4589ff);\n --rp-cancelled: var(--vscode-descriptionForeground, #5c6470);\n --rp-failed: var(--vscode-charts-red, #fa4d56);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --rp-bg: var(--vscode-editor-background, #101214);\n --rp-surface: var(--vscode-sideBar-background, #16191d);\n --rp-ink: var(--vscode-editor-foreground, #e8eaed);\n --rp-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\n --rp-border: var(--vscode-panel-border, #272c33);\n --rp-accent: var(--vscode-button-background, #4589ff);\n\n --rp-pass: var(--vscode-charts-green, #3edc81);\n --rp-block: var(--vscode-charts-red, #fa4d56);\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\n --rp-running: var(--vscode-charts-blue, #4589ff);\n --rp-cancelled: var(--vscode-descriptionForeground, #99a2ad);\n --rp-failed: var(--vscode-charts-red, #fa4d56);\n }\n}\n\n/* \u2500\u2500 Base \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n* { box-sizing: border-box; margin: 0; padding: 0; }\n\nhtml, body {\n height: 100%;\n font-family: \"IBM Plex Sans\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n font-size: 13px;\n background: var(--rp-bg);\n color: var(--rp-ink);\n overflow: hidden;\n}\n\n.mono {\n font-family: \"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, Monaco, \"Courier New\", monospace;\n}\n.muted { color: var(--rp-ink-muted); }\n\n#app { display: flex; flex-direction: column; height: 100%; }\n\n#toolbar {\n padding: 8px 12px;\n border-bottom: 1px solid var(--rp-border);\n display: flex;\n align-items: center;\n gap: 8px;\n background: var(--rp-surface);\n flex-shrink: 0;\n}\n#toolbar-label { font-size: 11px; color: var(--rp-ink-muted); }\n.tbtn {\n background: var(--rp-bg);\n border: 1px solid var(--rp-border);\n color: var(--rp-ink);\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n}\n.tbtn:hover { background: var(--rp-border); }\n.tbtn:disabled { opacity: 0.5; cursor: default; }\n.tbtn.danger { border-color: var(--rp-block); color: var(--rp-block); }\n\n#main { flex: 1; overflow: hidden; display: flex; }\n#list-pane { flex: 1; overflow-y: auto; }\n#detail-pane {\n width: 420px;\n min-width: 320px;\n border-left: 1px solid var(--rp-border);\n overflow-y: auto;\n display: none;\n flex-direction: column;\n}\n#detail-pane.open { display: flex; }\n#detail-close { align-self: flex-end; margin: 6px; }\n\ntable.list { width: 100%; border-collapse: collapse; }\ntable.list th {\n text-align: left;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--rp-ink-muted);\n padding: 6px 8px;\n border-bottom: 1px solid var(--rp-border);\n position: sticky;\n top: 0;\n background: var(--rp-surface);\n}\ntable.list td { padding: 6px 8px; border-bottom: 1px solid var(--rp-border); font-size: 12px; vertical-align: middle; }\ntr.row { cursor: pointer; }\ntr.row:hover, tr.row:focus { background: color-mix(in srgb, var(--rp-accent) 8%, transparent); outline: none; }\ntr.row.selected { background: color-mix(in srgb, var(--rp-accent) 14%, transparent); }\n\n.empty { padding: 24px 12px; color: var(--rp-ink-muted); font-size: 12px; text-align: center; }\n\n.chip { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }\n.chip-verdict.v-pass { background: color-mix(in srgb, var(--rp-pass) 18%, transparent); color: var(--rp-pass); }\n.chip-verdict.v-block { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\n.chip-verdict.v-incomplete { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\n.chip-verdict.v-running { background: color-mix(in srgb, var(--rp-running) 18%, transparent); color: var(--rp-running); }\n.chip-verdict.v-cancelled { background: color-mix(in srgb, var(--rp-cancelled) 15%, transparent); color: var(--rp-cancelled); }\n.chip-verdict.v-failed { background: color-mix(in srgb, var(--rp-failed) 18%, transparent); color: var(--rp-failed); }\n\n.chip-lane.l-pass { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\n.chip-lane.l-fail { background: color-mix(in srgb, var(--rp-block) 15%, transparent); color: var(--rp-block); }\n.chip-lane.l-skipped { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.chip-lane.l-timeout { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\n\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\n.t-dirty { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\n.t-cached { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.t-signed { background: color-mix(in srgb, var(--rp-pass) 12%, transparent); color: var(--rp-pass); }\n.t-unsigned { background: color-mix(in srgb, var(--rp-ink-muted) 10%, transparent); color: var(--rp-ink-muted); }\n.t-blocking { background: color-mix(in srgb, var(--rp-block) 12%, transparent); color: var(--rp-block); }\n.t-advisory { background: color-mix(in srgb, var(--rp-ink-muted) 12%, transparent); color: var(--rp-ink-muted); }\n.pr-open { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\n.pr-merged { background: color-mix(in srgb, var(--rp-accent) 15%, transparent); color: var(--rp-accent); }\n.pr-closed { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.sev-high { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\n.sev-medium { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\n.sev-low { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n\n.detail { padding: 10px; display: flex; flex-direction: column; gap: 10px; }\n.detail-hdr { display: flex; align-items: center; gap: 8px; }\n.detail-error { color: var(--rp-block); font-size: 12px; word-break: break-word; }\n.detail-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 10px 10px; }\n\n.lane {\n border: 1px solid var(--rp-border);\n border-radius: 6px;\n padding: 8px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.lane-hdr { display: flex; align-items: center; gap: 6px; }\n.lane-id { font-weight: 600; font-size: 12px; }\n.lane-error { color: var(--rp-block); font-size: 11px; word-break: break-word; }\n.lane-agent-meta { font-size: 11px; color: var(--rp-ink-muted); }\n.sess-link {\n background: none;\n border: none;\n color: var(--rp-accent);\n cursor: pointer;\n font: inherit;\n padding: 0;\n text-decoration: underline;\n}\n\n.findings { display: flex; flex-direction: column; gap: 4px; }\n.finding summary { cursor: pointer; font-size: 11px; list-style: none; }\n.finding summary::-webkit-details-marker { display: none; }\n.finding-detail { font-size: 11px; color: var(--rp-ink-muted); padding: 4px 0 0 4px; white-space: pre-wrap; word-break: break-word; }\n\n#statusbar {\n padding: 4px 12px;\n font-size: 11px;\n color: var(--rp-ink-muted);\n border-top: 1px solid var(--rp-border);\n background: var(--rp-surface);\n flex-shrink: 0;\n}</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"toolbar\">\n <span id=\"toolbar-label\">Reviews</span>\n <button class=\"tbtn\" id=\"refresh-btn\" title=\"Refresh\">&#8635;</button>\n </div>\n <div id=\"main\">\n <div id=\"list-pane\"></div>\n <div id=\"detail-pane\">\n <button class=\"tbtn\" id=\"detail-close\">&times; Close</button>\n <div id=\"detail-body\"></div>\n <div class=\"detail-actions\">\n <button class=\"tbtn danger\" id=\"cancel-btn\">Cancel</button>\n <button class=\"tbtn\" id=\"rerun-btn\">Re-run fresh</button>\n <button class=\"tbtn\" id=\"pr-btn\">Fetch PR status</button>\n <button class=\"tbtn\" id=\"export-btn\">Export</button>\n </div>\n </div>\n </div>\n <div id=\"statusbar\">Connecting to bridge&#8230;</div>\n</div>\n</body>\n</html>\n";
58
+ export declare const REVIEW_PANEL_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<title>agentproto reviews</title>\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\n vite.config.ts's injectPanelBridge plugin \u2014 a classic (non-module)\n script so its `initBridge`/`callTool` land as globals before the\n deferred module script below runs. -->\n<script>// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500\n// JSON-RPC 2.0 over window.parent.postMessage \u00B7 spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nvar _hostCaps = null;\nvar _standaloneApp = null;\nfunction _isStandalone(){\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\n}\n// \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Panels that iframe the daemon's standalone app host carry this proof so\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\n// registerMcpApps replaces the placeholder with a real per-boot token when\n// serving the panel as a resource; every other render (tests, standalone\n// tab, VS Code HTTP-iframe panel \u2014 the latter two already pass\n// iframeEmbedAllowed's origin checks) keeps the literal, and\n// withEmbedToken() then degrades to an identity function.\nwindow.__AGENPROTO_EMBED_TOKEN__ = \"__AGENPROTO_EMBED_TOKEN__\";\nfunction withEmbedToken(url){\n var t = window.__AGENPROTO_EMBED_TOKEN__;\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\n}\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\nfunction getHostContext(){ return _hostContext; }\nfunction getHostCapabilities(){ return _hostCaps; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== 'object') return;\n // ui/notifications/host-context-changed carries only the changed keys \u2014\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener('message', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === 'ui/notifications/host-context-changed'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n if (_isStandalone()){\n return window.McpApp.connect().then(function(conn){\n _standaloneApp = conn;\n // No host to advertise a hostContext \u2014 default to inline with no\n // other modes available, which keeps the display-mode toggle buttons\n // hidden (they only show for modes hostContext.availableDisplayModes\n // lists) instead of rendering two buttons with nothing to switch to.\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\n });\n }\n return rpcRequest('ui/initialize', {\n appInfo: {name: \"agentproto-review-panel\", version: '0.1.0'},\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\n protocolVersion: '2026-01-26'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) \u2014 capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 e.g. openLinks,\n // \"Host supports opening external URLs\". Captured once, read via\n // getHostCapabilities() by panels that need to branch on it.\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\n rpcNotify('ui/notifications/initialized', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest('ui/request-display-mode', {mode: mode});\n}\nfunction openLink(url){\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\n return rpcRequest('ui/open-link', {url: url});\n}\n// An MCP tool-result envelope: {content: [{type: 'text', text}, ...]}.\nfunction _isEnvelope(v){\n return !!v && typeof v === 'object' && Array.isArray(v.content) &&\n !!v.content[0] && v.content[0].type === 'text' && typeof v.content[0].text === 'string';\n}\n// Peels every envelope layer. The standalone path (POST /apps/:id/tool-call,\n// runtime app-tools.ts dispatchAllowlistedAppTool) wraps an already-wrapped\n// builtin result a second time, and an inner isError rides inside that outer\n// success \u2014 so isError is honoured at each layer, not just the first.\nfunction _unwrapToolResult(result){\n var env = result;\n while (true){\n var first = env && env.content && env.content[0];\n var text = (first && first.text) || '';\n if (env && env.isError) throw new Error(text || 'tool error');\n try { var value = JSON.parse(text || '{}'); } catch(_) { return text; }\n if (!_isEnvelope(value)) return value;\n env = value;\n }\n}\nfunction callTool(name, args){\n var raw = _standaloneApp\n ? _standaloneApp.callTool(name, args || {})\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\n return raw.then(_unwrapToolResult);\n}\n\n// \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500\n// The installer is inlined verbatim (it is self-contained and idempotent),\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\n// panel stays inline until the user clicks, and the buttons only appear for\n// the modes the host advertises in hostContext.availableDisplayModes.\n(function () {\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\n window.AgentprotoUI = window.AgentprotoUI || {};\n\n var STYLE_ID = \"agentproto-display-mode-style\";\n var BAR_ID = \"agentproto-display-mode-bar\";\n // Gap between the host's safe area (or the viewport edge) and the button.\n var EDGE_GAP = 8;\n\n // Glyph + accessible label per mode. The button is an icon; the words go\n // to title/aria-label so the control stays compact without going mute to\n // a screen reader.\n var LABELS = {\n fullscreen: {\n on: \"\\u2921\", off: \"\\u2922\",\n onLabel: \"R\\u00e9duire\", offLabel: \"Agrandir\"\n },\n pip: {\n on: \"\\u2921\", off: \"\\uD83D\\uDCCC\",\n onLabel: \"D\\u00e9tacher\", offLabel: \"\\u00c9pingler sur le c\\u00f4t\\u00e9 (pip)\"\n }\n };\n\n // Every colour and both offsets resolve through an app-overridable custom\n // property first, so an app can restyle/reposition the button with a\n // stylesheet and never touch this script. The safe-area vars are what the\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\n function ensureStyle() {\n if (!document.head || document.getElementById(STYLE_ID)) return;\n var style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent =\n \"#\" + BAR_ID + \"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\" +\n \"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\" +\n \"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\" +\n \".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\" +\n \"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\" +\n \"cursor:pointer;-webkit-appearance:none;appearance:none;\" +\n \"font:600 14px/1 system-ui,-apple-system,sans-serif;\" +\n \"border:1px solid var(--agentproto-dm-border,#d0d0d0);\" +\n \"background:var(--agentproto-dm-bg,#fff);\" +\n \"color:var(--agentproto-dm-fg,#1a1a1a);\" +\n \"box-shadow:0 1px 4px rgba(0,0,0,.18)}\" +\n \".agentproto-dm-btn:hover{filter:brightness(.95)}\" +\n \".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\" +\n \".agentproto-dm-btn[data-theme=\\\"dark\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\" +\n \"@media (prefers-color-scheme:dark){\" +\n \".agentproto-dm-btn:not([data-theme=\\\"light\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\";\n document.head.appendChild(style);\n }\n\n function ready(fn) {\n if (document.body) fn();\n else document.addEventListener(\"DOMContentLoaded\", fn);\n }\n\n function metaToggle() {\n var el = document.querySelector('meta[name=\"agentproto-display-toggle\"]');\n if (!el) return \"\";\n return (el.getAttribute(\"content\") || \"\").toLowerCase().replace(/^\\s+|\\s+$/g, \"\");\n }\n\n function ensureBar() {\n var bar = document.getElementById(BAR_ID);\n if (bar) return bar;\n bar = document.createElement(\"div\");\n bar.id = BAR_ID;\n document.body.appendChild(bar);\n return bar;\n }\n\n function px(v) {\n return typeof v === \"number\" && isFinite(v) && v > 0 ? v : 0;\n }\n\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\n api = api || {};\n opts = opts || {};\n\n var ctx = {};\n var listeners = [];\n var buttons = [];\n // Modes the host has refused once. Checked before availability, so a\n // later host-context-changed can never resurrect a dead button.\n var refused = {};\n var loggedKeys = false;\n\n var setting = opts.toggle || metaToggle() || \"auto\";\n var optimistic = setting === \"optimistic\";\n var autoMount = setting !== \"none\";\n\n function get() {\n return ctx.displayMode || \"inline\";\n }\n\n function available() {\n var a = ctx.availableDisplayModes;\n return a && a.slice ? a.slice() : [];\n }\n\n function supports(mode) {\n if (refused[mode]) return false;\n var a = ctx.availableDisplayModes;\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\n // Optimistic: the host advertises nothing (Claude Desktop, some\n // embedders) but may still honour the request. Fullscreen only \u2014 pip\n // is niche enough that guessing at it is just noise.\n return optimistic && mode === \"fullscreen\";\n }\n\n // Host chrome to stay clear of. Published as custom properties rather\n // than inline styles so an app's own --agentproto-dm-top/right still\n // wins (an inline style would beat any stylesheet it could write).\n function applyInsets() {\n var insets = ctx.safeAreaInsets || {};\n var root = document.documentElement;\n if (!root || !root.style || !root.style.setProperty) return;\n root.style.setProperty(\"--agentproto-dm-safe-top\", (px(insets.top) + EDGE_GAP) + \"px\");\n root.style.setProperty(\"--agentproto-dm-safe-right\", (px(insets.right) + EDGE_GAP) + \"px\");\n }\n\n function syncButton(btn) {\n var mode = btn.getAttribute(\"data-agentproto-dm\") || \"fullscreen\";\n var labels = LABELS[mode] || LABELS.fullscreen;\n var active = get() === mode;\n btn.style.display = supports(mode) ? \"\" : \"none\";\n btn.textContent = active ? labels.on : labels.off;\n var label = active ? labels.onLabel : labels.offLabel;\n btn.title = label;\n btn.setAttribute(\"aria-label\", label);\n btn.setAttribute(\"aria-pressed\", active ? \"true\" : \"false\");\n if (ctx.theme === \"dark\" || ctx.theme === \"light\") btn.setAttribute(\"data-theme\", ctx.theme);\n else btn.removeAttribute(\"data-theme\");\n }\n\n function sync() {\n applyInsets();\n for (var i = buttons.length - 1; i >= 0; i--) {\n // An app that removed its inline toggle stops being our problem.\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\n syncButton(buttons[i]);\n }\n }\n\n function emit() {\n var mode = get();\n for (var i = 0; i < listeners.length; i++) {\n try { listeners[i](mode, ctx); } catch (_) {}\n }\n }\n\n // host-context-changed carries only the changed keys \u2014 merge, matching\n // the official ext-apps App behaviour.\n function setContext(next) {\n if (!next || typeof next !== \"object\") return;\n for (var k in next) {\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\n }\n sync();\n emit();\n }\n\n function retire(mode) {\n refused[mode] = true;\n sync();\n }\n\n function request(mode) {\n var pending;\n try {\n pending = api.requestDisplayMode\n ? api.requestDisplayMode(mode)\n : Promise.reject(new Error(\"display mode: no host\"));\n } catch (err) {\n pending = Promise.reject(err);\n }\n return Promise.resolve(pending).then(function (result) {\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\n // the spec lets it differ from the one asked for \u2014 so a host that\n // answers \"still inline\" is a refusal, not a success.\n var settled = (result && result.mode) || mode;\n if (optimistic && mode !== \"inline\" && settled !== mode) {\n retire(mode);\n throw new Error(\"host refused display mode: \" + mode);\n }\n // Hosts are not required to follow up with host-context-changed \u2014\n // trust the result so the glyph flips either way.\n setContext({ displayMode: settled });\n return settled;\n }, function (err) {\n if (optimistic && mode !== \"inline\") retire(mode);\n throw err;\n });\n }\n\n function makeButton(mode) {\n var btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.className = \"agentproto-dm-btn\";\n // Stable id for the floating pair (styling hooks, tests). An app that\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\n var id = mode === \"fullscreen\"\n ? \"agentproto-display-mode\"\n : \"agentproto-display-mode-\" + mode;\n if (!document.getElementById(id)) btn.id = id;\n btn.setAttribute(\"data-agentproto-dm\", mode);\n btn.style.display = \"none\";\n btn.addEventListener(\"click\", function () {\n var cur = get();\n // Same rule both copies had: anything that isn't inline collapses\n // back to inline, so the button is always an escape hatch.\n var inPanel = cur === \"fullscreen\" || cur === \"pip\";\n request(inPanel ? \"inline\" : mode)[\"catch\"](function () {});\n });\n return btn;\n }\n\n function mountToggle(el, mode) {\n ensureStyle();\n mode = mode || \"fullscreen\";\n var btn = makeButton(mode);\n if (el) el.appendChild(btn);\n else ensureBar().appendChild(btn);\n buttons.push(btn);\n syncButton(btn);\n return btn;\n }\n\n function onChange(cb) {\n if (typeof cb !== \"function\") return function () {};\n listeners.push(cb);\n try { cb(get(), ctx); } catch (_) {}\n return function () {\n var i = listeners.indexOf(cb);\n if (i >= 0) listeners.splice(i, 1);\n };\n }\n\n ready(function () {\n ensureStyle();\n // Only the first installer in a document owns the floating bar: a\n // built-in panel served standalone gets the injected window.McpApp\n // bridge's copy AND its own, and two stacked buttons is a bug.\n if (autoMount && !document.getElementById(BAR_ID)) {\n ensureBar();\n mountToggle(null, \"pip\");\n mountToggle(null, \"fullscreen\");\n }\n sync();\n });\n\n // The diagnostic both copies had, now on BOTH paths: it used to exist\n // only in the built-in panel bridge, so an installed app debugged in\n // Claude Desktop (which advertises no fullscreen, hence no button)\n // printed nothing at all and left the user with no way to tell \"the\n // host refuses\" from \"the toggle is broken\".\n function observe(next) {\n console.log('[mcp-app] displayMode=', next && next.displayMode,\n 'availableDisplayModes=', next && next.availableDisplayModes);\n setContext(next);\n // What ELSE does this host send? Keys only, once \u2014 a host context can\n // carry toolInfo (the instantiating tool call's arguments) and other\n // host-private fields, so the names are the diagnostic and the values\n // stay out of the console.\n if (!loggedKeys && next && typeof next === \"object\") {\n loggedKeys = true;\n var keys = [];\n for (var k in ctx) {\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\n }\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\n }\n }\n\n if (typeof api.onHostContext === \"function\") api.onHostContext(observe);\n else if (typeof api.getHostContext === \"function\") observe(api.getHostContext());\n\n return {\n get: get,\n available: available,\n request: request,\n onChange: onChange,\n mountToggle: mountToggle\n };\n };\n})();\nvar displayMode = window.AgentprotoUI.installDisplayMode({\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n requestDisplayMode: requestDisplayMode\n});</script>\n <script>(function() {\n \"use strict\";\n function esc(value) {\n return value.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n }\n function liveSessionUrl(origin, sessionId) {\n return `${origin}/apps/@agentproto/live-session/ui?sessionId=${encodeURIComponent(sessionId)}`;\n }\n function shortRange(baseSha, headSha) {\n if (!baseSha || !headSha) return \"\u2014\";\n return `${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`;\n }\n function ageOf(createdAt, now = Date.now()) {\n const ms = now - new Date(createdAt).getTime();\n if (!Number.isFinite(ms) || ms < 0) return \"just now\";\n const mins = Math.floor(ms / 6e4);\n if (mins < 1) return \"just now\";\n if (mins < 60) return `${mins}m`;\n const hours = Math.floor(mins / 60);\n if (hours < 24) return `${hours}h`;\n return `${Math.floor(hours / 24)}d`;\n }\n function verdictChip(status) {\n return `<span class=\"chip chip-verdict v-${esc(status)}\">${esc(status)}</span>`;\n }\n const LANE_STATUS_LABEL = {\n pass: \"pass\",\n fail: \"fail\",\n skipped: \"skipped\",\n timeout: \"timeout\"\n };\n function laneStatusChip(status) {\n return `<span class=\"chip chip-lane l-${esc(status)}\">${esc(LANE_STATUS_LABEL[status])}</span>`;\n }\n function severityTag(severity) {\n return `<span class=\"tag sev-${esc(severity)}\">${esc(severity)}</span>`;\n }\n function requesterLabel(row) {\n const r = row.requester;\n if (!r) return \"\u2014\";\n const id = r.sessionId ? esc(r.sessionId) : \"\";\n const author = r.gitAuthor ? esc(r.gitAuthor.name) : \"\";\n if (id && author) return `${id} <span class=\"muted\">(${author})</span>`;\n return id || author || \"\u2014\";\n }\n function prLabel(row) {\n if (!row.pr) return \"\u2014\";\n const state = row.prState ? ` <span class=\"tag pr-${esc(row.prState)}\">${esc(row.prState)}</span>` : \"\";\n return `<a href=\"${esc(row.pr.url)}\" target=\"_blank\" rel=\"noopener\">#${row.pr.number}</a>${state}`;\n }\n function renderRow(row) {\n const status = row.status ?? row.verdict ?? \"incomplete\";\n const flags = [];\n if (row.dirty) flags.push('<span class=\"tag t-dirty\">dirty</span>');\n if (row.cached) flags.push('<span class=\"tag t-cached\">cached</span>');\n if (row.status !== \"running\") {\n flags.push(row.signed ? '<span class=\"tag t-signed\">signed</span>' : '<span class=\"tag t-unsigned\">unsigned</span>');\n }\n return `<tr class=\"row\" data-runid=\"${esc(row.runId)}\" tabindex=\"0\"><td>${verdictChip(status)}</td><td class=\"mono\">${esc(shortRange(row.baseSha, row.headSha))}</td><td>${esc(row.binding ?? \"\u2014\")}</td><td class=\"mono\">${esc(row.repoRemote ?? \"\u2014\")}</td><td>${requesterLabel(row)}</td><td>${prLabel(row)}</td><td>${flags.join(\" \") || \"\u2014\"}</td><td>${esc(ageOf(row.createdAt))}</td></tr>`;\n }\n function renderList(rows2) {\n if (rows2.length === 0) return '<div class=\"empty\">No reviews recorded yet.</div>';\n const header = \"<tr><th>Verdict</th><th>Range</th><th>Binding</th><th>Repo</th><th>Requester</th><th>PR</th><th>Flags</th><th>Age</th></tr>\";\n return `<table class=\"list\"><thead>${header}</thead><tbody>${rows2.map(renderRow).join(\"\")}</tbody></table>`;\n }\n function findingLine(f) {\n const loc = f.file ? `${esc(f.file)}${f.line ? `:${f.line}` : \"\"}` : \"\";\n return `<details class=\"finding\"><summary>${severityTag(f.severity)} ${esc(f.title)}${loc ? ` <span class=\"mono muted\">${loc}</span>` : \"\"}</summary><div class=\"finding-detail\">${esc(f.detail)}</div></details>`;\n }\n function renderLaneDetail(lane, rubrics = []) {\n const parts = [];\n parts.push(`<div class=\"lane-hdr\">`);\n parts.push(`<span class=\"lane-id\">${esc(lane.id)}</span>`);\n parts.push(laneStatusChip(lane.status));\n parts.push(`<span class=\"tag ${lane.blocking ? \"t-blocking\" : \"t-advisory\"}\">${lane.blocking ? \"blocking\" : \"advisory\"}</span>`);\n if (lane.durationMs !== void 0) parts.push(`<span class=\"muted\">${(lane.durationMs / 1e3).toFixed(1)}s</span>`);\n parts.push(`</div>`);\n if (lane.error) parts.push(`<div class=\"lane-error\">${esc(lane.error)}</div>`);\n if (lane.sessionId || lane.preset || lane.model || lane.fallbacks?.length) {\n const rubric = rubrics.find((r) => r.check === lane.id);\n const bits = [];\n if (lane.model) bits.push(`model: ${esc(lane.model)}`);\n if (lane.preset) bits.push(`preset: ${esc(lane.preset)}`);\n if (lane.fallbacks?.length) {\n bits.push(`after unavailable: ${lane.fallbacks.map((f) => `${esc(f.preset)} (${esc(f.error)})`).join(\", \")}`);\n }\n if (rubric) bits.push(`rubric: <span class=\"mono\">${esc(rubric.sha256.slice(0, 12))}</span>`);\n if (lane.sessionId) {\n bits.push(\n `reviewer: <button class=\"sess-link\" data-session-id=\"${esc(lane.sessionId)}\">${esc(lane.sessionId)}</button>`\n );\n }\n parts.push(`<div class=\"lane-agent-meta\">${bits.join(\" \u00B7 \")}</div>`);\n }\n if (lane.findings.length > 0) parts.push(`<div class=\"findings\">${lane.findings.map(findingLine).join(\"\")}</div>`);\n return `<div class=\"lane\">${parts.join(\"\")}</div>`;\n }\n function renderDetail(detail2) {\n const status = detail2.status === \"done\" ? detail2.verdict ?? \"incomplete\" : detail2.status;\n const header = [`<div class=\"detail-hdr\">`, verdictChip(status), `<span class=\"mono\">${esc(detail2.runId)}</span>`];\n if (detail2.binding) header.push(`<span class=\"tag\">${esc(detail2.binding)}</span>`);\n if (detail2.cached) header.push('<span class=\"tag t-cached\">cached</span>');\n if (detail2.status === \"done\" && detail2.attestation) {\n const sig = detail2.attestation.attestor?.signature;\n header.push(\n sig ? `<span class=\"tag t-signed\" title=\"${esc(sig.keyFingerprint)}\">signed: ${esc(sig.principal)}</span>` : '<span class=\"tag t-unsigned\">unsigned</span>'\n );\n }\n header.push(`</div>`);\n if (detail2.error) header.push(`<div class=\"detail-error\">${esc(detail2.error)}</div>`);\n if (detail2.supersededBy) header.push(`<div class=\"muted\">superseded by ${esc(detail2.supersededBy)}</div>`);\n const fullLanes = detail2.attestation?.lanes;\n const rubrics = detail2.attestation?.rubrics ?? [];\n let body;\n if (fullLanes) {\n body = fullLanes.map((l) => renderLaneDetail(l, rubrics)).join(\"\");\n } else if (detail2.lanes) {\n body = detail2.lanes.map((l) => `<div class=\"lane\"><div class=\"lane-hdr\"><span class=\"lane-id\">${esc(l.id)}</span>${laneStatusChip(l.status)}</div></div>`).join(\"\");\n } else {\n body = \"\";\n }\n return `<div class=\"detail\">${header.join(\"\")}${body}</div>`;\n }\n const POLL_RUNNING_MS = 4e3;\n const POLL_IDLE_MS = 15e3;\n function getEl(id) {\n const el = document.getElementById(id);\n if (!el) throw new Error(`review panel: missing #${id}`);\n return el;\n }\n let rows = [];\n let selectedRunId = null;\n let detail = null;\n let pollTimer = null;\n let loadInFlight = false;\n function boot() {\n const listPaneEl = getEl(\"list-pane\");\n const detailPaneEl = getEl(\"detail-pane\");\n const detailBodyEl = getEl(\"detail-body\");\n const statusbarEl = getEl(\"statusbar\");\n const cancelBtn = getEl(\"cancel-btn\");\n const rerunBtn = getEl(\"rerun-btn\");\n const prBtn = getEl(\"pr-btn\");\n const exportBtn = getEl(\"export-btn\");\n function setStatus(msg) {\n statusbarEl.textContent = msg;\n }\n function findRow(runId) {\n return rows.find((r) => r.runId === runId);\n }\n function renderListPane() {\n listPaneEl.innerHTML = renderList(rows);\n if (!selectedRunId) return;\n listPaneEl.querySelectorAll(\"tr[data-runid]\").forEach((tr) => {\n if (tr.dataset.runid === selectedRunId) tr.classList.add(\"selected\");\n });\n }\n function renderDetailPane() {\n if (!selectedRunId || !detail) {\n detailPaneEl.classList.remove(\"open\");\n return;\n }\n detailPaneEl.classList.add(\"open\");\n detailBodyEl.innerHTML = renderDetail(detail);\n const running = detail.status === \"running\";\n cancelBtn.disabled = !running;\n cancelBtn.style.display = running ? \"\" : \"none\";\n detailBodyEl.querySelectorAll(\".sess-link\").forEach((btn) => {\n btn.addEventListener(\"click\", () => openSession(btn.dataset.sessionId ?? \"\"));\n });\n }\n function openSession(sessionId) {\n if (!sessionId) return;\n const origin = window.location.origin && window.location.origin !== \"null\" ? window.location.origin : \"\";\n const url = liveSessionUrl(origin, sessionId);\n const caps = getHostCapabilities();\n if (caps?.openLinks) {\n openLink(url).catch(() => window.open(url, \"_blank\"));\n } else {\n window.open(url, \"_blank\");\n }\n }\n function loadList() {\n if (loadInFlight) return Promise.resolve();\n loadInFlight = true;\n return callTool(\"review_ledger\", { includeRunning: true }).then((result) => {\n rows = result.attestations;\n renderListPane();\n const runningCount = rows.filter((r) => r.status === \"running\").length;\n setStatus(`${result.total} review${result.total === 1 ? \"\" : \"s\"} \u00B7 ${runningCount} running \u00B7 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\n if (selectedRunId && findRow(selectedRunId)) return loadDetail(selectedRunId);\n return void 0;\n }).catch((e) => setStatus(`Error: ${e.message}`)).finally(() => {\n loadInFlight = false;\n });\n }\n function loadDetail(runId) {\n return callTool(\"review_status\", { runId }).then((result) => {\n detail = result;\n renderDetailPane();\n }).catch((e) => setStatus(`Error loading detail: ${e.message}`));\n }\n function openDetail(runId) {\n selectedRunId = runId;\n renderListPane();\n void loadDetail(runId);\n }\n function closeDetail() {\n selectedRunId = null;\n detail = null;\n renderListPane();\n renderDetailPane();\n }\n listPaneEl.addEventListener(\"click\", (evt) => {\n if (!(evt.target instanceof Element)) return;\n const tr = evt.target.closest(\"tr[data-runid]\");\n if (!(tr instanceof HTMLElement)) return;\n const runId = tr.dataset.runid;\n if (runId) openDetail(runId);\n });\n listPaneEl.addEventListener(\"keydown\", (evt) => {\n if (evt.key !== \"Enter\" && evt.key !== \" \") return;\n if (!(evt.target instanceof Element)) return;\n const tr = evt.target.closest(\"tr[data-runid]\");\n if (!(tr instanceof HTMLElement)) return;\n evt.preventDefault();\n const runId = tr.dataset.runid;\n if (runId) openDetail(runId);\n });\n getEl(\"detail-close\").addEventListener(\"click\", closeDetail);\n getEl(\"refresh-btn\").addEventListener(\"click\", () => {\n void loadList();\n });\n cancelBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_cancel\", { runId: selectedRunId }).then(() => {\n setStatus(\"Cancelled.\");\n return loadList();\n }).catch((e) => setStatus(`Cancel failed: ${e.message}`));\n });\n rerunBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n const row = findRow(selectedRunId);\n const cwd = row?.cwd;\n if (!cwd) {\n setStatus(\"Re-run failed: this row has no known checkout path.\");\n return;\n }\n callTool(\"review_run\", {\n cwd,\n ...row?.binding ? { binding: row.binding } : {},\n nocache: true,\n wait: false,\n supersede: true\n }).then(() => {\n setStatus(\"Re-run started.\");\n return loadList();\n }).catch((e) => setStatus(`Re-run failed: ${e.message}`));\n });\n prBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_pr\", { runId: selectedRunId }).then(() => {\n setStatus(\"Fetched PR status.\");\n return loadList();\n }).catch((e) => setStatus(`Fetch PR status failed: ${e.message}`));\n });\n exportBtn.addEventListener(\"click\", () => {\n if (!selectedRunId) return;\n callTool(\"review_export\", { runId: selectedRunId }).then((result) => {\n setStatus(result.path ? `Exported to ${result.path}` : \"Exported.\");\n }).catch((e) => setStatus(`Export failed: ${e.message}`));\n });\n function doPoll() {\n if (pollTimer) clearTimeout(pollTimer);\n loadList().finally(() => {\n const anyRunning = rows.some((r) => r.status === \"running\");\n pollTimer = setTimeout(doPoll, anyRunning ? POLL_RUNNING_MS : POLL_IDLE_MS);\n });\n }\n initBridge().then(() => loadList()).then(() => {\n pollTimer = setTimeout(doPoll, POLL_RUNNING_MS);\n }).catch((e) => setStatus(`Bridge error: ${e.message}`));\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", boot);\n } else {\n boot();\n }\n})();</script>\n <style rel=\"stylesheet\" crossorigin>/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Semantic tokens + runtime mapping sheets \u2014 same scheme as work-board's\n style.css (see that file's header doc for the two-mapping-sheet\n rationale): every rule below reads ONLY the --rp-* custom properties,\n each resolving through a live --vscode-* var() first, then the\n agentproto brand fallback.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n:root {\n --rp-bg: var(--vscode-editor-background, #f5f6f3);\n --rp-surface: var(--vscode-sideBar-background, #ffffff);\n --rp-ink: var(--vscode-editor-foreground, #171a1e);\n --rp-ink-muted: var(--vscode-descriptionForeground, #5c6470);\n --rp-border: var(--vscode-panel-border, #dde0da);\n --rp-accent: var(--vscode-button-background, #0f62fe);\n\n --rp-pass: var(--vscode-charts-green, #3edc81);\n --rp-block: var(--vscode-charts-red, #fa4d56);\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\n --rp-running: var(--vscode-charts-blue, #4589ff);\n --rp-cancelled: var(--vscode-descriptionForeground, #5c6470);\n --rp-failed: var(--vscode-charts-red, #fa4d56);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --rp-bg: var(--vscode-editor-background, #101214);\n --rp-surface: var(--vscode-sideBar-background, #16191d);\n --rp-ink: var(--vscode-editor-foreground, #e8eaed);\n --rp-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\n --rp-border: var(--vscode-panel-border, #272c33);\n --rp-accent: var(--vscode-button-background, #4589ff);\n\n --rp-pass: var(--vscode-charts-green, #3edc81);\n --rp-block: var(--vscode-charts-red, #fa4d56);\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\n --rp-running: var(--vscode-charts-blue, #4589ff);\n --rp-cancelled: var(--vscode-descriptionForeground, #99a2ad);\n --rp-failed: var(--vscode-charts-red, #fa4d56);\n }\n}\n\n/* \u2500\u2500 Base \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n* { box-sizing: border-box; margin: 0; padding: 0; }\n\nhtml, body {\n height: 100%;\n font-family: \"IBM Plex Sans\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n font-size: 13px;\n background: var(--rp-bg);\n color: var(--rp-ink);\n overflow: hidden;\n}\n\n.mono {\n font-family: \"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, Monaco, \"Courier New\", monospace;\n}\n.muted { color: var(--rp-ink-muted); }\n\n#app { display: flex; flex-direction: column; height: 100%; }\n\n#toolbar {\n padding: 8px 12px;\n border-bottom: 1px solid var(--rp-border);\n display: flex;\n align-items: center;\n gap: 8px;\n background: var(--rp-surface);\n flex-shrink: 0;\n}\n#toolbar-label { font-size: 11px; color: var(--rp-ink-muted); }\n.tbtn {\n background: var(--rp-bg);\n border: 1px solid var(--rp-border);\n color: var(--rp-ink);\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n}\n.tbtn:hover { background: var(--rp-border); }\n.tbtn:disabled { opacity: 0.5; cursor: default; }\n.tbtn.danger { border-color: var(--rp-block); color: var(--rp-block); }\n\n#main { flex: 1; overflow: hidden; display: flex; }\n#list-pane { flex: 1; overflow-y: auto; }\n#detail-pane {\n width: 420px;\n min-width: 320px;\n border-left: 1px solid var(--rp-border);\n overflow-y: auto;\n display: none;\n flex-direction: column;\n}\n#detail-pane.open { display: flex; }\n#detail-close { align-self: flex-end; margin: 6px; }\n\ntable.list { width: 100%; border-collapse: collapse; }\ntable.list th {\n text-align: left;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--rp-ink-muted);\n padding: 6px 8px;\n border-bottom: 1px solid var(--rp-border);\n position: sticky;\n top: 0;\n background: var(--rp-surface);\n}\ntable.list td { padding: 6px 8px; border-bottom: 1px solid var(--rp-border); font-size: 12px; vertical-align: middle; }\ntr.row { cursor: pointer; }\ntr.row:hover, tr.row:focus { background: color-mix(in srgb, var(--rp-accent) 8%, transparent); outline: none; }\ntr.row.selected { background: color-mix(in srgb, var(--rp-accent) 14%, transparent); }\n\n.empty { padding: 24px 12px; color: var(--rp-ink-muted); font-size: 12px; text-align: center; }\n\n.chip { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }\n.chip-verdict.v-pass { background: color-mix(in srgb, var(--rp-pass) 18%, transparent); color: var(--rp-pass); }\n.chip-verdict.v-block { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\n.chip-verdict.v-incomplete { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\n.chip-verdict.v-running { background: color-mix(in srgb, var(--rp-running) 18%, transparent); color: var(--rp-running); }\n.chip-verdict.v-cancelled { background: color-mix(in srgb, var(--rp-cancelled) 15%, transparent); color: var(--rp-cancelled); }\n.chip-verdict.v-failed { background: color-mix(in srgb, var(--rp-failed) 18%, transparent); color: var(--rp-failed); }\n\n.chip-lane.l-pass { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\n.chip-lane.l-fail { background: color-mix(in srgb, var(--rp-block) 15%, transparent); color: var(--rp-block); }\n.chip-lane.l-skipped { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.chip-lane.l-timeout { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\n\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\n.t-dirty { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\n.t-cached { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.t-signed { background: color-mix(in srgb, var(--rp-pass) 12%, transparent); color: var(--rp-pass); }\n.t-unsigned { background: color-mix(in srgb, var(--rp-ink-muted) 10%, transparent); color: var(--rp-ink-muted); }\n.t-blocking { background: color-mix(in srgb, var(--rp-block) 12%, transparent); color: var(--rp-block); }\n.t-advisory { background: color-mix(in srgb, var(--rp-ink-muted) 12%, transparent); color: var(--rp-ink-muted); }\n.pr-open { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\n.pr-merged { background: color-mix(in srgb, var(--rp-accent) 15%, transparent); color: var(--rp-accent); }\n.pr-closed { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n.sev-high { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\n.sev-medium { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\n.sev-low { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\n\n.detail { padding: 10px; display: flex; flex-direction: column; gap: 10px; }\n.detail-hdr { display: flex; align-items: center; gap: 8px; }\n.detail-error { color: var(--rp-block); font-size: 12px; word-break: break-word; }\n.detail-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 10px 10px; }\n\n.lane {\n border: 1px solid var(--rp-border);\n border-radius: 6px;\n padding: 8px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.lane-hdr { display: flex; align-items: center; gap: 6px; }\n.lane-id { font-weight: 600; font-size: 12px; }\n.lane-error { color: var(--rp-block); font-size: 11px; word-break: break-word; }\n.lane-agent-meta { font-size: 11px; color: var(--rp-ink-muted); }\n.sess-link {\n background: none;\n border: none;\n color: var(--rp-accent);\n cursor: pointer;\n font: inherit;\n padding: 0;\n text-decoration: underline;\n}\n\n.findings { display: flex; flex-direction: column; gap: 4px; }\n.finding summary { cursor: pointer; font-size: 11px; list-style: none; }\n.finding summary::-webkit-details-marker { display: none; }\n.finding-detail { font-size: 11px; color: var(--rp-ink-muted); padding: 4px 0 0 4px; white-space: pre-wrap; word-break: break-word; }\n\n#statusbar {\n padding: 4px 12px;\n font-size: 11px;\n color: var(--rp-ink-muted);\n border-top: 1px solid var(--rp-border);\n background: var(--rp-surface);\n flex-shrink: 0;\n}</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"toolbar\">\n <span id=\"toolbar-label\">Reviews</span>\n <button class=\"tbtn\" id=\"refresh-btn\" title=\"Refresh\">&#8635;</button>\n </div>\n <div id=\"main\">\n <div id=\"list-pane\"></div>\n <div id=\"detail-pane\">\n <button class=\"tbtn\" id=\"detail-close\">&times; Close</button>\n <div id=\"detail-body\"></div>\n <div class=\"detail-actions\">\n <button class=\"tbtn danger\" id=\"cancel-btn\">Cancel</button>\n <button class=\"tbtn\" id=\"rerun-btn\">Re-run fresh</button>\n <button class=\"tbtn\" id=\"pr-btn\">Fetch PR status</button>\n <button class=\"tbtn\" id=\"export-btn\">Export</button>\n </div>\n </div>\n </div>\n <div id=\"statusbar\">Connecting to bridge&#8230;</div>\n</div>\n</body>\n</html>\n";
59
59
  //# sourceMappingURL=panel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/review-panel/panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAIH;;;kDAGkD;AAClD,eAAO,MAAM,mBAAmB,6BAA6B,CAAA;AAE7D;oFACoF;AACpF,eAAO,MAAM,oBAAoB,uBAAuB,CAAA;AAExD;;;;6EAI6E;AAC7E,eAAO,MAAM,qBAAqB,0GAOxB,CAAA;AAEV,eAAO,MAAM,iBAAiB,yqyCAA8B,CAAA"}
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/review-panel/panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAIH;;;kDAGkD;AAClD,eAAO,MAAM,mBAAmB,6BAA6B,CAAA;AAE7D;oFACoF;AACpF,eAAO,MAAM,oBAAoB,uBAAuB,CAAA;AAExD;;;;6EAI6E;AAC7E,eAAO,MAAM,qBAAqB,0GAOxB,CAAA;AAEV,eAAO,MAAM,iBAAiB,01zCAA8B,CAAA"}