@agentproto/apps 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- 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 +4 -4
package/dist/mail-triage.mjs
CHANGED
|
@@ -30,7 +30,7 @@ var triager = {
|
|
|
30
30
|
],
|
|
31
31
|
workflows: [{ ref: "triage-inbox" }]
|
|
32
32
|
}),
|
|
33
|
-
body:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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…</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">
|
|
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
|
|
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…</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
|
-
|
|
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(
|
|
395
|
+
function renderSummary() {
|
|
202
396
|
var el = document.getElementById("summary");
|
|
203
397
|
el.innerHTML = "";
|
|
204
|
-
|
|
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 " +
|
|
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 =
|
|
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
|
|
216
|
-
|
|
217
|
-
return
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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 (
|
|
267
|
-
el.innerHTML = '<div class="empty">Plan
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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 =
|
|
280
|
-
row.querySelector(".tag").textContent =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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("
|
|
864
|
+
callApp(aliasTool("mailbox_search"), { mailbox: mailbox, query: query })
|
|
293
865
|
.then(function (result) {
|
|
294
|
-
renderSearchResults(asList(result, ["
|
|
295
|
-
|
|
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
|
-
|
|
897
|
+
clearPlanExpiry();
|
|
898
|
+
currentPlan = null;
|
|
899
|
+
callApp(aliasTool("mailbox_triage_plan"), { mailbox: mailbox, criteria: { query: query }, action: action })
|
|
307
900
|
.then(function (plan) {
|
|
308
|
-
|
|
901
|
+
setBusy(btn, false);
|
|
902
|
+
currentPlan = plan && plan.plan_id ? plan : null;
|
|
309
903
|
renderPlan(plan);
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
926
|
+
var btn = this;
|
|
927
|
+
if (!currentPlan || !currentPlan.plan_id) return;
|
|
928
|
+
setBusy(btn, true);
|
|
320
929
|
setStatus("Applying triage plan\\u2026");
|
|
321
|
-
callApp("
|
|
322
|
-
.then(function () {
|
|
323
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
1059
|
+
renderSummary();
|
|
383
1060
|
});
|
|
384
1061
|
|
|
385
|
-
renderSummary(
|
|
1062
|
+
renderSummary();
|
|
386
1063
|
</script>
|
|
387
1064
|
</body>
|
|
388
1065
|
</html>`;
|