@agentproto/apps 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
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"]}
@@ -0,0 +1,3 @@
1
+ import type { AgentEntry } from "@agentproto/app-kit";
2
+ export declare const narrator: AgentEntry;
3
+ //# sourceMappingURL=narrator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"narrator.d.ts","sourceRoot":"","sources":["../../../src/session-viewer/agents/narrator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAErD,eAAO,MAAM,QAAQ,EAAE,UA8BtB,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `session-viewer` — a single-agent agentproto app for reading daemon
3
+ * sessions as a clean conversation.
4
+ *
5
+ * The panel (`ui.ts`) picks a session — live or finished — from
6
+ * `session_list` and renders its transcript via `conversation_read`: user/
7
+ * assistant turns, collapsible tool calls, timestamps, a metadata header.
8
+ * Read-only, no spawn/kill/resume action. The bundled `session-narrator`
9
+ * agent is a complementary plain-English narrator over the same
10
+ * `conversation_read` data — usable standalone via `app_run`, independent
11
+ * of the panel.
12
+ */
13
+ import { type AppHandle } from "@agentproto/app-kit";
14
+ import { narrator } from "./agents/narrator.js";
15
+ import { narrateSession } from "./workflows/narrate-session.js";
16
+ export declare const sessionViewer: AppHandle;
17
+ export { narrator, narrateSession };
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session-viewer/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAG/D,eAAO,MAAM,aAAa,EAAE,SAY1B,CAAA;AAEF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Session Viewer — a self-contained MCP App panel for `session-viewer`.
3
+ *
4
+ * Talks to the daemon only through the `window.McpApp.connect()` bridge
5
+ * (never a direct `fetch`): every action round-trips through the app's own
6
+ * `app_tool_call` gateway tool, `{ appId, tool, args }` — same convention as
7
+ * `media-viewer`/`mail-triage`. Read-only by design: `session_list` picks a
8
+ * session (live or finished), `conversation_read` renders it as a clean
9
+ * conversation — no spawn/kill/resume action lives here.
10
+ *
11
+ * Live sessions are polled (`conversation_read` has no push channel over
12
+ * this bridge — see the PR's feasibility note on a real event/PTY stream);
13
+ * a finished session is fetched once and left alone.
14
+ */
15
+ export declare const SESSION_VIEWER_TOOLS: readonly ["session_list", "conversation_read"];
16
+ export declare const SESSION_VIEWER_HTML: string;
17
+ //# sourceMappingURL=ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/session-viewer/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,oBAAoB,gDAAiD,CAAA;AAIlF,eAAO,MAAM,mBAAmB,QA4cxB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export declare const narrateSession: Readonly<import("@agentproto/workflow").WorkflowDefinition>;
2
+ //# sourceMappingURL=narrate-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"narrate-session.d.ts","sourceRoot":"","sources":["../../../src/session-viewer/workflows/narrate-session.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,6DAkBzB,CAAA"}