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