@agenticmail/api 0.9.11 → 0.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +35 -21
- package/package.json +1 -1
- package/public/favicon.ico +0 -0
- package/public/index.html +10 -2
- package/public/js/activity-badges.js +14 -0
- package/public/styles.css +39 -4
package/dist/index.js
CHANGED
|
@@ -1471,6 +1471,20 @@ function createEventRoutes(accountManager, config, db) {
|
|
|
1471
1471
|
safeWrite(`data: ${JSON.stringify({ type: "connected", agentId: agent.id })}
|
|
1472
1472
|
|
|
1473
1473
|
`);
|
|
1474
|
+
const broadcastNew = (e) => {
|
|
1475
|
+
safeWrite(`data: ${JSON.stringify(e)}
|
|
1476
|
+
|
|
1477
|
+
`);
|
|
1478
|
+
try {
|
|
1479
|
+
pushSystemEvent({
|
|
1480
|
+
type: "new_mail",
|
|
1481
|
+
agentId: agent.id,
|
|
1482
|
+
agentName: agent.name,
|
|
1483
|
+
event: e
|
|
1484
|
+
});
|
|
1485
|
+
} catch {
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1474
1488
|
watcher.on("new", async (event) => {
|
|
1475
1489
|
if (db) touchActivity(db, agent.id);
|
|
1476
1490
|
if (db && event.uid) {
|
|
@@ -1512,9 +1526,7 @@ function createEventRoutes(accountManager, config, db) {
|
|
|
1512
1526
|
if (actions.move_to) await receiver.moveMessage(event.uid, "INBOX", actions.move_to);
|
|
1513
1527
|
event.ruleApplied = { ruleId: ruleResult2.ruleId, actions };
|
|
1514
1528
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
`);
|
|
1529
|
+
broadcastNew(event);
|
|
1518
1530
|
return;
|
|
1519
1531
|
}
|
|
1520
1532
|
const spamResult = scoreEmail(parsed);
|
|
@@ -1534,15 +1546,28 @@ function createEventRoutes(accountManager, config, db) {
|
|
|
1534
1546
|
} catch {
|
|
1535
1547
|
}
|
|
1536
1548
|
if (spamResult.isSpam) {
|
|
1549
|
+
let spamFolder = "Spam";
|
|
1537
1550
|
try {
|
|
1538
|
-
await receiver.
|
|
1551
|
+
const folders = await receiver.listFolders();
|
|
1552
|
+
const junkRe = /^junk\b|junk mail|^spam\b|\[gmail\]\/spam/i;
|
|
1553
|
+
const existing = folders.find((f) => f.specialUse === "\\Junk")?.path ?? folders.find((f) => junkRe.test(f.name) || junkRe.test(f.path))?.path;
|
|
1554
|
+
if (existing) {
|
|
1555
|
+
spamFolder = existing;
|
|
1556
|
+
} else {
|
|
1557
|
+
try {
|
|
1558
|
+
await receiver.createFolder("Spam");
|
|
1559
|
+
} catch {
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1539
1562
|
} catch {
|
|
1563
|
+
try {
|
|
1564
|
+
await receiver.createFolder("Spam");
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1540
1567
|
}
|
|
1541
|
-
await receiver.moveMessage(event.uid, "INBOX",
|
|
1542
|
-
event.spam = { score: spamResult.score, category: spamResult.topCategory, movedToSpam: true };
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
`);
|
|
1568
|
+
await receiver.moveMessage(event.uid, "INBOX", spamFolder);
|
|
1569
|
+
event.spam = { score: spamResult.score, category: spamResult.topCategory, movedToSpam: true, folder: spamFolder };
|
|
1570
|
+
broadcastNew(event);
|
|
1546
1571
|
return;
|
|
1547
1572
|
}
|
|
1548
1573
|
if (spamResult.isWarning) {
|
|
@@ -1566,18 +1591,7 @@ function createEventRoutes(accountManager, config, db) {
|
|
|
1566
1591
|
console.error("[SSE] Spam/rule evaluation error:", err.message);
|
|
1567
1592
|
}
|
|
1568
1593
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
`);
|
|
1572
|
-
try {
|
|
1573
|
-
pushSystemEvent({
|
|
1574
|
-
type: "new_mail",
|
|
1575
|
-
agentId: agent.id,
|
|
1576
|
-
agentName: agent.name,
|
|
1577
|
-
event
|
|
1578
|
-
});
|
|
1579
|
-
} catch {
|
|
1580
|
-
}
|
|
1594
|
+
broadcastNew(event);
|
|
1581
1595
|
});
|
|
1582
1596
|
watcher.on("expunge", (event) => {
|
|
1583
1597
|
safeWrite(`data: ${JSON.stringify(event)}
|
package/package.json
CHANGED
|
Binary file
|
package/public/index.html
CHANGED
|
@@ -42,8 +42,16 @@
|
|
|
42
42
|
<!-- Real-time worker activity badges. Populated by SSE; one
|
|
43
43
|
badge per active dispatcher worker showing what each
|
|
44
44
|
agent is doing right now (reading, editing, running
|
|
45
|
-
shell, etc.). Empty when no workers are active.
|
|
46
|
-
|
|
45
|
+
shell, etc.). Empty when no workers are active.
|
|
46
|
+
|
|
47
|
+
The count pill is always rendered first by activity-badges.js
|
|
48
|
+
when there's at least one worker — gives users a stable
|
|
49
|
+
"3 active" affordance independent of how many badges fit
|
|
50
|
+
in the horizontal-scroll viewport. -->
|
|
51
|
+
<div id="activity-badges-shell" class="activity-badges-shell" hidden>
|
|
52
|
+
<span id="activity-badges-count" class="activity-badges-count" title="Active agents"></span>
|
|
53
|
+
<div id="activity-badges" class="activity-badges"></div>
|
|
54
|
+
</div>
|
|
47
55
|
<button class="icon-btn" id="sound-toggle-btn" title="Notification sound"></button>
|
|
48
56
|
<button class="icon-btn" id="refresh-btn" title="Refresh (r)" data-icon="refresh"></button>
|
|
49
57
|
<button id="profile-btn" class="profile-trigger" title="Account">
|
|
@@ -15,6 +15,8 @@ import { onSystemEvent } from './system-stream.js';
|
|
|
15
15
|
import { state, API_URL } from './state.js';
|
|
16
16
|
|
|
17
17
|
const BADGE_CONTAINER_ID = 'activity-badges';
|
|
18
|
+
const SHELL_ID = 'activity-badges-shell';
|
|
19
|
+
const COUNT_ID = 'activity-badges-count';
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Map of workerId → { agentName, kind, lastTool, turnCount,
|
|
@@ -59,8 +61,20 @@ function statusFor(lastTool) {
|
|
|
59
61
|
|
|
60
62
|
function render() {
|
|
61
63
|
const root = document.getElementById(BADGE_CONTAINER_ID);
|
|
64
|
+
const shell = document.getElementById(SHELL_ID);
|
|
65
|
+
const countEl = document.getElementById(COUNT_ID);
|
|
62
66
|
if (!root) return;
|
|
63
67
|
const list = Array.from(workers.values()).sort((a, b) => (a.startedAtMs ?? 0) - (b.startedAtMs ?? 0));
|
|
68
|
+
// Drive the shell + count pill so users see "3 active" even when only
|
|
69
|
+
// 2 badges fit in the scroll viewport. Empty state hides the whole
|
|
70
|
+
// shell so the topbar doesn't carry a dangling "0" pill.
|
|
71
|
+
if (shell) shell.hidden = list.length === 0;
|
|
72
|
+
if (countEl) {
|
|
73
|
+
countEl.textContent = String(list.length);
|
|
74
|
+
countEl.title = list.length === 1
|
|
75
|
+
? '1 agent active'
|
|
76
|
+
: `${list.length} agents active`;
|
|
77
|
+
}
|
|
64
78
|
if (list.length === 0) { root.innerHTML = ''; return; }
|
|
65
79
|
root.innerHTML = list.map(w => {
|
|
66
80
|
const initial = (w.agentName ?? '?').slice(0, 1).toUpperCase();
|
package/public/styles.css
CHANGED
|
@@ -170,14 +170,49 @@ a { color: var(--accent-strong); }
|
|
|
170
170
|
events. Each badge shows the agent's initial + name + a short
|
|
171
171
|
verb derived from the worker's `lastTool`. The green dot
|
|
172
172
|
gently pulses so the row reads as "alive" rather than static. */
|
|
173
|
+
/* Shell wraps the always-visible count pill + the scrollable badge
|
|
174
|
+
row. Two children, side-by-side; the count never scrolls so the
|
|
175
|
+
user can see how many agents are active even when the scroll viewport
|
|
176
|
+
is full. */
|
|
177
|
+
.activity-badges-shell {
|
|
178
|
+
display: flex; align-items: center; gap: 6px;
|
|
179
|
+
margin-right: 4px;
|
|
180
|
+
min-width: 0; /* lets the inner scrollable strip shrink without
|
|
181
|
+
pushing siblings off the topbar */
|
|
182
|
+
}
|
|
183
|
+
.activity-badges-count {
|
|
184
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
185
|
+
height: 22px; min-width: 22px; padding: 0 7px;
|
|
186
|
+
background: var(--pink, #ff4d8c);
|
|
187
|
+
color: #fff;
|
|
188
|
+
font-size: 11px; font-weight: 700;
|
|
189
|
+
border-radius: 999px;
|
|
190
|
+
flex-shrink: 0;
|
|
191
|
+
user-select: none;
|
|
192
|
+
}
|
|
193
|
+
/* Horizontal-scrolling strip of badges. We deliberately allow
|
|
194
|
+
`overflow-x: auto` so the user can swipe / trackpad-scroll through
|
|
195
|
+
more than fit on screen, and use an edge fade mask so it's visually
|
|
196
|
+
clear there's more content past the right edge. Scrollbar stays
|
|
197
|
+
hidden (cleaner topbar) but the count pill + edge fade make
|
|
198
|
+
"there's more" discoverable. */
|
|
173
199
|
.activity-badges {
|
|
174
200
|
display: flex; align-items: center; gap: 6px;
|
|
175
201
|
max-width: 480px;
|
|
176
202
|
overflow-x: auto;
|
|
177
203
|
scrollbar-width: none;
|
|
178
|
-
|
|
204
|
+
-ms-overflow-style: none;
|
|
205
|
+
/* Mask creates a gentle fade-out on each end so badges visually
|
|
206
|
+
hint "I'm continuing past the viewport edge". Pure CSS, no JS. */
|
|
207
|
+
mask-image: linear-gradient(to right, transparent 0, black 12px, black calc(100% - 16px), transparent 100%);
|
|
208
|
+
-webkit-mask-image: linear-gradient(to right, transparent 0, black 12px, black calc(100% - 16px), transparent 100%);
|
|
209
|
+
/* Snap-scroll so swiping never leaves a badge half-clipped. */
|
|
210
|
+
scroll-snap-type: x proximity;
|
|
211
|
+
/* Ensure the inner strip can shrink inside its flex parent. */
|
|
212
|
+
min-width: 0;
|
|
179
213
|
}
|
|
180
214
|
.activity-badges::-webkit-scrollbar { display: none; }
|
|
215
|
+
.activity-badge { scroll-snap-align: start; }
|
|
181
216
|
.activity-badge {
|
|
182
217
|
display: inline-flex; align-items: center; gap: 6px;
|
|
183
218
|
padding: 4px 10px 4px 6px;
|
|
@@ -213,10 +248,10 @@ a { color: var(--accent-strong); }
|
|
|
213
248
|
.activity-badge .badge-status { color: var(--muted); }
|
|
214
249
|
.activity-badge .badge-status::before { content: '· '; opacity: .6; }
|
|
215
250
|
|
|
216
|
-
/* Mobile — hide
|
|
217
|
-
|
|
251
|
+
/* Mobile — hide the whole activity-badge shell (count pill + scrolling
|
|
252
|
+
strip) on narrow viewports; folds into the profile dropdown anyway. */
|
|
218
253
|
@media (max-width: 800px) {
|
|
219
|
-
.activity-badges { display: none; }
|
|
254
|
+
.activity-badges-shell { display: none !important; }
|
|
220
255
|
}
|
|
221
256
|
|
|
222
257
|
/* ─── Profile (top right) ─────────────────────────────────────────── */
|