@agentproto/apps 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/sync.mjs CHANGED
@@ -199,7 +199,7 @@ var triager = {
199
199
  ],
200
200
  workflows: [{ ref: "triage-inbox" }]
201
201
  }),
202
- 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."
202
+ 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.'
203
203
  };
204
204
  var triageInbox = defineWorkflow({
205
205
  id: "triage-inbox",
@@ -213,20 +213,33 @@ var triageInbox = defineWorkflow({
213
213
  id: "triage",
214
214
  kind: "agent",
215
215
  agent: { ref: "@agentproto/triager" },
216
- 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."
216
+ 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.'
217
217
  }
218
218
  ]
219
219
  });
220
220
 
221
221
  // src/mail-triage/ui.ts
222
+ var ALIAS_TOOL_NAMES = [
223
+ "mailbox_list",
224
+ "mailbox_search",
225
+ "mailbox_triage_plan",
226
+ "mailbox_triage_apply"
227
+ ];
228
+ var DEFAULT_MCP_ALIASES = ["agentpush-prod", "agentpush"];
229
+ function envAliases() {
230
+ if (typeof process === "undefined" || !process.env) return null;
231
+ const raw = process.env["MAIL_TRIAGE_MCP_ALIASES"];
232
+ if (!raw) return null;
233
+ const list = raw.split(",").map((s) => s.trim()).filter(Boolean);
234
+ return list.length > 0 ? list : null;
235
+ }
236
+ var MAIL_TRIAGE_MCP_ALIASES = envAliases() ?? [...DEFAULT_MCP_ALIASES];
222
237
  var MAIL_TRIAGE_TOOLS = [
223
- "imported:agentpush/mailbox_list",
224
- "imported:agentpush/mailbox_search",
225
- "imported:agentpush/mailbox_triage_plan",
226
- "imported:agentpush/mailbox_triage_apply",
238
+ ...MAIL_TRIAGE_MCP_ALIASES.flatMap((alias) => ALIAS_TOOL_NAMES.map((t) => `imported:${alias}/${t}`)),
227
239
  "app_run",
228
240
  "app_status",
229
- "agent_output"
241
+ "agent_output",
242
+ "app_list"
230
243
  ];
231
244
  var APP_ID = "@agentproto/mail-triage";
232
245
  var AGENT_ID = "@agentproto/triager";
