@agentproto/apps 0.5.1 → 0.7.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/index.mjs CHANGED
@@ -194,7 +194,7 @@ var triager = {
194
194
  ],
195
195
  workflows: [{ ref: "triage-inbox" }]
196
196
  }),
197
- body: "You scan the inbox for unread messages using mailbox_search. Categorize them (urgent, needs-reply, newsletter, notification, spam). Create a triage plan: label each category, archive newsletters+notifications, keep urgent+needs-reply in inbox. Show the plan before applying. Use mailbox_triage_plan to build the plan and mailbox_triage_apply to execute it after user confirmation."
197
+ body: 'You start by calling mailbox_list to discover connected mailboxes. Pick one with `capabilities.triage: true` (triage requires the gmail.modify scope). Every subsequent mailbox tool requires the `mailbox` parameter (UUID or alias from this list).\n\nScan the mailbox for unread messages using mailbox_search with query="is:unread" and the mailbox from step 1. Categorize them (urgent, needs-reply, newsletter, notification, spam). Create one triage plan per bulk action with mailbox_triage_plan \u2014 its exact contract is `{ mailbox, criteria: { query } | { messageIds }, action: { type: "markRead"|"archive"|"trash"|"label", addLabelIds?, removeLabelIds? } }` \u2014 e.g. label each category, archive newsletters+notifications, keep urgent+needs-reply in inbox. Each plan returns a `plan_id`, a `count`, and a `sample`; it expires after 15 minutes. Show the plan before applying. Execute only after user confirmation, with mailbox_triage_apply `{ plan_id, confirm: true }` \u2014 that is the sole mutation path.'
198
198
  };
199
199
  var triageInbox = defineWorkflow({
200
200
  id: "triage-inbox",
@@ -208,20 +208,33 @@ var triageInbox = defineWorkflow({
208
208
  id: "triage",
209
209
  kind: "agent",
210
210
  agent: { ref: "@agentproto/triager" },
211
- prompt: "Scan the inbox for unread messages. Categorize them (urgent, needs-reply, newsletter, notification, spam). Create a triage plan: label each category, archive newsletters+notifications, keep urgent+needs-reply in inbox. Show me the plan before applying."
211
+ prompt: 'Call mailbox_list to discover connected mailboxes, then scan the selected mailbox for unread messages using mailbox_search with query="is:unread". Categorize them (urgent, needs-reply, newsletter, notification, spam). Create a triage plan: label each category, archive newsletters+notifications, keep urgent+needs-reply in inbox. Show me the plan before applying.'
212
212
  }
213
213
  ]
214
214
  });
215
215
 
216
216
  // src/mail-triage/ui.ts
217
+ var ALIAS_TOOL_NAMES = [
218
+ "mailbox_list",
219
+ "mailbox_search",
220
+ "mailbox_triage_plan",
221
+ "mailbox_triage_apply"
222
+ ];
223
+ var DEFAULT_MCP_ALIASES = ["agentpush-prod", "agentpush"];
224
+ function envAliases() {
225
+ if (typeof process === "undefined" || !process.env) return null;
226
+ const raw = process.env["MAIL_TRIAGE_MCP_ALIASES"];
227
+ if (!raw) return null;
228
+ const list = raw.split(",").map((s) => s.trim()).filter(Boolean);
229
+ return list.length > 0 ? list : null;
230
+ }
231
+ var MAIL_TRIAGE_MCP_ALIASES = envAliases() ?? [...DEFAULT_MCP_ALIASES];
217
232
  var MAIL_TRIAGE_TOOLS = [
218
- "imported:agentpush/mailbox_list",
219
- "imported:agentpush/mailbox_search",
220
- "imported:agentpush/mailbox_triage_plan",
221
- "imported:agentpush/mailbox_triage_apply",
233
+ ...MAIL_TRIAGE_MCP_ALIASES.flatMap((alias) => ALIAS_TOOL_NAMES.map((t) => `imported:${alias}/${t}`)),
222
234
  "app_run",
223
235
  "app_status",
224
- "agent_output"
236
+ "agent_output",
237
+ "app_list"
225
238
  ];
226
239
  var APP_ID = "@agentproto/mail-triage";
227
240
  var AGENT_ID = "@agentproto/triager";
