@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.
package/dist/bin/sync.mjs CHANGED
@@ -1566,13 +1566,1320 @@ var mediaViewer = defineApp({
1566
1566
  tools: [...MEDIA_VIEWER_TOOLS]
1567
1567
  }
1568
1568
  });
1569
+ var MANAGER_LABEL = "Sessions Manager";
1570
+ var manager = {
1571
+ agent: defineAgent({
1572
+ schema: "agent/v1",
1573
+ id: "@agentproto/sessions-manager",
1574
+ description: "Durable coordinator over the daemon's sessions \u2014 routes questions to the right session, re-prompts stalled work, restarts dead sessions, reports status.",
1575
+ model: "claude-sonnet-5",
1576
+ boundaries: [
1577
+ "Never kill a session unless the human explicitly asks for that session by id/label",
1578
+ "Never merge PRs or bypass a repo's declared review flow from a supervised session",
1579
+ "Prefer re-prompting an existing session over spawning a duplicate for the same task"
1580
+ ],
1581
+ tools: [
1582
+ "session_list",
1583
+ "conversation_read",
1584
+ "agent_prompt",
1585
+ "session_restart",
1586
+ "agent_start",
1587
+ "session_events_poll"
1588
+ ]
1589
+ }),
1590
+ body: [
1591
+ "You are the manager of this agentproto daemon's sessions: the human talks to you, and you talk",
1592
+ "to the other sessions. You are durable \u2014 the same session across many turns \u2014 so keep a working",
1593
+ "memory of who is doing what and don't re-derive it from scratch every turn.",
1594
+ "",
1595
+ "What you do:",
1596
+ "- Locate: given a task or question, find the relevant session (`session_list`, labels, cwd,",
1597
+ " recency) and read its transcript (`conversation_read`) before answering \u2014 never guess status.",
1598
+ "- Drive: re-prompt an idle/parked session onto its task with `agent_prompt`; restart a",
1599
+ " killed/exited session with `session_restart` when its work is clearly unfinished.",
1600
+ "- Spawn: only when no existing session owns the task \u2014 check for duplicates first.",
1601
+ "- Report: answer with short, concrete status (who, doing what, since when, blocked on what).",
1602
+ "",
1603
+ "Boundaries:",
1604
+ "- Never kill a session unless the human explicitly asks for that session.",
1605
+ "- A session's own repo conventions (review flow, merge policy) always win \u2014 you coordinate,",
1606
+ " you don't override.",
1607
+ "- If a session awaits a human decision, surface the question to the human instead of answering",
1608
+ " in their place."
1609
+ ].join("\n")
1610
+ };
1611
+ var WATCHDOG_LABEL = "Session Watchdog";
1612
+ var WATCHDOG_TICK_LABEL = "watchdog-tick";
1613
+ var WATCHDOG_TICK_SCHEDULE = "*/5 * * * *";
1614
+ var WATCHDOG_TICK_PROMPT = "Run your maintenance check now.";
1615
+ var watchdog = {
1616
+ agent: defineAgent({
1617
+ schema: "agent/v1",
1618
+ id: "@agentproto/session-watchdog",
1619
+ description: "Durable daemon-health watchdog \u2014 classifies sessions (needs-you, stalled, duplicated, abandonable), corrects flags, archives dead rows. Never kills or spawns.",
1620
+ model: "claude-sonnet-5",
1621
+ boundaries: [
1622
+ "NEVER kill a live session, message/interrupt another session's conversation, or spawn new sessions",
1623
+ "Housekeeping only on terminal-status sessions (exited/killed/error) \u2014 never touches a live one",
1624
+ "Report only what's NEW since the last tick \u2014 no padding, no repeats unless something got worse"
1625
+ ],
1626
+ tools: [
1627
+ "session_list",
1628
+ "session_events_poll",
1629
+ "session_flag_status",
1630
+ "session_archive",
1631
+ "session_gc"
1632
+ ]
1633
+ }),
1634
+ body: [
1635
+ "You are the Session Watchdog for this agentproto daemon. You run on a recurring cron tick",
1636
+ "(~every 5 minutes) as the SAME durable session, re-prompted each time \u2014 you retain memory of",
1637
+ "what you already flagged, so don't repeat an alert unless something got meaningfully worse or new.",
1638
+ "",
1639
+ "Each tick:",
1640
+ "1. Call `session_list` to see current session state (status, awaitingInput/awaitingQuestion,",
1641
+ " stalledSinceMs, busy, blockedOn, childrenBusy).",
1642
+ "2. Call `session_events_poll` for events since your last tick (stalled, exited, awaiting-input)",
1643
+ " to catch anything between ticks.",
1644
+ "3. Classify:",
1645
+ " - Needs you: `awaitingInput: true` with a real unanswered question, older than ~10 minutes.",
1646
+ " - Stalled: busy but silent well past normal turn latency, no legitimate blockedOn excuse.",
1647
+ " - Possibly duplicated: two or more live sessions share workspace/cwd and a highly similar",
1648
+ " recent prompt/label \u2014 use judgment, not exact string matching.",
1649
+ " - Looks done/abandonable: idle/parked with no plausible remaining purpose.",
1650
+ "4. Correct misclassifications when confident: if `session_flag_status` is available and you can",
1651
+ " tell a session is actually waiting on a human (or isn't, despite being flagged), call it with",
1652
+ " a clear `reason`. If that tool is unknown on this daemon build, skip silently.",
1653
+ "5. Autonomous housekeeping (reversible, so no need to ask): `session_archive` or `session_gc` on",
1654
+ " sessions already terminal-status (exited/killed/error) and old. Never touches a live session.",
1655
+ "6. NEVER: kill a live session, message/interrupt another session's conversation, or spawn new",
1656
+ " sessions. You only have these five tools on purpose \u2014 if you'd want another one, report the",
1657
+ " suggestion instead of working around it.",
1658
+ "7. Report only what's NEW since last tick \u2014 one line per finding. If nothing new, reply with a",
1659
+ ` short "nothing new" \u2014 don't pad it.`,
1660
+ "",
1661
+ "You are not a general coding assistant across ticks \u2014 stay scoped to this watchdog role."
1662
+ ].join("\n")
1663
+ };
1664
+
1665
+ // src/ops-panel/ui.ts
1666
+ var OPS_PANEL_TOOLS = [
1667
+ "daemon_health",
1668
+ "session_list",
1669
+ "agent_start",
1670
+ "agent_prompt",
1671
+ "agent_kill",
1672
+ "session_restart",
1673
+ "session_archive",
1674
+ "session_gc",
1675
+ "cron_list",
1676
+ "cron_create",
1677
+ "cron_delete",
1678
+ "cron_run",
1679
+ "worktree_gc"
1680
+ ];
1681
+ var APP_ID3 = "@agentproto/ops-panel";
1682
+ var SPECIALS = [
1683
+ {
1684
+ key: "watchdog",
1685
+ label: WATCHDOG_LABEL,
1686
+ tagline: "Health ticks every 5 min \u2014 flags, archives, never kills.",
1687
+ prompt: watchdog.body ?? "",
1688
+ cron: {
1689
+ label: WATCHDOG_TICK_LABEL,
1690
+ schedule: WATCHDOG_TICK_SCHEDULE,
1691
+ prompt: WATCHDOG_TICK_PROMPT
1692
+ }
1693
+ },
1694
+ {
1695
+ key: "manager",
1696
+ label: MANAGER_LABEL,
1697
+ tagline: "You talk to it, it drives the other sessions.",
1698
+ prompt: manager.body ?? "",
1699
+ cron: null
1700
+ }
1701
+ ];
1702
+ var OPS_PANEL_HTML = `<!DOCTYPE html>
1703
+ <html lang="en">
1704
+ <head>
1705
+ <meta charset="UTF-8">
1706
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1707
+ <title>Ops Panel</title>
1708
+ <style>
1709
+ *{box-sizing:border-box;margin:0;padding:0}
1710
+ :root{
1711
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
1712
+ --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;
1713
+ --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
1714
+ }
1715
+ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}
1716
+ #app{display:flex;flex-direction:column;height:100%;overflow:hidden}
1717
+
1718
+ #topbar{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}
1719
+ #topbar h1{font-size:13px;font-weight:700;letter-spacing:.02em}
1720
+ .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}
1721
+ .chip .d{width:7px;height:7px;border-radius:50%;background:var(--text3);flex:none}
1722
+ .chip.ok .d{background:var(--green)}
1723
+ .chip.bad .d{background:var(--red)}
1724
+ #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}
1725
+ #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}
1726
+ #refresh-btn:hover{color:var(--text);background:var(--bg3)}
1727
+
1728
+ #body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:16px}
1729
+ .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}
1730
+ .sect h2 .count{font-weight:600;color:var(--text3);text-transform:none;letter-spacing:0}
1731
+
1732
+ #specials{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}
1733
+ .card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:12px}
1734
+ .card .ct{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:700}
1735
+ .card .tagline{font-size:11px;color:var(--text2);margin-top:3px}
1736
+ .card .meta{font-size:10.5px;color:var(--text3);margin-top:7px;display:flex;flex-wrap:wrap;gap:5px 10px}
1737
+ .card .cronline{font-size:10.5px;margin-top:7px;display:flex;align-items:center;gap:6px;color:var(--text2)}
1738
+ .card .actions{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
1739
+
1740
+ .badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em}
1741
+ .b-run{background:rgba(63,185,80,.15);color:var(--green)}
1742
+ .b-busy{background:rgba(63,185,80,.25);color:var(--green)}
1743
+ .b-wait{background:rgba(88,166,255,.15);color:var(--blue)}
1744
+ .b-idle{background:rgba(139,148,158,.12);color:var(--text2)}
1745
+ .b-end{background:rgba(139,148,158,.1);color:var(--text3)}
1746
+ .b-err{background:rgba(248,81,73,.15);color:var(--red)}
1747
+ .b-warn{background:rgba(210,153,34,.15);color:var(--yellow)}
1748
+
1749
+ button.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}
1750
+ button.act:hover{border-color:var(--text3)}
1751
+ button.act.primary{background:rgba(88,166,255,.15);border-color:rgba(88,166,255,.4);color:var(--blue)}
1752
+ button.act.danger{color:var(--red)}
1753
+ button.act.danger:hover{border-color:var(--red)}
1754
+ button.act.confirm{background:rgba(248,81,73,.2);border-color:var(--red);color:var(--red)}
1755
+ button.act:disabled{opacity:.45;cursor:default}
1756
+
1757
+ table{width:100%;border-collapse:collapse}
1758
+ th{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)}
1759
+ td{font-size:11.5px;padding:6px 8px;border-bottom:1px solid var(--bg3);vertical-align:middle}
1760
+ tr:hover td{background:var(--bg2)}
1761
+ td .sn{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:260px;display:block}
1762
+ td .sid{font-size:10px;color:var(--text3);font-family:Menlo,Monaco,monospace}
1763
+ .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)}
1764
+ td.actions{white-space:nowrap;text-align:right}
1765
+ td.actions button{margin-left:4px}
1766
+
1767
+ .hk{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}
1768
+ .hk .card .nums{display:flex;gap:14px;margin-top:8px}
1769
+ .hk .num{display:flex;flex-direction:column}
1770
+ .hk .num b{font-size:17px;font-weight:700}
1771
+ .hk .num span{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.04em}
1772
+
1773
+ #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}
1774
+ #toast.err{border-color:var(--red);color:var(--red)}
1775
+ .empty{color:var(--text3);font-size:11.5px;padding:8px}
1776
+ </style>
1777
+ </head>
1778
+ <body>
1779
+ <div id="app">
1780
+ <div id="topbar">
1781
+ <h1>Agentproto Ops</h1>
1782
+ <span class="chip" id="daemon-chip"><span class="d"></span><span id="daemon-txt">daemon&hellip;</span></span>
1783
+ <select id="ws-select"><option value="">all workspaces</option></select>
1784
+ <button id="refresh-btn" title="Refresh" type="button">&#8635; refresh</button>
1785
+ </div>
1786
+ <div id="body">
1787
+ <div class="sect">
1788
+ <h2>Special sessions</h2>
1789
+ <div id="specials"></div>
1790
+ </div>
1791
+ <div class="sect">
1792
+ <h2>Sessions <span class="count" id="sess-count"></span></h2>
1793
+ <div id="sessions"></div>
1794
+ </div>
1795
+ <div class="sect">
1796
+ <h2>Housekeeping</h2>
1797
+ <div class="hk" id="housekeeping"></div>
1798
+ </div>
1799
+ <div class="sect">
1800
+ <h2>Crons <span class="count" id="cron-count"></span></h2>
1801
+ <div id="crons"></div>
1802
+ </div>
1803
+ </div>
1804
+ </div>
1805
+ <div id="toast"></div>
1806
+ <script>
1807
+ var callTool = null;
1808
+ var APP_ID = ${JSON.stringify(APP_ID3)};
1809
+ var SPECIALS = ${JSON.stringify(SPECIALS)};
1810
+
1811
+ // app_tool_call wraps its dispatch result at least once (its own text-result
1812
+ // body is the JSON-stringified inner MCP tool result); peel back through
1813
+ // nested {content:[{type:"text",text:...}]} shells until the payload stops
1814
+ // looking like one \u2014 same convention as session-viewer.
1815
+ function unwrapText(result) {
1816
+ var cur = result;
1817
+ for (var i = 0; i < 4; i++) {
1818
+ if (typeof cur === "string") {
1819
+ try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }
1820
+ }
1821
+ if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === "string") {
1822
+ cur = cur.content[0].text;
1823
+ continue;
1824
+ }
1825
+ break;
1826
+ }
1827
+ return cur;
1828
+ }
1829
+
1830
+ function callApp(tool, args) {
1831
+ return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);
1832
+ }
1833
+
1834
+ function escHtml(s) {
1835
+ return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1836
+ }
1837
+
1838
+ function fmtRelative(iso) {
1839
+ if (!iso) return "";
1840
+ var ms = Date.now() - new Date(iso).getTime();
1841
+ if (!isFinite(ms)) return "";
1842
+ var s = Math.round(ms / 1000);
1843
+ if (s < 5) return "just now";
1844
+ if (s < 60) return s + "s ago";
1845
+ var m = Math.round(s / 60);
1846
+ if (m < 60) return m + "m ago";
1847
+ var h = Math.round(m / 60);
1848
+ if (h < 24) return h + "h ago";
1849
+ return Math.round(h / 24) + "d ago";
1850
+ }
1851
+
1852
+ var toastTimer = null;
1853
+ function toast(msg, isErr) {
1854
+ var el = document.getElementById("toast");
1855
+ el.textContent = msg;
1856
+ el.className = isErr ? "err" : "";
1857
+ el.style.display = "block";
1858
+ if (toastTimer) clearTimeout(toastTimer);
1859
+ toastTimer = setTimeout(function () { el.style.display = "none"; }, isErr ? 6000 : 3000);
1860
+ }
1861
+
1862
+ // Two-click confirm: first click arms the button, second within 4s fires.
1863
+ var armed = {};
1864
+ function confirmClick(key, btn, fn) {
1865
+ if (armed[key]) {
1866
+ clearTimeout(armed[key].t);
1867
+ delete armed[key];
1868
+ fn();
1869
+ return;
1870
+ }
1871
+ var prev = btn.textContent;
1872
+ btn.classList.add("confirm");
1873
+ btn.textContent = "sure?";
1874
+ armed[key] = { t: setTimeout(function () {
1875
+ delete armed[key];
1876
+ btn.classList.remove("confirm");
1877
+ btn.textContent = prev;
1878
+ }, 4000) };
1879
+ }
1880
+
1881
+ // ============================================================
1882
+ // state
1883
+ // ============================================================
1884
+
1885
+ var sessions = [];
1886
+ var crons = [];
1887
+ var wsFilter = "";
1888
+ var pollTimer = null;
1889
+ var busyActions = false;
1890
+
1891
+ function isLive(s) { return !!s && (s.status === "running" || s.status === "starting"); }
1892
+
1893
+ function displayBadge(s) {
1894
+ if (s.status === "running") {
1895
+ if (s.busy) return { label: "working", cls: "b-busy" };
1896
+ if (s.awaitingInput) return { label: "waiting", cls: "b-wait" };
1897
+ return { label: "idle", cls: "b-idle" };
1898
+ }
1899
+ if (s.status === "starting") return { label: "starting", cls: "b-wait" };
1900
+ if (s.status === "error" || s.status === "killed") return { label: s.status, cls: "b-err" };
1901
+ return { label: s.status || "exited", cls: "b-end" };
1902
+ }
1903
+
1904
+ function labelOf(s) {
1905
+ return s.label || s.title || s.name || (s.command ? String(s.command).split("/").pop() : null) || s.id;
1906
+ }
1907
+
1908
+ function specialSessionFor(sp) {
1909
+ var best = null;
1910
+ for (var i = 0; i < sessions.length; i++) {
1911
+ var s = sessions[i];
1912
+ if ((s.label || s.title) !== sp.label) continue;
1913
+ if (!best) { best = s; continue; }
1914
+ // Prefer a live one; among equals prefer the newest.
1915
+ if (isLive(s) && !isLive(best)) { best = s; continue; }
1916
+ if (isLive(s) === isLive(best) && String(s.startedAt) > String(best.startedAt)) best = s;
1917
+ }
1918
+ return best;
1919
+ }
1920
+
1921
+ function cronByLabel(label) {
1922
+ var hits = [];
1923
+ for (var i = 0; i < crons.length; i++) if (crons[i].label === label) hits.push(crons[i]);
1924
+ return hits;
1925
+ }
1926
+
1927
+ // ============================================================
1928
+ // data loading
1929
+ // ============================================================
1930
+
1931
+ function loadAll() {
1932
+ return Promise.all([
1933
+ callApp("session_list", { kind: "all" }).then(function (d) {
1934
+ sessions = (d && d.sessions) || (Array.isArray(d) ? d : []);
1935
+ }),
1936
+ callApp("cron_list", {}).then(function (d) {
1937
+ crons = Array.isArray(d) ? d : (d && d.jobs) || [];
1938
+ }).catch(function () { crons = []; }),
1939
+ ]).then(function () {
1940
+ renderWorkspaces();
1941
+ renderSpecials();
1942
+ renderSessions();
1943
+ renderCrons();
1944
+ updateHousekeepingNums();
1945
+ });
1946
+ }
1947
+
1948
+ function loadHealth() {
1949
+ return callApp("daemon_health", {}).then(function (d) {
1950
+ var chip = document.getElementById("daemon-chip");
1951
+ chip.className = "chip ok";
1952
+ var v = (d && (d.version || (d.build && d.build.sha) || d.status)) || "ok";
1953
+ document.getElementById("daemon-txt").textContent = "daemon " + String(v).slice(0, 16);
1954
+ }).catch(function (e) {
1955
+ var chip = document.getElementById("daemon-chip");
1956
+ chip.className = "chip bad";
1957
+ document.getElementById("daemon-txt").textContent = "daemon unreachable";
1958
+ });
1959
+ }
1960
+
1961
+ // Housekeeping card is built ONCE (so the worktree plan text and armed
1962
+ // confirm buttons survive the 5s poll); only the counters re-render \u2014 see
1963
+ // updateHousekeepingNums. The worktree plan is a separate dry-run call, on
1964
+ // demand only (it walks git state \u2014 not something to hammer on every poll).
1965
+ function buildHousekeeping() {
1966
+ var el = document.getElementById("housekeeping");
1967
+ el.innerHTML =
1968
+ '<div class="card"><div class="ct">Sessions GC</div>'
1969
+ + '<div class="nums" id="gc-nums"></div>'
1970
+ + '<div class="actions">'
1971
+ + '<button class="act" id="gc-archive">Archive &gt;7d</button>'
1972
+ + '<button class="act danger" id="gc-forget">Forget &gt;30d</button>'
1973
+ + '</div></div>'
1974
+ + '<div class="card"><div class="ct">Worktrees</div>'
1975
+ + '<div class="meta" id="wt-plan">No plan yet \u2014 run a dry run.</div>'
1976
+ + '<div class="actions">'
1977
+ + '<button class="act" id="wt-dry">Dry run</button>'
1978
+ + '<button class="act danger" id="wt-apply" disabled>Reclaim now</button>'
1979
+ + '</div></div>';
1980
+
1981
+ document.getElementById("gc-archive").addEventListener("click", function () {
1982
+ runAction("session_gc", { olderThanDays: 7 }, "GC: archived old terminal sessions");
1983
+ });
1984
+ var forgetBtn = document.getElementById("gc-forget");
1985
+ forgetBtn.addEventListener("click", function () {
1986
+ confirmClick("gc-forget", forgetBtn, function () {
1987
+ runAction("session_gc", { olderThanDays: 30, forget: true }, "GC: forgot >30d terminal sessions");
1988
+ });
1989
+ });
1990
+ document.getElementById("wt-dry").addEventListener("click", function () { worktreePlan(false); });
1991
+ var applyBtn = document.getElementById("wt-apply");
1992
+ applyBtn.addEventListener("click", function () {
1993
+ confirmClick("wt-apply", applyBtn, function () { worktreePlan(true); });
1994
+ });
1995
+ updateHousekeepingNums();
1996
+ }
1997
+
1998
+ function updateHousekeepingNums() {
1999
+ var el = document.getElementById("gc-nums");
2000
+ if (!el) return;
2001
+ var terminal = 0, live = 0;
2002
+ for (var i = 0; i < sessions.length; i++) {
2003
+ if (isLive(sessions[i])) live++;
2004
+ else terminal++;
2005
+ }
2006
+ el.innerHTML =
2007
+ '<div class="num"><b>' + live + '</b><span>live</span></div>'
2008
+ + '<div class="num"><b>' + terminal + '</b><span>terminal</span></div>';
2009
+ }
2010
+
2011
+ function worktreePlan(apply) {
2012
+ var el = document.getElementById("wt-plan");
2013
+ el.textContent = apply ? "reclaiming\\u2026" : "planning\\u2026";
2014
+ callApp("worktree_gc", apply ? { apply: true } : {}).then(function (d) {
2015
+ var entries = (d && (d.plan || d.entries || d.worktrees)) || [];
2016
+ var counts = { reclaim: 0, salvage: 0, hold: 0 };
2017
+ for (var i = 0; i < entries.length; i++) {
2018
+ var c = entries[i].classification || entries[i].class || entries[i].action;
2019
+ if (counts[c] != null) counts[c]++;
2020
+ }
2021
+ el.textContent = (apply ? "applied \u2014 " : "dry run \u2014 ")
2022
+ + counts.reclaim + " reclaim / " + counts.salvage + " salvage / " + counts.hold + " hold"
2023
+ + (entries.length === 0 ? " (no linked worktrees)" : "");
2024
+ document.getElementById("wt-apply").disabled = counts.reclaim === 0 || apply;
2025
+ if (apply) toast("Worktree GC applied");
2026
+ }).catch(function (e) {
2027
+ el.textContent = "worktree_gc: " + e.message;
2028
+ });
2029
+ }
2030
+
2031
+ // ============================================================
2032
+ // rendering
2033
+ // ============================================================
2034
+
2035
+ function renderWorkspaces() {
2036
+ var sel = document.getElementById("ws-select");
2037
+ var seen = {};
2038
+ var slugs = [];
2039
+ for (var i = 0; i < sessions.length; i++) {
2040
+ var w = sessions[i].workspaceSlug;
2041
+ if (w && !seen[w]) { seen[w] = true; slugs.push(w); }
2042
+ }
2043
+ slugs.sort();
2044
+ var cur = wsFilter;
2045
+ var html = '<option value="">all workspaces</option>';
2046
+ for (var j = 0; j < slugs.length; j++) {
2047
+ html += '<option value="' + escHtml(slugs[j]) + '"' + (slugs[j] === cur ? " selected" : "") + '>' + escHtml(slugs[j]) + "</option>";
2048
+ }
2049
+ sel.innerHTML = html;
2050
+ }
2051
+
2052
+ function renderSpecials() {
2053
+ var el = document.getElementById("specials");
2054
+ var html = "";
2055
+ for (var i = 0; i < SPECIALS.length; i++) {
2056
+ var sp = SPECIALS[i];
2057
+ var s = specialSessionFor(sp);
2058
+ var badge = s ? displayBadge(s) : { label: "not running", cls: "b-end" };
2059
+ var meta = "";
2060
+ if (s) {
2061
+ meta += "<span>" + escHtml(s.id) + "</span>";
2062
+ if (s.startedAt) meta += "<span>started " + escHtml(fmtRelative(s.startedAt)) + "</span>";
2063
+ if (s.costUsd != null) meta += "<span>$" + Number(s.costUsd).toFixed(2) + "</span>";
2064
+ }
2065
+ var cronline = "";
2066
+ if (sp.cron) {
2067
+ var jobs = cronByLabel(sp.cron.label);
2068
+ if (jobs.length === 0) {
2069
+ cronline = '<span class="badge b-end">no cron</span> tick not scheduled';
2070
+ } else {
2071
+ var job = jobs[0];
2072
+ var ok = !job.lastResult || job.lastResult.ok;
2073
+ var pointsAt = job.action && job.action.sessionId;
2074
+ var stale = s && isLive(s) && pointsAt && pointsAt !== s.id;
2075
+ cronline = '<span class="badge ' + (job.active ? (ok && !stale ? "b-run" : "b-warn") : "b-end") + '">'
2076
+ + (job.active ? (ok && !stale ? "tick ok" : (stale ? "tick stale" : "tick failing")) : "tick off") + "</span> "
2077
+ + escHtml(job.schedule) + (pointsAt ? " &rarr; " + escHtml(pointsAt) : "")
2078
+ + (!ok && job.lastResult && job.lastResult.summary ? " &middot; " + escHtml(String(job.lastResult.summary).slice(0, 60)) : "");
2079
+ }
2080
+ }
2081
+ var actions = "";
2082
+ if (!s || !isLive(s)) {
2083
+ actions += '<button class="act primary" data-sp="' + sp.key + '" data-do="launch">Launch</button>';
2084
+ if (s) actions += '<button class="act" data-sp="' + sp.key + '" data-do="restart">Restart last</button>';
2085
+ } else {
2086
+ if (sp.cron) {
2087
+ actions += '<button class="act" data-sp="' + sp.key + '" data-do="tick">Tick now</button>';
2088
+ actions += '<button class="act" data-sp="' + sp.key + '" data-do="fixcron">Re-wire cron</button>';
2089
+ } else {
2090
+ actions += '<button class="act" data-sp="' + sp.key + '" data-do="tick">Ping</button>';
2091
+ }
2092
+ actions += '<button class="act danger" data-sp="' + sp.key + '" data-do="kill">Kill</button>';
2093
+ }
2094
+ html += '<div class="card">'
2095
+ + '<div class="ct">' + escHtml(sp.label) + ' <span class="badge ' + badge.cls + '">' + escHtml(badge.label) + "</span></div>"
2096
+ + '<div class="tagline">' + escHtml(sp.tagline) + "</div>"
2097
+ + (meta ? '<div class="meta">' + meta + "</div>" : "")
2098
+ + (cronline ? '<div class="cronline">' + cronline + "</div>" : "")
2099
+ + '<div class="actions">' + actions + "</div>"
2100
+ + "</div>";
2101
+ }
2102
+ el.innerHTML = html;
2103
+ var btns = el.querySelectorAll("button[data-sp]");
2104
+ for (var k = 0; k < btns.length; k++) {
2105
+ (function (btn) {
2106
+ btn.addEventListener("click", function () { specialAction(btn.getAttribute("data-sp"), btn.getAttribute("data-do"), btn); });
2107
+ })(btns[k]);
2108
+ }
2109
+ }
2110
+
2111
+ function renderSessions() {
2112
+ var el = document.getElementById("sessions");
2113
+ var rows = [];
2114
+ for (var i = 0; i < sessions.length; i++) {
2115
+ var s = sessions[i];
2116
+ if (wsFilter && s.workspaceSlug !== wsFilter) continue;
2117
+ rows.push(s);
2118
+ }
2119
+ rows.sort(function (a, b) {
2120
+ var la = isLive(a) ? 1 : 0, lb = isLive(b) ? 1 : 0;
2121
+ if (la !== lb) return lb - la;
2122
+ return String(b.startedAt || "").localeCompare(String(a.startedAt || ""));
2123
+ });
2124
+ var liveCount = 0;
2125
+ for (var c = 0; c < rows.length; c++) if (isLive(rows[c])) liveCount++;
2126
+ document.getElementById("sess-count").textContent = liveCount + " live / " + rows.length + " shown";
2127
+ if (rows.length === 0) {
2128
+ el.innerHTML = '<div class="empty">No sessions' + (wsFilter ? " in " + escHtml(wsFilter) : "") + ".</div>";
2129
+ return;
2130
+ }
2131
+ 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>";
2132
+ var shown = rows.slice(0, 80);
2133
+ for (var j = 0; j < shown.length; j++) {
2134
+ var s2 = shown[j];
2135
+ var db = displayBadge(s2);
2136
+ var acts = "";
2137
+ if (isLive(s2)) {
2138
+ acts += '<button class="act danger" data-id="' + escHtml(s2.id) + '" data-do="kill">Kill</button>';
2139
+ } else {
2140
+ acts += '<button class="act" data-id="' + escHtml(s2.id) + '" data-do="restart">Restart</button>';
2141
+ if (!s2.archived) acts += '<button class="act" data-id="' + escHtml(s2.id) + '" data-do="archive">Archive</button>';
2142
+ }
2143
+ html += "<tr>"
2144
+ + '<td><span class="sn" title="' + escHtml(labelOf(s2)) + '">' + escHtml(labelOf(s2)) + '</span><span class="sid">' + escHtml(s2.id) + "</span></td>"
2145
+ + "<td>" + (s2.workspaceSlug ? '<span class="ws-tag">' + escHtml(s2.workspaceSlug) + "</span>" : "") + "</td>"
2146
+ + '<td><span class="badge ' + db.cls + '">' + escHtml(db.label) + "</span></td>"
2147
+ + "<td>" + escHtml(fmtRelative(s2.startedAt)) + "</td>"
2148
+ + "<td>" + (s2.costUsd != null ? "$" + Number(s2.costUsd).toFixed(2) : "") + "</td>"
2149
+ + '<td class="actions">' + acts + "</td>"
2150
+ + "</tr>";
2151
+ }
2152
+ html += "</tbody></table>";
2153
+ if (rows.length > shown.length) html += '<div class="empty">&hellip; ' + (rows.length - shown.length) + " more (narrow with the workspace filter)</div>";
2154
+ el.innerHTML = html;
2155
+ var btns = el.querySelectorAll("button[data-id]");
2156
+ for (var k = 0; k < btns.length; k++) {
2157
+ (function (btn) {
2158
+ btn.addEventListener("click", function () { sessionAction(btn.getAttribute("data-id"), btn.getAttribute("data-do"), btn); });
2159
+ })(btns[k]);
2160
+ }
2161
+ }
2162
+
2163
+ function renderCrons() {
2164
+ var el = document.getElementById("crons");
2165
+ document.getElementById("cron-count").textContent = crons.length ? crons.length + " jobs" : "";
2166
+ if (crons.length === 0) {
2167
+ el.innerHTML = '<div class="empty">No cron jobs.</div>';
2168
+ return;
2169
+ }
2170
+ 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>";
2171
+ for (var i = 0; i < crons.length; i++) {
2172
+ var c = crons[i];
2173
+ var last = c.lastResult == null ? "" : (c.lastResult.ok ? "ok" : "failed");
2174
+ var lastCls = c.lastResult == null ? "b-end" : (c.lastResult.ok ? "b-run" : "b-err");
2175
+ html += "<tr>"
2176
+ + '<td><span class="sn">' + escHtml(c.label || c.id) + "</span></td>"
2177
+ + "<td>" + escHtml(c.schedule) + "</td>"
2178
+ + '<td><span class="badge ' + (c.active ? "b-run" : "b-end") + '">' + (c.active ? "active" : "off") + "</span></td>"
2179
+ + "<td>" + (last ? '<span class="badge ' + lastCls + '" title="' + escHtml((c.lastResult && c.lastResult.summary) || "") + '">' + last + "</span>" : "") + "</td>"
2180
+ + "<td>" + escHtml(c.nextRunAt ? fmtRelative(c.nextRunAt).replace(" ago", "") : "") + "</td>"
2181
+ + '<td class="actions">'
2182
+ + '<button class="act" data-cron="' + escHtml(c.id) + '" data-do="run">Run</button>'
2183
+ + '<button class="act danger" data-cron="' + escHtml(c.id) + '" data-do="delete">Delete</button>'
2184
+ + "</td></tr>";
2185
+ }
2186
+ html += "</tbody></table>";
2187
+ el.innerHTML = html;
2188
+ var btns = el.querySelectorAll("button[data-cron]");
2189
+ for (var k = 0; k < btns.length; k++) {
2190
+ (function (btn) {
2191
+ btn.addEventListener("click", function () { cronAction(btn.getAttribute("data-cron"), btn.getAttribute("data-do"), btn); });
2192
+ })(btns[k]);
2193
+ }
2194
+ }
2195
+
2196
+ // ============================================================
2197
+ // actions
2198
+ // ============================================================
2199
+
2200
+ function runAction(tool, args, okMsg) {
2201
+ if (busyActions) return Promise.resolve();
2202
+ busyActions = true;
2203
+ return callApp(tool, args).then(function (d) {
2204
+ busyActions = false;
2205
+ if (okMsg) toast(okMsg);
2206
+ return loadAll().then(function () { return d; });
2207
+ }).catch(function (e) {
2208
+ busyActions = false;
2209
+ toast(tool + ": " + e.message, true);
2210
+ throw e;
2211
+ });
2212
+ }
2213
+
2214
+ function findSpecial(key) {
2215
+ for (var i = 0; i < SPECIALS.length; i++) if (SPECIALS[i].key === key) return SPECIALS[i];
2216
+ return null;
2217
+ }
2218
+
2219
+ function rewireTickCron(sp, sessionId) {
2220
+ if (!sp.cron) return Promise.resolve();
2221
+ var stale = cronByLabel(sp.cron.label);
2222
+ var chain = Promise.resolve();
2223
+ for (var i = 0; i < stale.length; i++) {
2224
+ (function (jobId) {
2225
+ chain = chain.then(function () { return callApp("cron_delete", { jobId: jobId }); });
2226
+ })(stale[i].id);
2227
+ }
2228
+ return chain.then(function () {
2229
+ return callApp("cron_create", {
2230
+ schedule: sp.cron.schedule,
2231
+ recurring: true,
2232
+ label: sp.cron.label,
2233
+ action: { kind: "prompt-session", sessionId: sessionId, prompt: sp.cron.prompt },
2234
+ });
2235
+ });
2236
+ }
2237
+
2238
+ function specialAction(key, what, btn) {
2239
+ var sp = findSpecial(key);
2240
+ if (!sp) return;
2241
+ var s = specialSessionFor(sp);
2242
+ if (what === "launch") {
2243
+ btn.disabled = true;
2244
+ callApp("agent_start", {
2245
+ adapter: "claude-code",
2246
+ label: sp.label,
2247
+ prompt: sp.prompt,
2248
+ keepAlive: true,
2249
+ }).then(function (d) {
2250
+ var id = d && d.id;
2251
+ toast(sp.label + " launched" + (id ? " (" + id + ")" : ""));
2252
+ return id && sp.cron ? rewireTickCron(sp, id) : null;
2253
+ }).then(function () { return loadAll(); })
2254
+ .catch(function (e) { toast("launch: " + e.message, true); btn.disabled = false; });
2255
+ return;
2256
+ }
2257
+ if (what === "restart" && s) {
2258
+ btn.disabled = true;
2259
+ callApp("session_restart", { idOrName: s.id }).then(function (d) {
2260
+ var id = (d && d.id) || s.id;
2261
+ toast(sp.label + " restarted");
2262
+ return sp.cron ? rewireTickCron(sp, id) : null;
2263
+ }).then(function () { return loadAll(); })
2264
+ .catch(function (e) { toast("restart: " + e.message, true); btn.disabled = false; });
2265
+ return;
2266
+ }
2267
+ if (what === "tick" && s) {
2268
+ var prompt = sp.cron ? sp.cron.prompt : "Status check: report where things stand, briefly.";
2269
+ runAction("agent_prompt", { sessionId: s.id, prompt: prompt }, sp.label + " prompted");
2270
+ return;
2271
+ }
2272
+ if (what === "fixcron" && s) {
2273
+ btn.disabled = true;
2274
+ rewireTickCron(sp, s.id).then(function () {
2275
+ toast("Cron re-wired to " + s.id);
2276
+ return loadAll();
2277
+ }).catch(function (e) { toast("cron: " + e.message, true); btn.disabled = false; });
2278
+ return;
2279
+ }
2280
+ if (what === "kill" && s) {
2281
+ confirmClick("kill-" + s.id, btn, function () {
2282
+ runAction("agent_kill", { sessionId: s.id }, sp.label + " killed");
2283
+ });
2284
+ }
2285
+ }
2286
+
2287
+ function sessionAction(id, what, btn) {
2288
+ if (what === "kill") {
2289
+ confirmClick("kill-" + id, btn, function () {
2290
+ runAction("agent_kill", { sessionId: id }, "Killed " + id);
2291
+ });
2292
+ return;
2293
+ }
2294
+ if (what === "restart") { runAction("session_restart", { idOrName: id }, "Restarted " + id); return; }
2295
+ if (what === "archive") { runAction("session_archive", { idOrName: id }, "Archived " + id); }
2296
+ }
2297
+
2298
+ function cronAction(jobId, what, btn) {
2299
+ if (what === "run") { runAction("cron_run", { jobId: jobId }, "Cron fired"); return; }
2300
+ if (what === "delete") {
2301
+ confirmClick("cron-" + jobId, btn, function () {
2302
+ runAction("cron_delete", { jobId: jobId }, "Cron deleted");
2303
+ });
2304
+ }
2305
+ }
2306
+
2307
+ // ============================================================
2308
+ // boot
2309
+ // ============================================================
2310
+
2311
+ document.getElementById("refresh-btn").addEventListener("click", function () { loadHealth(); loadAll(); });
2312
+ document.getElementById("ws-select").addEventListener("change", function (e) {
2313
+ wsFilter = e.target.value;
2314
+ renderSessions();
2315
+ });
2316
+
2317
+ window.McpApp.connect()
2318
+ .then(function (bridge) {
2319
+ callTool = bridge.callTool;
2320
+ buildHousekeeping();
2321
+ loadHealth();
2322
+ return loadAll();
2323
+ })
2324
+ .then(function () {
2325
+ pollTimer = setInterval(loadAll, 5000);
2326
+ })
2327
+ .catch(function (err) {
2328
+ document.getElementById("body").innerHTML =
2329
+ '<div class="empty">Standalone mode \\u2014 no host bridge (' + escHtml(err.message) + ")</div>";
2330
+ });
2331
+ </script>
2332
+ </body>
2333
+ </html>`;
2334
+
2335
+ // src/ops-panel/index.ts
2336
+ var opsPanel = defineApp({
2337
+ id: "@agentproto/ops-panel",
2338
+ name: "Ops Panel",
2339
+ version: "0.1.0",
2340
+ description: "Daemon ops cockpit \u2014 watchdog & manager sessions, workspace-filtered session list, GC and cron housekeeping.",
2341
+ agents: [watchdog, manager],
2342
+ ui: {
2343
+ html: OPS_PANEL_HTML,
2344
+ title: "Ops Panel",
2345
+ tools: [...OPS_PANEL_TOOLS]
2346
+ }
2347
+ });
2348
+ var narrator = {
2349
+ agent: defineAgent({
2350
+ schema: "agent/v1",
2351
+ id: "@agentproto/session-narrator",
2352
+ description: "Reads a daemon session's transcript and narrates it in plain English \u2014 what happened, what was decided, what's left.",
2353
+ model: "claude-sonnet-5",
2354
+ boundaries: [
2355
+ "Read-only \u2014 never spawn, kill, resume, or write to any session",
2356
+ "Narrate only what the transcript actually shows, never speculate about intent"
2357
+ ],
2358
+ tools: ["conversation_read", "session_list"],
2359
+ workflows: [{ ref: "narrate-session" }]
2360
+ }),
2361
+ body: [
2362
+ "You narrate a daemon session's conversation for a human who doesn't want to read the raw transcript.",
2363
+ "",
2364
+ 'Given a session id or name, call `conversation_read` with `format: "json"` to get the',
2365
+ "structured turn list (`meta` + `messages`, each `{ role, text, toolCalls?, ts }`). If the",
2366
+ "session id is ambiguous or unknown, call `session_list` first to resolve it by label.",
2367
+ "",
2368
+ "Write a short plain-English narration:",
2369
+ "- What the user asked for.",
2370
+ "- What the assistant did, grouped into a few natural beats (not a line-by-line tool log).",
2371
+ "- Any open question, decision point, or unfinished step at the end of the transcript.",
2372
+ "",
2373
+ "Skip tool-call plumbing details unless they're the point (e.g. a failing command, a real",
2374
+ "decision). If `conversation.conversation` comes back null, say plainly that this session has",
2375
+ "no readable conversation (e.g. a bare shell PTY) instead of guessing."
2376
+ ].join("\n")
2377
+ };
2378
+ var narrateSession = defineWorkflow({
2379
+ id: "narrate-session",
2380
+ name: "Narrate Session",
2381
+ description: "Read a daemon session's transcript and narrate it in plain English.",
2382
+ version: "0.1.0",
2383
+ inputs: {},
2384
+ outputs: {},
2385
+ steps: [
2386
+ {
2387
+ id: "narrate",
2388
+ kind: "agent",
2389
+ agent: { ref: "@agentproto/session-narrator" },
2390
+ prompt: "Narrate the session named in the run input (or the most recently started session if none was given): what the user asked for, what the assistant did, and what \u2014 if anything \u2014 is still open at the end of the transcript."
2391
+ }
2392
+ ]
2393
+ });
2394
+
2395
+ // src/session-viewer/ui.ts
2396
+ var SESSION_VIEWER_TOOLS = ["session_list", "conversation_read"];
2397
+ var APP_ID4 = "@agentproto/session-viewer";
2398
+ var SESSION_VIEWER_HTML = `<!DOCTYPE html>
2399
+ <html lang="en">
2400
+ <head>
2401
+ <meta charset="UTF-8">
2402
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2403
+ <title>Session Viewer</title>
2404
+ <style>
2405
+ *{box-sizing:border-box;margin:0;padding:0}
2406
+ :root{
2407
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
2408
+ --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;
2409
+ --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
2410
+ }
2411
+ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}
2412
+ #app{display:flex;height:100%;overflow:hidden}
2413
+
2414
+ /* sidebar */
2415
+ #sidebar{width:250px;min-width:190px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
2416
+ #sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
2417
+ #sidebar-hdr h1{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;flex:1}
2418
+ #archived-toggle{display:flex;align-items:center;gap:5px;font-size:10.5px;color:var(--text2);padding:6px 12px;border-bottom:1px solid var(--border);cursor:pointer;user-select:none}
2419
+ #archived-toggle input{cursor:pointer}
2420
+ #refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:14px;line-height:1;padding:2px 4px;border-radius:4px;font-family:inherit}
2421
+ #refresh-btn:hover{color:var(--text);background:var(--bg3)}
2422
+ #session-list{flex:1;overflow-y:auto;padding:4px 0}
2423
+ .si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}
2424
+ .si:hover{background:var(--bg3)}
2425
+ .si.active{background:var(--bg3);border-left-color:var(--blue)}
2426
+ .si .sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2427
+ .si .sm{font-size:10.5px;color:var(--text2);margin-top:3px;display:flex;gap:5px;align-items:center;flex-wrap:wrap}
2428
+ .badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em}
2429
+ .b-run{background:rgba(63,185,80,.15);color:var(--green)}
2430
+ .b-wait{background:rgba(88,166,255,.15);color:var(--blue)}
2431
+ .b-idle{background:rgba(139,148,158,.12);color:var(--text2)}
2432
+ .b-end{background:rgba(139,148,158,.1);color:var(--text3)}
2433
+ .b-err{background:rgba(248,81,73,.15);color:var(--red)}
2434
+ #empty-sidebar{padding:14px 12px;color:var(--text2);font-size:11.5px}
2435
+
2436
+ /* main */
2437
+ #main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
2438
+ #header{padding:12px 18px;border-bottom:1px solid var(--border);background:var(--bg2);flex-shrink:0}
2439
+ #header .htitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}
2440
+ .pulse{width:7px;height:7px;border-radius:50%;background:var(--green);flex:none;box-shadow:0 0 0 0 rgba(63,185,80,.4);animation:pulse 2s infinite}
2441
+ @keyframes pulse{70%{box-shadow:0 0 0 6px rgba(63,185,80,0)}100%{box-shadow:0 0 0 0 rgba(63,185,80,0)}}
2442
+ #header .hfacts{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
2443
+ .fact{font-size:10.5px;font-weight:600;background:var(--bg3);border:1px solid var(--border);color:var(--text2);padding:3px 9px;border-radius:999px}
2444
+ #empty-main,#loading-main,#error-main{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;text-align:center;padding:24px}
2445
+ #empty-main .ico,#loading-main .ico{font-size:30px}
2446
+ #error-main{color:var(--red)}
2447
+
2448
+ /* transcript */
2449
+ #transcript{flex:1;overflow-y:auto;padding:14px 18px;display:flex;flex-direction:column;gap:10px}
2450
+ .msg{display:flex;flex-direction:column;max-width:78%}
2451
+ .msg.role-user{align-self:flex-end;align-items:flex-end}
2452
+ .msg.role-assistant,.msg.role-tool{align-self:flex-start;align-items:flex-start}
2453
+ .msg.role-system{align-self:center;max-width:90%}
2454
+ .bubble{border-radius:10px;padding:9px 12px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
2455
+ .role-user .bubble{background:var(--blue);color:#04101c;border-bottom-right-radius:3px}
2456
+ .role-assistant .bubble{background:var(--bg2);border:1px solid var(--border);border-bottom-left-radius:3px}
2457
+ .role-system .bubble{background:transparent;border:1px dashed var(--border);color:var(--text2);font-size:11.5px;text-align:center}
2458
+ .mrow{display:flex;align-items:center;gap:6px;margin-bottom:3px}
2459
+ .mrole{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text3)}
2460
+ .mtime{font-size:10px;color:var(--text3);font-variant-numeric:tabular-nums}
2461
+
2462
+ .chip{margin-top:6px;border:1px solid var(--border);border-radius:8px;background:var(--bg3);overflow:hidden;cursor:pointer;width:100%}
2463
+ .chip .ch{padding:6px 10px;font-size:11.5px;display:flex;align-items:center;gap:7px;color:var(--text2)}
2464
+ .chip .ch .ci{color:var(--yellow);flex:none}
2465
+ .chip.tool-result .ch .ci{color:var(--purple)}
2466
+ .chip .ch .cname{font-weight:600;color:var(--text)}
2467
+ .chip .ch .cprev{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text3)}
2468
+ .chip .ch .cchev{flex:none;transition:transform .12s;color:var(--text3);font-size:10px}
2469
+ .chip.open .ch .cchev{transform:rotate(90deg)}
2470
+ .chip pre{display:none;margin:0;border-top:1px solid var(--border);padding:9px 11px;font-family:Menlo,Monaco,'Courier New',monospace;font-size:11px;line-height:1.5;color:var(--text2);white-space:pre-wrap;word-break:break-word;max-height:320px;overflow:auto}
2471
+ .chip.open pre{display:block}
2472
+ .reasoning{margin-bottom:6px;font-size:11.5px;font-style:italic;color:var(--text2);border-left:2px solid var(--border);padding-left:8px}
2473
+
2474
+ #statusbar{padding:5px 18px;font-size:10.5px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0;display:flex;justify-content:space-between}
2475
+ </style>
2476
+ </head>
2477
+ <body>
2478
+ <div id="app">
2479
+ <div id="sidebar">
2480
+ <div id="sidebar-hdr">
2481
+ <h1>Sessions</h1>
2482
+ <button id="refresh-btn" title="Refresh" type="button">&#8635;</button>
2483
+ </div>
2484
+ <label id="archived-toggle"><input type="checkbox" id="archived-check"> show archived</label>
2485
+ <div id="session-list"></div>
2486
+ </div>
2487
+ <div id="main">
2488
+ <div id="header" style="display:none"></div>
2489
+ <div id="empty-main"><div class="ico">&#128172;</div><div>Pick a session to read its conversation</div></div>
2490
+ <div id="transcript" style="display:none"></div>
2491
+ <div id="statusbar" style="display:none">
2492
+ <span id="status-left"></span>
2493
+ <span id="status-right"></span>
2494
+ </div>
2495
+ </div>
2496
+ </div>
2497
+ <script>
2498
+ var callTool = null;
2499
+ var APP_ID = ${JSON.stringify(APP_ID4)};
2500
+
2501
+ // app_tool_call wraps its dispatch result at least once (its own text-result
2502
+ // body is the JSON-stringified inner MCP tool result); peel back through
2503
+ // nested {content:[{type:"text",text:...}]} shells until the payload stops
2504
+ // looking like one \u2014 works whichever layer the host's bridge already
2505
+ // unwrapped. With conversation_read's format:"json" this resolves straight
2506
+ // to the { meta, messages } object; with session_list, straight to
2507
+ // { sessions: [...] }.
2508
+ function unwrapText(result) {
2509
+ var cur = result;
2510
+ for (var i = 0; i < 4; i++) {
2511
+ if (typeof cur === "string") {
2512
+ try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }
2513
+ }
2514
+ if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === "string") {
2515
+ cur = cur.content[0].text;
2516
+ continue;
2517
+ }
2518
+ break;
2519
+ }
2520
+ return cur;
2521
+ }
2522
+
2523
+ function callApp(tool, args) {
2524
+ return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);
2525
+ }
2526
+
2527
+ function escHtml(s) {
2528
+ return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2529
+ }
2530
+
2531
+ function fmtTime(ts) {
2532
+ if (!ts) return "";
2533
+ try { return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); }
2534
+ catch (e) { return ""; }
2535
+ }
2536
+
2537
+ function fmtClock(iso) {
2538
+ if (!iso) return "";
2539
+ try { return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }
2540
+ catch (e) { return ""; }
2541
+ }
2542
+
2543
+ function fmtRelative(iso) {
2544
+ if (!iso) return "";
2545
+ var ms = Date.now() - new Date(iso).getTime();
2546
+ if (!isFinite(ms)) return "";
2547
+ var s = Math.round(ms / 1000);
2548
+ if (s < 5) return "just now";
2549
+ if (s < 60) return s + "s ago";
2550
+ var m = Math.round(s / 60);
2551
+ if (m < 60) return m + "m ago";
2552
+ var h = Math.round(m / 60);
2553
+ if (h < 24) return h + "h ago";
2554
+ return Math.round(h / 24) + "d ago";
2555
+ }
2556
+
2557
+ function fmtDuration(startIso, endIso) {
2558
+ var end = endIso ? new Date(endIso).getTime() : Date.now();
2559
+ var start = new Date(startIso).getTime();
2560
+ if (!isFinite(start) || !isFinite(end)) return "";
2561
+ var s = Math.max(0, Math.round((end - start) / 1000));
2562
+ if (s < 60) return s + "s";
2563
+ var m = Math.floor(s / 60);
2564
+ if (m < 60) return m + "m " + (s % 60) + "s";
2565
+ var h = Math.floor(m / 60);
2566
+ return h + "h " + (m % 60) + "m";
2567
+ }
2568
+
2569
+ function prettyArgs(raw) {
2570
+ if (raw == null) return "";
2571
+ if (typeof raw !== "string") return JSON.stringify(raw, null, 2);
2572
+ try { return JSON.stringify(JSON.parse(raw), null, 2); } catch (e) { return raw; }
2573
+ }
2574
+
2575
+ function firstLine(s, max) {
2576
+ var t = String(s == null ? "" : s).split("\\n")[0];
2577
+ return t.length > max ? t.slice(0, max) + "\\u2026" : t;
2578
+ }
2579
+
2580
+ // ============================================================
2581
+ // state
2582
+ // ============================================================
2583
+
2584
+ var sessions = [];
2585
+ var activeId = null;
2586
+ var includeArchived = false;
2587
+ var currentMeta = null;
2588
+ var lastMessageCount = -1;
2589
+ var sidebarTimer = null;
2590
+ var convoTimer = null;
2591
+
2592
+ function findSession(id) {
2593
+ for (var i = 0; i < sessions.length; i++) if (sessions[i].id === id) return sessions[i];
2594
+ return null;
2595
+ }
2596
+
2597
+ function isLive(s) {
2598
+ return !!s && (s.status === "running" || s.status === "starting");
2599
+ }
2600
+
2601
+ // ============================================================
2602
+ // sidebar
2603
+ // ============================================================
2604
+
2605
+ function displayBadge(s) {
2606
+ if (s.status === "running") {
2607
+ if (s.busy) return { label: "working", cls: "b-run" };
2608
+ if (s.awaitingInput) return { label: "waiting", cls: "b-wait" };
2609
+ return { label: "idle", cls: "b-idle" };
2610
+ }
2611
+ if (s.status === "starting") return { label: "starting", cls: "b-wait" };
2612
+ if (s.status === "error" || s.status === "killed") return { label: s.status, cls: "b-err" };
2613
+ return { label: s.status || "exited", cls: "b-end" };
2614
+ }
2615
+
2616
+ function renderSidebar() {
2617
+ var el = document.getElementById("session-list");
2618
+ if (sessions.length === 0) {
2619
+ el.innerHTML = '<div id="empty-sidebar">No sessions yet.</div>';
2620
+ return;
2621
+ }
2622
+ var html = "";
2623
+ for (var i = 0; i < sessions.length; i++) {
2624
+ var s = sessions[i];
2625
+ var label = s.label || s.title || s.name || (s.command ? String(s.command).split("/").pop() : null) || s.id.slice(0, 10);
2626
+ var db = displayBadge(s);
2627
+ var active = s.id === activeId ? " active" : "";
2628
+ html += '<div class="si' + active + '" data-id="' + escHtml(s.id) + '">'
2629
+ + '<div class="sn">' + escHtml(label) + "</div>"
2630
+ + '<div class="sm">'
2631
+ + '<span class="badge ' + db.cls + '">' + escHtml(db.label) + "</span>"
2632
+ + "<span>" + escHtml(s.kind || "") + "</span>"
2633
+ + "<span>" + escHtml(fmtRelative(s.startedAt)) + "</span>"
2634
+ + "</div></div>";
2635
+ }
2636
+ el.innerHTML = html;
2637
+ var rows = el.querySelectorAll(".si");
2638
+ for (var j = 0; j < rows.length; j++) {
2639
+ rows[j].addEventListener("click", (function (id) { return function () { selectSession(id); }; })(rows[j].getAttribute("data-id")));
2640
+ }
2641
+ }
2642
+
2643
+ function loadSessions() {
2644
+ return callApp("session_list", { kind: "all", includeArchived: includeArchived }).then(function (data) {
2645
+ sessions = (data && data.sessions) || [];
2646
+ renderSidebar();
2647
+ if (activeId) {
2648
+ var hdr = document.getElementById("header");
2649
+ if (hdr.style.display !== "none") renderHeader();
2650
+ }
2651
+ }).catch(function (e) {
2652
+ document.getElementById("status-left").textContent = "sessions: " + e.message;
2653
+ });
2654
+ }
2655
+
2656
+ // ============================================================
2657
+ // header
2658
+ // ============================================================
2659
+
2660
+ function renderHeader() {
2661
+ var s = findSession(activeId);
2662
+ var meta = currentMeta || {};
2663
+ var el = document.getElementById("header");
2664
+ el.style.display = "block";
2665
+
2666
+ var title = meta.title || (s && (s.label || s.title)) || activeId;
2667
+ var live = s && isLive(s);
2668
+ var pulse = live ? '<span class="pulse"></span>' : "";
2669
+
2670
+ var facts = [];
2671
+ if (meta.model || (s && s.model)) facts.push(escHtml(meta.model || (s && s.model)));
2672
+ if (s && s.adapterSlug) facts.push(escHtml(s.adapterSlug));
2673
+ if (s && s.kind) facts.push(escHtml(s.kind));
2674
+ if (s) { var db = displayBadge(s); facts.push(escHtml(db.label)); }
2675
+ if (s && s.cwd) facts.push('<span title="' + escHtml(s.cwd) + '">' + escHtml(s.cwd.length > 46 ? "\\u2026" + s.cwd.slice(-44) : s.cwd) + "</span>");
2676
+ var started = (meta.startedAt || (s && s.startedAt));
2677
+ if (started) facts.push("started " + escHtml(fmtClock(started)) + " (" + escHtml(fmtRelative(started)) + ")");
2678
+ var ended = meta.endedAt || (s && s.endedAt);
2679
+ if (ended) facts.push("ran " + escHtml(fmtDuration(started, ended)));
2680
+ else if (started) facts.push("running " + escHtml(fmtDuration(started)));
2681
+ if (meta.messageCount != null) facts.push(meta.messageCount + " messages");
2682
+ if (meta.toolCallCount != null) facts.push(meta.toolCallCount + " tool calls");
2683
+ if (meta.tokens && (meta.tokens.input != null || meta.tokens.output != null)) {
2684
+ facts.push("tokens " + (meta.tokens.input || 0) + "\\u2192" + (meta.tokens.output || 0));
2685
+ }
2686
+ if (meta.costUsd != null) facts.push("$" + Number(meta.costUsd).toFixed(4));
2687
+
2688
+ el.innerHTML = '<div class="htitle">' + pulse + escHtml(title) + "</div>"
2689
+ + '<div class="hfacts">' + facts.map(function (f) { return '<span class="fact">' + f + "</span>"; }).join("") + "</div>";
2690
+ }
2691
+
2692
+ // ============================================================
2693
+ // transcript
2694
+ // ============================================================
2695
+
2696
+ function renderToolCallChip(tc) {
2697
+ var args = prettyArgs(tc.args);
2698
+ return '<div class="chip tool-call"><div class="ch"><span class="ci">&#9656;</span>'
2699
+ + '<span class="cname">' + escHtml(tc.name || "tool") + "</span>"
2700
+ + '<span class="cprev">' + escHtml(firstLine(args, 64)) + "</span>"
2701
+ + '<span class="cchev">&#9656;</span></div><pre>' + escHtml(args) + "</pre></div>";
2702
+ }
2703
+
2704
+ function renderToolResultChip(m) {
2705
+ var text = m.text || "";
2706
+ return '<div class="chip tool-result"><div class="ch"><span class="ci">&#128295;</span>'
2707
+ + '<span class="cname">' + escHtml(m.toolName || "result") + "</span>"
2708
+ + '<span class="cprev">' + escHtml(firstLine(text, 64)) + "</span>"
2709
+ + '<span class="cchev">&#9656;</span></div><pre>' + escHtml(text) + "</pre></div>";
2710
+ }
2711
+
2712
+ function renderMessage(m) {
2713
+ var time = m.ts ? '<span class="mtime">' + fmtTime(m.ts) + "</span>" : "";
2714
+ if (m.role === "system") {
2715
+ return '<div class="msg role-system"><div class="bubble">' + escHtml(m.text || "") + "</div></div>";
2716
+ }
2717
+ if (m.role === "tool") {
2718
+ return '<div class="msg role-tool">' + renderToolResultChip(m) + "</div>";
2719
+ }
2720
+ if (m.role === "user") {
2721
+ return '<div class="msg role-user"><div class="mrow"><span class="mrole">You</span>' + time + "</div>"
2722
+ + '<div class="bubble">' + escHtml(m.text || "") + "</div></div>";
2723
+ }
2724
+ // assistant
2725
+ var inner = "";
2726
+ if (m.reasoning) inner += '<div class="reasoning">' + escHtml(m.reasoning) + "</div>";
2727
+ if (m.text) inner += escHtml(m.text);
2728
+ var body = inner ? '<div class="bubble">' + inner + "</div>" : "";
2729
+ var chips = (m.toolCalls || []).map(renderToolCallChip).join("");
2730
+ return '<div class="msg role-assistant"><div class="mrow"><span class="mrole">Assistant</span>' + time + "</div>"
2731
+ + body + chips + "</div>";
2732
+ }
2733
+
2734
+ function renderTranscript(messages) {
2735
+ var el = document.getElementById("transcript");
2736
+ var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
2737
+ el.style.display = "flex";
2738
+ document.getElementById("empty-main").style.display = "none";
2739
+ var errEl = document.getElementById("error-main");
2740
+ if (errEl) errEl.style.display = "none";
2741
+ if (messages.length === 0) {
2742
+ el.innerHTML = '<div style="color:var(--text2);text-align:center;padding:20px">(empty conversation)</div>';
2743
+ return;
2744
+ }
2745
+ el.innerHTML = messages.map(renderMessage).join("");
2746
+ if (atBottom) el.scrollTop = el.scrollHeight;
2747
+
2748
+ var chips = el.querySelectorAll(".chip");
2749
+ for (var i = 0; i < chips.length; i++) {
2750
+ chips[i].querySelector(".ch").addEventListener("click", (function (chip) {
2751
+ return function () { chip.classList.toggle("open"); };
2752
+ })(chips[i]));
2753
+ }
2754
+ }
2755
+
2756
+ function renderNoConversation(reason) {
2757
+ document.getElementById("transcript").style.display = "none";
2758
+ document.getElementById("empty-main").style.display = "none";
2759
+ var el = document.getElementById("error-main");
2760
+ el.style.display = "flex";
2761
+ el.innerHTML = '<div class="ico">&#128683;</div><div>No readable conversation for this session'
2762
+ + (reason ? '<br><span style="font-size:11px;color:var(--text3)">' + escHtml(reason) + "</span>" : "")
2763
+ + "</div>";
2764
+ }
2765
+
2766
+ // ============================================================
2767
+ // selection + polling
2768
+ // ============================================================
2769
+
2770
+ function clearConvoPoll() {
2771
+ if (convoTimer) { clearTimeout(convoTimer); convoTimer = null; }
2772
+ }
2773
+
2774
+ function scheduleConvoPoll() {
2775
+ clearConvoPoll();
2776
+ var s = findSession(activeId);
2777
+ if (!isLive(s)) return;
2778
+ convoTimer = setTimeout(function () {
2779
+ loadConversation(activeId, { silent: true });
2780
+ }, 3500);
2781
+ }
2782
+
2783
+ function loadConversation(id, opts) {
2784
+ opts = opts || {};
2785
+ var requestedId = id;
2786
+ if (!opts.silent) {
2787
+ document.getElementById("transcript").style.display = "none";
2788
+ document.getElementById("empty-main").style.display = "none";
2789
+ var errEl = document.getElementById("error-main");
2790
+ errEl.style.display = "flex";
2791
+ errEl.innerHTML = '<div class="ico">&#8987;</div><div>Loading\\u2026</div>';
2792
+ }
2793
+ document.getElementById("status-left").textContent = "loading\\u2026";
2794
+ return callApp("conversation_read", { idOrName: id, format: "json" }).then(function (data) {
2795
+ if (activeId !== requestedId) return; // switched away meanwhile
2796
+ if (!data || !Array.isArray(data.messages)) {
2797
+ currentMeta = null;
2798
+ renderHeader();
2799
+ renderNoConversation(data && data.reason);
2800
+ clearConvoPoll();
2801
+ document.getElementById("status-left").textContent = "no conversation";
2802
+ return;
2803
+ }
2804
+ currentMeta = data.meta || {};
2805
+ lastMessageCount = data.messages.length;
2806
+ renderHeader();
2807
+ renderTranscript(data.messages);
2808
+ document.getElementById("status-left").textContent = data.messages.length + " messages";
2809
+ document.getElementById("status-right").textContent = "updated " + fmtTime(Date.now());
2810
+ scheduleConvoPoll();
2811
+ }).catch(function (e) {
2812
+ if (activeId !== requestedId) return;
2813
+ document.getElementById("status-left").textContent = "error: " + e.message;
2814
+ if (!opts.silent) renderNoConversation(e.message);
2815
+ });
2816
+ }
2817
+
2818
+ function selectSession(id) {
2819
+ if (activeId === id) return;
2820
+ activeId = id;
2821
+ currentMeta = null;
2822
+ lastMessageCount = -1;
2823
+ clearConvoPoll();
2824
+ renderSidebar();
2825
+ document.getElementById("statusbar").style.display = "flex";
2826
+ renderHeader();
2827
+ loadConversation(id, {});
2828
+ }
2829
+
2830
+ // ============================================================
2831
+ // boot
2832
+ // ============================================================
2833
+
2834
+ document.getElementById("refresh-btn").addEventListener("click", function () {
2835
+ loadSessions();
2836
+ if (activeId) loadConversation(activeId, {});
2837
+ });
2838
+ document.getElementById("archived-check").addEventListener("change", function (e) {
2839
+ includeArchived = !!e.target.checked;
2840
+ loadSessions();
2841
+ });
2842
+
2843
+ window.McpApp.connect()
2844
+ .then(function (bridge) {
2845
+ callTool = bridge.callTool;
2846
+ return loadSessions();
2847
+ })
2848
+ .then(function () {
2849
+ sidebarTimer = setInterval(loadSessions, 5000);
2850
+ if (!activeId && sessions.length > 0) selectSession(sessions[0].id);
2851
+ })
2852
+ .catch(function (err) {
2853
+ document.getElementById("empty-main").innerHTML =
2854
+ '<div class="ico">&#9888;</div><div>Standalone mode \\u2014 no host bridge (' + escHtml(err.message) + ")</div>";
2855
+ });
2856
+ </script>
2857
+ </body>
2858
+ </html>`;
2859
+
2860
+ // src/session-viewer/index.ts
2861
+ var sessionViewer = defineApp({
2862
+ id: "@agentproto/session-viewer",
2863
+ name: "Session Viewer",
2864
+ version: "0.1.0",
2865
+ description: "Read a daemon session as a clean conversation \u2014 turns, tool calls, timestamps.",
2866
+ agents: [narrator],
2867
+ workflows: [narrateSession],
2868
+ ui: {
2869
+ html: SESSION_VIEWER_HTML,
2870
+ title: "Session Viewer",
2871
+ tools: [...SESSION_VIEWER_TOOLS]
2872
+ }
2873
+ });
1569
2874
 
1570
2875
  // src/bin/sync.ts
1571
2876
  var APPS = [
1572
2877
  { slug: "code-team", category: "team", handle: codeTeam },
1573
2878
  { slug: "content-team", category: "team", handle: contentTeam },
1574
2879
  { slug: "mail-triage", category: "app", handle: mailTriage },
1575
- { slug: "media-viewer", category: "app", handle: mediaViewer }
2880
+ { slug: "media-viewer", category: "app", handle: mediaViewer },
2881
+ { slug: "ops-panel", category: "app", handle: opsPanel },
2882
+ { slug: "session-viewer", category: "app", handle: sessionViewer }
1576
2883
  ];
1577
2884
  async function syncApps(baseDir) {
1578
2885
  const catalog = [];