@agenticmail/api 0.9.7 → 0.9.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/api",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
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",
@@ -12,6 +12,7 @@
12
12
  // reflects what the agent is doing right now.
13
13
 
14
14
  import { onSystemEvent } from './system-stream.js';
15
+ import { state, API_URL } from './state.js';
15
16
 
16
17
  const BADGE_CONTAINER_ID = 'activity-badges';
17
18
 
@@ -92,6 +93,32 @@ function handleEvent(event) {
92
93
  }
93
94
  }
94
95
 
96
+ /**
97
+ * One-shot backfill: pull the dispatcher's currently-active workers
98
+ * so the badges appear IMMEDIATELY on page load if anything is in
99
+ * flight. Without this, the user only sees badges when the next
100
+ * worker_heartbeat / worker_started event fires — which can be up
101
+ * to 30 s away (heartbeat cadence), or never if the worker happens
102
+ * to finish first.
103
+ *
104
+ * Failures here are silent — the SSE stream is the source of truth
105
+ * for subsequent updates and will paint badges as events arrive.
106
+ */
107
+ async function backfillActiveWorkers() {
108
+ try {
109
+ const res = await fetch(`${API_URL}/api/agenticmail/dispatcher/activity`, {
110
+ headers: { Authorization: `Bearer ${state.masterKey}` },
111
+ });
112
+ if (!res.ok) return;
113
+ const data = await res.json();
114
+ const active = Array.isArray(data?.active) ? data.active : [];
115
+ for (const w of active) {
116
+ if (w?.workerId) workers.set(w.workerId, w);
117
+ }
118
+ render();
119
+ } catch { /* silent — SSE will repaint as events come in */ }
120
+ }
121
+
95
122
  /**
96
123
  * Subscribe to worker_* events on the shared /system/events stream.
97
124
  * Idempotent — safe to call after agent-list refresh.
@@ -103,6 +130,12 @@ export function subscribeToActivity() {
103
130
  unsubWorkerStarted = onSystemEvent('worker_started', handleEvent);
104
131
  unsubWorkerHeartbeat = onSystemEvent('worker_heartbeat', handleEvent);
105
132
  unsubWorkerFinished = onSystemEvent('worker_finished', handleEvent);
133
+ // Paint whatever's already running BEFORE the first SSE event
134
+ // arrives. Without this, an in-flight worker stays invisible
135
+ // until its next heartbeat (~30 s) or until it finishes (which
136
+ // then never paints since worker_finished just removes the
137
+ // badge that never showed up).
138
+ backfillActiveWorkers();
106
139
  }
107
140
 
108
141
  // Tiny HTML escapers (kept local to avoid an import cycle).
package/public/js/app.js CHANGED
@@ -179,10 +179,20 @@ function onFolderSelect(folder) {
179
179
  // Folder switches go through here too so the URL is the source of truth
180
180
  // for "what's on screen". If you bookmark or copy-paste a URL like
181
181
  // http://127.0.0.1:3829/#/folder/sent, opening it lands you on Sent.
182
+ // Track which view shape is currently on screen so the router knows
183
+ // whether navigating back to #/folder/<x> for the SAME folder should
184
+ // re-render the list. Without this, hitting Back from #/m/54 to
185
+ // #/folder/inbox would early-return because state.selectedFolder is
186
+ // still 'inbox' (it never changed when the message opened) — leaving
187
+ // the message-detail view stuck on screen even though the URL bar
188
+ // flipped back to the folder.
189
+ let currentView = 'folder'; // 'folder' | 'message' | 'draft'
190
+
182
191
  function route() {
183
192
  const hash = location.hash || '#/inbox';
184
193
  const msgMatch = hash.match(/^#\/m\/(\d+)$/);
185
194
  if (msgMatch) {
195
+ currentView = 'message';
186
196
  openMessage(Number(msgMatch[1]));
187
197
  return;
188
198
  }
@@ -191,23 +201,27 @@ function route() {
191
201
  // row click handler emits #/d/<uuid> for draft rows.
192
202
  const draftMatch = hash.match(/^#\/d\/([a-zA-Z0-9-]+)$/);
193
203
  if (draftMatch) {
204
+ currentView = 'draft';
194
205
  openDraft(draftMatch[1]);
195
206
  return;
196
207
  }
197
208
  const folderMatch = hash.match(/^#\/folder\/([a-z]+)$/);
198
209
  const folder = folderMatch ? folderMatch[1] : 'inbox';
199
- // Only do work if the folder actually changed. Re-firing loadList
200
- // for the SAME folder on every hashchange (e.g. closing a message
201
- // detail back to the list) makes the UI feel sluggish — the list
202
- // is already rendered, and a second digest fetch just churns
203
- // through 50 messages on the IMAP server for no visible change.
204
- if (state.selectedFolder === folder) return;
210
+ // Skip the reload ONLY when we're already showing this folder's
211
+ // list view. Coming back from a message / draft folder must
212
+ // always re-render the list, even if state.selectedFolder hasn't
213
+ // changed since the message was opened.
214
+ if (currentView === 'folder' && state.selectedFolder === folder) return;
215
+ const folderChanged = state.selectedFolder !== folder;
205
216
  state.selectedFolder = folder;
206
- // Reset pagination on every folder switch — a fresh folder
207
- // starts at page 1. Preserved across silent SSE refreshes so
208
- // a new arrival doesn't yank the user back from page 3.
209
- state.pagination = { offset: 0, limit: 50, total: 0 };
210
- renderSidebar(onFolderSelect);
217
+ currentView = 'folder';
218
+ if (folderChanged) {
219
+ // Fresh folder page 1. Preserved across silent SSE refreshes so
220
+ // a new arrival doesn't yank the user back from page 3. We also
221
+ // re-render the sidebar so the active-folder highlight updates.
222
+ state.pagination = { offset: 0, limit: 50, total: 0 };
223
+ renderSidebar(onFolderSelect);
224
+ }
211
225
  if (state.selectedAgent) loadList(state.selectedAgent, folder);
212
226
  }
213
227
  window.addEventListener('hashchange', route);