@agenticmail/api 0.9.11 → 0.9.12

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 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
- safeWrite(`data: ${JSON.stringify(event)}
1516
-
1517
- `);
1529
+ broadcastNew(event);
1518
1530
  return;
1519
1531
  }
1520
1532
  const spamResult = scoreEmail(parsed);
@@ -1540,9 +1552,7 @@ function createEventRoutes(accountManager, config, db) {
1540
1552
  }
1541
1553
  await receiver.moveMessage(event.uid, "INBOX", "Spam");
1542
1554
  event.spam = { score: spamResult.score, category: spamResult.topCategory, movedToSpam: true };
1543
- safeWrite(`data: ${JSON.stringify(event)}
1544
-
1545
- `);
1555
+ broadcastNew(event);
1546
1556
  return;
1547
1557
  }
1548
1558
  if (spamResult.isWarning) {
@@ -1566,18 +1576,7 @@ function createEventRoutes(accountManager, config, db) {
1566
1576
  console.error("[SSE] Spam/rule evaluation error:", err.message);
1567
1577
  }
1568
1578
  }
1569
- safeWrite(`data: ${JSON.stringify(event)}
1570
-
1571
- `);
1572
- try {
1573
- pushSystemEvent({
1574
- type: "new_mail",
1575
- agentId: agent.id,
1576
- agentName: agent.name,
1577
- event
1578
- });
1579
- } catch {
1580
- }
1579
+ broadcastNew(event);
1581
1580
  });
1582
1581
  watcher.on("expunge", (event) => {
1583
1582
  safeWrite(`data: ${JSON.stringify(event)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/api",
3
- "version": "0.9.11",
3
+ "version": "0.9.12",
4
4
  "description": "REST API server for AgenticMail — email and SMS endpoints for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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
- <div id="activity-badges" class="activity-badges"></div>
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
- margin-right: 4px;
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 badges on narrow viewports; they fold to the
217
- topbar's account avatar dropdown anyway. */
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) ─────────────────────────────────────────── */