@@ -241,7 +254,7 @@ var MAIL_TRIAGE_HTML = `<!DOCTYPE html>
241
254
  }
242
255
  html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}
243
256
  #app{display:flex;flex-direction:column;height:100%;overflow-y:auto}
244
- #toolbar{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2);flex-shrink:0}
257
+ #toolbar{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2);flex-shrink:0;flex-wrap:wrap}
245
258
  #toolbar .title{font-size:13px;font-weight:600;flex:1}
246
259
  select,input[type=text]{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 8px;border-radius:4px;font-size:12px;font-family:inherit}
247
260
  .abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}
@@ -249,37 +262,68 @@ select,input[type=text]{background:var(--bg3);border:1px solid var(--border);col
249
262
  .abtn:disabled{opacity:.4;cursor:default}
250
263
  .abtn.primary{background:var(--blue);border-color:var(--blue);color:#04101c}
251
264
  .abtn.primary:hover{opacity:.9}
265
+ #caps{padding:6px 14px;display:flex;gap:6px;align-items:center;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}
266
+ .badge{font-size:11px;padding:2px 8px;border-radius:10px;border:1px solid var(--border);color:var(--text2)}
267
+ .badge.on{color:var(--green);border-color:var(--green)}
268
+ .badge.off{opacity:.5;text-decoration:line-through}
252
269
  #content{padding:14px;display:flex;flex-direction:column;gap:16px}
253
270
  section{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:12px}
254
- section h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:10px}
271
+ section h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center}
255
272
  #summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}
256
273
  .tile{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:10px;text-align:center}
257
274
  .tile .count{font-size:22px;font-weight:700}
258
275
  .tile .label{font-size:11px;color:var(--text2);text-transform:capitalize}
276
+ .tile.unread .count{color:var(--green)}
259
277
  .tile.urgent .count{color:var(--red)}
260
278
  .tile.needs-reply .count{color:var(--yellow)}
261
279
  .tile.newsletter .count{color:var(--blue)}
262
280
  .tile.notification .count{color:var(--purple)}
263
- #plan-list,#search-results,#log{max-height:260px;overflow-y:auto;font-size:12px}
281
+ #plan-list,#search-results,#log-events,#runs-list,#connect-hint-list{max-height:260px;overflow-y:auto;font-size:12px}
264
282
  .plan-item{display:flex;justify-content:space-between;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)}
265
283
  .plan-item .subj{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
266
284
  .plan-item .tag{color:var(--text2);font-size:11px;white-space:nowrap}
267
- #log{font-family:Menlo,Monaco,monospace;white-space:pre-wrap;word-break:break-word;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:8px}
285
+ .runs-item{display:flex;gap:10px;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);cursor:pointer}
286
+ .runs-item:hover{background:var(--bg3)}
287
+ .runs-item .rid{font-family:Menlo,Monaco,monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
288
+ .runs-item .st{font-size:11px;white-space:nowrap}
289
+ .runs-item .st.running{color:var(--yellow)}
290
+ .runs-item .st.ended{color:var(--green)}
291
+ .runs-item .meta{color:var(--text2);font-size:11px;white-space:nowrap}
292
+ #log-tail{font-family:Menlo,Monaco,monospace;white-space:pre-wrap;word-break:break-word;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:8px;max-height:260px;overflow-y:auto}
268
293
  .searchbar{display:flex;gap:8px;margin-bottom:8px}
269
294
  .searchbar input{flex:1}
270
295
  .actions{display:flex;gap:8px;margin-top:10px}
271
296
  #status{padding:6px 14px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
272
297
  .empty{color:var(--text2);padding:8px 0}
298
+ .mini{font-size:10px;text-transform:none;letter-spacing:0;cursor:pointer;color:var(--blue);background:none;border:none;font-family:inherit}
299
+ .caps-hint{font-size:11px;color:var(--yellow)}
300
+ .snippet{color:var(--text2);font-size:11px;margin-top:2px}
301
+ .mail-checkbox{accent-color:var(--blue);cursor:pointer;flex-shrink:0;margin-top:1px}
302
+ .mail-open{font-size:10px;cursor:pointer;color:var(--blue);background:none;border:none;font-family:inherit;white-space:nowrap;padding:0;flex-shrink:0}
303
+ .mail-open:hover{text-decoration:underline}
304
+ #selection-bar{display:none;margin-bottom:8px;gap:8px;align-items:center}
305
+ #selection-bar.show{display:flex}
273
306
  </style>
274
307
  </head>
275
308
  <body>
276
309
  <div id="app">
277
310
  <div id="toolbar">
278
311
  <span class="title">Mail Triage</span>
312
+ <select id="alias-select" style="display:none" title="MCP server"></select>
279
313
  <select id="mailbox-select"></select>
280
314
  <button class="abtn" id="refresh-btn">Refresh</button>
281
315
  </div>
316
+ <div id="caps" style="display:none"></div>
282
317
  <div id="content">
318
+ <section id="connect-hint" style="display:none">
319
+ <h3>Connect a mailbox</h3>
320
+ <div id="connect-hint-list"></div>
321
+ <div class="empty">Import/start an agentpush MCP server and connect a Gmail account in agentpush, then retry.</div>
322
+ <div class="actions">
323
+ <button class="abtn primary" id="connect-retry-btn">Retry</button>
324
+ </div>
325
+ </section>
326
+
283
327
  <section>
284
328
  <h3>Inbox summary</h3>
285
329
  <div id="summary"></div>
@@ -291,16 +335,30 @@ section h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spa
291
335
  <input type="text" id="search-input" placeholder="e.g. is:unread">
292
336
  <button class="abtn" id="search-btn">Search</button>
293
337
  </div>
338
+ <div id="selection-bar">
339
+ <span id="sel-count" style="font-size:12px;color:var(--text2)">0 selected</span>
340
+ <button class="abtn primary" data-action="send-selection">Send selection to Claude</button>
341
+ </div>
294
342
  <div id="search-results"><div class="empty">No search run yet.</div></div>
295
343
  </section>
296
344
 
297
345
  <section>
298
346
  <h3>Triage plan</h3>
347
+ <div class="searchbar">
348
+ <input type="text" id="plan-query" placeholder="criteria query" value="is:unread category:promotions older_than:7d">
349
+ <select id="plan-action">
350
+ <option value="markRead">mark read</option>
351
+ <option value="archive">archive</option>
352
+ <option value="label">label&#8230;</option>
353
+ <option value="trash">trash</option>
354
+ </select>
355
+ <input type="text" id="plan-label" placeholder="label name" style="display:none">
356
+ </div>
299
357
  <div class="actions">
300
- <button class="abtn primary" id="triage-btn">Triage</button>
358
+ <button class="abtn primary" id="triage-btn">Build plan</button>
301
359
  <button class="abtn" id="apply-btn" disabled>Apply</button>
302
360
  </div>
303
- <div id="plan-list" style="margin-top:10px"><div class="empty">No plan yet \u2014 click Triage.</div></div>
361
+ <div id="plan-list" style="margin-top:10px"><div class="empty">No plan yet \u2014 click Build plan.</div></div>
304
362
  </section>
305
363
 
306
364
  <section>
@@ -308,7 +366,13 @@ section h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spa
308
366
  <div class="actions">
309
367
  <button class="abtn" id="run-agent-btn">Run Triager Agent</button>
310
368
  </div>
311
- <div id="log" style="margin-top:10px"><div class="empty">Agent has not run yet.</div></div>
369
+ <div id="log-events" style="margin-top:10px"><div class="empty">Agent has not run yet.</div></div>
370
+ <div id="log-tail" style="margin-top:6px"></div>
371
+ </section>
372
+
373
+ <section>
374
+ <h3><span>Past runs</span><button class="mini" id="runs-refresh-btn">refresh</button></h3>
375
+ <div id="runs-list"><div class="empty">No runs yet.</div></div>
312
376
  </section>
313
377
  </div>
314
378
  <div id="status">Connecting&#8230;</div>
@@ -317,9 +381,21 @@ section h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spa
317
381
  var APP_ID = ${JSON.stringify(APP_ID)};
318
382
  var AGENT_ID = ${JSON.stringify(AGENT_ID)};
319
383
  var CATEGORIES = ${JSON.stringify(CATEGORIES)};
384
+ var ALIASES = ${JSON.stringify(MAIL_TRIAGE_MCP_ALIASES)};
385
+ var POLL_TIMEOUT_MS = 10 * 60 * 1000;
320
386
  var callTool = null;
387
+ var updateModelContext = null;
388
+ var openLink = null;
389
+ var onTeardown = null;
321
390
  var currentPlan = null;
322
391
  var pollHandle = null;
392
+ var planExpiryHandle = null;
393
+ var activeAlias = ALIASES[0];
394
+ var currentMailboxes = [];
395
+ var categoryCounts = null;
396
+ var unreadCount = null;
397
+ var aliasStates = {};
398
+ var selectedMessages = {};
323
399
 
324
400
  function setStatus(msg) {
325
401
  document.getElementById("status").textContent = msg;
@@ -349,7 +425,125 @@ function unwrapText(result) {
349
425
  }
350
426
 
351
427
  function callApp(tool, args) {
352
- return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);
428
+ if (!callTool) {
429
+ return Promise.reject(new Error("Not connected to host bridge (standalone mode)."));
430
+ }
431
+ return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(function (result) {
432
+ var value = unwrapText(result);
433
+ if (value && typeof value === "object" && !Array.isArray(value) && typeof value.error === "string") {
434
+ throw new Error(value.error);
435
+ }
436
+ return value;
437
+ });
438
+ }
439
+
440
+ function aliasTool(name) {
441
+ return "imported:" + activeAlias + "/" + name;
442
+ }
443
+
444
+ // Disables a button and swaps its label to an ellipsis while a call is in
445
+ // flight, so double-submit is impossible; restores the original label after.
446
+ function setBusy(btn, busy) {
447
+ if (busy) {
448
+ if (btn.getAttribute("data-label") === null) btn.setAttribute("data-label", btn.textContent);
449
+ btn.disabled = true;
450
+ btn.textContent = "\\u2026";
451
+ } else {
452
+ btn.disabled = false;
453
+ var label = btn.getAttribute("data-label");
454
+ if (label !== null) btn.textContent = label;
455
+ }
456
+ }
457
+
458
+ function isSelected(id) {
459
+ return selectedMessages[id] ? true : false;
460
+ }
461
+
462
+ function toggleSelection(id, item) {
463
+ if (selectedMessages[id]) {
464
+ delete selectedMessages[id];
465
+ } else {
466
+ selectedMessages[id] = item;
467
+ }
468
+ updateSelectionBar();
469
+ }
470
+
471
+ function updateSelectionBar() {
472
+ var bar = document.getElementById("selection-bar");
473
+ var countEl = document.getElementById("sel-count");
474
+ var count = 0;
475
+ for (var k in selectedMessages) { if (selectedMessages.hasOwnProperty(k)) count++; }
476
+ countEl.textContent = count + " selected";
477
+ bar.className = count > 0 ? "show" : "";
478
+ // Sync checkboxes
479
+ var cbs = document.querySelectorAll("#search-results .mail-checkbox");
480
+ for (var i = 0; i < cbs.length; i++) {
481
+ var mid = cbs[i].getAttribute("data-message-id");
482
+ cbs[i].checked = mid ? isSelected(mid) : false;
483
+ }
484
+ }
485
+
486
+ function sendSelectionToClaude() {
487
+ if (!updateModelContext) {
488
+ setStatus("Host does not support updateModelContext.");
489
+ return;
490
+ }
491
+ var items = [];
492
+ var names = [];
493
+ for (var k in selectedMessages) {
494
+ if (!selectedMessages.hasOwnProperty(k)) continue;
495
+ var msg = selectedMessages[k];
496
+ items.push({
497
+ id: msg.id || k,
498
+ threadId: msg.threadId || "",
499
+ from: msg.from || "",
500
+ subject: msg.subject || "",
501
+ timestamp: msg.internalDate || msg.date || ""
502
+ });
503
+ if (msg.subject) names.push(msg.subject);
504
+ }
505
+ if (items.length === 0) {
506
+ setStatus("No messages selected.");
507
+ return;
508
+ }
509
+ var summary = items.length + " emails selected for triage: " + names.join(", ");
510
+ setStatus("Sending selection to Claude\\u2026");
511
+ updateModelContext({
512
+ content: [{ type: "text", text: summary }],
513
+ structuredContent: { selection: items }
514
+ }).then(function () {
515
+ setStatus("Selection sent to Claude (" + items.length + " emails).");
516
+ }).catch(function (err) {
517
+ setStatus("Failed to send selection: " + (err && err.message ? err.message : String(err)));
518
+ });
519
+ }
520
+
521
+ function clearPlanExpiry() {
522
+ if (planExpiryHandle) {
523
+ clearTimeout(planExpiryHandle);
524
+ planExpiryHandle = null;
525
+ }
526
+ }
527
+
528
+ function onPlanExpired() {
529
+ planExpiryHandle = null;
530
+ currentPlan = null;
531
+ document.getElementById("apply-btn").disabled = true;
532
+ setStatus("Plan expired \\u2014 rebuild it.");
533
+ }
534
+
535
+ // Arms a timeout that invalidates the plan at its server-declared expires_at
536
+ // (plans expire after 15 minutes) so Apply never fires against a stale plan.
537
+ function armPlanExpiry(expiresAt) {
538
+ clearPlanExpiry();
539
+ if (!expiresAt) return;
540
+ var ms = new Date(expiresAt).getTime() - Date.now();
541
+ if (isNaN(ms)) return;
542
+ if (ms <= 0) {
543
+ onPlanExpired();
544
+ return;
545
+ }
546
+ planExpiryHandle = setTimeout(onPlanExpired, ms);
353
547
  }
354
548
 
355
549
  function asList(value, keys) {
@@ -362,145 +556,593 @@ function asList(value, keys) {
362
556
  return [];
363
557
  }
364
558
 
365
- function renderSummary(counts) {
559
+ function renderSummary() {
366
560
  var el = document.getElementById("summary");
367
561
  el.innerHTML = "";
368
- CATEGORIES.forEach(function (cat) {
562
+ var tiles = [{ key: "unread", count: unreadCount }].concat(
563
+ CATEGORIES.map(function (cat) {
564
+ return { key: cat, count: categoryCounts ? categoryCounts[cat] : undefined };
565
+ })
566
+ );
567
+ tiles.forEach(function (t) {
369
568
  var tile = document.createElement("div");
370
- tile.className = "tile " + cat;
371
- var count = counts && counts[cat] !== undefined ? counts[cat] : "\u2013";
569
+ tile.className = "tile " + t.key;
372
570
  tile.innerHTML = '<div class="count"></div><div class="label"></div>';
373
- tile.querySelector(".count").textContent = String(count);
374
- tile.querySelector(".label").textContent = cat.replace("-", " ");
571
+ tile.querySelector(".count").textContent = t.count === undefined || t.count === null ? "\\u2013" : String(t.count);
572
+ tile.querySelector(".label").textContent = t.key.replace("-", " ");
375
573
  el.appendChild(tile);
376
574
  });
377
575
  }
378
576
 
379
- function loadMailboxes() {
380
- setStatus("Loading mailboxes\\u2026");
381
- return callApp("imported:agentpush/mailbox_list", {})
577
+ function mailboxId(mb) {
578
+ if (typeof mb === "string") return mb;
579
+ return mb.id || mb.name || JSON.stringify(mb);
580
+ }
581
+
582
+ function mailboxText(mb) {
583
+ if (typeof mb === "string") return mb;
584
+ var name = mb.address || mb.label || mb.name || mb.id || JSON.stringify(mb);
585
+ var caps = mb.capabilities;
586
+ if (caps && typeof caps === "object") {
587
+ return name + (caps.triage ? " \\u2014 triage \\u2713" : " \\u2014 read-only");
588
+ }
589
+ return name;
590
+ }
591
+
592
+ function selectedMailbox() {
593
+ var id = document.getElementById("mailbox-select").value;
594
+ for (var i = 0; i < currentMailboxes.length; i++) {
595
+ if (mailboxId(currentMailboxes[i]) === id) return currentMailboxes[i];
596
+ }
597
+ return null;
598
+ }
599
+
600
+ function renderCaps(mb) {
601
+ var el = document.getElementById("caps");
602
+ var caps = mb && typeof mb === "object" ? mb.capabilities : null;
603
+ if (!caps || typeof caps !== "object") {
604
+ el.style.display = "none";
605
+ return;
606
+ }
607
+ el.style.display = "flex";
608
+ el.innerHTML = "";
609
+ ["read", "send", "triage", "compose"].forEach(function (cap) {
610
+ var b = document.createElement("span");
611
+ b.className = "badge " + (caps[cap] ? "on" : "off");
612
+ b.textContent = cap;
613
+ el.appendChild(b);
614
+ });
615
+ if (!caps.triage) {
616
+ var hint = document.createElement("span");
617
+ hint.className = "caps-hint";
618
+ hint.textContent = "triage scope missing \\u2014 grant it via agentpush (mailbox_request_elevation)";
619
+ el.appendChild(hint);
620
+ }
621
+ }
622
+
623
+ // Gmail category labels -> the dashboard's triage buckets. IMPORTANT wins,
624
+ // so an important promo counts as urgent, not newsletter.
625
+ function categoryOf(labelIds) {
626
+ var l = labelIds || [];
627
+ if (l.indexOf("IMPORTANT") !== -1) return "urgent";
628
+ if (l.indexOf("CATEGORY_PROMOTIONS") !== -1) return "newsletter";
629
+ if (l.indexOf("CATEGORY_UPDATES") !== -1 || l.indexOf("CATEGORY_SOCIAL") !== -1 || l.indexOf("CATEGORY_FORUMS") !== -1) return "notification";
630
+ return "needs-reply";
631
+ }
632
+
633
+ function onMailboxChange() {
634
+ clearPlanExpiry();
635
+ var mb = selectedMailbox();
636
+ renderCaps(mb);
637
+ var caps = mb && typeof mb === "object" ? mb.capabilities : null;
638
+ var noTriage = !!(caps && typeof caps === "object" && !caps.triage);
639
+ var scopeHint = noTriage ? "Mailbox lacks triage capability \\u2014 grant it via agentpush (mailbox_request_elevation)" : "";
640
+ document.getElementById("triage-btn").disabled = noTriage;
641
+ document.getElementById("triage-btn").title = scopeHint;
642
+ document.getElementById("apply-btn").disabled = true;
643
+ document.getElementById("apply-btn").title = scopeHint;
644
+ currentPlan = null;
645
+ categoryCounts = null;
646
+ unreadCount = null;
647
+ renderSummary();
648
+ if (!mb) return;
649
+ callApp(aliasTool("mailbox_search"), { mailbox: mailboxId(mb), query: "is:unread", limit: 50 })
382
650
  .then(function (result) {
383
- var mailboxes = asList(result, ["mailboxes", "items"]);
384
- var select = document.getElementById("mailbox-select");
385
- select.innerHTML = "";
386
- if (mailboxes.length === 0) {
387
- var opt = document.createElement("option");
388
- opt.textContent = "inbox";
389
- opt.value = "inbox";
390
- select.appendChild(opt);
651
+ var items = asList(result, ["items", "messages", "results"]);
652
+ if (result && typeof result === "object" && typeof result.resultSizeEstimate === "number") {
653
+ unreadCount = result.resultSizeEstimate;
391
654
  } else {
392
- mailboxes.forEach(function (mb) {
393
- var name = typeof mb === "string" ? mb : mb.name || mb.id || JSON.stringify(mb);
394
- var opt = document.createElement("option");
395
- opt.value = name;
396
- opt.textContent = name;
397
- select.appendChild(opt);
398
- });
655
+ unreadCount = items.length;
399
656
  }
400
- setStatus("Ready");
657
+ // Category breakdown over the newest page of unread \u2014 a sample when
658
+ // the mailbox has more unread than one page.
659
+ var counts = {};
660
+ items.forEach(function (item) {
661
+ var cat = categoryOf(item && item.labelIds);
662
+ counts[cat] = (counts[cat] || 0) + 1;
663
+ });
664
+ categoryCounts = items.length > 0 ? counts : null;
665
+ renderSummary();
666
+ })
667
+ .catch(function (err) {
668
+ // KPI is best-effort: keep the dashes, but say why instead of silence.
669
+ setStatus("Inbox summary unavailable: " + err.message);
670
+ });
671
+ }
672
+
673
+ function renderMailboxes(mailboxes) {
674
+ currentMailboxes = mailboxes;
675
+ var select = document.getElementById("mailbox-select");
676
+ select.innerHTML = "";
677
+ if (mailboxes.length === 0) {
678
+ var opt = document.createElement("option");
679
+ opt.textContent = "no mailboxes";
680
+ opt.value = "";
681
+ select.appendChild(opt);
682
+ renderCaps(null);
683
+ return;
684
+ }
685
+ mailboxes.forEach(function (mb) {
686
+ var opt = document.createElement("option");
687
+ opt.value = mailboxId(mb);
688
+ opt.textContent = mailboxText(mb);
689
+ select.appendChild(opt);
690
+ });
691
+ onMailboxChange();
692
+ }
693
+
694
+ function renderAliasSelect(liveAliases) {
695
+ var select = document.getElementById("alias-select");
696
+ select.innerHTML = "";
697
+ var shown = liveAliases.length > 0 ? liveAliases : ALIASES;
698
+ shown.forEach(function (alias) {
699
+ var opt = document.createElement("option");
700
+ opt.value = alias;
701
+ opt.textContent = alias;
702
+ select.appendChild(opt);
703
+ });
704
+ select.value = activeAlias;
705
+ select.style.display = shown.length > 1 ? "" : "none";
706
+ }
707
+
708
+ function loadMailboxes() {
709
+ setStatus("Loading mailboxes from " + activeAlias + "\\u2026");
710
+ return callApp(aliasTool("mailbox_list"), {})
711
+ .then(function (result) {
712
+ renderMailboxes(asList(result, ["mailboxes", "items"]));
713
+ setStatus("Ready \\u2014 " + activeAlias);
401
714
  })
402
715
  .catch(function (err) {
403
716
  setStatus("Error loading mailboxes: " + err.message);
404
717
  });
405
718
  }
406
719
 
407
- function renderSearchResults(items) {
720
+ function renderConnectHint() {
721
+ var el = document.getElementById("connect-hint");
722
+ var anyLive = false;
723
+ for (var i = 0; i < ALIASES.length; i++) {
724
+ if (aliasStates[ALIASES[i]] && aliasStates[ALIASES[i]].state === "live") anyLive = true;
725
+ }
726
+ if (anyLive) {
727
+ el.style.display = "none";
728
+ return;
729
+ }
730
+ el.style.display = "";
731
+ var list = document.getElementById("connect-hint-list");
732
+ list.innerHTML = "";
733
+ ALIASES.forEach(function (alias) {
734
+ var st = aliasStates[alias];
735
+ var row = document.createElement("div");
736
+ row.className = "plan-item";
737
+ var text;
738
+ if (!st) text = alias + " \\u2014 not probed yet";
739
+ else if (st.state === "unreachable") text = alias + " \\u2014 unreachable: " + st.message;
740
+ else if (st.state === "empty") text = alias + " \\u2014 connected, no mailboxes";
741
+ else text = alias + " \\u2014 live";
742
+ row.textContent = text;
743
+ list.appendChild(row);
744
+ });
745
+ }
746
+
747
+ function probeAliases() {
748
+ setStatus("Detecting mail servers\\u2026");
749
+ return Promise.all(
750
+ ALIASES.map(function (alias) {
751
+ return callApp("imported:" + alias + "/mailbox_list", {})
752
+ .then(function (result) {
753
+ var mailboxes = asList(result, ["mailboxes", "items"]);
754
+ aliasStates[alias] = { state: mailboxes.length > 0 ? "live" : "empty" };
755
+ return { alias: alias, mailboxes: mailboxes };
756
+ })
757
+ .catch(function (err) {
758
+ aliasStates[alias] = { state: "unreachable", message: err && err.message ? err.message : String(err) };
759
+ return { alias: alias, mailboxes: [] };
760
+ });
761
+ })
762
+ ).then(function (results) {
763
+ var live = results.filter(function (r) { return r.mailboxes.length > 0; });
764
+ if (live.length > 0) {
765
+ activeAlias = live[0].alias;
766
+ renderAliasSelect(live.map(function (r) { return r.alias; }));
767
+ renderMailboxes(live[0].mailboxes);
768
+ renderConnectHint();
769
+ setStatus("Ready \\u2014 " + activeAlias + (live.length > 1 ? " (" + live.length + " servers available)" : ""));
770
+ } else {
771
+ renderAliasSelect([]);
772
+ renderMailboxes([]);
773
+ renderConnectHint();
774
+ setStatus("No mailboxes reachable \\u2014 see connection panel below.");
775
+ }
776
+ });
777
+ }
778
+
779
+ function renderSearchResults(items, raw) {
408
780
  var el = document.getElementById("search-results");
409
781
  el.innerHTML = "";
782
+ if (raw && typeof raw === "object" && typeof raw.resultSizeEstimate === "number") {
783
+ var count = document.createElement("div");
784
+ count.className = "empty";
785
+ count.textContent = items.length + " shown \\u00b7 ~" + raw.resultSizeEstimate + " total";
786
+ el.appendChild(count);
787
+ }
410
788
  if (items.length === 0) {
411
- el.innerHTML = '<div class="empty">No results.</div>';
789
+ var none = document.createElement("div");
790
+ none.className = "empty";
791
+ none.textContent = "No results.";
792
+ el.appendChild(none);
412
793
  return;
413
794
  }
414
795
  items.forEach(function (item) {
415
796
  var row = document.createElement("div");
416
797
  row.className = "plan-item";
798
+ row.style.flexDirection = "column";
799
+ row.style.alignItems = "stretch";
800
+ var msgId = item && (item.id || item.messageId || "");
801
+ var threadId = item && (item.threadId || "");
417
802
  var subj = typeof item === "string" ? item : item.subject || item.title || JSON.stringify(item);
418
803
  var from = item && item.from ? item.from : "";
419
- row.innerHTML = '<span class="subj"></span><span class="tag"></span>';
420
- row.querySelector(".subj").textContent = subj;
421
- row.querySelector(".tag").textContent = from;
804
+ var snippet = item && item.snippet ? item.snippet : "";
805
+ var top = document.createElement("div");
806
+ top.style.display = "flex";
807
+ top.style.alignItems = "flex-start";
808
+ top.style.gap = "6px";
809
+ var cb = document.createElement("input");
810
+ cb.type = "checkbox";
811
+ cb.className = "mail-checkbox";
812
+ cb.setAttribute("data-action", "toggle-select");
813
+ if (msgId) cb.setAttribute("data-message-id", msgId);
814
+ if (isSelected(msgId)) cb.checked = true;
815
+ top.appendChild(cb);
816
+ var subjSpan = document.createElement("span");
817
+ subjSpan.className = "subj";
818
+ subjSpan.style.flex = "1";
819
+ subjSpan.textContent = subj;
820
+ top.appendChild(subjSpan);
821
+ var tagSpan = document.createElement("span");
822
+ tagSpan.className = "tag";
823
+ tagSpan.textContent = from;
824
+ top.appendChild(tagSpan);
825
+ if (threadId) {
826
+ var openBtn = document.createElement("button");
827
+ openBtn.className = "mail-open";
828
+ openBtn.textContent = "Open";
829
+ openBtn.setAttribute("data-action", "open-gmail");
830
+ openBtn.setAttribute("data-thread-id", threadId);
831
+ top.appendChild(openBtn);
832
+ }
833
+ row.appendChild(top);
834
+ if (snippet) {
835
+ var sn = document.createElement("div");
836
+ sn.className = "snippet";
837
+ sn.textContent = snippet;
838
+ row.appendChild(sn);
839
+ }
422
840
  el.appendChild(row);
423
841
  });
424
842
  }
425
843
 
844
+ // Renders a mailbox_triage_plan result: { plan_id, action, count, sample,
845
+ // truncated, expires_at }. The sample rows are search-shaped messages.
426
846
  function renderPlan(plan) {
427
- var items = asList(plan, ["actions", "items", "plan"]);
428
847
  var el = document.getElementById("plan-list");
429
848
  el.innerHTML = "";
430
- if (items.length === 0) {
431
- el.innerHTML = '<div class="empty">Plan is empty.</div>';
849
+ if (!plan || typeof plan !== "object" || !plan.plan_id) {
850
+ el.innerHTML = '<div class="empty">Plan failed \\u2014 unexpected response.</div>';
432
851
  return;
433
852
  }
434
- var counts = {};
435
- items.forEach(function (item) {
436
- var cat = (item && (item.category || item.tag)) || "notification";
437
- counts[cat] = (counts[cat] || 0) + 1;
853
+ var head = document.createElement("div");
854
+ head.className = "plan-item";
855
+ head.innerHTML = '<span class="subj"></span><span class="tag"></span>';
856
+ head.querySelector(".subj").textContent =
857
+ plan.count + " message" + (plan.count === 1 ? "" : "s") + " \\u2192 " + (plan.action && plan.action.type);
858
+ head.querySelector(".tag").textContent = plan.expires_at ? "expires " + fmtDate(plan.expires_at) : "";
859
+ el.appendChild(head);
860
+ var sample = asList(plan, ["sample"]);
861
+ sample.forEach(function (item) {
438
862
  var row = document.createElement("div");
439
863
  row.className = "plan-item";
440
- var subj = (item && (item.subject || item.title)) || JSON.stringify(item);
441
- var action = (item && (item.action || item.category)) || "";
442
864
  row.innerHTML = '<span class="subj"></span><span class="tag"></span>';
443
- row.querySelector(".subj").textContent = subj;
444
- row.querySelector(".tag").textContent = action;
865
+ row.querySelector(".subj").textContent = (item && (item.subject || item.title)) || JSON.stringify(item);
866
+ row.querySelector(".tag").textContent = (item && item.from) || "";
445
867
  el.appendChild(row);
446
868
  });
447
- renderSummary(counts);
869
+ if (plan.truncated) {
870
+ var more = document.createElement("div");
871
+ more.className = "empty";
872
+ more.textContent = "\\u2026sample truncated \\u2014 " + plan.count + " total.";
873
+ el.appendChild(more);
874
+ }
448
875
  }
449
876
 
450
- document.getElementById("refresh-btn").addEventListener("click", loadMailboxes);
877
+ function fmtDate(iso) {
878
+ if (!iso) return "";
879
+ var d = new Date(iso);
880
+ return isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
881
+ }
882
+
883
+ function renderRuns(runs) {
884
+ var el = document.getElementById("runs-list");
885
+ el.innerHTML = "";
886
+ if (runs.length === 0) {
887
+ el.innerHTML = '<div class="empty">No runs yet.</div>';
888
+ return;
889
+ }
890
+ var shown = runs.slice(0, 20);
891
+ shown.forEach(function (run) {
892
+ var row = document.createElement("div");
893
+ row.className = "runs-item";
894
+ row.title = "Show sessions + output for " + run.appRunId;
895
+ row.innerHTML = '<span class="st"></span><span class="rid"></span><span class="meta"></span>';
896
+ var st = row.querySelector(".st");
897
+ st.textContent = run.status || "?";
898
+ st.className = "st " + (run.status || "");
899
+ row.querySelector(".rid").textContent = run.appRunId;
900
+ row.querySelector(".meta").textContent =
901
+ fmtDate(run.startedAt) + (run.endedAt ? " \\u2192 " + fmtDate(run.endedAt) : "") + " \\u00b7 " + (run.sessions || 0) + " session" + (run.sessions === 1 ? "" : "s");
902
+ row.addEventListener("click", function () { viewRun(run.appRunId); });
903
+ el.appendChild(row);
904
+ });
905
+ if (runs.length > 20) {
906
+ var more = document.createElement("div");
907
+ more.className = "empty";
908
+ more.textContent = "showing 20 of " + runs.length;
909
+ el.appendChild(more);
910
+ }
911
+ }
912
+
913
+ function loadRuns() {
914
+ return callApp("app_list", {})
915
+ .then(function (result) {
916
+ var apps = Array.isArray(result) ? result : [];
917
+ var mine = null;
918
+ for (var i = 0; i < apps.length; i++) {
919
+ if (apps[i] && apps[i].appId === APP_ID) { mine = apps[i]; break; }
920
+ }
921
+ var runs = (mine && Array.isArray(mine.runs) ? mine.runs.slice() : []).sort(function (a, b) {
922
+ return String(b.startedAt || "").localeCompare(String(a.startedAt || ""));
923
+ });
924
+ renderRuns(runs);
925
+ })
926
+ .catch(function (err) {
927
+ setStatus("Could not load runs: " + err.message);
928
+ });
929
+ }
930
+
931
+ function viewRun(appRunId) {
932
+ setStatus("Loading run " + appRunId + "\\u2026");
933
+ callApp("app_status", { appRunId: appRunId })
934
+ .then(function (status) {
935
+ var sessions = (status && status.sessions) || [];
936
+ appendLog("\\u2500\\u2500 run " + appRunId + " (" + (status && status.status) + ") \\u2500\\u2500");
937
+ if (sessions.length === 0) {
938
+ appendLog("(no sessions)");
939
+ setStatus("Ready \\u2014 " + activeAlias);
940
+ return;
941
+ }
942
+ sessions.forEach(function (s) {
943
+ appendLog("session " + s.sessionId + " (" + (s.agentId || "?") + ")");
944
+ });
945
+ return callApp("agent_output", { sessionId: sessions[0].sessionId, lastN: 200, clean: true })
946
+ .then(function (output) {
947
+ if (output) appendLog(typeof output === "string" ? output : JSON.stringify(output, null, 2));
948
+ setStatus("Ready \\u2014 " + activeAlias);
949
+ });
950
+ })
951
+ .catch(function (err) {
952
+ setStatus("Run status failed: " + err.message);
953
+ });
954
+ }
955
+
956
+ // Event delegation: search-results (checkboxes + open buttons)
957
+ document.getElementById("search-results").addEventListener("change", function (e) {
958
+ var target = e.target;
959
+ if (target.getAttribute("data-action") === "toggle-select") {
960
+ var mid = target.getAttribute("data-message-id");
961
+ if (mid) {
962
+ var item = null;
963
+ var el = target.parentNode;
964
+ while (el && el.parentNode && el !== document) {
965
+ if (el.tagName === "DIV" && el.className.indexOf("plan-item") !== -1) {
966
+ var subjEl = el.querySelector(".subj");
967
+ var tagEl = el.querySelector(".tag");
968
+ var subjText = subjEl ? subjEl.textContent : "";
969
+ var tagText = tagEl ? tagEl.textContent : "";
970
+ // Reconstruct a minimal item from the DOM
971
+ item = { id: mid, threadId: "", subject: subjText, from: tagText };
972
+ break;
973
+ }
974
+ el = el.parentNode;
975
+ }
976
+ toggleSelection(mid, item || { id: mid });
977
+ }
978
+ }
979
+ });
980
+ document.getElementById("search-results").addEventListener("click", function (e) {
981
+ var target = e.target;
982
+ if (target.getAttribute("data-action") === "open-gmail") {
983
+ var tid = target.getAttribute("data-thread-id");
984
+ if (tid && openLink) {
985
+ openLink("https://mail.google.com/mail/u/0/#inbox/" + tid).catch(function (err) {
986
+ setStatus("Failed to open link: " + (err && err.message ? err.message : String(err)));
987
+ });
988
+ } else if (tid) {
989
+ setStatus("Host does not support openLink.");
990
+ }
991
+ }
992
+ });
993
+ // Event delegation: selection-bar (send-selection)
994
+ document.getElementById("selection-bar").addEventListener("click", function (e) {
995
+ if (e.target.getAttribute("data-action") === "send-selection") {
996
+ sendSelectionToClaude();
997
+ }
998
+ });
999
+
1000
+ document.getElementById("refresh-btn").addEventListener("click", function () {
1001
+ probeAliases();
1002
+ loadRuns();
1003
+ });
1004
+
1005
+ document.getElementById("connect-retry-btn").addEventListener("click", function () {
1006
+ probeAliases();
1007
+ loadRuns();
1008
+ });
1009
+
1010
+ document.getElementById("alias-select").addEventListener("change", function () {
1011
+ activeAlias = this.value;
1012
+ clearPlanExpiry();
1013
+ currentPlan = null;
1014
+ document.getElementById("apply-btn").disabled = true;
1015
+ loadMailboxes();
1016
+ });
1017
+
1018
+ document.getElementById("mailbox-select").addEventListener("change", onMailboxChange);
1019
+
1020
+ document.getElementById("runs-refresh-btn").addEventListener("click", loadRuns);
451
1021
 
452
1022
  document.getElementById("search-btn").addEventListener("click", function () {
453
- var query = document.getElementById("search-input").value;
1023
+ var btn = this;
1024
+ var query = document.getElementById("search-input").value.trim() || "is:unread";
454
1025
  var mailbox = document.getElementById("mailbox-select").value;
1026
+ setBusy(btn, true);
455
1027
  setStatus("Searching\\u2026");
456
- callApp("imported:agentpush/mailbox_search", { mailbox: mailbox, query: query })
1028
+ callApp(aliasTool("mailbox_search"), { mailbox: mailbox, query: query })
457
1029
  .then(function (result) {
458
- renderSearchResults(asList(result, ["messages", "items", "results"]));
459
- setStatus("Ready");
1030
+ renderSearchResults(asList(result, ["items", "messages", "results"]), result);
1031
+ setBusy(btn, false);
1032
+ setStatus("Ready \\u2014 " + activeAlias);
460
1033
  })
461
1034
  .catch(function (err) {
1035
+ setBusy(btn, false);
462
1036
  setStatus("Search failed: " + err.message);
463
1037
  });
464
1038
  });
465
1039
 
1040
+ document.getElementById("plan-action").addEventListener("change", function () {
1041
+ document.getElementById("plan-label").style.display = this.value === "label" ? "" : "none";
1042
+ });
1043
+
466
1044
  document.getElementById("triage-btn").addEventListener("click", function () {
1045
+ var btn = this;
467
1046
  var mailbox = document.getElementById("mailbox-select").value;
1047
+ var query = document.getElementById("plan-query").value.trim() || "is:unread";
1048
+ var actionType = document.getElementById("plan-action").value;
1049
+ var action = { type: actionType };
1050
+ if (actionType === "label") {
1051
+ var label = document.getElementById("plan-label").value.trim();
1052
+ if (!label) {
1053
+ setStatus("Enter a label name for the label action.");
1054
+ return;
1055
+ }
1056
+ action.addLabelIds = [label];
1057
+ }
1058
+ setBusy(btn, true);
468
1059
  setStatus("Building triage plan\\u2026");
469
1060
  document.getElementById("apply-btn").disabled = true;
470
- callApp("imported:agentpush/mailbox_triage_plan", { mailbox: mailbox })
1061
+ clearPlanExpiry();
1062
+ currentPlan = null;
1063
+ callApp(aliasTool("mailbox_triage_plan"), { mailbox: mailbox, criteria: { query: query }, action: action })
471
1064
  .then(function (plan) {
472
- currentPlan = plan;
1065
+ setBusy(btn, false);
1066
+ currentPlan = plan && plan.plan_id ? plan : null;
473
1067
  renderPlan(plan);
474
- document.getElementById("apply-btn").disabled = false;
475
- setStatus("Plan ready \\u2014 review, then Apply.");
1068
+ if (currentPlan && plan.count > 0) {
1069
+ document.getElementById("apply-btn").disabled = false;
1070
+ armPlanExpiry(plan.expires_at);
1071
+ setStatus("Plan ready (" + plan.count + " messages) \\u2014 review, then Apply.");
1072
+ } else if (currentPlan) {
1073
+ setStatus("Plan matches no messages \\u2014 nothing to apply.");
1074
+ } else {
1075
+ setStatus("Triage plan failed \\u2014 see plan panel.");
1076
+ }
476
1077
  })
477
1078
  .catch(function (err) {
478
- setStatus("Triage plan failed: " + err.message);
1079
+ setBusy(btn, false);
1080
+ var msg = err && err.message ? err.message : String(err);
1081
+ if (msg.indexOf("mailbox_scope_missing") !== -1) {
1082
+ setStatus("Triage scope missing \\u2014 grant it via agentpush (mailbox_request_elevation).");
1083
+ } else {
1084
+ setStatus("Triage plan failed: " + msg);
1085
+ }
479
1086
  });
480
1087
  });
481
1088
 
482
1089
  document.getElementById("apply-btn").addEventListener("click", function () {
483
- if (!currentPlan) return;
1090
+ var btn = this;
1091
+ if (!currentPlan || !currentPlan.plan_id) return;
1092
+ setBusy(btn, true);
484
1093
  setStatus("Applying triage plan\\u2026");
485
- callApp("imported:agentpush/mailbox_triage_apply", { plan: currentPlan })
486
- .then(function () {
487
- setStatus("Plan applied.");
1094
+ callApp(aliasTool("mailbox_triage_apply"), { plan_id: currentPlan.plan_id, confirm: true })
1095
+ .then(function (result) {
1096
+ setBusy(btn, false);
1097
+ var count = currentPlan.count;
1098
+ var extra = [];
1099
+ if (result && typeof result === "object") {
1100
+ ["applied", "modified", "count"].forEach(function (key) {
1101
+ if (typeof result[key] === "number") extra.push(key + ": " + result[key]);
1102
+ });
1103
+ }
1104
+ setStatus(
1105
+ extra.length > 0
1106
+ ? "Plan applied \\u2014 " + extra.join(", ") + "."
1107
+ : "Plan applied (" + count + " message" + (count === 1 ? "" : "s") + ")."
1108
+ );
488
1109
  document.getElementById("apply-btn").disabled = true;
1110
+ clearPlanExpiry();
1111
+ currentPlan = null;
1112
+ onMailboxChange();
489
1113
  })
490
1114
  .catch(function (err) {
1115
+ setBusy(btn, false);
491
1116
  setStatus("Apply failed: " + err.message);
492
1117
  });
493
1118
  });
494
1119
 
495
1120
  function appendLog(text) {
496
- var el = document.getElementById("log");
1121
+ var el = document.getElementById("log-events");
497
1122
  if (el.querySelector(".empty")) el.innerHTML = "";
498
- el.textContent += (el.textContent ? "\\n" : "") + text;
1123
+ var line = document.createElement("div");
1124
+ line.textContent = text;
1125
+ el.appendChild(line);
499
1126
  el.scrollTop = el.scrollHeight;
500
1127
  }
501
1128
 
502
- function pollAgentRun(appRunId) {
503
- if (pollHandle) clearTimeout(pollHandle);
1129
+ // Replaces the whole tail on every poll tick (the daemon returns the last
1130
+ // N lines, not a delta) \u2014 appending it instead would duplicate the log.
1131
+ function renderLogTail(text) {
1132
+ var el = document.getElementById("log-tail");
1133
+ el.textContent = text || "";
1134
+ el.scrollTop = el.scrollHeight;
1135
+ }
1136
+
1137
+ function pollAgentRun(appRunId, startedAt) {
1138
+ if (pollHandle) {
1139
+ clearTimeout(pollHandle);
1140
+ pollHandle = null;
1141
+ }
1142
+ if (Date.now() - startedAt > POLL_TIMEOUT_MS) {
1143
+ setStatus("Still running \\u2014 check Past runs later.");
1144
+ return;
1145
+ }
504
1146
  callApp("app_status", { appRunId: appRunId })
505
1147
  .then(function (status) {
506
1148
  var sessions = (status && status.sessions) || [];
@@ -509,12 +1151,14 @@ function pollAgentRun(appRunId) {
509
1151
  ? callApp("agent_output", { sessionId: sessionId, lastN: 200, clean: true })
510
1152
  : Promise.resolve("");
511
1153
  return tail.then(function (output) {
512
- if (output) appendLog(typeof output === "string" ? output : JSON.stringify(output));
1154
+ renderLogTail(typeof output === "string" ? output : output ? JSON.stringify(output, null, 2) : "");
513
1155
  var done = status && (status.status === "ended" || status.status === "done" || status.endedAt);
514
1156
  if (!done) {
515
- pollHandle = setTimeout(function () { pollAgentRun(appRunId); }, 2000);
1157
+ pollHandle = setTimeout(function () { pollAgentRun(appRunId, startedAt); }, 2000);
516
1158
  } else {
1159
+ pollHandle = null;
517
1160
  setStatus("Agent run finished.");
1161
+ loadRuns();
518
1162
  }
519
1163
  });
520
1164
  })
@@ -524,29 +1168,62 @@ function pollAgentRun(appRunId) {
524
1168
  }
525
1169
 
526
1170
  document.getElementById("run-agent-btn").addEventListener("click", function () {
527
- document.getElementById("log").innerHTML = "";
1171
+ var btn = this;
1172
+ if (pollHandle) {
1173
+ clearTimeout(pollHandle);
1174
+ pollHandle = null;
1175
+ }
1176
+ document.getElementById("log-events").innerHTML = "";
1177
+ document.getElementById("log-tail").textContent = "";
1178
+ setBusy(btn, true);
528
1179
  setStatus("Starting triager agent\\u2026");
529
1180
  callApp("app_run", { appId: APP_ID, agents: [AGENT_ID], prompt: "Triage my inbox." })
530
1181
  .then(function (run) {
1182
+ setBusy(btn, false);
1183
+ var sessions = (run && run.sessions) || [];
1184
+ var errors = (run && run.errors) || [];
1185
+ if (sessions.length === 0 && errors.length > 0) {
1186
+ appendLog(
1187
+ "Run failed to start: " +
1188
+ errors.map(function (e) { return (e.agentId || "?") + ": " + e.error; }).join("; ")
1189
+ );
1190
+ setStatus("Run failed to start.");
1191
+ return;
1192
+ }
531
1193
  appendLog("Started app run " + (run && run.appRunId));
532
- pollAgentRun(run.appRunId);
1194
+ loadRuns();
1195
+ pollAgentRun(run.appRunId, Date.now());
533
1196
  })
534
1197
  .catch(function (err) {
1198
+ setBusy(btn, false);
535
1199
  setStatus("Run failed: " + err.message);
536
1200
  });
537
1201
  });
538
1202
 
539
- window.McpApp.connect()
1203
+ // window.McpApp is normally injected by the serving layer (postMessage bridge
1204
+ // on the MCP resource path, REST bridge on GET /apps/:id/ui). Guard anyway:
1205
+ // without it a missing bridge throws here and the panel dies silently on
1206
+ // "Connecting\u2026" instead of reporting itself.
1207
+ (window.McpApp ? window.McpApp.connect() : Promise.reject(new Error("window.McpApp bridge missing")))
540
1208
  .then(function (bridge) {
541
1209
  callTool = bridge.callTool;
542
- return loadMailboxes();
1210
+ updateModelContext = bridge.updateModelContext;
1211
+ openLink = bridge.openLink;
1212
+ onTeardown = bridge.onTeardown;
1213
+ if (onTeardown) {
1214
+ onTeardown(function () {
1215
+ if (pollHandle) { clearTimeout(pollHandle); pollHandle = null; }
1216
+ if (planExpiryHandle) { clearTimeout(planExpiryHandle); planExpiryHandle = null; }
1217
+ });
1218
+ }
1219
+ return Promise.all([probeAliases(), loadRuns()]);
543
1220
  })
544
1221
  .catch(function (err) {
545
1222
  setStatus("Standalone mode \\u2014 no host bridge (" + err.message + ")");
546
- renderSummary(null);
1223
+ renderSummary();
547
1224
  });
548
1225
 
549
- renderSummary(null);
1226
+ renderSummary();
550
1227
  </script>
551
1228
  </body>
552
1229
  </html>`;
