@agenticmail/cli 0.8.34 → 0.8.35

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.
@@ -55,9 +55,16 @@ function showAuthErr(msg) {
55
55
  }
56
56
  function signOut() {
57
57
  localStorage.removeItem('agenticmail.masterKey');
58
+ localStorage.removeItem('agenticmail.selectedAgentId');
58
59
  location.reload();
59
60
  }
60
61
 
62
+ // localStorage key for the inbox the user was last viewing.
63
+ // Persisted on every successful agent switch and consulted on
64
+ // bootstrap so a refresh / reopen lands on the same account
65
+ // instead of bouncing back to the bridge.
66
+ const STORAGE_LAST_AGENT = 'agenticmail.selectedAgentId';
67
+
61
68
  document.getElementById('auth-submit').addEventListener('click', signIn);
62
69
  document.getElementById('auth-key').addEventListener('keydown', e => {
63
70
  if (e.key === 'Enter') signIn();
@@ -77,7 +84,15 @@ async function bootstrap() {
77
84
  return a.name.localeCompare(b.name);
78
85
  });
79
86
  state.agents = all;
80
- const initial = state.agents.find(isBridgeAgent) ?? state.agents[0];
87
+ // Prefer the inbox the user was last viewing (persisted in
88
+ // localStorage on every selectAgent call). Falls back to the
89
+ // bridge if the stored id is gone (agent was deleted) or the
90
+ // user never switched. Fixes the "refresh always bounces me
91
+ // back to the host account" bug.
92
+ const lastId = localStorage.getItem(STORAGE_LAST_AGENT);
93
+ const initial = (lastId && state.agents.find(a => a.id === lastId))
94
+ ?? state.agents.find(isBridgeAgent)
95
+ ?? state.agents[0];
81
96
  if (initial) await selectAgent(initial);
82
97
  renderProfile();
83
98
  populateComposeFrom();
@@ -96,6 +111,11 @@ async function selectAgent(agent) {
96
111
  state.selectedAgent = agent;
97
112
  state.selectedUid = null;
98
113
  state.currentMessage = null;
114
+ // Persist the selection so a page refresh lands back on this
115
+ // inbox rather than bouncing to the bridge. Stored under a
116
+ // separate key from the master key so signing out clears it
117
+ // cleanly without affecting auth.
118
+ try { localStorage.setItem(STORAGE_LAST_AGENT, agent.id); } catch { /* private mode etc. */ }
99
119
  // Reset the per-agent folder cache so a fresh discovery runs
100
120
  // against the new agent's IMAP. Otherwise switching to an
101
121
  // account that uses different folder names (e.g. Gmail relay
@@ -102,8 +102,10 @@ export async function openDraft(draftId) {
102
102
  wireAutosave();
103
103
  wireAttachmentPicker();
104
104
  try {
105
- const data = await apiGet('/drafts', { agentKey: state.selectedAgent.apiKey });
106
- const draft = (data?.drafts ?? []).find(d => d.id === draftId);
105
+ // Use the single-draft endpoint, which returns attachment
106
+ // content in full (the list endpoint only sends metadata to
107
+ // keep the sidebar payload small).
108
+ const draft = await apiGet(`/drafts/${encodeURIComponent(draftId)}`, { agentKey: state.selectedAgent.apiKey });
107
109
  if (!draft) throw new Error('Draft not found');
108
110
  document.getElementById('compose-to').value = draft.to_addr ?? '';
109
111
  document.getElementById('compose-cc').value = draft.cc ?? '';
@@ -111,6 +113,19 @@ export async function openDraft(draftId) {
111
113
  document.getElementById('compose-body').value = draft.text_body ?? '';
112
114
  document.getElementById('compose-title').textContent =
113
115
  `Draft: ${draft.subject || '(no subject)'}`;
116
+ // Rehydrate attachment chips with the persisted blobs. Map
117
+ // the server-side `size` field back into the in-memory
118
+ // `sizeBytes` alias the rest of the compose code uses for
119
+ // the UI-side 20 MB cap.
120
+ pendingAttachments = Array.isArray(draft.attachments)
121
+ ? draft.attachments.map(a => ({
122
+ filename: a.filename,
123
+ contentType: a.contentType,
124
+ content: a.content,
125
+ encoding: 'base64',
126
+ sizeBytes: typeof a.size === 'number' ? a.size : 0,
127
+ }))
128
+ : [];
114
129
  renderAttachmentChips();
115
130
  setComposeStatus('Loaded from Drafts');
116
131
  setTimeout(() => document.getElementById('compose-body').focus(), 50);
@@ -174,12 +189,27 @@ function readComposeFields() {
174
189
  const subject = document.getElementById('compose-subject').value.trim();
175
190
  const text = document.getElementById('compose-body').value;
176
191
  const cc = document.getElementById('compose-cc').value.trim();
177
- if (!to && !subject && !text.trim() && !cc) return null;
192
+ if (!to && !subject && !text.trim() && !cc && pendingAttachments.length === 0) return null;
193
+ // The API expects `{ filename, contentType, content (base64),
194
+ // size }` per attachment. Drop the local-only sizeBytes alias
195
+ // and the redundant encoding field — the server defaults to
196
+ // base64 anyway.
197
+ const attachments = pendingAttachments.map(a => ({
198
+ filename: a.filename,
199
+ contentType: a.contentType,
200
+ content: a.content,
201
+ size: a.sizeBytes,
202
+ }));
178
203
  return {
179
204
  to: to || null,
180
205
  subject: subject || null,
181
206
  text: text || null,
182
207
  cc: cc || null,
208
+ // Always send `attachments` (even empty) so the server clears
209
+ // the stored blob when the user removes every chip. The PUT
210
+ // route uses `hasOwnProperty('attachments')` to distinguish
211
+ // "leave alone" from "set to empty".
212
+ attachments,
183
213
  };
184
214
  }
185
215
 
@@ -278,6 +308,11 @@ function wireAttachmentPicker() {
278
308
  }
279
309
  }
280
310
  renderAttachmentChips();
311
+ // Persist the new attachments to the draft so a "close and
312
+ // reopen" round-trip keeps them. Without this, attachments
313
+ // only ever lived in memory until the user typed in another
314
+ // field and triggered autosave organically.
315
+ scheduleAutosave();
281
316
  });
282
317
  }
283
318
 
@@ -310,6 +345,9 @@ function renderAttachmentChips() {
310
345
  el.addEventListener('click', () => {
311
346
  pendingAttachments.splice(Number(el.dataset.attRemove), 1);
312
347
  renderAttachmentChips();
348
+ // Same reason as the picker: removing a chip needs to
349
+ // persist or the draft round-trip will resurrect the file.
350
+ scheduleAutosave();
313
351
  });
314
352
  });
315
353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/cli",
3
- "version": "0.8.34",
3
+ "version": "0.8.35",
4
4
  "description": "Email and SMS infrastructure for AI agents — the first platform to give agents real email addresses and phone numbers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "prepublishOnly": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@agenticmail/api": "^0.7.19",
32
+ "@agenticmail/api": "^0.7.20",
33
33
  "@agenticmail/core": "^0.7.0",
34
34
  "json5": "^2.2.3"
35
35
  },