@@ -246,7 +259,7 @@ var MAIL_TRIAGE_HTML = `<!DOCTYPE html>
246
259
  }
247
260
  html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}
248
261
  #app{display:flex;flex-direction:column;height:100%;overflow-y:auto}
249
- #toolbar{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2);flex-shrink:0}
262
+ #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}
250
263
  #toolbar .title{font-size:13px;font-weight:600;flex:1}
251
264
  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}
252
265
  .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}
@@ -254,37 +267,68 @@ select,input[type=text]{background:var(--bg3);border:1px solid var(--border);col
254
267
  .abtn:disabled{opacity:.4;cursor:default}
255
268
  .abtn.primary{background:var(--blue);border-color:var(--blue);color:#04101c}
256
269
  .abtn.primary:hover{opacity:.9}
270
+ #caps{padding:6px 14px;display:flex;gap:6px;align-items:center;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}
271
+ .badge{font-size:11px;padding:2px 8px;border-radius:10px;border:1px solid var(--border);color:var(--text2)}
272
+ .badge.on{color:var(--green);border-color:var(--green)}
273
+ .badge.off{opacity:.5;text-decoration:line-through}
257
274
  #content{padding:14px;display:flex;flex-direction:column;gap:16px}
258
275
  section{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:12px}
259
- section h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:10px}
276
+ 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}
260
277
  #summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}
261
278
  .tile{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:10px;text-align:center}
262
279
  .tile .count{font-size:22px;font-weight:700}
263
280
  .tile .label{font-size:11px;color:var(--text2);text-transform:capitalize}
281
+ .tile.unread .count{color:var(--green)}
264
282
  .tile.urgent .count{color:var(--red)}
265
283
  .tile.needs-reply .count{color:var(--yellow)}
266
284
  .tile.newsletter .count{color:var(--blue)}
267
285
  .tile.notification .count{color:var(--purple)}
268
- #plan-list,#search-results,#log{max-height:260px;overflow-y:auto;font-size:12px}
286
+ #plan-list,#search-results,#log-events,#runs-list,#connect-hint-list{max-height:260px;overflow-y:auto;font-size:12px}
269
287
  .plan-item{display:flex;justify-content:space-between;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)}
270
288
  .plan-item .subj{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
271
289
  .plan-item .tag{color:var(--text2);font-size:11px;white-space:nowrap}
272
- #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}
290
+ .runs-item{display:flex;gap:10px;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);cursor:pointer}
291
+ .runs-item:hover{background:var(--bg3)}
292
+ .runs-item .rid{font-family:Menlo,Monaco,monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
293
+ .runs-item .st{font-size:11px;white-space:nowrap}
294
+ .runs-item .st.running{color:var(--yellow)}
295
+ .runs-item .st.ended{color:var(--green)}
296
+ .runs-item .meta{color:var(--text2);font-size:11px;white-space:nowrap}
297
+ #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}
273
298
  .searchbar{display:flex;gap:8px;margin-bottom:8px}
274
299
  .searchbar input{flex:1}
275
300
  .actions{display:flex;gap:8px;margin-top:10px}
276
301
  #status{padding:6px 14px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
277
302
  .empty{color:var(--text2);padding:8px 0}
303
+ .mini{font-size:10px;text-transform:none;letter-spacing:0;cursor:pointer;color:var(--blue);background:none;border:none;font-family:inherit}
304
+ .caps-hint{font-size:11px;color:var(--yellow)}
305
+ .snippet{color:var(--text2);font-size:11px;margin-top:2px}
306
+ .mail-checkbox{accent-color:var(--blue);cursor:pointer;flex-shrink:0;margin-top:1px}
307
+ .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}
308
+ .mail-open:hover{text-decoration:underline}
309
+ #selection-bar{display:none;margin-bottom:8px;gap:8px;align-items:center}
310
+ #selection-bar.show{display:flex}
278
311
  </style>
279
312
  </head>
280
313
  <body>
281
314
  <div id="app">
282
315
  <div id="toolbar">
283
316
  <span class="title">Mail Triage</span>
317
+ <select id="alias-select" style="display:none" title="MCP server"></select>
284
318
  <select id="mailbox-select"></select>
285
319
  <button class="abtn" id="refresh-btn">Refresh</button>
286
320
  </div>
321
+ <div id="caps" style="display:none"></div>
287
322
  <div id="content">
323
+ <section id="connect-hint" style="display:none">
324
+ <h3>Connect a mailbox</h3>
325
+ <div id="connect-hint-list"></div>
326
+ <div class="empty">Import/start an agentpush MCP server and connect a Gmail account in agentpush, then retry.</div>
327
+ <div class="actions">
328
+ <button class="abtn primary" id="connect-retry-btn">Retry</button>
329
+ </div>
330
+ </section>
331
+
288
332
  <section>
289
333
  <h3>Inbox summary</h3>
290
334
  <div id="summary"></div>
@@ -296,16 +340,30 @@ section h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spa
296
340
  <input type="text" id="search-input" placeholder="e.g. is:unread">
297
341
  <button class="abtn" id="search-btn">Search</button>
298
342
  </div>
343
+ <div id="selection-bar">
344
+ <span id="sel-count" style="font-size:12px;color:var(--text2)">0 selected</span>
345
+ <button class="abtn primary" data-action="send-selection">Send selection to Claude</button>
346
+ </div>
299
347
  <div id="search-results"><div class="empty">No search run yet.</div></div>
300
348
  </section>
301
349
 
302
350
  <section>
303
351
  <h3>Triage plan</h3>
352
+ <div class="searchbar">
353
+ <input type="text" id="plan-query" placeholder="criteria query" value="is:unread category:promotions older_than:7d">
354
+ <select id="plan-action">
355
+ <option value="markRead">mark read</option>
356
+ <option value="archive">archive</option>
357
+ <option value="label">label&#8230;</option>
358
+ <option value="trash">trash</option>
359
+ </select>
360
+ <input type="text" id="plan-label" placeholder="label name" style="display:none">
361
+ </div>
304
362
  <div class="actions">
305
- <button class="abtn primary" id="triage-btn">Triage</button>
363
+ <button class="abtn primary" id="triage-btn">Build plan</button>
306
364
  <button class="abtn" id="apply-btn" disabled>Apply</button>
307
365
  </div>
308
- <div id="plan-list" style="margin-top:10px"><div class="empty">No plan yet \u2014 click Triage.</div></div>
366
+ <div id="plan-list" style="margin-top:10px"><div class="empty">No plan yet \u2014 click Build plan.</div></div>
309
367
  </section>
310
368
 
311
369
  <section>
@@ -313,7 +371,13 @@ section h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spa
313
371
  <div class="actions">
314
372
  <button class="abtn" id="run-agent-btn">Run Triager Agent</button>
315
373
  </div>
316
- <div id="log" style="margin-top:10px"><div class="empty">Agent has not run yet.</div></div>
374
+ <div id="log-events" style="margin-top:10px"><div class="empty">Agent has not run yet.</div></div>
375
+ <div id="log-tail" style="margin-top:6px"></div>
376
+ </section>
377
+
378
+ <section>
379
+ <h3><span>Past runs</span><button class="mini" id="runs-refresh-btn">refresh</button></h3>
380
+ <div id="runs-list"><div class="empty">No runs yet.</div></div>
317
381
  </section>
318
382
  </div>
319
383
  <div id="status">Connecting&#8230;</div>
@@ -322,9 +386,21 @@ section h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spa
322
386
  var APP_ID = ${JSON.stringify(APP_ID)};
323
387
  var AGENT_ID = ${JSON.stringify(AGENT_ID)};
324
388
  var CATEGORIES = ${JSON.stringify(CATEGORIES)};
389
+ var ALIASES = ${JSON.stringify(MAIL_TRIAGE_MCP_ALIASES)};
390
+ var POLL_TIMEOUT_MS = 10 * 60 * 1000;
325
391
  var callTool = null;
392
+ var updateModelContext = null;
393
+ var openLink = null;
394
+ var onTeardown = null;
326
395
  var currentPlan = null;
327
396
  var pollHandle = null;
397
+ var planExpiryHandle = null;
398
+ var activeAlias = ALIASES[0];
399
+ var currentMailboxes = [];
400
+ var categoryCounts = null;
401
+ var unreadCount = null;
402
+ var aliasStates = {};
403
+ var selectedMessages = {};
328
404
 
329
405
  function setStatus(msg) {
330
406
  document.getElementById("status").textContent = msg;
@@ -354,7 +430,125 @@ function unwrapText(result) {
354
430
  }
355
431
 
356
432
  function callApp(tool, args) {
357
- return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);
433
+ if (!callTool) {
434
+ return Promise.reject(new Error("Not connected to host bridge (standalone mode)."));
435
+ }
436
+ return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(function (result) {
437
+ var value = unwrapText(result);
438
+ if (value && typeof value === "object" && !Array.isArray(value) && typeof value.error === "string") {
439
+ throw new Error(value.error);
440
+ }
441
+ return value;
442
+ });
443
+ }
444
+
445
+ function aliasTool(name) {
446
+ return "imported:" + activeAlias + "/" + name;
447
+ }
448
+
449
+ // Disables a button and swaps its label to an ellipsis while a call is in
450
+ // flight, so double-submit is impossible; restores the original label after.
451
+ function setBusy(btn, busy) {
452
+ if (busy) {
453
+ if (btn.getAttribute("data-label") === null) btn.setAttribute("data-label", btn.textContent);
454
+ btn.disabled = true;
455
+ btn.textContent = "\\u2026";
456
+ } else {
457
+ btn.disabled = false;
458
+ var label = btn.getAttribute("data-label");
459
+ if (label !== null) btn.textContent = label;
460
+ }
461
+ }
462
+
463
+ function isSelected(id) {
464
+ return selectedMessages[id] ? true : false;
465
+ }
466
+
467
+ function toggleSelection(id, item) {
468
+ if (selectedMessages[id]) {
469
+ delete selectedMessages[id];
470
+ } else {
471
+ selectedMessages[id] = item;
472
+ }
473
+ updateSelectionBar();
474
+ }
475
+
476
+ function updateSelectionBar() {
477
+ var bar = document.getElementById("selection-bar");
478
+ var countEl = document.getElementById("sel-count");
479
+ var count = 0;
480
+ for (var k in selectedMessages) { if (selectedMessages.hasOwnProperty(k)) count++; }
481
+ countEl.textContent = count + " selected";
482
+ bar.className = count > 0 ? "show" : "";
483
+ // Sync checkboxes
484
+ var cbs = document.querySelectorAll("#search-results .mail-checkbox");
485
+ for (var i = 0; i < cbs.length; i++) {
486
+ var mid = cbs[i].getAttribute("data-message-id");
487
+ cbs[i].checked = mid ? isSelected(mid) : false;
488
+ }
489
+ }
490
+
491
+ function sendSelectionToClaude() {
492
+ if (!updateModelContext) {
493
+ setStatus("Host does not support updateModelContext.");
494
+ return;
495
+ }
496
+ var items = [];
497
+ var names = [];
498
+ for (var k in selectedMessages) {
499
+ if (!selectedMessages.hasOwnProperty(k)) continue;
500
+ var msg = selectedMessages[k];
501
+ items.push({
502
+ id: msg.id || k,
503
+ threadId: msg.threadId || "",
504
+ from: msg.from || "",
505
+ subject: msg.subject || "",
506
+ timestamp: msg.internalDate || msg.date || ""
507
+ });
508
+ if (msg.subject) names.push(msg.subject);
509
+ }
510
+ if (items.length === 0) {
511
+ setStatus("No messages selected.");
512
+ return;
513
+ }
514
+ var summary = items.length + " emails selected for triage: " + names.join(", ");
515
+ setStatus("Sending selection to Claude\\u2026");
516
+ updateModelContext({
517
+ content: [{ type: "text", text: summary }],
518
+ structuredContent: { selection: items }
519
+ }).then(function () {
520
+ setStatus("Selection sent to Claude (" + items.length + " emails).");
521
+ }).catch(function (err) {
522
+ setStatus("Failed to send selection: " + (err && err.message ? err.message : String(err)));
523
+ });
524
+ }
525
+
526
+ function clearPlanExpiry() {
527
+ if (planExpiryHandle) {
528
+ clearTimeout(planExpiryHandle);
529
+ planExpiryHandle = null;
530
+ }
531
+ }
532
+
533
+ function onPlanExpired() {
534
+ planExpiryHandle = null;
535
+ currentPlan = null;
536
+ document.getElementById("apply-btn").disabled = true;
537
+ setStatus("Plan expired \\u2014 rebuild it.");
538
+ }
539
+
540
+ // Arms a timeout that invalidates the plan at its server-declared expires_at
541
+ // (plans expire after 15 minutes) so Apply never fires against a stale plan.
542
+ function armPlanExpiry(expiresAt) {
543
+ clearPlanExpiry();
544
+ if (!expiresAt) return;
545
+ var ms = new Date(expiresAt).getTime() - Date.now();
546
+ if (isNaN(ms)) return;
547
+ if (ms <= 0) {
548
+ onPlanExpired();
549
+ return;
550
+ }
551
+ planExpiryHandle = setTimeout(onPlanExpired, ms);
358
552
  }
359
553
 
360
554
  function asList(value, keys) {
@@ -367,145 +561,593 @@ function asList(value, keys) {
367
561
  return [];
368
562
  }
369
563
 
370
- function renderSummary(counts) {
564
+ function renderSummary() {
371
565
  var el = document.getElementById("summary");
372
566
  el.innerHTML = "";
373
- CATEGORIES.forEach(function (cat) {
567
+ var tiles = [{ key: "unread", count: unreadCount }].concat(
568
+ CATEGORIES.map(function (cat) {
569
+ return { key: cat, count: categoryCounts ? categoryCounts[cat] : undefined };
570
+ })
571
+ );
572
+ tiles.forEach(function (t) {
374
573
  var tile = document.createElement("div");
375
- tile.className = "tile " + cat;
376
- var count = counts && counts[cat] !== undefined ? counts[cat] : "\u2013";
574
+ tile.className = "tile " + t.key;
377
575
  tile.innerHTML = '<div class="count"></div><div class="label"></div>';
378
- tile.querySelector(".count").textContent = String(count);
379
- tile.querySelector(".label").textContent = cat.replace("-", " ");
576
+ tile.querySelector(".count").textContent = t.count === undefined || t.count === null ? "\\u2013" : String(t.count);
577
+ tile.querySelector(".label").textContent = t.key.replace("-", " ");
380
578
  el.appendChild(tile);
381
579
  });
382
580
  }
383
581
 
384
- function loadMailboxes() {
385
- setStatus("Loading mailboxes\\u2026");
386
- return callApp("imported:agentpush/mailbox_list", {})
582
+ function mailboxId(mb) {
583
+ if (typeof mb === "string") return mb;
584
+ return mb.id || mb.name || JSON.stringify(mb);
585
+ }
586
+
587
+ function mailboxText(mb) {
588
+ if (typeof mb === "string") return mb;
589
+ var name = mb.address || mb.label || mb.name || mb.id || JSON.stringify(mb);
590
+ var caps = mb.capabilities;
591
+ if (caps && typeof caps === "object") {
592
+ return name + (caps.triage ? " \\u2014 triage \\u2713" : " \\u2014 read-only");
593
+ }
594
+ return name;
595
+ }
596
+
597
+ function selectedMailbox() {
598
+ var id = document.getElementById("mailbox-select").value;
599
+ for (var i = 0; i < currentMailboxes.length; i++) {
600
+ if (mailboxId(currentMailboxes[i]) === id) return currentMailboxes[i];
601
+ }
602
+ return null;
603
+ }
604
+
605
+ function renderCaps(mb) {
606
+ var el = document.getElementById("caps");
607
+ var caps = mb && typeof mb === "object" ? mb.capabilities : null;
608
+ if (!caps || typeof caps !== "object") {
609
+ el.style.display = "none";
610
+ return;
611
+ }
612
+ el.style.display = "flex";
613
+ el.innerHTML = "";
614
+ ["read", "send", "triage", "compose"].forEach(function (cap) {
615
+ var b = document.createElement("span");
616
+ b.className = "badge " + (caps[cap] ? "on" : "off");
617
+ b.textContent = cap;
618
+ el.appendChild(b);
619
+ });
620
+ if (!caps.triage) {
621
+ var hint = document.createElement("span");
622
+ hint.className = "caps-hint";
623
+ hint.textContent = "triage scope missing \\u2014 grant it via agentpush (mailbox_request_elevation)";
624
+ el.appendChild(hint);
625
+ }
626
+ }
627
+
628
+ // Gmail category labels -> the dashboard's triage buckets. IMPORTANT wins,
629
+ // so an important promo counts as urgent, not newsletter.
630
+ function categoryOf(labelIds) {
631
+ var l = labelIds || [];
632
+ if (l.indexOf("IMPORTANT") !== -1) return "urgent";
633
+ if (l.indexOf("CATEGORY_PROMOTIONS") !== -1) return "newsletter";
634
+ if (l.indexOf("CATEGORY_UPDATES") !== -1 || l.indexOf("CATEGORY_SOCIAL") !== -1 || l.indexOf("CATEGORY_FORUMS") !== -1) return "notification";
635
+ return "needs-reply";
636
+ }
637
+
638
+ function onMailboxChange() {
639
+ clearPlanExpiry();
640
+ var mb = selectedMailbox();
641
+ renderCaps(mb);
642
+ var caps = mb && typeof mb === "object" ? mb.capabilities : null;
643
+ var noTriage = !!(caps && typeof caps === "object" && !caps.triage);
644
+ var scopeHint = noTriage ? "Mailbox lacks triage capability \\u2014 grant it via agentpush (mailbox_request_elevation)" : "";
645
+ document.getElementById("triage-btn").disabled = noTriage;
646
+ document.getElementById("triage-btn").title = scopeHint;
647
+ document.getElementById("apply-btn").disabled = true;
648
+ document.getElementById("apply-btn").title = scopeHint;
649
+ currentPlan = null;
650
+ categoryCounts = null;
651
+ unreadCount = null;
652
+ renderSummary();
653
+ if (!mb) return;
654
+ callApp(aliasTool("mailbox_search"), { mailbox: mailboxId(mb), query: "is:unread", limit: 50 })
387
655
  .then(function (result) {
388
- var mailboxes = asList(result, ["mailboxes", "items"]);
389
- var select = document.getElementById("mailbox-select");
390
- select.innerHTML = "";
391
- if (mailboxes.length === 0) {
392
- var opt = document.createElement("option");
393
- opt.textContent = "inbox";
394
- opt.value = "inbox";
395
- select.appendChild(opt);
656
+ var items = asList(result, ["items", "messages", "results"]);
657
+ if (result && typeof result === "object" && typeof result.resultSizeEstimate === "number") {
658
+ unreadCount = result.resultSizeEstimate;
396
659
  } else {
397
- mailboxes.forEach(function (mb) {
398
- var name = typeof mb === "string" ? mb : mb.name || mb.id || JSON.stringify(mb);
399
- var opt = document.createElement("option");
400
- opt.value = name;
401
- opt.textContent = name;
402
- select.appendChild(opt);
403
- });
660
+ unreadCount = items.length;
404
661
  }
405
- setStatus("Ready");
662
+ // Category breakdown over the newest page of unread \u2014 a sample when
663
+ // the mailbox has more unread than one page.
664
+ var counts = {};
665
+ items.forEach(function (item) {
666
+ var cat = categoryOf(item && item.labelIds);
667
+ counts[cat] = (counts[cat] || 0) + 1;
668
+ });
669
+ categoryCounts = items.length > 0 ? counts : null;
670
+ renderSummary();
671
+ })
672
+ .catch(function (err) {
673
+ // KPI is best-effort: keep the dashes, but say why instead of silence.
674
+ setStatus("Inbox summary unavailable: " + err.message);
675
+ });
676
+ }
677
+
678
+ function renderMailboxes(mailboxes) {
679
+ currentMailboxes = mailboxes;
680
+ var select = document.getElementById("mailbox-select");
681
+ select.innerHTML = "";
682
+ if (mailboxes.length === 0) {
683
+ var opt = document.createElement("option");
684
+ opt.textContent = "no mailboxes";
685
+ opt.value = "";
686
+ select.appendChild(opt);
687
+ renderCaps(null);
688
+ return;
689
+ }
690
+ mailboxes.forEach(function (mb) {
691
+ var opt = document.createElement("option");
692
+ opt.value = mailboxId(mb);
693
+ opt.textContent = mailboxText(mb);
694
+ select.appendChild(opt);
695
+ });
696
+ onMailboxChange();
697
+ }
698
+
699
+ function renderAliasSelect(liveAliases) {
700
+ var select = document.getElementById("alias-select");
701
+ select.innerHTML = "";
702
+ var shown = liveAliases.length > 0 ? liveAliases : ALIASES;
703
+ shown.forEach(function (alias) {
704
+ var opt = document.createElement("option");
705
+ opt.value = alias;
706
+ opt.textContent = alias;
707
+ select.appendChild(opt);
708
+ });
709
+ select.value = activeAlias;
710
+ select.style.display = shown.length > 1 ? "" : "none";
711
+ }
712
+
713
+ function loadMailboxes() {
714
+ setStatus("Loading mailboxes from " + activeAlias + "\\u2026");
715
+ return callApp(aliasTool("mailbox_list"), {})
716
+ .then(function (result) {
717
+ renderMailboxes(asList(result, ["mailboxes", "items"]));
718
+ setStatus("Ready \\u2014 " + activeAlias);
406
719
  })
407
720
  .catch(function (err) {
408
721
  setStatus("Error loading mailboxes: " + err.message);
409
722
  });
410
723
  }
411
724
 
412
- function renderSearchResults(items) {
725
+ function renderConnectHint() {
726
+ var el = document.getElementById("connect-hint");
727
+ var anyLive = false;
728
+ for (var i = 0; i < ALIASES.length; i++) {
729
+ if (aliasStates[ALIASES[i]] && aliasStates[ALIASES[i]].state === "live") anyLive = true;
730
+ }
731
+ if (anyLive) {
732
+ el.style.display = "none";
733
+ return;
734
+ }
735
+ el.style.display = "";
736
+ var list = document.getElementById("connect-hint-list");
737
+ list.innerHTML = "";
738
+ ALIASES.forEach(function (alias) {
739
+ var st = aliasStates[alias];
740
+ var row = document.createElement("div");
741
+ row.className = "plan-item";
742
+ var text;
743
+ if (!st) text = alias + " \\u2014 not probed yet";
744
+ else if (st.state === "unreachable") text = alias + " \\u2014 unreachable: " + st.message;
745
+ else if (st.state === "empty") text = alias + " \\u2014 connected, no mailboxes";
746
+ else text = alias + " \\u2014 live";
747
+ row.textContent = text;
748
+ list.appendChild(row);
749
+ });
750
+ }
751
+
752
+ function probeAliases() {
753
+ setStatus("Detecting mail servers\\u2026");
754
+ return Promise.all(
755
+ ALIASES.map(function (alias) {
756
+ return callApp("imported:" + alias + "/mailbox_list", {})
757
+ .then(function (result) {
758
+ var mailboxes = asList(result, ["mailboxes", "items"]);
759
+ aliasStates[alias] = { state: mailboxes.length > 0 ? "live" : "empty" };
760
+ return { alias: alias, mailboxes: mailboxes };
761
+ })
762
+ .catch(function (err) {
763
+ aliasStates[alias] = { state: "unreachable", message: err && err.message ? err.message : String(err) };
764
+ return { alias: alias, mailboxes: [] };
765
+ });
766
+ })
767
+ ).then(function (results) {
768
+ var live = results.filter(function (r) { return r.mailboxes.length > 0; });
769
+ if (live.length > 0) {
770
+ activeAlias = live[0].alias;
771
+ renderAliasSelect(live.map(function (r) { return r.alias; }));
772
+ renderMailboxes(live[0].mailboxes);
773
+ renderConnectHint();
774
+ setStatus("Ready \\u2014 " + activeAlias + (live.length > 1 ? " (" + live.length + " servers available)" : ""));
775
+ } else {
776
+ renderAliasSelect([]);
777
+ renderMailboxes([]);
778
+ renderConnectHint();
779
+ setStatus("No mailboxes reachable \\u2014 see connection panel below.");
780
+ }
781
+ });
782
+ }
783
+
784
+ function renderSearchResults(items, raw) {
413
785
  var el = document.getElementById("search-results");
414
786
  el.innerHTML = "";
787
+ if (raw && typeof raw === "object" && typeof raw.resultSizeEstimate === "number") {
788
+ var count = document.createElement("div");
789
+ count.className = "empty";
790
+ count.textContent = items.length + " shown \\u00b7 ~" + raw.resultSizeEstimate + " total";
791
+ el.appendChild(count);
792
+ }
415
793
  if (items.length === 0) {
416
- el.innerHTML = '<div class="empty">No results.</div>';
794
+ var none = document.createElement("div");
795
+ none.className = "empty";
796
+ none.textContent = "No results.";
797
+ el.appendChild(none);
417
798
  return;
418
799
  }
419
800
  items.forEach(function (item) {
420
801
  var row = document.createElement("div");
421
802
  row.className = "plan-item";
803
+ row.style.flexDirection = "column";
804
+ row.style.alignItems = "stretch";
805
+ var msgId = item && (item.id || item.messageId || "");
806
+ var threadId = item && (item.threadId || "");
422
807
  var subj = typeof item === "string" ? item : item.subject || item.title || JSON.stringify(item);
423
808
  var from = item && item.from ? item.from : "";
424
- row.innerHTML = '<span class="subj"></span><span class="tag"></span>';
425
- row.querySelector(".subj").textContent = subj;
426
- row.querySelector(".tag").textContent = from;
809
+ var snippet = item && item.snippet ? item.snippet : "";
810
+ var top = document.createElement("div");
811
+ top.style.display = "flex";
812
+ top.style.alignItems = "flex-start";
813
+ top.style.gap = "6px";
814
+ var cb = document.createElement("input");
815
+ cb.type = "checkbox";
816
+ cb.className = "mail-checkbox";
817
+ cb.setAttribute("data-action", "toggle-select");
818
+ if (msgId) cb.setAttribute("data-message-id", msgId);
819
+ if (isSelected(msgId)) cb.checked = true;
820
+ top.appendChild(cb);
821
+ var subjSpan = document.createElement("span");
822
+ subjSpan.className = "subj";
823
+ subjSpan.style.flex = "1";
824
+ subjSpan.textContent = subj;
825
+ top.appendChild(subjSpan);
826
+ var tagSpan = document.createElement("span");
827
+ tagSpan.className = "tag";
828
+ tagSpan.textContent = from;
829
+ top.appendChild(tagSpan);
830
+ if (threadId) {
831
+ var openBtn = document.createElement("button");
832
+ openBtn.className = "mail-open";
833
+ openBtn.textContent = "Open";
834
+ openBtn.setAttribute("data-action", "open-gmail");
835
+ openBtn.setAttribute("data-thread-id", threadId);
836
+ top.appendChild(openBtn);
837
+ }
838
+ row.appendChild(top);
839
+ if (snippet) {
840
+ var sn = document.createElement("div");
841
+ sn.className = "snippet";
842
+ sn.textContent = snippet;
843
+ row.appendChild(sn);
844
+ }
427
845
  el.appendChild(row);
428
846
  });
429
847
  }
430
848
 
849
+ // Renders a mailbox_triage_plan result: { plan_id, action, count, sample,
850
+ // truncated, expires_at }. The sample rows are search-shaped messages.
431
851
  function renderPlan(plan) {
432
- var items = asList(plan, ["actions", "items", "plan"]);
433
852
  var el = document.getElementById("plan-list");
434
853
  el.innerHTML = "";
435
- if (items.length === 0) {
436
- el.innerHTML = '<div class="empty">Plan is empty.</div>';
854
+ if (!plan || typeof plan !== "object" || !plan.plan_id) {
855
+ el.innerHTML = '<div class="empty">Plan failed \\u2014 unexpected response.</div>';
437
856
  return;
438
857
  }
439
- var counts = {};
440
- items.forEach(function (item) {
441
- var cat = (item && (item.category || item.tag)) || "notification";
442
- counts[cat] = (counts[cat] || 0) + 1;
858
+ var head = document.createElement("div");
859
+ head.className = "plan-item";
860
+ head.innerHTML = '<span class="subj"></span><span class="tag"></span>';
861
+ head.querySelector(".subj").textContent =
862
+ plan.count + " message" + (plan.count === 1 ? "" : "s") + " \\u2192 " + (plan.action && plan.action.type);
863
+ head.querySelector(".tag").textContent = plan.expires_at ? "expires " + fmtDate(plan.expires_at) : "";
864
+ el.appendChild(head);
865
+ var sample = asList(plan, ["sample"]);
866
+ sample.forEach(function (item) {
443
867
  var row = document.createElement("div");
444
868
  row.className = "plan-item";
445
- var subj = (item && (item.subject || item.title)) || JSON.stringify(item);
446
- var action = (item && (item.action || item.category)) || "";
447
869
  row.innerHTML = '<span class="subj"></span><span class="tag"></span>';
448
- row.querySelector(".subj").textContent = subj;
449
- row.querySelector(".tag").textContent = action;
870
+ row.querySelector(".subj").textContent = (item && (item.subject || item.title)) || JSON.stringify(item);
871
+ row.querySelector(".tag").textContent = (item && item.from) || "";
450
872
  el.appendChild(row);
451
873
  });
452
- renderSummary(counts);
874
+ if (plan.truncated) {
875
+ var more = document.createElement("div");
876
+ more.className = "empty";
877
+ more.textContent = "\\u2026sample truncated \\u2014 " + plan.count + " total.";
878
+ el.appendChild(more);
879
+ }
880
+ }
881
+
882
+ function fmtDate(iso) {
883
+ if (!iso) return "";
884
+ var d = new Date(iso);
885
+ return isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
886
+ }
887
+
888
+ function renderRuns(runs) {
889
+ var el = document.getElementById("runs-list");
890
+ el.innerHTML = "";
891
+ if (runs.length === 0) {
892
+ el.innerHTML = '<div class="empty">No runs yet.</div>';
893
+ return;
894
+ }
895
+ var shown = runs.slice(0, 20);
896
+ shown.forEach(function (run) {
897
+ var row = document.createElement("div");
898
+ row.className = "runs-item";
899
+ row.title = "Show sessions + output for " + run.appRunId;
900
+ row.innerHTML = '<span class="st"></span><span class="rid"></span><span class="meta"></span>';
901
+ var st = row.querySelector(".st");
902
+ st.textContent = run.status || "?";
903
+ st.className = "st " + (run.status || "");
904
+ row.querySelector(".rid").textContent = run.appRunId;
905
+ row.querySelector(".meta").textContent =
906
+ fmtDate(run.startedAt) + (run.endedAt ? " \\u2192 " + fmtDate(run.endedAt) : "") + " \\u00b7 " + (run.sessions || 0) + " session" + (run.sessions === 1 ? "" : "s");
907
+ row.addEventListener("click", function () { viewRun(run.appRunId); });
908
+ el.appendChild(row);
909
+ });
910
+ if (runs.length > 20) {
911
+ var more = document.createElement("div");
912
+ more.className = "empty";
913
+ more.textContent = "showing 20 of " + runs.length;
914
+ el.appendChild(more);
915
+ }
916
+ }
917
+
918
+ function loadRuns() {
919
+ return callApp("app_list", {})
920
+ .then(function (result) {
921
+ var apps = Array.isArray(result) ? result : [];
922
+ var mine = null;
923
+ for (var i = 0; i < apps.length; i++) {
924
+ if (apps[i] && apps[i].appId === APP_ID) { mine = apps[i]; break; }
925
+ }
926
+ var runs = (mine && Array.isArray(mine.runs) ? mine.runs.slice() : []).sort(function (a, b) {
927
+ return String(b.startedAt || "").localeCompare(String(a.startedAt || ""));
928
+ });
929
+ renderRuns(runs);
930
+ })
931
+ .catch(function (err) {
932
+ setStatus("Could not load runs: " + err.message);
933
+ });
934
+ }
935
+
936
+ function viewRun(appRunId) {
937
+ setStatus("Loading run " + appRunId + "\\u2026");
938
+ callApp("app_status", { appRunId: appRunId })
939
+ .then(function (status) {
940
+ var sessions = (status && status.sessions) || [];
941
+ appendLog("\\u2500\\u2500 run " + appRunId + " (" + (status && status.status) + ") \\u2500\\u2500");
942
+ if (sessions.length === 0) {
943
+ appendLog("(no sessions)");
944
+ setStatus("Ready \\u2014 " + activeAlias);
945
+ return;
946
+ }
947
+ sessions.forEach(function (s) {
948
+ appendLog("session " + s.sessionId + " (" + (s.agentId || "?") + ")");
949
+ });
950
+ return callApp("agent_output", { sessionId: sessions[0].sessionId, lastN: 200, clean: true })
951
+ .then(function (output) {
952
+ if (output) appendLog(typeof output === "string" ? output : JSON.stringify(output, null, 2));
953
+ setStatus("Ready \\u2014 " + activeAlias);
954
+ });
955
+ })
956
+ .catch(function (err) {
957
+ setStatus("Run status failed: " + err.message);
958
+ });
453
959
  }
454
960
 
455
- document.getElementById("refresh-btn").addEventListener("click", loadMailboxes);
961
+ // Event delegation: search-results (checkboxes + open buttons)
962
+ document.getElementById("search-results").addEventListener("change", function (e) {
963
+ var target = e.target;
964
+ if (target.getAttribute("data-action") === "toggle-select") {
965
+ var mid = target.getAttribute("data-message-id");
966
+ if (mid) {
967
+ var item = null;
968
+ var el = target.parentNode;
969
+ while (el && el.parentNode && el !== document) {
970
+ if (el.tagName === "DIV" && el.className.indexOf("plan-item") !== -1) {
971
+ var subjEl = el.querySelector(".subj");
972
+ var tagEl = el.querySelector(".tag");
973
+ var subjText = subjEl ? subjEl.textContent : "";
974
+ var tagText = tagEl ? tagEl.textContent : "";
975
+ // Reconstruct a minimal item from the DOM
976
+ item = { id: mid, threadId: "", subject: subjText, from: tagText };
977
+ break;
978
+ }
979
+ el = el.parentNode;
980
+ }
981
+ toggleSelection(mid, item || { id: mid });
982
+ }
983
+ }
984
+ });
985
+ document.getElementById("search-results").addEventListener("click", function (e) {
986
+ var target = e.target;
987
+ if (target.getAttribute("data-action") === "open-gmail") {
988
+ var tid = target.getAttribute("data-thread-id");
989
+ if (tid && openLink) {
990
+ openLink("https://mail.google.com/mail/u/0/#inbox/" + tid).catch(function (err) {
991
+ setStatus("Failed to open link: " + (err && err.message ? err.message : String(err)));
992
+ });
993
+ } else if (tid) {
994
+ setStatus("Host does not support openLink.");
995
+ }
996
+ }
997
+ });
998
+ // Event delegation: selection-bar (send-selection)
999
+ document.getElementById("selection-bar").addEventListener("click", function (e) {
1000
+ if (e.target.getAttribute("data-action") === "send-selection") {
1001
+ sendSelectionToClaude();
1002
+ }
1003
+ });
1004
+
1005
+ document.getElementById("refresh-btn").addEventListener("click", function () {
1006
+ probeAliases();
1007
+ loadRuns();
1008
+ });
1009
+
1010
+ document.getElementById("connect-retry-btn").addEventListener("click", function () {
1011
+ probeAliases();
1012
+ loadRuns();
1013
+ });
1014
+
1015
+ document.getElementById("alias-select").addEventListener("change", function () {
1016
+ activeAlias = this.value;
1017
+ clearPlanExpiry();
1018
+ currentPlan = null;
1019
+ document.getElementById("apply-btn").disabled = true;
1020
+ loadMailboxes();
1021
+ });
1022
+
1023
+ document.getElementById("mailbox-select").addEventListener("change", onMailboxChange);
1024
+
1025
+ document.getElementById("runs-refresh-btn").addEventListener("click", loadRuns);
456
1026
 
457
1027
  document.getElementById("search-btn").addEventListener("click", function () {
458
- var query = document.getElementById("search-input").value;
1028
+ var btn = this;
1029
+ var query = document.getElementById("search-input").value.trim() || "is:unread";
459
1030
  var mailbox = document.getElementById("mailbox-select").value;
1031
+ setBusy(btn, true);
460
1032
  setStatus("Searching\\u2026");
461
- callApp("imported:agentpush/mailbox_search", { mailbox: mailbox, query: query })
1033
+ callApp(aliasTool("mailbox_search"), { mailbox: mailbox, query: query })
462
1034
  .then(function (result) {
463
- renderSearchResults(asList(result, ["messages", "items", "results"]));
464
- setStatus("Ready");
1035
+ renderSearchResults(asList(result, ["items", "messages", "results"]), result);
1036
+ setBusy(btn, false);
1037
+ setStatus("Ready \\u2014 " + activeAlias);
465
1038
  })
466
1039
  .catch(function (err) {
1040
+ setBusy(btn, false);
467
1041
  setStatus("Search failed: " + err.message);
468
1042
  });
469
1043
  });
470
1044
 
1045
+ document.getElementById("plan-action").addEventListener("change", function () {
1046
+ document.getElementById("plan-label").style.display = this.value === "label" ? "" : "none";
1047
+ });
1048
+
471
1049
  document.getElementById("triage-btn").addEventListener("click", function () {
1050
+ var btn = this;
472
1051
  var mailbox = document.getElementById("mailbox-select").value;
1052
+ var query = document.getElementById("plan-query").value.trim() || "is:unread";
1053
+ var actionType = document.getElementById("plan-action").value;
1054
+ var action = { type: actionType };
1055
+ if (actionType === "label") {
1056
+ var label = document.getElementById("plan-label").value.trim();
1057
+ if (!label) {
1058
+ setStatus("Enter a label name for the label action.");
1059
+ return;
1060
+ }
1061
+ action.addLabelIds = [label];
1062
+ }
1063
+ setBusy(btn, true);
473
1064
  setStatus("Building triage plan\\u2026");
474
1065
  document.getElementById("apply-btn").disabled = true;
475
- callApp("imported:agentpush/mailbox_triage_plan", { mailbox: mailbox })
1066
+ clearPlanExpiry();
1067
+ currentPlan = null;
1068
+ callApp(aliasTool("mailbox_triage_plan"), { mailbox: mailbox, criteria: { query: query }, action: action })
476
1069
  .then(function (plan) {
477
- currentPlan = plan;
1070
+ setBusy(btn, false);
1071
+ currentPlan = plan && plan.plan_id ? plan : null;
478
1072
  renderPlan(plan);
479
- document.getElementById("apply-btn").disabled = false;
480
- setStatus("Plan ready \\u2014 review, then Apply.");
1073
+ if (currentPlan && plan.count > 0) {
1074
+ document.getElementById("apply-btn").disabled = false;
1075
+ armPlanExpiry(plan.expires_at);
1076
+ setStatus("Plan ready (" + plan.count + " messages) \\u2014 review, then Apply.");
1077
+ } else if (currentPlan) {
1078
+ setStatus("Plan matches no messages \\u2014 nothing to apply.");
1079
+ } else {
1080
+ setStatus("Triage plan failed \\u2014 see plan panel.");
1081
+ }
481
1082
  })
482
1083
  .catch(function (err) {
483
- setStatus("Triage plan failed: " + err.message);
1084
+ setBusy(btn, false);
1085
+ var msg = err && err.message ? err.message : String(err);
1086
+ if (msg.indexOf("mailbox_scope_missing") !== -1) {
1087
+ setStatus("Triage scope missing \\u2014 grant it via agentpush (mailbox_request_elevation).");
1088
+ } else {
1089
+ setStatus("Triage plan failed: " + msg);
1090
+ }
484
1091
  });
485
1092
  });
486
1093
 
487
1094
  document.getElementById("apply-btn").addEventListener("click", function () {
488
- if (!currentPlan) return;
1095
+ var btn = this;
1096
+ if (!currentPlan || !currentPlan.plan_id) return;
1097
+ setBusy(btn, true);
489
1098
  setStatus("Applying triage plan\\u2026");
490
- callApp("imported:agentpush/mailbox_triage_apply", { plan: currentPlan })
491
- .then(function () {
492
- setStatus("Plan applied.");
1099
+ callApp(aliasTool("mailbox_triage_apply"), { plan_id: currentPlan.plan_id, confirm: true })
1100
+ .then(function (result) {
1101
+ setBusy(btn, false);
1102
+ var count = currentPlan.count;
1103
+ var extra = [];
1104
+ if (result && typeof result === "object") {
1105
+ ["applied", "modified", "count"].forEach(function (key) {
1106
+ if (typeof result[key] === "number") extra.push(key + ": " + result[key]);
1107
+ });
1108
+ }
1109
+ setStatus(
1110
+ extra.length > 0
1111
+ ? "Plan applied \\u2014 " + extra.join(", ") + "."
1112
+ : "Plan applied (" + count + " message" + (count === 1 ? "" : "s") + ")."
1113
+ );
493
1114
  document.getElementById("apply-btn").disabled = true;
1115
+ clearPlanExpiry();
1116
+ currentPlan = null;
1117
+ onMailboxChange();
494
1118
  })
495
1119
  .catch(function (err) {
1120
+ setBusy(btn, false);
496
1121
  setStatus("Apply failed: " + err.message);
497
1122
  });
498
1123
  });
499
1124
 
500
1125
  function appendLog(text) {
501
- var el = document.getElementById("log");
1126
+ var el = document.getElementById("log-events");
502
1127
  if (el.querySelector(".empty")) el.innerHTML = "";
503
- el.textContent += (el.textContent ? "\\n" : "") + text;
1128
+ var line = document.createElement("div");
1129
+ line.textContent = text;
1130
+ el.appendChild(line);
504
1131
  el.scrollTop = el.scrollHeight;
505
1132
  }
506
1133
 
507
- function pollAgentRun(appRunId) {
508
- if (pollHandle) clearTimeout(pollHandle);
1134
+ // Replaces the whole tail on every poll tick (the daemon returns the last
1135
+ // N lines, not a delta) \u2014 appending it instead would duplicate the log.
1136
+ function renderLogTail(text) {
1137
+ var el = document.getElementById("log-tail");
1138
+ el.textContent = text || "";
1139
+ el.scrollTop = el.scrollHeight;
1140
+ }
1141
+
1142
+ function pollAgentRun(appRunId, startedAt) {
1143
+ if (pollHandle) {
1144
+ clearTimeout(pollHandle);
1145
+ pollHandle = null;
1146
+ }
1147
+ if (Date.now() - startedAt > POLL_TIMEOUT_MS) {
1148
+ setStatus("Still running \\u2014 check Past runs later.");
1149
+ return;
1150
+ }
509
1151
  callApp("app_status", { appRunId: appRunId })
510
1152
  .then(function (status) {
511
1153
  var sessions = (status && status.sessions) || [];
@@ -514,12 +1156,14 @@ function pollAgentRun(appRunId) {
514
1156
  ? callApp("agent_output", { sessionId: sessionId, lastN: 200, clean: true })
515
1157
  : Promise.resolve("");
516
1158
  return tail.then(function (output) {
517
- if (output) appendLog(typeof output === "string" ? output : JSON.stringify(output));
1159
+ renderLogTail(typeof output === "string" ? output : output ? JSON.stringify(output, null, 2) : "");
518
1160
  var done = status && (status.status === "ended" || status.status === "done" || status.endedAt);
519
1161
  if (!done) {
520
- pollHandle = setTimeout(function () { pollAgentRun(appRunId); }, 2000);
1162
+ pollHandle = setTimeout(function () { pollAgentRun(appRunId, startedAt); }, 2000);
521
1163
  } else {
1164
+ pollHandle = null;
522
1165
  setStatus("Agent run finished.");
1166
+ loadRuns();
523
1167
  }
524
1168
  });
525
1169
  })
@@ -529,29 +1173,62 @@ function pollAgentRun(appRunId) {
529
1173
  }
530
1174
 
531
1175
  document.getElementById("run-agent-btn").addEventListener("click", function () {
532
- document.getElementById("log").innerHTML = "";
1176
+ var btn = this;
1177
+ if (pollHandle) {
1178
+ clearTimeout(pollHandle);
1179
+ pollHandle = null;
1180
+ }
1181
+ document.getElementById("log-events").innerHTML = "";
1182
+ document.getElementById("log-tail").textContent = "";
1183
+ setBusy(btn, true);
533
1184
  setStatus("Starting triager agent\\u2026");
534
1185
  callApp("app_run", { appId: APP_ID, agents: [AGENT_ID], prompt: "Triage my inbox." })
535
1186
  .then(function (run) {
1187
+ setBusy(btn, false);
1188
+ var sessions = (run && run.sessions) || [];
1189
+ var errors = (run && run.errors) || [];
1190
+ if (sessions.length === 0 && errors.length > 0) {
1191
+ appendLog(
1192
+ "Run failed to start: " +
1193
+ errors.map(function (e) { return (e.agentId || "?") + ": " + e.error; }).join("; ")
1194
+ );
1195
+ setStatus("Run failed to start.");
1196
+ return;
1197
+ }
536
1198
  appendLog("Started app run " + (run && run.appRunId));
537
- pollAgentRun(run.appRunId);
1199
+ loadRuns();
1200
+ pollAgentRun(run.appRunId, Date.now());
538
1201
  })
539
1202
  .catch(function (err) {
1203
+ setBusy(btn, false);
540
1204
  setStatus("Run failed: " + err.message);
541
1205
  });
542
1206
  });
543
1207
 
544
- window.McpApp.connect()
1208
+ // window.McpApp is normally injected by the serving layer (postMessage bridge
1209
+ // on the MCP resource path, REST bridge on GET /apps/:id/ui). Guard anyway:
1210
+ // without it a missing bridge throws here and the panel dies silently on
1211
+ // "Connecting\u2026" instead of reporting itself.
1212
+ (window.McpApp ? window.McpApp.connect() : Promise.reject(new Error("window.McpApp bridge missing")))
545
1213
  .then(function (bridge) {
546
1214
  callTool = bridge.callTool;
547
- return loadMailboxes();
1215
+ updateModelContext = bridge.updateModelContext;
1216
+ openLink = bridge.openLink;
1217
+ onTeardown = bridge.onTeardown;
1218
+ if (onTeardown) {
1219
+ onTeardown(function () {
1220
+ if (pollHandle) { clearTimeout(pollHandle); pollHandle = null; }
1221
+ if (planExpiryHandle) { clearTimeout(planExpiryHandle); planExpiryHandle = null; }
1222
+ });
1223
+ }
1224
+ return Promise.all([probeAliases(), loadRuns()]);
548
1225
  })
549
1226
  .catch(function (err) {
550
1227
  setStatus("Standalone mode \\u2014 no host bridge (" + err.message + ")");
551
- renderSummary(null);
1228
+ renderSummary();
552
1229
  });
553
1230
 
554
- renderSummary(null);
1231
+ renderSummary();
555
1232
  </script>
556
1233
  </body>
557
1234
  </html>`;