@@ -884,7 +1561,533 @@ var mediaViewer = defineApp({
884
1561
  tools: [...MEDIA_VIEWER_TOOLS]
885
1562
  }
886
1563
  });
1564
+ var narrator = {
1565
+ agent: defineAgent({
1566
+ schema: "agent/v1",
1567
+ id: "@agentproto/session-narrator",
1568
+ description: "Reads a daemon session's transcript and narrates it in plain English \u2014 what happened, what was decided, what's left.",
1569
+ model: "claude-sonnet-5",
1570
+ boundaries: [
1571
+ "Read-only \u2014 never spawn, kill, resume, or write to any session",
1572
+ "Narrate only what the transcript actually shows, never speculate about intent"
1573
+ ],
1574
+ tools: ["conversation_read", "session_list"],
1575
+ workflows: [{ ref: "narrate-session" }]
1576
+ }),
1577
+ body: [
1578
+ "You narrate a daemon session's conversation for a human who doesn't want to read the raw transcript.",
1579
+ "",
1580
+ 'Given a session id or name, call `conversation_read` with `format: "json"` to get the',
1581
+ "structured turn list (`meta` + `messages`, each `{ role, text, toolCalls?, ts }`). If the",
1582
+ "session id is ambiguous or unknown, call `session_list` first to resolve it by label.",
1583
+ "",
1584
+ "Write a short plain-English narration:",
1585
+ "- What the user asked for.",
1586
+ "- What the assistant did, grouped into a few natural beats (not a line-by-line tool log).",
1587
+ "- Any open question, decision point, or unfinished step at the end of the transcript.",
1588
+ "",
1589
+ "Skip tool-call plumbing details unless they're the point (e.g. a failing command, a real",
1590
+ "decision). If `conversation.conversation` comes back null, say plainly that this session has",
1591
+ "no readable conversation (e.g. a bare shell PTY) instead of guessing."
1592
+ ].join("\n")
1593
+ };
1594
+ var narrateSession = defineWorkflow({
1595
+ id: "narrate-session",
1596
+ name: "Narrate Session",
1597
+ description: "Read a daemon session's transcript and narrate it in plain English.",
1598
+ version: "0.1.0",
1599
+ inputs: {},
1600
+ outputs: {},
1601
+ steps: [
1602
+ {
1603
+ id: "narrate",
1604
+ kind: "agent",
1605
+ agent: { ref: "@agentproto/session-narrator" },
1606
+ 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."
1607
+ }
1608
+ ]
1609
+ });
1610
+
1611
+ // src/session-viewer/ui.ts
1612
+ var SESSION_VIEWER_TOOLS = ["session_list", "conversation_read"];
1613
+ var APP_ID3 = "@agentproto/session-viewer";
1614
+ var SESSION_VIEWER_HTML = `<!DOCTYPE html>
1615
+ <html lang="en">
1616
+ <head>
1617
+ <meta charset="UTF-8">
1618
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1619
+ <title>Session Viewer</title>
1620
+ <style>
1621
+ *{box-sizing:border-box;margin:0;padding:0}
1622
+ :root{
1623
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
1624
+ --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;
1625
+ --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
1626
+ }
1627
+ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}
1628
+ #app{display:flex;height:100%;overflow:hidden}
1629
+
1630
+ /* sidebar */
1631
+ #sidebar{width:250px;min-width:190px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
1632
+ #sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
1633
+ #sidebar-hdr h1{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;flex:1}
1634
+ #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}
1635
+ #archived-toggle input{cursor:pointer}
1636
+ #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}
1637
+ #refresh-btn:hover{color:var(--text);background:var(--bg3)}
1638
+ #session-list{flex:1;overflow-y:auto;padding:4px 0}
1639
+ .si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}
1640
+ .si:hover{background:var(--bg3)}
1641
+ .si.active{background:var(--bg3);border-left-color:var(--blue)}
1642
+ .si .sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1643
+ .si .sm{font-size:10.5px;color:var(--text2);margin-top:3px;display:flex;gap:5px;align-items:center;flex-wrap:wrap}
1644
+ .badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em}
1645
+ .b-run{background:rgba(63,185,80,.15);color:var(--green)}
1646
+ .b-wait{background:rgba(88,166,255,.15);color:var(--blue)}
1647
+ .b-idle{background:rgba(139,148,158,.12);color:var(--text2)}
1648
+ .b-end{background:rgba(139,148,158,.1);color:var(--text3)}
1649
+ .b-err{background:rgba(248,81,73,.15);color:var(--red)}
1650
+ #empty-sidebar{padding:14px 12px;color:var(--text2);font-size:11.5px}
1651
+
1652
+ /* main */
1653
+ #main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
1654
+ #header{padding:12px 18px;border-bottom:1px solid var(--border);background:var(--bg2);flex-shrink:0}
1655
+ #header .htitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}
1656
+ .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}
1657
+ @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)}}
1658
+ #header .hfacts{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
1659
+ .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}
1660
+ #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}
1661
+ #empty-main .ico,#loading-main .ico{font-size:30px}
1662
+ #error-main{color:var(--red)}
1663
+
1664
+ /* transcript */
1665
+ #transcript{flex:1;overflow-y:auto;padding:14px 18px;display:flex;flex-direction:column;gap:10px}
1666
+ .msg{display:flex;flex-direction:column;max-width:78%}
1667
+ .msg.role-user{align-self:flex-end;align-items:flex-end}
1668
+ .msg.role-assistant,.msg.role-tool{align-self:flex-start;align-items:flex-start}
1669
+ .msg.role-system{align-self:center;max-width:90%}
1670
+ .bubble{border-radius:10px;padding:9px 12px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
1671
+ .role-user .bubble{background:var(--blue);color:#04101c;border-bottom-right-radius:3px}
1672
+ .role-assistant .bubble{background:var(--bg2);border:1px solid var(--border);border-bottom-left-radius:3px}
1673
+ .role-system .bubble{background:transparent;border:1px dashed var(--border);color:var(--text2);font-size:11.5px;text-align:center}
1674
+ .mrow{display:flex;align-items:center;gap:6px;margin-bottom:3px}
1675
+ .mrole{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text3)}
1676
+ .mtime{font-size:10px;color:var(--text3);font-variant-numeric:tabular-nums}
1677
+
1678
+ .chip{margin-top:6px;border:1px solid var(--border);border-radius:8px;background:var(--bg3);overflow:hidden;cursor:pointer;width:100%}
1679
+ .chip .ch{padding:6px 10px;font-size:11.5px;display:flex;align-items:center;gap:7px;color:var(--text2)}
1680
+ .chip .ch .ci{color:var(--yellow);flex:none}
1681
+ .chip.tool-result .ch .ci{color:var(--purple)}
1682
+ .chip .ch .cname{font-weight:600;color:var(--text)}
1683
+ .chip .ch .cprev{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text3)}
1684
+ .chip .ch .cchev{flex:none;transition:transform .12s;color:var(--text3);font-size:10px}
1685
+ .chip.open .ch .cchev{transform:rotate(90deg)}
1686
+ .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}
1687
+ .chip.open pre{display:block}
1688
+ .reasoning{margin-bottom:6px;font-size:11.5px;font-style:italic;color:var(--text2);border-left:2px solid var(--border);padding-left:8px}
1689
+
1690
+ #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}
1691
+ </style>
1692
+ </head>
1693
+ <body>
1694
+ <div id="app">
1695
+ <div id="sidebar">
1696
+ <div id="sidebar-hdr">
1697
+ <h1>Sessions</h1>
1698
+ <button id="refresh-btn" title="Refresh" type="button">&#8635;</button>
1699
+ </div>
1700
+ <label id="archived-toggle"><input type="checkbox" id="archived-check"> show archived</label>
1701
+ <div id="session-list"></div>
1702
+ </div>
1703
+ <div id="main">
1704
+ <div id="header" style="display:none"></div>
1705
+ <div id="empty-main"><div class="ico">&#128172;</div><div>Pick a session to read its conversation</div></div>
1706
+ <div id="transcript" style="display:none"></div>
1707
+ <div id="statusbar" style="display:none">
1708
+ <span id="status-left"></span>
1709
+ <span id="status-right"></span>
1710
+ </div>
1711
+ </div>
1712
+ </div>
1713
+ <script>
1714
+ var callTool = null;
1715
+ var APP_ID = ${JSON.stringify(APP_ID3)};
1716
+
1717
+ // app_tool_call wraps its dispatch result at least once (its own text-result
1718
+ // body is the JSON-stringified inner MCP tool result); peel back through
1719
+ // nested {content:[{type:"text",text:...}]} shells until the payload stops
1720
+ // looking like one \u2014 works whichever layer the host's bridge already
1721
+ // unwrapped. With conversation_read's format:"json" this resolves straight
1722
+ // to the { meta, messages } object; with session_list, straight to
1723
+ // { sessions: [...] }.
1724
+ function unwrapText(result) {
1725
+ var cur = result;
1726
+ for (var i = 0; i < 4; i++) {
1727
+ if (typeof cur === "string") {
1728
+ try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }
1729
+ }
1730
+ if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === "string") {
1731
+ cur = cur.content[0].text;
1732
+ continue;
1733
+ }
1734
+ break;
1735
+ }
1736
+ return cur;
1737
+ }
1738
+
1739
+ function callApp(tool, args) {
1740
+ return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);
1741
+ }
1742
+
1743
+ function escHtml(s) {
1744
+ return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1745
+ }
1746
+
1747
+ function fmtTime(ts) {
1748
+ if (!ts) return "";
1749
+ try { return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); }
1750
+ catch (e) { return ""; }
1751
+ }
1752
+
1753
+ function fmtClock(iso) {
1754
+ if (!iso) return "";
1755
+ try { return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }
1756
+ catch (e) { return ""; }
1757
+ }
1758
+
1759
+ function fmtRelative(iso) {
1760
+ if (!iso) return "";
1761
+ var ms = Date.now() - new Date(iso).getTime();
1762
+ if (!isFinite(ms)) return "";
1763
+ var s = Math.round(ms / 1000);
1764
+ if (s < 5) return "just now";
1765
+ if (s < 60) return s + "s ago";
1766
+ var m = Math.round(s / 60);
1767
+ if (m < 60) return m + "m ago";
1768
+ var h = Math.round(m / 60);
1769
+ if (h < 24) return h + "h ago";
1770
+ return Math.round(h / 24) + "d ago";
1771
+ }
1772
+
1773
+ function fmtDuration(startIso, endIso) {
1774
+ var end = endIso ? new Date(endIso).getTime() : Date.now();
1775
+ var start = new Date(startIso).getTime();
1776
+ if (!isFinite(start) || !isFinite(end)) return "";
1777
+ var s = Math.max(0, Math.round((end - start) / 1000));
1778
+ if (s < 60) return s + "s";
1779
+ var m = Math.floor(s / 60);
1780
+ if (m < 60) return m + "m " + (s % 60) + "s";
1781
+ var h = Math.floor(m / 60);
1782
+ return h + "h " + (m % 60) + "m";
1783
+ }
1784
+
1785
+ function prettyArgs(raw) {
1786
+ if (raw == null) return "";
1787
+ if (typeof raw !== "string") return JSON.stringify(raw, null, 2);
1788
+ try { return JSON.stringify(JSON.parse(raw), null, 2); } catch (e) { return raw; }
1789
+ }
1790
+
1791
+ function firstLine(s, max) {
1792
+ var t = String(s == null ? "" : s).split("\\n")[0];
1793
+ return t.length > max ? t.slice(0, max) + "\\u2026" : t;
1794
+ }
1795
+
1796
+ // ============================================================
1797
+ // state
1798
+ // ============================================================
1799
+
1800
+ var sessions = [];
1801
+ var activeId = null;
1802
+ var includeArchived = false;
1803
+ var currentMeta = null;
1804
+ var lastMessageCount = -1;
1805
+ var sidebarTimer = null;
1806
+ var convoTimer = null;
1807
+
1808
+ function findSession(id) {
1809
+ for (var i = 0; i < sessions.length; i++) if (sessions[i].id === id) return sessions[i];
1810
+ return null;
1811
+ }
1812
+
1813
+ function isLive(s) {
1814
+ return !!s && (s.status === "running" || s.status === "starting");
1815
+ }
1816
+
1817
+ // ============================================================
1818
+ // sidebar
1819
+ // ============================================================
1820
+
1821
+ function displayBadge(s) {
1822
+ if (s.status === "running") {
1823
+ if (s.busy) return { label: "working", cls: "b-run" };
1824
+ if (s.awaitingInput) return { label: "waiting", cls: "b-wait" };
1825
+ return { label: "idle", cls: "b-idle" };
1826
+ }
1827
+ if (s.status === "starting") return { label: "starting", cls: "b-wait" };
1828
+ if (s.status === "error" || s.status === "killed") return { label: s.status, cls: "b-err" };
1829
+ return { label: s.status || "exited", cls: "b-end" };
1830
+ }
1831
+
1832
+ function renderSidebar() {
1833
+ var el = document.getElementById("session-list");
1834
+ if (sessions.length === 0) {
1835
+ el.innerHTML = '<div id="empty-sidebar">No sessions yet.</div>';
1836
+ return;
1837
+ }
1838
+ var html = "";
1839
+ for (var i = 0; i < sessions.length; i++) {
1840
+ var s = sessions[i];
1841
+ var label = s.label || s.title || s.name || (s.command ? String(s.command).split("/").pop() : null) || s.id.slice(0, 10);
1842
+ var db = displayBadge(s);
1843
+ var active = s.id === activeId ? " active" : "";
1844
+ html += '<div class="si' + active + '" data-id="' + escHtml(s.id) + '">'
1845
+ + '<div class="sn">' + escHtml(label) + "</div>"
1846
+ + '<div class="sm">'
1847
+ + '<span class="badge ' + db.cls + '">' + escHtml(db.label) + "</span>"
1848
+ + "<span>" + escHtml(s.kind || "") + "</span>"
1849
+ + "<span>" + escHtml(fmtRelative(s.startedAt)) + "</span>"
1850
+ + "</div></div>";
1851
+ }
1852
+ el.innerHTML = html;
1853
+ var rows = el.querySelectorAll(".si");
1854
+ for (var j = 0; j < rows.length; j++) {
1855
+ rows[j].addEventListener("click", (function (id) { return function () { selectSession(id); }; })(rows[j].getAttribute("data-id")));
1856
+ }
1857
+ }
1858
+
1859
+ function loadSessions() {
1860
+ return callApp("session_list", { kind: "all", includeArchived: includeArchived }).then(function (data) {
1861
+ sessions = (data && data.sessions) || [];
1862
+ renderSidebar();
1863
+ if (activeId) {
1864
+ var hdr = document.getElementById("header");
1865
+ if (hdr.style.display !== "none") renderHeader();
1866
+ }
1867
+ }).catch(function (e) {
1868
+ document.getElementById("status-left").textContent = "sessions: " + e.message;
1869
+ });
1870
+ }
1871
+
1872
+ // ============================================================
1873
+ // header
1874
+ // ============================================================
1875
+
1876
+ function renderHeader() {
1877
+ var s = findSession(activeId);
1878
+ var meta = currentMeta || {};
1879
+ var el = document.getElementById("header");
1880
+ el.style.display = "block";
1881
+
1882
+ var title = meta.title || (s && (s.label || s.title)) || activeId;
1883
+ var live = s && isLive(s);
1884
+ var pulse = live ? '<span class="pulse"></span>' : "";
1885
+
1886
+ var facts = [];
1887
+ if (meta.model || (s && s.model)) facts.push(escHtml(meta.model || (s && s.model)));
1888
+ if (s && s.adapterSlug) facts.push(escHtml(s.adapterSlug));
1889
+ if (s && s.kind) facts.push(escHtml(s.kind));
1890
+ if (s) { var db = displayBadge(s); facts.push(escHtml(db.label)); }
1891
+ if (s && s.cwd) facts.push('<span title="' + escHtml(s.cwd) + '">' + escHtml(s.cwd.length > 46 ? "\\u2026" + s.cwd.slice(-44) : s.cwd) + "</span>");
1892
+ var started = (meta.startedAt || (s && s.startedAt));
1893
+ if (started) facts.push("started " + escHtml(fmtClock(started)) + " (" + escHtml(fmtRelative(started)) + ")");
1894
+ var ended = meta.endedAt || (s && s.endedAt);
1895
+ if (ended) facts.push("ran " + escHtml(fmtDuration(started, ended)));
1896
+ else if (started) facts.push("running " + escHtml(fmtDuration(started)));
1897
+ if (meta.messageCount != null) facts.push(meta.messageCount + " messages");
1898
+ if (meta.toolCallCount != null) facts.push(meta.toolCallCount + " tool calls");
1899
+ if (meta.tokens && (meta.tokens.input != null || meta.tokens.output != null)) {
1900
+ facts.push("tokens " + (meta.tokens.input || 0) + "\\u2192" + (meta.tokens.output || 0));
1901
+ }
1902
+ if (meta.costUsd != null) facts.push("$" + Number(meta.costUsd).toFixed(4));
1903
+
1904
+ el.innerHTML = '<div class="htitle">' + pulse + escHtml(title) + "</div>"
1905
+ + '<div class="hfacts">' + facts.map(function (f) { return '<span class="fact">' + f + "</span>"; }).join("") + "</div>";
1906
+ }
1907
+
1908
+ // ============================================================
1909
+ // transcript
1910
+ // ============================================================
1911
+
1912
+ function renderToolCallChip(tc) {
1913
+ var args = prettyArgs(tc.args);
1914
+ return '<div class="chip tool-call"><div class="ch"><span class="ci">&#9656;</span>'
1915
+ + '<span class="cname">' + escHtml(tc.name || "tool") + "</span>"
1916
+ + '<span class="cprev">' + escHtml(firstLine(args, 64)) + "</span>"
1917
+ + '<span class="cchev">&#9656;</span></div><pre>' + escHtml(args) + "</pre></div>";
1918
+ }
1919
+
1920
+ function renderToolResultChip(m) {
1921
+ var text = m.text || "";
1922
+ return '<div class="chip tool-result"><div class="ch"><span class="ci">&#128295;</span>'
1923
+ + '<span class="cname">' + escHtml(m.toolName || "result") + "</span>"
1924
+ + '<span class="cprev">' + escHtml(firstLine(text, 64)) + "</span>"
1925
+ + '<span class="cchev">&#9656;</span></div><pre>' + escHtml(text) + "</pre></div>";
1926
+ }
1927
+
1928
+ function renderMessage(m) {
1929
+ var time = m.ts ? '<span class="mtime">' + fmtTime(m.ts) + "</span>" : "";
1930
+ if (m.role === "system") {
1931
+ return '<div class="msg role-system"><div class="bubble">' + escHtml(m.text || "") + "</div></div>";
1932
+ }
1933
+ if (m.role === "tool") {
1934
+ return '<div class="msg role-tool">' + renderToolResultChip(m) + "</div>";
1935
+ }
1936
+ if (m.role === "user") {
1937
+ return '<div class="msg role-user"><div class="mrow"><span class="mrole">You</span>' + time + "</div>"
1938
+ + '<div class="bubble">' + escHtml(m.text || "") + "</div></div>";
1939
+ }
1940
+ // assistant
1941
+ var inner = "";
1942
+ if (m.reasoning) inner += '<div class="reasoning">' + escHtml(m.reasoning) + "</div>";
1943
+ if (m.text) inner += escHtml(m.text);
1944
+ var body = inner ? '<div class="bubble">' + inner + "</div>" : "";
1945
+ var chips = (m.toolCalls || []).map(renderToolCallChip).join("");
1946
+ return '<div class="msg role-assistant"><div class="mrow"><span class="mrole">Assistant</span>' + time + "</div>"
1947
+ + body + chips + "</div>";
1948
+ }
1949
+
1950
+ function renderTranscript(messages) {
1951
+ var el = document.getElementById("transcript");
1952
+ var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
1953
+ el.style.display = "flex";
1954
+ document.getElementById("empty-main").style.display = "none";
1955
+ var errEl = document.getElementById("error-main");
1956
+ if (errEl) errEl.style.display = "none";
1957
+ if (messages.length === 0) {
1958
+ el.innerHTML = '<div style="color:var(--text2);text-align:center;padding:20px">(empty conversation)</div>';
1959
+ return;
1960
+ }
1961
+ el.innerHTML = messages.map(renderMessage).join("");
1962
+ if (atBottom) el.scrollTop = el.scrollHeight;
1963
+
1964
+ var chips = el.querySelectorAll(".chip");
1965
+ for (var i = 0; i < chips.length; i++) {
1966
+ chips[i].querySelector(".ch").addEventListener("click", (function (chip) {
1967
+ return function () { chip.classList.toggle("open"); };
1968
+ })(chips[i]));
1969
+ }
1970
+ }
1971
+
1972
+ function renderNoConversation(reason) {
1973
+ document.getElementById("transcript").style.display = "none";
1974
+ document.getElementById("empty-main").style.display = "none";
1975
+ var el = document.getElementById("error-main");
1976
+ el.style.display = "flex";
1977
+ el.innerHTML = '<div class="ico">&#128683;</div><div>No readable conversation for this session'
1978
+ + (reason ? '<br><span style="font-size:11px;color:var(--text3)">' + escHtml(reason) + "</span>" : "")
1979
+ + "</div>";
1980
+ }
1981
+
1982
+ // ============================================================
1983
+ // selection + polling
1984
+ // ============================================================
1985
+
1986
+ function clearConvoPoll() {
1987
+ if (convoTimer) { clearTimeout(convoTimer); convoTimer = null; }
1988
+ }
1989
+
1990
+ function scheduleConvoPoll() {
1991
+ clearConvoPoll();
1992
+ var s = findSession(activeId);
1993
+ if (!isLive(s)) return;
1994
+ convoTimer = setTimeout(function () {
1995
+ loadConversation(activeId, { silent: true });
1996
+ }, 3500);
1997
+ }
1998
+
1999
+ function loadConversation(id, opts) {
2000
+ opts = opts || {};
2001
+ var requestedId = id;
2002
+ if (!opts.silent) {
2003
+ document.getElementById("transcript").style.display = "none";
2004
+ document.getElementById("empty-main").style.display = "none";
2005
+ var errEl = document.getElementById("error-main");
2006
+ errEl.style.display = "flex";
2007
+ errEl.innerHTML = '<div class="ico">&#8987;</div><div>Loading\\u2026</div>';
2008
+ }
2009
+ document.getElementById("status-left").textContent = "loading\\u2026";
2010
+ return callApp("conversation_read", { idOrName: id, format: "json" }).then(function (data) {
2011
+ if (activeId !== requestedId) return; // switched away meanwhile
2012
+ if (!data || !Array.isArray(data.messages)) {
2013
+ currentMeta = null;
2014
+ renderHeader();
2015
+ renderNoConversation(data && data.reason);
2016
+ clearConvoPoll();
2017
+ document.getElementById("status-left").textContent = "no conversation";
2018
+ return;
2019
+ }
2020
+ currentMeta = data.meta || {};
2021
+ lastMessageCount = data.messages.length;
2022
+ renderHeader();
2023
+ renderTranscript(data.messages);
2024
+ document.getElementById("status-left").textContent = data.messages.length + " messages";
2025
+ document.getElementById("status-right").textContent = "updated " + fmtTime(Date.now());
2026
+ scheduleConvoPoll();
2027
+ }).catch(function (e) {
2028
+ if (activeId !== requestedId) return;
2029
+ document.getElementById("status-left").textContent = "error: " + e.message;
2030
+ if (!opts.silent) renderNoConversation(e.message);
2031
+ });
2032
+ }
2033
+
2034
+ function selectSession(id) {
2035
+ if (activeId === id) return;
2036
+ activeId = id;
2037
+ currentMeta = null;
2038
+ lastMessageCount = -1;
2039
+ clearConvoPoll();
2040
+ renderSidebar();
2041
+ document.getElementById("statusbar").style.display = "flex";
2042
+ renderHeader();
2043
+ loadConversation(id, {});
2044
+ }
2045
+
2046
+ // ============================================================
2047
+ // boot
2048
+ // ============================================================
2049
+
2050
+ document.getElementById("refresh-btn").addEventListener("click", function () {
2051
+ loadSessions();
2052
+ if (activeId) loadConversation(activeId, {});
2053
+ });
2054
+ document.getElementById("archived-check").addEventListener("change", function (e) {
2055
+ includeArchived = !!e.target.checked;
2056
+ loadSessions();
2057
+ });
2058
+
2059
+ window.McpApp.connect()
2060
+ .then(function (bridge) {
2061
+ callTool = bridge.callTool;
2062
+ return loadSessions();
2063
+ })
2064
+ .then(function () {
2065
+ sidebarTimer = setInterval(loadSessions, 5000);
2066
+ if (!activeId && sessions.length > 0) selectSession(sessions[0].id);
2067
+ })
2068
+ .catch(function (err) {
2069
+ document.getElementById("empty-main").innerHTML =
2070
+ '<div class="ico">&#9888;</div><div>Standalone mode \\u2014 no host bridge (' + escHtml(err.message) + ")</div>";
2071
+ });
2072
+ </script>
2073
+ </body>
2074
+ </html>`;
2075
+
2076
+ // src/session-viewer/index.ts
2077
+ var sessionViewer = defineApp({
2078
+ id: "@agentproto/session-viewer",
2079
+ name: "Session Viewer",
2080
+ version: "0.1.0",
2081
+ description: "Read a daemon session as a clean conversation \u2014 turns, tool calls, timestamps.",
2082
+ agents: [narrator],
2083
+ workflows: [narrateSession],
2084
+ ui: {
2085
+ html: SESSION_VIEWER_HTML,
2086
+ title: "Session Viewer",
2087
+ tools: [...SESSION_VIEWER_TOOLS]
2088
+ }
2089
+ });
887
2090
 
888
- export { codeTeam, contentTeam, mailTriage, mediaViewer };
2091
+ export { codeTeam, contentTeam, mailTriage, mediaViewer, sessionViewer };
889
2092
  //# sourceMappingURL=index.mjs.map
890
2093
  //# sourceMappingURL=index.mjs.map