@agenticmail/cli 0.9.41 → 0.9.43
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/public/js/message-view.js +81 -22
- package/dist/public/styles.css +54 -3
- package/package.json +3 -3
|
@@ -171,7 +171,7 @@ function renderMessage(msg) {
|
|
|
171
171
|
<div class="message-date">${escapeHtml(formatDateFull(msg.date))}</div>
|
|
172
172
|
</div>
|
|
173
173
|
</div>
|
|
174
|
-
<div class="message-body">${renderBodyWithThreading(bodyText, buildAudienceLookup())}</div>
|
|
174
|
+
<div class="message-body">${renderBodyWithThreading(bodyText, buildAudienceLookup(msg))}</div>
|
|
175
175
|
${attachmentsHtml}
|
|
176
176
|
`;
|
|
177
177
|
|
|
@@ -239,12 +239,43 @@ function formatBytes(bytes) {
|
|
|
239
239
|
* header serialises the date with second precision and stripped
|
|
240
240
|
* timezone info.
|
|
241
241
|
*/
|
|
242
|
-
function buildAudienceLookup() {
|
|
242
|
+
function buildAudienceLookup(currentMessage = null) {
|
|
243
|
+
// Two layered sources, server-authoritative wins:
|
|
244
|
+
//
|
|
245
|
+
// 1. currentMessage.quotedAudiences — populated by the API when
|
|
246
|
+
// it parses the body, scans for `On <date>, <addr> wrote:`
|
|
247
|
+
// headers, and matches them against the IMAP envelopes
|
|
248
|
+
// (which carry Cc/Bcc on the wire — we just weren't surfacing
|
|
249
|
+
// them before 0.9.30). This works even on direct deep links
|
|
250
|
+
// where state.messages is empty.
|
|
251
|
+
//
|
|
252
|
+
// 2. state.messages — the inbox list view the operator is
|
|
253
|
+
// currently looking at. Fallback for older API responses
|
|
254
|
+
// that don't yet include the server-side resolution.
|
|
255
|
+
//
|
|
256
|
+
// Layer 1 takes precedence because the server actually has access
|
|
257
|
+
// to the full mailbox envelopes; the client only sees what was
|
|
258
|
+
// paginated into state.messages.
|
|
259
|
+
const serverIndex = new Map();
|
|
260
|
+
if (currentMessage && Array.isArray(currentMessage.quotedAudiences)) {
|
|
261
|
+
for (const a of currentMessage.quotedAudiences) {
|
|
262
|
+
if (!a || !a.sender) continue;
|
|
263
|
+
// Index by sender alone AND by sender::dateIso for fuzzy date match
|
|
264
|
+
const senderKey = String(a.sender).toLowerCase();
|
|
265
|
+
const t = a.dateIso ? new Date(a.dateIso).getTime() : NaN;
|
|
266
|
+
serverIndex.set(senderKey + '::' + (Number.isFinite(t) ? t : ''), { to: a.to, cc: a.cc, bcc: a.bcc, timeMs: t });
|
|
267
|
+
// Also keep a "most recent for sender" entry as last-resort fallback
|
|
268
|
+
const allKey = senderKey + '::*';
|
|
269
|
+
const prev = serverIndex.get(allKey);
|
|
270
|
+
if (!prev || (Number.isFinite(t) && t > prev.timeMs)) {
|
|
271
|
+
serverIndex.set(allKey, { to: a.to, cc: a.cc, bcc: a.bcc, timeMs: t });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
243
275
|
// Defensive: state.messages may be empty if the operator navigated
|
|
244
|
-
// straight to /#/m/N. Lookup degrades to
|
|
245
|
-
// falls back to sender-only.
|
|
276
|
+
// straight to /#/m/N. Lookup degrades to layer 1 only.
|
|
246
277
|
const list = Array.isArray(state.messages) ? state.messages : [];
|
|
247
|
-
if (list.length === 0) return () => null;
|
|
278
|
+
if (list.length === 0 && serverIndex.size === 0) return () => null;
|
|
248
279
|
// Flatten to a normalised array we can scan. Each entry carries
|
|
249
280
|
// the same shape the renderer expects.
|
|
250
281
|
const entries = list.map(m => {
|
|
@@ -265,23 +296,29 @@ function buildAudienceLookup() {
|
|
|
265
296
|
if (!senderAddr || !dateStr) return null;
|
|
266
297
|
const senderL = senderAddr.toLowerCase();
|
|
267
298
|
const t = new Date(dateStr).getTime();
|
|
299
|
+
|
|
300
|
+
// Layer 1: server-resolved index. Exact date match first, then
|
|
301
|
+
// sender-only fallback. The server already matched against IMAP
|
|
302
|
+
// envelopes so this is the authoritative source.
|
|
303
|
+
if (serverIndex.size > 0) {
|
|
304
|
+
const exactKey = senderL + '::' + (Number.isFinite(t) ? t : '');
|
|
305
|
+
const exact = serverIndex.get(exactKey);
|
|
306
|
+
if (exact) return { to: exact.to, cc: exact.cc, bcc: exact.bcc };
|
|
307
|
+
const senderOnly = serverIndex.get(senderL + '::*');
|
|
308
|
+
if (senderOnly) return { to: senderOnly.to, cc: senderOnly.cc, bcc: senderOnly.bcc };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Layer 2: state.messages — surrounding inbox list. Bounded by
|
|
312
|
+
// what the operator's folder view happens to have loaded.
|
|
268
313
|
if (!Number.isFinite(t)) return null;
|
|
269
|
-
// Match by sender + nearest date within 2 s. Quote headers
|
|
270
|
-
// typically lose timezone info during render so wall-clock can
|
|
271
|
-
// shift by hours; the inbox-list date is the canonical UTC.
|
|
272
|
-
// We prefer the closest match by absolute delta.
|
|
273
314
|
let best = null;
|
|
274
|
-
let bestDelta = 2000;
|
|
315
|
+
let bestDelta = 2000;
|
|
275
316
|
for (const e of entries) {
|
|
276
317
|
if (e.from !== senderL) continue;
|
|
277
318
|
const delta = Math.abs(e.timeMs - t);
|
|
278
319
|
if (delta < bestDelta) { best = e; bestDelta = delta; }
|
|
279
320
|
}
|
|
280
321
|
if (!best) {
|
|
281
|
-
// Fallback: if the date didn't parse close to anything, just
|
|
282
|
-
// take the most recent message from that sender. Better than
|
|
283
|
-
// nothing for the common case of a reply to the most recent
|
|
284
|
-
// prior reply.
|
|
285
322
|
for (const e of entries) {
|
|
286
323
|
if (e.from !== senderL) continue;
|
|
287
324
|
if (!best || e.timeMs > best.timeMs) best = e;
|
|
@@ -324,6 +361,17 @@ function renderBodyWithThreading(src, audienceLookup = null) {
|
|
|
324
361
|
i++;
|
|
325
362
|
continue;
|
|
326
363
|
}
|
|
364
|
+
// Strip any trailing blank lines from the prose buffer before
|
|
365
|
+
// flushing. Reply builders typically pad the new content with
|
|
366
|
+
// `\n\n` before the `On <date>, <addr> wrote:` header so the
|
|
367
|
+
// quote stands apart in plaintext clients. In the web view those
|
|
368
|
+
// blank lines render as empty <p> blocks with their own margins,
|
|
369
|
+
// producing the huge gap operators flagged between the latest
|
|
370
|
+
// message and the quoted thread. Trimming here keeps plain-text
|
|
371
|
+
// readers happy and tightens the rendered HTML at the same time.
|
|
372
|
+
while (prose.length > 0 && prose[prose.length - 1].trim() === '') {
|
|
373
|
+
prose.pop();
|
|
374
|
+
}
|
|
327
375
|
flushProse();
|
|
328
376
|
const dateRaw = m[1];
|
|
329
377
|
const sender = m[2];
|
|
@@ -361,16 +409,27 @@ function renderBodyWithThreading(src, audienceLookup = null) {
|
|
|
361
409
|
}
|
|
362
410
|
break;
|
|
363
411
|
}
|
|
364
|
-
// Fall back to cross-message lookup
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
412
|
+
// Fall back to cross-message lookup to fill ANY missing audience
|
|
413
|
+
// field — not just the all-empty case.
|
|
414
|
+
//
|
|
415
|
+
// The previous gate (`!toAddrs && !ccAddrs && !bccAddrs`) was wrong:
|
|
416
|
+
// it treated a body that had a `To:` line but no `Cc:` line as
|
|
417
|
+
// "audience encoded, lookup not needed", silently dropping the Cc
|
|
418
|
+
// info that state.messages had. This surfaced as quoted blocks
|
|
419
|
+
// showing only `TO:` even when the original message had Cc'd
|
|
420
|
+
// multiple agents — operators correctly flagged this as "the
|
|
421
|
+
// earlier email's CCs aren't being shown".
|
|
422
|
+
//
|
|
423
|
+
// Now: run the lookup whenever any field is missing, and only
|
|
424
|
+
// backfill the fields that weren't already in the body. The
|
|
425
|
+
// body-encoded value always wins (it's authoritative); the
|
|
426
|
+
// lookup is purely a backfill.
|
|
427
|
+
if ((!toAddrs || !ccAddrs || !bccAddrs) && typeof audienceLookup === 'function') {
|
|
369
428
|
const fromLookup = audienceLookup(sender, dateRaw);
|
|
370
429
|
if (fromLookup) {
|
|
371
|
-
toAddrs = fromLookup.to;
|
|
372
|
-
ccAddrs = fromLookup.cc;
|
|
373
|
-
bccAddrs = fromLookup.bcc;
|
|
430
|
+
if (!toAddrs) toAddrs = fromLookup.to || '';
|
|
431
|
+
if (!ccAddrs) ccAddrs = fromLookup.cc || '';
|
|
432
|
+
if (!bccAddrs) bccAddrs = fromLookup.bcc || '';
|
|
374
433
|
}
|
|
375
434
|
}
|
|
376
435
|
out.push(renderThreadQuote(dateRaw, sender, quoted.join('\n'), { to: toAddrs, cc: ccAddrs, bcc: bccAddrs }, audienceLookup));
|
package/dist/public/styles.css
CHANGED
|
@@ -1019,14 +1019,65 @@ mark.search-hl {
|
|
|
1019
1019
|
styled card with proper avatar + name + friendly date instead
|
|
1020
1020
|
of leaving the raw ISO timestamp visible. Nested replies recurse
|
|
1021
1021
|
so a 3-deep thread gets 3 nested cards. */
|
|
1022
|
+
/* Thread-quote: the boxed-up older email a reply quotes. Issues we
|
|
1023
|
+
* sequentially squashed:
|
|
1024
|
+
*
|
|
1025
|
+
* 1. Huge vertical gap before the quote — initially 16px margin-top
|
|
1026
|
+
* on .thread-quote PLUS empty <p>/<ul> blocks from the markdown
|
|
1027
|
+
* renderer treating reply sig-lines like `- marlow` as bullet
|
|
1028
|
+
* lists. message-view.js now trims trailing blank lines from
|
|
1029
|
+
* the prose buffer; we tighten the rendered block margins below
|
|
1030
|
+
* so the last <p>/<ul>/<ol> before a quote collapses its
|
|
1031
|
+
* bottom margin and the gap reads as a single hairline.
|
|
1032
|
+
* 2. The pink left rule looked like a stub. ::before lays a 1px
|
|
1033
|
+
* full-width divider tucked behind the border so the visual
|
|
1034
|
+
* break is unambiguous and the rule reads as a continuous
|
|
1035
|
+
* thread spine through the quoted block.
|
|
1036
|
+
*
|
|
1037
|
+
* Nested quotes (replies-of-replies) get progressively warmer rule
|
|
1038
|
+
* colours (pink → purple → amber) so the depth is visible at a
|
|
1039
|
+
* glance without staring at indentation. */
|
|
1022
1040
|
.thread-quote {
|
|
1023
|
-
|
|
1041
|
+
position: relative;
|
|
1042
|
+
margin: 4px 0 8px;
|
|
1043
|
+
padding: 14px 0 4px 16px;
|
|
1024
1044
|
border-left: 3px solid var(--pink-rule);
|
|
1025
|
-
|
|
1045
|
+
}
|
|
1046
|
+
.thread-quote::before {
|
|
1047
|
+
content: '';
|
|
1048
|
+
position: absolute;
|
|
1049
|
+
top: 0;
|
|
1050
|
+
left: -3px;
|
|
1051
|
+
right: 0;
|
|
1052
|
+
height: 1px;
|
|
1053
|
+
background: var(--line, rgba(255,255,255,0.08));
|
|
1054
|
+
}
|
|
1055
|
+
/* Collapse the margin-bottom on whichever block lands immediately
|
|
1056
|
+
* before a thread-quote. Markdown's default 1em paragraph/list
|
|
1057
|
+
* margins were stacking with .thread-quote's margin-top, producing
|
|
1058
|
+
* a ~50px dead zone above quoted replies. With :has() we target
|
|
1059
|
+
* only the preceding sibling (other blocks elsewhere keep their
|
|
1060
|
+
* default margins). Modern browsers all support :has() now. */
|
|
1061
|
+
.message-body p:has(+ .thread-quote),
|
|
1062
|
+
.message-body ul:has(+ .thread-quote),
|
|
1063
|
+
.message-body ol:has(+ .thread-quote),
|
|
1064
|
+
.message-body h1:has(+ .thread-quote),
|
|
1065
|
+
.message-body h2:has(+ .thread-quote),
|
|
1066
|
+
.message-body h3:has(+ .thread-quote),
|
|
1067
|
+
.message-body pre:has(+ .thread-quote),
|
|
1068
|
+
.message-body blockquote:has(+ .thread-quote) {
|
|
1069
|
+
margin-bottom: 0;
|
|
1070
|
+
}
|
|
1071
|
+
/* Same tightening on the LAST child of a thread-quote-body — keeps
|
|
1072
|
+
* nested quote ladders from accumulating spacing as they descend. */
|
|
1073
|
+
.thread-quote-body > p:last-child,
|
|
1074
|
+
.thread-quote-body > ul:last-child,
|
|
1075
|
+
.thread-quote-body > ol:last-child {
|
|
1076
|
+
margin-bottom: 0;
|
|
1026
1077
|
}
|
|
1027
1078
|
.thread-quote .thread-quote {
|
|
1028
1079
|
border-left-color: #c084fc;
|
|
1029
|
-
margin:
|
|
1080
|
+
margin: 4px 0 8px;
|
|
1030
1081
|
}
|
|
1031
1082
|
.thread-quote .thread-quote .thread-quote { border-left-color: #f59e0b; }
|
|
1032
1083
|
.thread-quote-head {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.43",
|
|
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",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"prepublishOnly": "npm run build"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@agenticmail/api": "^0.9.
|
|
35
|
-
"@agenticmail/core": "^0.9.
|
|
34
|
+
"@agenticmail/api": "^0.9.30",
|
|
35
|
+
"@agenticmail/core": "^0.9.8",
|
|
36
36
|
"json5": "^2.2.3"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|