@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,539 @@
1
+ import { defineApp } from '@agentproto/app-kit';
2
+ import { defineAgent } from '@agentproto/agent';
3
+ import { defineWorkflow } from '@agentproto/workflow';
4
+
5
+ /**
6
+ * @agentproto/apps v0.1.0
7
+ * Ready-made agentproto apps (teams of agents + workflows) built on app-kit.
8
+ */
9
+
10
+ var narrator = {
11
+ agent: defineAgent({
12
+ schema: "agent/v1",
13
+ id: "@agentproto/session-narrator",
14
+ description: "Reads a daemon session's transcript and narrates it in plain English \u2014 what happened, what was decided, what's left.",
15
+ model: "claude-sonnet-5",
16
+ boundaries: [
17
+ "Read-only \u2014 never spawn, kill, resume, or write to any session",
18
+ "Narrate only what the transcript actually shows, never speculate about intent"
19
+ ],
20
+ tools: ["conversation_read", "session_list"],
21
+ workflows: [{ ref: "narrate-session" }]
22
+ }),
23
+ body: [
24
+ "You narrate a daemon session's conversation for a human who doesn't want to read the raw transcript.",
25
+ "",
26
+ 'Given a session id or name, call `conversation_read` with `format: "json"` to get the',
27
+ "structured turn list (`meta` + `messages`, each `{ role, text, toolCalls?, ts }`). If the",
28
+ "session id is ambiguous or unknown, call `session_list` first to resolve it by label.",
29
+ "",
30
+ "Write a short plain-English narration:",
31
+ "- What the user asked for.",
32
+ "- What the assistant did, grouped into a few natural beats (not a line-by-line tool log).",
33
+ "- Any open question, decision point, or unfinished step at the end of the transcript.",
34
+ "",
35
+ "Skip tool-call plumbing details unless they're the point (e.g. a failing command, a real",
36
+ "decision). If `conversation.conversation` comes back null, say plainly that this session has",
37
+ "no readable conversation (e.g. a bare shell PTY) instead of guessing."
38
+ ].join("\n")
39
+ };
40
+ var narrateSession = defineWorkflow({
41
+ id: "narrate-session",
42
+ name: "Narrate Session",
43
+ description: "Read a daemon session's transcript and narrate it in plain English.",
44
+ version: "0.1.0",
45
+ inputs: {},
46
+ outputs: {},
47
+ steps: [
48
+ {
49
+ id: "narrate",
50
+ kind: "agent",
51
+ agent: { ref: "@agentproto/session-narrator" },
52
+ 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."
53
+ }
54
+ ]
55
+ });
56
+
57
+ // src/session-viewer/ui.ts
58
+ var SESSION_VIEWER_TOOLS = ["session_list", "conversation_read"];
59
+ var APP_ID = "@agentproto/session-viewer";
60
+ var SESSION_VIEWER_HTML = `<!DOCTYPE html>
61
+ <html lang="en">
62
+ <head>
63
+ <meta charset="UTF-8">
64
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
65
+ <title>Session Viewer</title>
66
+ <style>
67
+ *{box-sizing:border-box;margin:0;padding:0}
68
+ :root{
69
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
70
+ --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;
71
+ --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
72
+ }
73
+ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}
74
+ #app{display:flex;height:100%;overflow:hidden}
75
+
76
+ /* sidebar */
77
+ #sidebar{width:250px;min-width:190px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
78
+ #sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
79
+ #sidebar-hdr h1{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;flex:1}
80
+ #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}
81
+ #archived-toggle input{cursor:pointer}
82
+ #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}
83
+ #refresh-btn:hover{color:var(--text);background:var(--bg3)}
84
+ #session-list{flex:1;overflow-y:auto;padding:4px 0}
85
+ .si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}
86
+ .si:hover{background:var(--bg3)}
87
+ .si.active{background:var(--bg3);border-left-color:var(--blue)}
88
+ .si .sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
89
+ .si .sm{font-size:10.5px;color:var(--text2);margin-top:3px;display:flex;gap:5px;align-items:center;flex-wrap:wrap}
90
+ .badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em}
91
+ .b-run{background:rgba(63,185,80,.15);color:var(--green)}
92
+ .b-wait{background:rgba(88,166,255,.15);color:var(--blue)}
93
+ .b-idle{background:rgba(139,148,158,.12);color:var(--text2)}
94
+ .b-end{background:rgba(139,148,158,.1);color:var(--text3)}
95
+ .b-err{background:rgba(248,81,73,.15);color:var(--red)}
96
+ #empty-sidebar{padding:14px 12px;color:var(--text2);font-size:11.5px}
97
+
98
+ /* main */
99
+ #main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
100
+ #header{padding:12px 18px;border-bottom:1px solid var(--border);background:var(--bg2);flex-shrink:0}
101
+ #header .htitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}
102
+ .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}
103
+ @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)}}
104
+ #header .hfacts{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
105
+ .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}
106
+ #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}
107
+ #empty-main .ico,#loading-main .ico{font-size:30px}
108
+ #error-main{color:var(--red)}
109
+
110
+ /* transcript */
111
+ #transcript{flex:1;overflow-y:auto;padding:14px 18px;display:flex;flex-direction:column;gap:10px}
112
+ .msg{display:flex;flex-direction:column;max-width:78%}
113
+ .msg.role-user{align-self:flex-end;align-items:flex-end}
114
+ .msg.role-assistant,.msg.role-tool{align-self:flex-start;align-items:flex-start}
115
+ .msg.role-system{align-self:center;max-width:90%}
116
+ .bubble{border-radius:10px;padding:9px 12px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
117
+ .role-user .bubble{background:var(--blue);color:#04101c;border-bottom-right-radius:3px}
118
+ .role-assistant .bubble{background:var(--bg2);border:1px solid var(--border);border-bottom-left-radius:3px}
119
+ .role-system .bubble{background:transparent;border:1px dashed var(--border);color:var(--text2);font-size:11.5px;text-align:center}
120
+ .mrow{display:flex;align-items:center;gap:6px;margin-bottom:3px}
121
+ .mrole{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text3)}
122
+ .mtime{font-size:10px;color:var(--text3);font-variant-numeric:tabular-nums}
123
+
124
+ .chip{margin-top:6px;border:1px solid var(--border);border-radius:8px;background:var(--bg3);overflow:hidden;cursor:pointer;width:100%}
125
+ .chip .ch{padding:6px 10px;font-size:11.5px;display:flex;align-items:center;gap:7px;color:var(--text2)}
126
+ .chip .ch .ci{color:var(--yellow);flex:none}
127
+ .chip.tool-result .ch .ci{color:var(--purple)}
128
+ .chip .ch .cname{font-weight:600;color:var(--text)}
129
+ .chip .ch .cprev{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text3)}
130
+ .chip .ch .cchev{flex:none;transition:transform .12s;color:var(--text3);font-size:10px}
131
+ .chip.open .ch .cchev{transform:rotate(90deg)}
132
+ .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}
133
+ .chip.open pre{display:block}
134
+ .reasoning{margin-bottom:6px;font-size:11.5px;font-style:italic;color:var(--text2);border-left:2px solid var(--border);padding-left:8px}
135
+
136
+ #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}
137
+ </style>
138
+ </head>
139
+ <body>
140
+ <div id="app">
141
+ <div id="sidebar">
142
+ <div id="sidebar-hdr">
143
+ <h1>Sessions</h1>
144
+ <button id="refresh-btn" title="Refresh" type="button">&#8635;</button>
145
+ </div>
146
+ <label id="archived-toggle"><input type="checkbox" id="archived-check"> show archived</label>
147
+ <div id="session-list"></div>
148
+ </div>
149
+ <div id="main">
150
+ <div id="header" style="display:none"></div>
151
+ <div id="empty-main"><div class="ico">&#128172;</div><div>Pick a session to read its conversation</div></div>
152
+ <div id="transcript" style="display:none"></div>
153
+ <div id="statusbar" style="display:none">
154
+ <span id="status-left"></span>
155
+ <span id="status-right"></span>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ <script>
160
+ var callTool = null;
161
+ var APP_ID = ${JSON.stringify(APP_ID)};
162
+
163
+ // app_tool_call wraps its dispatch result at least once (its own text-result
164
+ // body is the JSON-stringified inner MCP tool result); peel back through
165
+ // nested {content:[{type:"text",text:...}]} shells until the payload stops
166
+ // looking like one \u2014 works whichever layer the host's bridge already
167
+ // unwrapped. With conversation_read's format:"json" this resolves straight
168
+ // to the { meta, messages } object; with session_list, straight to
169
+ // { sessions: [...] }.
170
+ function unwrapText(result) {
171
+ var cur = result;
172
+ for (var i = 0; i < 4; i++) {
173
+ if (typeof cur === "string") {
174
+ try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }
175
+ }
176
+ if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === "string") {
177
+ cur = cur.content[0].text;
178
+ continue;
179
+ }
180
+ break;
181
+ }
182
+ return cur;
183
+ }
184
+
185
+ function callApp(tool, args) {
186
+ return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);
187
+ }
188
+
189
+ function escHtml(s) {
190
+ return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
191
+ }
192
+
193
+ function fmtTime(ts) {
194
+ if (!ts) return "";
195
+ try { return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); }
196
+ catch (e) { return ""; }
197
+ }
198
+
199
+ function fmtClock(iso) {
200
+ if (!iso) return "";
201
+ try { return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }
202
+ catch (e) { return ""; }
203
+ }
204
+
205
+ function fmtRelative(iso) {
206
+ if (!iso) return "";
207
+ var ms = Date.now() - new Date(iso).getTime();
208
+ if (!isFinite(ms)) return "";
209
+ var s = Math.round(ms / 1000);
210
+ if (s < 5) return "just now";
211
+ if (s < 60) return s + "s ago";
212
+ var m = Math.round(s / 60);
213
+ if (m < 60) return m + "m ago";
214
+ var h = Math.round(m / 60);
215
+ if (h < 24) return h + "h ago";
216
+ return Math.round(h / 24) + "d ago";
217
+ }
218
+
219
+ function fmtDuration(startIso, endIso) {
220
+ var end = endIso ? new Date(endIso).getTime() : Date.now();
221
+ var start = new Date(startIso).getTime();
222
+ if (!isFinite(start) || !isFinite(end)) return "";
223
+ var s = Math.max(0, Math.round((end - start) / 1000));
224
+ if (s < 60) return s + "s";
225
+ var m = Math.floor(s / 60);
226
+ if (m < 60) return m + "m " + (s % 60) + "s";
227
+ var h = Math.floor(m / 60);
228
+ return h + "h " + (m % 60) + "m";
229
+ }
230
+
231
+ function prettyArgs(raw) {
232
+ if (raw == null) return "";
233
+ if (typeof raw !== "string") return JSON.stringify(raw, null, 2);
234
+ try { return JSON.stringify(JSON.parse(raw), null, 2); } catch (e) { return raw; }
235
+ }
236
+
237
+ function firstLine(s, max) {
238
+ var t = String(s == null ? "" : s).split("\\n")[0];
239
+ return t.length > max ? t.slice(0, max) + "\\u2026" : t;
240
+ }
241
+
242
+ // ============================================================
243
+ // state
244
+ // ============================================================
245
+
246
+ var sessions = [];
247
+ var activeId = null;
248
+ var includeArchived = false;
249
+ var currentMeta = null;
250
+ var lastMessageCount = -1;
251
+ var sidebarTimer = null;
252
+ var convoTimer = null;
253
+
254
+ function findSession(id) {
255
+ for (var i = 0; i < sessions.length; i++) if (sessions[i].id === id) return sessions[i];
256
+ return null;
257
+ }
258
+
259
+ function isLive(s) {
260
+ return !!s && (s.status === "running" || s.status === "starting");
261
+ }
262
+
263
+ // ============================================================
264
+ // sidebar
265
+ // ============================================================
266
+
267
+ function displayBadge(s) {
268
+ if (s.status === "running") {
269
+ if (s.busy) return { label: "working", cls: "b-run" };
270
+ if (s.awaitingInput) return { label: "waiting", cls: "b-wait" };
271
+ return { label: "idle", cls: "b-idle" };
272
+ }
273
+ if (s.status === "starting") return { label: "starting", cls: "b-wait" };
274
+ if (s.status === "error" || s.status === "killed") return { label: s.status, cls: "b-err" };
275
+ return { label: s.status || "exited", cls: "b-end" };
276
+ }
277
+
278
+ function renderSidebar() {
279
+ var el = document.getElementById("session-list");
280
+ if (sessions.length === 0) {
281
+ el.innerHTML = '<div id="empty-sidebar">No sessions yet.</div>';
282
+ return;
283
+ }
284
+ var html = "";
285
+ for (var i = 0; i < sessions.length; i++) {
286
+ var s = sessions[i];
287
+ var label = s.label || s.title || s.name || (s.command ? String(s.command).split("/").pop() : null) || s.id.slice(0, 10);
288
+ var db = displayBadge(s);
289
+ var active = s.id === activeId ? " active" : "";
290
+ html += '<div class="si' + active + '" data-id="' + escHtml(s.id) + '">'
291
+ + '<div class="sn">' + escHtml(label) + "</div>"
292
+ + '<div class="sm">'
293
+ + '<span class="badge ' + db.cls + '">' + escHtml(db.label) + "</span>"
294
+ + "<span>" + escHtml(s.kind || "") + "</span>"
295
+ + "<span>" + escHtml(fmtRelative(s.startedAt)) + "</span>"
296
+ + "</div></div>";
297
+ }
298
+ el.innerHTML = html;
299
+ var rows = el.querySelectorAll(".si");
300
+ for (var j = 0; j < rows.length; j++) {
301
+ rows[j].addEventListener("click", (function (id) { return function () { selectSession(id); }; })(rows[j].getAttribute("data-id")));
302
+ }
303
+ }
304
+
305
+ function loadSessions() {
306
+ return callApp("session_list", { kind: "all", includeArchived: includeArchived }).then(function (data) {
307
+ sessions = (data && data.sessions) || [];
308
+ renderSidebar();
309
+ if (activeId) {
310
+ var hdr = document.getElementById("header");
311
+ if (hdr.style.display !== "none") renderHeader();
312
+ }
313
+ }).catch(function (e) {
314
+ document.getElementById("status-left").textContent = "sessions: " + e.message;
315
+ });
316
+ }
317
+
318
+ // ============================================================
319
+ // header
320
+ // ============================================================
321
+
322
+ function renderHeader() {
323
+ var s = findSession(activeId);
324
+ var meta = currentMeta || {};
325
+ var el = document.getElementById("header");
326
+ el.style.display = "block";
327
+
328
+ var title = meta.title || (s && (s.label || s.title)) || activeId;
329
+ var live = s && isLive(s);
330
+ var pulse = live ? '<span class="pulse"></span>' : "";
331
+
332
+ var facts = [];
333
+ if (meta.model || (s && s.model)) facts.push(escHtml(meta.model || (s && s.model)));
334
+ if (s && s.adapterSlug) facts.push(escHtml(s.adapterSlug));
335
+ if (s && s.kind) facts.push(escHtml(s.kind));
336
+ if (s) { var db = displayBadge(s); facts.push(escHtml(db.label)); }
337
+ if (s && s.cwd) facts.push('<span title="' + escHtml(s.cwd) + '">' + escHtml(s.cwd.length > 46 ? "\\u2026" + s.cwd.slice(-44) : s.cwd) + "</span>");
338
+ var started = (meta.startedAt || (s && s.startedAt));
339
+ if (started) facts.push("started " + escHtml(fmtClock(started)) + " (" + escHtml(fmtRelative(started)) + ")");
340
+ var ended = meta.endedAt || (s && s.endedAt);
341
+ if (ended) facts.push("ran " + escHtml(fmtDuration(started, ended)));
342
+ else if (started) facts.push("running " + escHtml(fmtDuration(started)));
343
+ if (meta.messageCount != null) facts.push(meta.messageCount + " messages");
344
+ if (meta.toolCallCount != null) facts.push(meta.toolCallCount + " tool calls");
345
+ if (meta.tokens && (meta.tokens.input != null || meta.tokens.output != null)) {
346
+ facts.push("tokens " + (meta.tokens.input || 0) + "\\u2192" + (meta.tokens.output || 0));
347
+ }
348
+ if (meta.costUsd != null) facts.push("$" + Number(meta.costUsd).toFixed(4));
349
+
350
+ el.innerHTML = '<div class="htitle">' + pulse + escHtml(title) + "</div>"
351
+ + '<div class="hfacts">' + facts.map(function (f) { return '<span class="fact">' + f + "</span>"; }).join("") + "</div>";
352
+ }
353
+
354
+ // ============================================================
355
+ // transcript
356
+ // ============================================================
357
+
358
+ function renderToolCallChip(tc) {
359
+ var args = prettyArgs(tc.args);
360
+ return '<div class="chip tool-call"><div class="ch"><span class="ci">&#9656;</span>'
361
+ + '<span class="cname">' + escHtml(tc.name || "tool") + "</span>"
362
+ + '<span class="cprev">' + escHtml(firstLine(args, 64)) + "</span>"
363
+ + '<span class="cchev">&#9656;</span></div><pre>' + escHtml(args) + "</pre></div>";
364
+ }
365
+
366
+ function renderToolResultChip(m) {
367
+ var text = m.text || "";
368
+ return '<div class="chip tool-result"><div class="ch"><span class="ci">&#128295;</span>'
369
+ + '<span class="cname">' + escHtml(m.toolName || "result") + "</span>"
370
+ + '<span class="cprev">' + escHtml(firstLine(text, 64)) + "</span>"
371
+ + '<span class="cchev">&#9656;</span></div><pre>' + escHtml(text) + "</pre></div>";
372
+ }
373
+
374
+ function renderMessage(m) {
375
+ var time = m.ts ? '<span class="mtime">' + fmtTime(m.ts) + "</span>" : "";
376
+ if (m.role === "system") {
377
+ return '<div class="msg role-system"><div class="bubble">' + escHtml(m.text || "") + "</div></div>";
378
+ }
379
+ if (m.role === "tool") {
380
+ return '<div class="msg role-tool">' + renderToolResultChip(m) + "</div>";
381
+ }
382
+ if (m.role === "user") {
383
+ return '<div class="msg role-user"><div class="mrow"><span class="mrole">You</span>' + time + "</div>"
384
+ + '<div class="bubble">' + escHtml(m.text || "") + "</div></div>";
385
+ }
386
+ // assistant
387
+ var inner = "";
388
+ if (m.reasoning) inner += '<div class="reasoning">' + escHtml(m.reasoning) + "</div>";
389
+ if (m.text) inner += escHtml(m.text);
390
+ var body = inner ? '<div class="bubble">' + inner + "</div>" : "";
391
+ var chips = (m.toolCalls || []).map(renderToolCallChip).join("");
392
+ return '<div class="msg role-assistant"><div class="mrow"><span class="mrole">Assistant</span>' + time + "</div>"
393
+ + body + chips + "</div>";
394
+ }
395
+
396
+ function renderTranscript(messages) {
397
+ var el = document.getElementById("transcript");
398
+ var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
399
+ el.style.display = "flex";
400
+ document.getElementById("empty-main").style.display = "none";
401
+ var errEl = document.getElementById("error-main");
402
+ if (errEl) errEl.style.display = "none";
403
+ if (messages.length === 0) {
404
+ el.innerHTML = '<div style="color:var(--text2);text-align:center;padding:20px">(empty conversation)</div>';
405
+ return;
406
+ }
407
+ el.innerHTML = messages.map(renderMessage).join("");
408
+ if (atBottom) el.scrollTop = el.scrollHeight;
409
+
410
+ var chips = el.querySelectorAll(".chip");
411
+ for (var i = 0; i < chips.length; i++) {
412
+ chips[i].querySelector(".ch").addEventListener("click", (function (chip) {
413
+ return function () { chip.classList.toggle("open"); };
414
+ })(chips[i]));
415
+ }
416
+ }
417
+
418
+ function renderNoConversation(reason) {
419
+ document.getElementById("transcript").style.display = "none";
420
+ document.getElementById("empty-main").style.display = "none";
421
+ var el = document.getElementById("error-main");
422
+ el.style.display = "flex";
423
+ el.innerHTML = '<div class="ico">&#128683;</div><div>No readable conversation for this session'
424
+ + (reason ? '<br><span style="font-size:11px;color:var(--text3)">' + escHtml(reason) + "</span>" : "")
425
+ + "</div>";
426
+ }
427
+
428
+ // ============================================================
429
+ // selection + polling
430
+ // ============================================================
431
+
432
+ function clearConvoPoll() {
433
+ if (convoTimer) { clearTimeout(convoTimer); convoTimer = null; }
434
+ }
435
+
436
+ function scheduleConvoPoll() {
437
+ clearConvoPoll();
438
+ var s = findSession(activeId);
439
+ if (!isLive(s)) return;
440
+ convoTimer = setTimeout(function () {
441
+ loadConversation(activeId, { silent: true });
442
+ }, 3500);
443
+ }
444
+
445
+ function loadConversation(id, opts) {
446
+ opts = opts || {};
447
+ var requestedId = id;
448
+ if (!opts.silent) {
449
+ document.getElementById("transcript").style.display = "none";
450
+ document.getElementById("empty-main").style.display = "none";
451
+ var errEl = document.getElementById("error-main");
452
+ errEl.style.display = "flex";
453
+ errEl.innerHTML = '<div class="ico">&#8987;</div><div>Loading\\u2026</div>';
454
+ }
455
+ document.getElementById("status-left").textContent = "loading\\u2026";
456
+ return callApp("conversation_read", { idOrName: id, format: "json" }).then(function (data) {
457
+ if (activeId !== requestedId) return; // switched away meanwhile
458
+ if (!data || !Array.isArray(data.messages)) {
459
+ currentMeta = null;
460
+ renderHeader();
461
+ renderNoConversation(data && data.reason);
462
+ clearConvoPoll();
463
+ document.getElementById("status-left").textContent = "no conversation";
464
+ return;
465
+ }
466
+ currentMeta = data.meta || {};
467
+ lastMessageCount = data.messages.length;
468
+ renderHeader();
469
+ renderTranscript(data.messages);
470
+ document.getElementById("status-left").textContent = data.messages.length + " messages";
471
+ document.getElementById("status-right").textContent = "updated " + fmtTime(Date.now());
472
+ scheduleConvoPoll();
473
+ }).catch(function (e) {
474
+ if (activeId !== requestedId) return;
475
+ document.getElementById("status-left").textContent = "error: " + e.message;
476
+ if (!opts.silent) renderNoConversation(e.message);
477
+ });
478
+ }
479
+
480
+ function selectSession(id) {
481
+ if (activeId === id) return;
482
+ activeId = id;
483
+ currentMeta = null;
484
+ lastMessageCount = -1;
485
+ clearConvoPoll();
486
+ renderSidebar();
487
+ document.getElementById("statusbar").style.display = "flex";
488
+ renderHeader();
489
+ loadConversation(id, {});
490
+ }
491
+
492
+ // ============================================================
493
+ // boot
494
+ // ============================================================
495
+
496
+ document.getElementById("refresh-btn").addEventListener("click", function () {
497
+ loadSessions();
498
+ if (activeId) loadConversation(activeId, {});
499
+ });
500
+ document.getElementById("archived-check").addEventListener("change", function (e) {
501
+ includeArchived = !!e.target.checked;
502
+ loadSessions();
503
+ });
504
+
505
+ window.McpApp.connect()
506
+ .then(function (bridge) {
507
+ callTool = bridge.callTool;
508
+ return loadSessions();
509
+ })
510
+ .then(function () {
511
+ sidebarTimer = setInterval(loadSessions, 5000);
512
+ if (!activeId && sessions.length > 0) selectSession(sessions[0].id);
513
+ })
514
+ .catch(function (err) {
515
+ document.getElementById("empty-main").innerHTML =
516
+ '<div class="ico">&#9888;</div><div>Standalone mode \\u2014 no host bridge (' + escHtml(err.message) + ")</div>";
517
+ });
518
+ </script>
519
+ </body>
520
+ </html>`;
521
+
522
+ // src/session-viewer/index.ts
523
+ var sessionViewer = defineApp({
524
+ id: "@agentproto/session-viewer",
525
+ name: "Session Viewer",
526
+ version: "0.1.0",
527
+ description: "Read a daemon session as a clean conversation \u2014 turns, tool calls, timestamps.",
528
+ agents: [narrator],
529
+ workflows: [narrateSession],
530
+ ui: {
531
+ html: SESSION_VIEWER_HTML,
532
+ title: "Session Viewer",
533
+ tools: [...SESSION_VIEWER_TOOLS]
534
+ }
535
+ });
536
+
537
+ export { narrateSession, narrator, sessionViewer };
538
+ //# sourceMappingURL=session-viewer.mjs.map
539
+ //# sourceMappingURL=session-viewer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/session-viewer/agents/narrator.ts","../src/session-viewer/workflows/narrate-session.ts","../src/session-viewer/ui.ts","../src/session-viewer/index.ts"],"names":[],"mappings":";;;;;;;;;AAGO,IAAM,QAAA,GAAuB;AAAA,EAClC,OAAO,WAAA,CAAY;AAAA,IACjB,MAAA,EAAQ,UAAA;AAAA,IACR,EAAA,EAAI,8BAAA;AAAA,IACJ,WAAA,EACE,2HAAA;AAAA,IACF,KAAA,EAAO,iBAAA;AAAA,IACP,UAAA,EAAY;AAAA,MACV,qEAAA;AAAA,MACA;AAAA,KACF;AAAA,IACA,KAAA,EAAO,CAAC,mBAAA,EAAqB,cAAc,CAAA;AAAA,IAC3C,SAAA,EAAW,CAAC,EAAE,GAAA,EAAK,mBAAmB;AAAA,GACvC,CAAA;AAAA,EACD,IAAA,EAAM;AAAA,IACJ,sGAAA;AAAA,IACA,EAAA;AAAA,IACA,uFAAA;AAAA,IACA,2FAAA;AAAA,IACA,uFAAA;AAAA,IACA,EAAA;AAAA,IACA,wCAAA;AAAA,IACA,4BAAA;AAAA,IACA,2FAAA;AAAA,IACA,uFAAA;AAAA,IACA,EAAA;AAAA,IACA,0FAAA;AAAA,IACA,8FAAA;AAAA,IACA;AAAA,GACF,CAAE,KAAK,IAAI;AACb;AC/BO,IAAM,iBAAiB,cAAA,CAAe;AAAA,EAC3C,EAAA,EAAI,iBAAA;AAAA,EACJ,IAAA,EAAM,iBAAA;AAAA,EACN,WAAA,EAAa,qEAAA;AAAA,EACb,OAAA,EAAS,OAAA;AAAA,EACT,QAAQ,EAAC;AAAA,EACT,SAAS,EAAC;AAAA,EACV,KAAA,EAAO;AAAA,IACL;AAAA,MACE,EAAA,EAAI,SAAA;AAAA,MACJ,IAAA,EAAM,OAAA;AAAA,MACN,KAAA,EAAO,EAAE,GAAA,EAAK,8BAAA,EAA+B;AAAA,MAC7C,MAAA,EACE;AAAA;AAGJ;AAEJ,CAAC;;;ACLM,IAAM,oBAAA,GAAuB,CAAC,cAAA,EAAgB,mBAAmB,CAAA;AAExE,IAAM,MAAA,GAAS,4BAAA;AAER,IAAM,mBAAA,GAAsB,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,EAqGpB,IAAA,CAAK,SAAA,CAAU,MAAM,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,OAAA,CAAA;;;ACtG9B,IAAM,gBAA2B,SAAA,CAAU;AAAA,EAChD,EAAA,EAAI,4BAAA;AAAA,EACJ,IAAA,EAAM,gBAAA;AAAA,EACN,OAAA,EAAS,OAAA;AAAA,EACT,WAAA,EAAa,qFAAA;AAAA,EACb,MAAA,EAAQ,CAAC,QAAQ,CAAA;AAAA,EACjB,SAAA,EAAW,CAAC,cAAc,CAAA;AAAA,EAC1B,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,mBAAA;AAAA,IACN,KAAA,EAAO,gBAAA;AAAA,IACP,KAAA,EAAO,CAAC,GAAG,oBAAoB;AAAA;AAEnC,CAAC","file":"session-viewer.mjs","sourcesContent":["import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\nexport const narrator: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/session-narrator\",\n description:\n \"Reads a daemon session's transcript and narrates it in plain English — what happened, what was decided, what's left.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Read-only — never spawn, kill, resume, or write to any session\",\n \"Narrate only what the transcript actually shows, never speculate about intent\",\n ],\n tools: [\"conversation_read\", \"session_list\"],\n workflows: [{ ref: \"narrate-session\" }],\n }),\n body: [\n \"You narrate a daemon session's conversation for a human who doesn't want to read the raw transcript.\",\n \"\",\n \"Given a session id or name, call `conversation_read` with `format: \\\"json\\\"` to get the\",\n \"structured turn list (`meta` + `messages`, each `{ role, text, toolCalls?, ts }`). If the\",\n \"session id is ambiguous or unknown, call `session_list` first to resolve it by label.\",\n \"\",\n \"Write a short plain-English narration:\",\n \"- What the user asked for.\",\n \"- What the assistant did, grouped into a few natural beats (not a line-by-line tool log).\",\n \"- Any open question, decision point, or unfinished step at the end of the transcript.\",\n \"\",\n \"Skip tool-call plumbing details unless they're the point (e.g. a failing command, a real\",\n \"decision). If `conversation.conversation` comes back null, say plainly that this session has\",\n \"no readable conversation (e.g. a bare shell PTY) instead of guessing.\",\n ].join(\"\\n\"),\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\nexport const narrateSession = defineWorkflow({\n id: \"narrate-session\",\n name: \"Narrate Session\",\n description: \"Read a daemon session's transcript and narrate it in plain English.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"narrate\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/session-narrator\" },\n prompt:\n \"Narrate the session named in the run input (or the most recently started session if \" +\n \"none was given): what the user asked for, what the assistant did, and what — if \" +\n \"anything — is still open at the end of the transcript.\",\n },\n ],\n})\n","/**\n * Session Viewer — a self-contained MCP App panel for `session-viewer`.\n *\n * Talks to the daemon only through the `window.McpApp.connect()` bridge\n * (never a direct `fetch`): every action round-trips through the app's own\n * `app_tool_call` gateway tool, `{ appId, tool, args }` — same convention as\n * `media-viewer`/`mail-triage`. Read-only by design: `session_list` picks a\n * session (live or finished), `conversation_read` renders it as a clean\n * conversation — no spawn/kill/resume action lives here.\n *\n * Live sessions are polled (`conversation_read` has no push channel over\n * this bridge — see the PR's feasibility note on a real event/PTY stream);\n * a finished session is fetched once and left alone.\n */\n\nexport const SESSION_VIEWER_TOOLS = [\"session_list\", \"conversation_read\"] as const\n\nconst APP_ID = \"@agentproto/session-viewer\"\n\nexport const SESSION_VIEWER_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>Session Viewer</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;height:100%;overflow:hidden}\n\n/* sidebar */\n#sidebar{width:250px;min-width:190px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}\n#sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}\n#sidebar-hdr h1{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;flex:1}\n#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}\n#archived-toggle input{cursor:pointer}\n#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}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#session-list{flex:1;overflow-y:auto;padding:4px 0}\n.si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}\n.si:hover{background:var(--bg3)}\n.si.active{background:var(--bg3);border-left-color:var(--blue)}\n.si .sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.si .sm{font-size:10.5px;color:var(--text2);margin-top:3px;display:flex;gap:5px;align-items:center;flex-wrap:wrap}\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-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#empty-sidebar{padding:14px 12px;color:var(--text2);font-size:11.5px}\n\n/* main */\n#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}\n#header{padding:12px 18px;border-bottom:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n#header .htitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}\n.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}\n@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)}}\n#header .hfacts{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}\n.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}\n#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}\n#empty-main .ico,#loading-main .ico{font-size:30px}\n#error-main{color:var(--red)}\n\n/* transcript */\n#transcript{flex:1;overflow-y:auto;padding:14px 18px;display:flex;flex-direction:column;gap:10px}\n.msg{display:flex;flex-direction:column;max-width:78%}\n.msg.role-user{align-self:flex-end;align-items:flex-end}\n.msg.role-assistant,.msg.role-tool{align-self:flex-start;align-items:flex-start}\n.msg.role-system{align-self:center;max-width:90%}\n.bubble{border-radius:10px;padding:9px 12px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word}\n.role-user .bubble{background:var(--blue);color:#04101c;border-bottom-right-radius:3px}\n.role-assistant .bubble{background:var(--bg2);border:1px solid var(--border);border-bottom-left-radius:3px}\n.role-system .bubble{background:transparent;border:1px dashed var(--border);color:var(--text2);font-size:11.5px;text-align:center}\n.mrow{display:flex;align-items:center;gap:6px;margin-bottom:3px}\n.mrole{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text3)}\n.mtime{font-size:10px;color:var(--text3);font-variant-numeric:tabular-nums}\n\n.chip{margin-top:6px;border:1px solid var(--border);border-radius:8px;background:var(--bg3);overflow:hidden;cursor:pointer;width:100%}\n.chip .ch{padding:6px 10px;font-size:11.5px;display:flex;align-items:center;gap:7px;color:var(--text2)}\n.chip .ch .ci{color:var(--yellow);flex:none}\n.chip.tool-result .ch .ci{color:var(--purple)}\n.chip .ch .cname{font-weight:600;color:var(--text)}\n.chip .ch .cprev{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text3)}\n.chip .ch .cchev{flex:none;transition:transform .12s;color:var(--text3);font-size:10px}\n.chip.open .ch .cchev{transform:rotate(90deg)}\n.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}\n.chip.open pre{display:block}\n.reasoning{margin-bottom:6px;font-size:11.5px;font-style:italic;color:var(--text2);border-left:2px solid var(--border);padding-left:8px}\n\n#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}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"sidebar\">\n <div id=\"sidebar-hdr\">\n <h1>Sessions</h1>\n <button id=\"refresh-btn\" title=\"Refresh\" type=\"button\">&#8635;</button>\n </div>\n <label id=\"archived-toggle\"><input type=\"checkbox\" id=\"archived-check\"> show archived</label>\n <div id=\"session-list\"></div>\n </div>\n <div id=\"main\">\n <div id=\"header\" style=\"display:none\"></div>\n <div id=\"empty-main\"><div class=\"ico\">&#128172;</div><div>Pick a session to read its conversation</div></div>\n <div id=\"transcript\" style=\"display:none\"></div>\n <div id=\"statusbar\" style=\"display:none\">\n <span id=\"status-left\"></span>\n <span id=\"status-right\"></span>\n </div>\n </div>\n</div>\n<script>\nvar callTool = null;\nvar APP_ID = ${JSON.stringify(APP_ID)};\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 — works whichever layer the host's bridge already\n// unwrapped. With conversation_read's format:\"json\" this resolves straight\n// to the { meta, messages } object; with session_list, straight to\n// { sessions: [...] }.\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 fmtTime(ts) {\n if (!ts) return \"\";\n try { return new Date(ts).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" }); }\n catch (e) { return \"\"; }\n}\n\nfunction fmtClock(iso) {\n if (!iso) return \"\";\n try { return new Date(iso).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" }); }\n catch (e) { return \"\"; }\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\nfunction fmtDuration(startIso, endIso) {\n var end = endIso ? new Date(endIso).getTime() : Date.now();\n var start = new Date(startIso).getTime();\n if (!isFinite(start) || !isFinite(end)) return \"\";\n var s = Math.max(0, Math.round((end - start) / 1000));\n if (s < 60) return s + \"s\";\n var m = Math.floor(s / 60);\n if (m < 60) return m + \"m \" + (s % 60) + \"s\";\n var h = Math.floor(m / 60);\n return h + \"h \" + (m % 60) + \"m\";\n}\n\nfunction prettyArgs(raw) {\n if (raw == null) return \"\";\n if (typeof raw !== \"string\") return JSON.stringify(raw, null, 2);\n try { return JSON.stringify(JSON.parse(raw), null, 2); } catch (e) { return raw; }\n}\n\nfunction firstLine(s, max) {\n var t = String(s == null ? \"\" : s).split(\"\\\\n\")[0];\n return t.length > max ? t.slice(0, max) + \"\\\\u2026\" : t;\n}\n\n// ============================================================\n// state\n// ============================================================\n\nvar sessions = [];\nvar activeId = null;\nvar includeArchived = false;\nvar currentMeta = null;\nvar lastMessageCount = -1;\nvar sidebarTimer = null;\nvar convoTimer = null;\n\nfunction findSession(id) {\n for (var i = 0; i < sessions.length; i++) if (sessions[i].id === id) return sessions[i];\n return null;\n}\n\nfunction isLive(s) {\n return !!s && (s.status === \"running\" || s.status === \"starting\");\n}\n\n// ============================================================\n// sidebar\n// ============================================================\n\nfunction displayBadge(s) {\n if (s.status === \"running\") {\n if (s.busy) return { label: \"working\", cls: \"b-run\" };\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 renderSidebar() {\n var el = document.getElementById(\"session-list\");\n if (sessions.length === 0) {\n el.innerHTML = '<div id=\"empty-sidebar\">No sessions yet.</div>';\n return;\n }\n var html = \"\";\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n var label = s.label || s.title || s.name || (s.command ? String(s.command).split(\"/\").pop() : null) || s.id.slice(0, 10);\n var db = displayBadge(s);\n var active = s.id === activeId ? \" active\" : \"\";\n html += '<div class=\"si' + active + '\" data-id=\"' + escHtml(s.id) + '\">'\n + '<div class=\"sn\">' + escHtml(label) + \"</div>\"\n + '<div class=\"sm\">'\n + '<span class=\"badge ' + db.cls + '\">' + escHtml(db.label) + \"</span>\"\n + \"<span>\" + escHtml(s.kind || \"\") + \"</span>\"\n + \"<span>\" + escHtml(fmtRelative(s.startedAt)) + \"</span>\"\n + \"</div></div>\";\n }\n el.innerHTML = html;\n var rows = el.querySelectorAll(\".si\");\n for (var j = 0; j < rows.length; j++) {\n rows[j].addEventListener(\"click\", (function (id) { return function () { selectSession(id); }; })(rows[j].getAttribute(\"data-id\")));\n }\n}\n\nfunction loadSessions() {\n return callApp(\"session_list\", { kind: \"all\", includeArchived: includeArchived }).then(function (data) {\n sessions = (data && data.sessions) || [];\n renderSidebar();\n if (activeId) {\n var hdr = document.getElementById(\"header\");\n if (hdr.style.display !== \"none\") renderHeader();\n }\n }).catch(function (e) {\n document.getElementById(\"status-left\").textContent = \"sessions: \" + e.message;\n });\n}\n\n// ============================================================\n// header\n// ============================================================\n\nfunction renderHeader() {\n var s = findSession(activeId);\n var meta = currentMeta || {};\n var el = document.getElementById(\"header\");\n el.style.display = \"block\";\n\n var title = meta.title || (s && (s.label || s.title)) || activeId;\n var live = s && isLive(s);\n var pulse = live ? '<span class=\"pulse\"></span>' : \"\";\n\n var facts = [];\n if (meta.model || (s && s.model)) facts.push(escHtml(meta.model || (s && s.model)));\n if (s && s.adapterSlug) facts.push(escHtml(s.adapterSlug));\n if (s && s.kind) facts.push(escHtml(s.kind));\n if (s) { var db = displayBadge(s); facts.push(escHtml(db.label)); }\n if (s && s.cwd) facts.push('<span title=\"' + escHtml(s.cwd) + '\">' + escHtml(s.cwd.length > 46 ? \"\\\\u2026\" + s.cwd.slice(-44) : s.cwd) + \"</span>\");\n var started = (meta.startedAt || (s && s.startedAt));\n if (started) facts.push(\"started \" + escHtml(fmtClock(started)) + \" (\" + escHtml(fmtRelative(started)) + \")\");\n var ended = meta.endedAt || (s && s.endedAt);\n if (ended) facts.push(\"ran \" + escHtml(fmtDuration(started, ended)));\n else if (started) facts.push(\"running \" + escHtml(fmtDuration(started)));\n if (meta.messageCount != null) facts.push(meta.messageCount + \" messages\");\n if (meta.toolCallCount != null) facts.push(meta.toolCallCount + \" tool calls\");\n if (meta.tokens && (meta.tokens.input != null || meta.tokens.output != null)) {\n facts.push(\"tokens \" + (meta.tokens.input || 0) + \"\\\\u2192\" + (meta.tokens.output || 0));\n }\n if (meta.costUsd != null) facts.push(\"$\" + Number(meta.costUsd).toFixed(4));\n\n el.innerHTML = '<div class=\"htitle\">' + pulse + escHtml(title) + \"</div>\"\n + '<div class=\"hfacts\">' + facts.map(function (f) { return '<span class=\"fact\">' + f + \"</span>\"; }).join(\"\") + \"</div>\";\n}\n\n// ============================================================\n// transcript\n// ============================================================\n\nfunction renderToolCallChip(tc) {\n var args = prettyArgs(tc.args);\n return '<div class=\"chip tool-call\"><div class=\"ch\"><span class=\"ci\">&#9656;</span>'\n + '<span class=\"cname\">' + escHtml(tc.name || \"tool\") + \"</span>\"\n + '<span class=\"cprev\">' + escHtml(firstLine(args, 64)) + \"</span>\"\n + '<span class=\"cchev\">&#9656;</span></div><pre>' + escHtml(args) + \"</pre></div>\";\n}\n\nfunction renderToolResultChip(m) {\n var text = m.text || \"\";\n return '<div class=\"chip tool-result\"><div class=\"ch\"><span class=\"ci\">&#128295;</span>'\n + '<span class=\"cname\">' + escHtml(m.toolName || \"result\") + \"</span>\"\n + '<span class=\"cprev\">' + escHtml(firstLine(text, 64)) + \"</span>\"\n + '<span class=\"cchev\">&#9656;</span></div><pre>' + escHtml(text) + \"</pre></div>\";\n}\n\nfunction renderMessage(m) {\n var time = m.ts ? '<span class=\"mtime\">' + fmtTime(m.ts) + \"</span>\" : \"\";\n if (m.role === \"system\") {\n return '<div class=\"msg role-system\"><div class=\"bubble\">' + escHtml(m.text || \"\") + \"</div></div>\";\n }\n if (m.role === \"tool\") {\n return '<div class=\"msg role-tool\">' + renderToolResultChip(m) + \"</div>\";\n }\n if (m.role === \"user\") {\n return '<div class=\"msg role-user\"><div class=\"mrow\"><span class=\"mrole\">You</span>' + time + \"</div>\"\n + '<div class=\"bubble\">' + escHtml(m.text || \"\") + \"</div></div>\";\n }\n // assistant\n var inner = \"\";\n if (m.reasoning) inner += '<div class=\"reasoning\">' + escHtml(m.reasoning) + \"</div>\";\n if (m.text) inner += escHtml(m.text);\n var body = inner ? '<div class=\"bubble\">' + inner + \"</div>\" : \"\";\n var chips = (m.toolCalls || []).map(renderToolCallChip).join(\"\");\n return '<div class=\"msg role-assistant\"><div class=\"mrow\"><span class=\"mrole\">Assistant</span>' + time + \"</div>\"\n + body + chips + \"</div>\";\n}\n\nfunction renderTranscript(messages) {\n var el = document.getElementById(\"transcript\");\n var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;\n el.style.display = \"flex\";\n document.getElementById(\"empty-main\").style.display = \"none\";\n var errEl = document.getElementById(\"error-main\");\n if (errEl) errEl.style.display = \"none\";\n if (messages.length === 0) {\n el.innerHTML = '<div style=\"color:var(--text2);text-align:center;padding:20px\">(empty conversation)</div>';\n return;\n }\n el.innerHTML = messages.map(renderMessage).join(\"\");\n if (atBottom) el.scrollTop = el.scrollHeight;\n\n var chips = el.querySelectorAll(\".chip\");\n for (var i = 0; i < chips.length; i++) {\n chips[i].querySelector(\".ch\").addEventListener(\"click\", (function (chip) {\n return function () { chip.classList.toggle(\"open\"); };\n })(chips[i]));\n }\n}\n\nfunction renderNoConversation(reason) {\n document.getElementById(\"transcript\").style.display = \"none\";\n document.getElementById(\"empty-main\").style.display = \"none\";\n var el = document.getElementById(\"error-main\");\n el.style.display = \"flex\";\n el.innerHTML = '<div class=\"ico\">&#128683;</div><div>No readable conversation for this session'\n + (reason ? '<br><span style=\"font-size:11px;color:var(--text3)\">' + escHtml(reason) + \"</span>\" : \"\")\n + \"</div>\";\n}\n\n// ============================================================\n// selection + polling\n// ============================================================\n\nfunction clearConvoPoll() {\n if (convoTimer) { clearTimeout(convoTimer); convoTimer = null; }\n}\n\nfunction scheduleConvoPoll() {\n clearConvoPoll();\n var s = findSession(activeId);\n if (!isLive(s)) return;\n convoTimer = setTimeout(function () {\n loadConversation(activeId, { silent: true });\n }, 3500);\n}\n\nfunction loadConversation(id, opts) {\n opts = opts || {};\n var requestedId = id;\n if (!opts.silent) {\n document.getElementById(\"transcript\").style.display = \"none\";\n document.getElementById(\"empty-main\").style.display = \"none\";\n var errEl = document.getElementById(\"error-main\");\n errEl.style.display = \"flex\";\n errEl.innerHTML = '<div class=\"ico\">&#8987;</div><div>Loading\\\\u2026</div>';\n }\n document.getElementById(\"status-left\").textContent = \"loading\\\\u2026\";\n return callApp(\"conversation_read\", { idOrName: id, format: \"json\" }).then(function (data) {\n if (activeId !== requestedId) return; // switched away meanwhile\n if (!data || !Array.isArray(data.messages)) {\n currentMeta = null;\n renderHeader();\n renderNoConversation(data && data.reason);\n clearConvoPoll();\n document.getElementById(\"status-left\").textContent = \"no conversation\";\n return;\n }\n currentMeta = data.meta || {};\n lastMessageCount = data.messages.length;\n renderHeader();\n renderTranscript(data.messages);\n document.getElementById(\"status-left\").textContent = data.messages.length + \" messages\";\n document.getElementById(\"status-right\").textContent = \"updated \" + fmtTime(Date.now());\n scheduleConvoPoll();\n }).catch(function (e) {\n if (activeId !== requestedId) return;\n document.getElementById(\"status-left\").textContent = \"error: \" + e.message;\n if (!opts.silent) renderNoConversation(e.message);\n });\n}\n\nfunction selectSession(id) {\n if (activeId === id) return;\n activeId = id;\n currentMeta = null;\n lastMessageCount = -1;\n clearConvoPoll();\n renderSidebar();\n document.getElementById(\"statusbar\").style.display = \"flex\";\n renderHeader();\n loadConversation(id, {});\n}\n\n// ============================================================\n// boot\n// ============================================================\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () {\n loadSessions();\n if (activeId) loadConversation(activeId, {});\n});\ndocument.getElementById(\"archived-check\").addEventListener(\"change\", function (e) {\n includeArchived = !!e.target.checked;\n loadSessions();\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n return loadSessions();\n })\n .then(function () {\n sidebarTimer = setInterval(loadSessions, 5000);\n if (!activeId && sessions.length > 0) selectSession(sessions[0].id);\n })\n .catch(function (err) {\n document.getElementById(\"empty-main\").innerHTML =\n '<div class=\"ico\">&#9888;</div><div>Standalone mode \\\\u2014 no host bridge (' + escHtml(err.message) + \")</div>\";\n });\n</script>\n</body>\n</html>`\n","/**\n * `session-viewer` — a single-agent agentproto app for reading daemon\n * sessions as a clean conversation.\n *\n * The panel (`ui.ts`) picks a session — live or finished — from\n * `session_list` and renders its transcript via `conversation_read`: user/\n * assistant turns, collapsible tool calls, timestamps, a metadata header.\n * Read-only, no spawn/kill/resume action. The bundled `session-narrator`\n * agent is a complementary plain-English narrator over the same\n * `conversation_read` data — usable standalone via `app_run`, independent\n * of the panel.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { narrator } from \"./agents/narrator.js\"\nimport { narrateSession } from \"./workflows/narrate-session.js\"\nimport { SESSION_VIEWER_HTML, SESSION_VIEWER_TOOLS } from \"./ui.js\"\n\nexport const sessionViewer: AppHandle = defineApp({\n id: \"@agentproto/session-viewer\",\n name: \"Session Viewer\",\n version: \"0.1.0\",\n description: \"Read a daemon session as a clean conversation — turns, tool calls, timestamps.\",\n agents: [narrator],\n workflows: [narrateSession],\n ui: {\n html: SESSION_VIEWER_HTML,\n title: \"Session Viewer\",\n tools: [...SESSION_VIEWER_TOOLS],\n },\n})\n\nexport { narrator, narrateSession }\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/apps",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "@agentproto/apps — a home for ready-made agentproto apps (teams of agents + their workflows), declared with @agentproto/app-kit. Import a team (code-team, content-team, …) and use any of its agents/workflows from your own host: in-process via toMastraAgents(), or emit its manifests to disk.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -54,6 +54,16 @@
54
54
  "import": "./dist/media-viewer.mjs",
55
55
  "default": "./dist/media-viewer.mjs"
56
56
  },
57
+ "./ops-panel": {
58
+ "types": "./dist/ops-panel/index.d.ts",
59
+ "import": "./dist/ops-panel.mjs",
60
+ "default": "./dist/ops-panel.mjs"
61
+ },
62
+ "./session-viewer": {
63
+ "types": "./dist/session-viewer/index.d.ts",
64
+ "import": "./dist/session-viewer.mjs",
65
+ "default": "./dist/session-viewer.mjs"
66
+ },
57
67
  "./package.json": "./package.json"
58
68
  },
59
69
  "files": [
@@ -66,7 +76,7 @@
66
76
  },
67
77
  "dependencies": {
68
78
  "@agentproto/agent": "0.2.1",
69
- "@agentproto/app-kit": "0.6.0",
79
+ "@agentproto/app-kit": "0.6.1",
70
80
  "@agentproto/workflow": "0.2.0"
71
81
  },
72
82
  "peerDependencies": {