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