@agenticmail/api 0.9.21 → 0.9.23
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 +8 -2
- package/package.json +1 -1
- package/public/js/compose.js +16 -1
- package/public/js/message-view.js +39 -2
- package/public/styles.css +24 -0
package/dist/index.js
CHANGED
|
@@ -401,7 +401,11 @@ function createAccountRoutes(accountManager, db, config) {
|
|
|
401
401
|
router.get("/accounts/directory", requireAuth, async (_req, res, next) => {
|
|
402
402
|
try {
|
|
403
403
|
const agents = await accountManager.list();
|
|
404
|
-
const directory = agents.map((a) =>
|
|
404
|
+
const directory = agents.map((a) => {
|
|
405
|
+
const meta = a.metadata ?? {};
|
|
406
|
+
const host2 = typeof meta.host === "string" ? meta.host : null;
|
|
407
|
+
return { name: a.name, email: a.email, role: a.role, host: host2 };
|
|
408
|
+
});
|
|
405
409
|
res.json({ agents: directory });
|
|
406
410
|
} catch (err) {
|
|
407
411
|
next(err);
|
|
@@ -414,7 +418,9 @@ function createAccountRoutes(accountManager, db, config) {
|
|
|
414
418
|
res.status(404).json({ error: "Agent not found" });
|
|
415
419
|
return;
|
|
416
420
|
}
|
|
417
|
-
|
|
421
|
+
const meta = agent.metadata ?? {};
|
|
422
|
+
const host2 = typeof meta.host === "string" ? meta.host : null;
|
|
423
|
+
res.json({ name: agent.name, email: agent.email, role: agent.role, host: host2 });
|
|
418
424
|
} catch (err) {
|
|
419
425
|
next(err);
|
|
420
426
|
}
|
package/package.json
CHANGED
package/public/js/compose.js
CHANGED
|
@@ -85,8 +85,23 @@ export function openReply(replyAll) {
|
|
|
85
85
|
document.getElementById('compose-wake').value = '';
|
|
86
86
|
document.getElementById('compose-subject').value =
|
|
87
87
|
(msg.subject ?? '').startsWith('Re:') ? msg.subject : `Re: ${msg.subject ?? ''}`;
|
|
88
|
+
// Quote header — extend the standard "On <date>, <addr> wrote:"
|
|
89
|
+
// line with To: / Cc: lines pulled from the original message so
|
|
90
|
+
// the recipient of THIS reply can see who was on the previous
|
|
91
|
+
// round of the thread. The renderer in message-view.js parses
|
|
92
|
+
// these optional lines and surfaces them in the thread-quote
|
|
93
|
+
// header alongside the sender name.
|
|
94
|
+
const fmtAddrList = (arr) => (arr || [])
|
|
95
|
+
.map(a => (typeof a === 'string' ? a : (a?.address ?? '')))
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.join(', ');
|
|
98
|
+
const origTo = fmtAddrList(msg.to);
|
|
99
|
+
const origCc = fmtAddrList(msg.cc);
|
|
100
|
+
const headerLines = [`On ${msg.date}, ${fromAddr} wrote:`];
|
|
101
|
+
if (origTo) headerLines.push(`To: ${origTo}`);
|
|
102
|
+
if (origCc) headerLines.push(`Cc: ${origCc}`);
|
|
88
103
|
const quoted = (msg.text ?? '').split('\n').map(l => `> ${l}`).join('\n');
|
|
89
|
-
const stub = `\n\
|
|
104
|
+
const stub = `\n\n${headerLines.join('\n')}\n${quoted}`;
|
|
90
105
|
document.getElementById('compose-body').value = stub;
|
|
91
106
|
pendingAttachments = [];
|
|
92
107
|
renderAttachmentChips();
|
|
@@ -239,6 +239,14 @@ function renderBodyWithThreading(src) {
|
|
|
239
239
|
// angle-bracket form `<addr@host>`. Date is anything up to the
|
|
240
240
|
// comma; addr is anything not whitespace + an @.
|
|
241
241
|
const headerRe = /^On (.+?), <?([^\s<>]+@[^\s<>]+)>? wrote:\s*$/;
|
|
242
|
+
// Optional follow-up address lines emitted by AgenticMail's
|
|
243
|
+
// reply builders: `To: a@x, b@y` / `Cc: …` / `Bcc: …`. These
|
|
244
|
+
// sit between the `wrote:` line and the first `> ` quoted body
|
|
245
|
+
// line; the parser collects them so the rendered thread-quote
|
|
246
|
+
// header can show the previous round's full audience, not just
|
|
247
|
+
// the sender. Optional — older replies (pre-0.9.32) won't have
|
|
248
|
+
// them and degrade to sender-only.
|
|
249
|
+
const audienceRe = /^(To|Cc|Bcc):\s*(.+)$/i;
|
|
242
250
|
|
|
243
251
|
while (i < lines.length) {
|
|
244
252
|
const m = lines[i].match(headerRe);
|
|
@@ -251,6 +259,23 @@ function renderBodyWithThreading(src) {
|
|
|
251
259
|
const dateRaw = m[1];
|
|
252
260
|
const sender = m[2];
|
|
253
261
|
i++;
|
|
262
|
+
// Collect optional audience lines (To/Cc/Bcc) immediately
|
|
263
|
+
// after the wrote: header. Stop at the first line that
|
|
264
|
+
// doesn't match; non-matching content falls through to the
|
|
265
|
+
// body-quote collection loop below.
|
|
266
|
+
let toAddrs = '';
|
|
267
|
+
let ccAddrs = '';
|
|
268
|
+
let bccAddrs = '';
|
|
269
|
+
while (i < lines.length) {
|
|
270
|
+
const a = lines[i].match(audienceRe);
|
|
271
|
+
if (!a) break;
|
|
272
|
+
const field = a[1].toLowerCase();
|
|
273
|
+
const value = a[2].trim();
|
|
274
|
+
if (field === 'to') toAddrs = value;
|
|
275
|
+
else if (field === 'cc') ccAddrs = value;
|
|
276
|
+
else if (field === 'bcc') bccAddrs = value;
|
|
277
|
+
i++;
|
|
278
|
+
}
|
|
254
279
|
// Collect contiguous `>` lines (with possible blank-line gaps
|
|
255
280
|
// inside the quote, which most clients tolerate). Stop at the
|
|
256
281
|
// first non-quote, non-blank line.
|
|
@@ -267,13 +292,13 @@ function renderBodyWithThreading(src) {
|
|
|
267
292
|
}
|
|
268
293
|
break;
|
|
269
294
|
}
|
|
270
|
-
out.push(renderThreadQuote(dateRaw, sender, quoted.join('\n')));
|
|
295
|
+
out.push(renderThreadQuote(dateRaw, sender, quoted.join('\n'), { to: toAddrs, cc: ccAddrs, bcc: bccAddrs }));
|
|
271
296
|
}
|
|
272
297
|
flushProse();
|
|
273
298
|
return out.join('');
|
|
274
299
|
}
|
|
275
300
|
|
|
276
|
-
function renderThreadQuote(dateRaw, sender, quotedBody) {
|
|
301
|
+
function renderThreadQuote(dateRaw, sender, quotedBody, audience = {}) {
|
|
277
302
|
// Try to format the ISO date through the same helper the
|
|
278
303
|
// message header uses; fall back to the raw string on parse fail.
|
|
279
304
|
const friendlyDate = (() => {
|
|
@@ -282,6 +307,17 @@ function renderThreadQuote(dateRaw, sender, quotedBody) {
|
|
|
282
307
|
return dateRaw;
|
|
283
308
|
})();
|
|
284
309
|
const sub = renderBodyWithThreading(quotedBody); // recurse for nested threads
|
|
310
|
+
// Render the optional audience lines (To/Cc/Bcc) inside the
|
|
311
|
+
// thread-quote header so the reader can see who was on the
|
|
312
|
+
// previous round. Missing values are simply omitted — a quote
|
|
313
|
+
// from an older email that didn't include them degrades cleanly
|
|
314
|
+
// to the sender + date line.
|
|
315
|
+
const audienceRow = (label, value) => value
|
|
316
|
+
? `<div class="thread-quote-audience-row"><span class="thread-quote-audience-label">${label}:</span> <span class="thread-quote-audience-value">${escapeHtml(value)}</span></div>`
|
|
317
|
+
: '';
|
|
318
|
+
const audienceBlock = (audience.to || audience.cc || audience.bcc)
|
|
319
|
+
? `<div class="thread-quote-audience">${audienceRow('To', audience.to)}${audienceRow('Cc', audience.cc)}${audienceRow('Bcc', audience.bcc)}</div>`
|
|
320
|
+
: '';
|
|
285
321
|
return `
|
|
286
322
|
<div class="thread-quote">
|
|
287
323
|
<div class="thread-quote-head">
|
|
@@ -290,6 +326,7 @@ function renderThreadQuote(dateRaw, sender, quotedBody) {
|
|
|
290
326
|
<span class="thread-quote-dot">·</span>
|
|
291
327
|
<span class="thread-quote-date">${escapeHtml(friendlyDate)}</span>
|
|
292
328
|
</div>
|
|
329
|
+
${audienceBlock}
|
|
293
330
|
<div class="thread-quote-body">${sub}</div>
|
|
294
331
|
</div>
|
|
295
332
|
`;
|
package/public/styles.css
CHANGED
|
@@ -938,6 +938,30 @@ mark.search-hl {
|
|
|
938
938
|
.thread-quote-from { font-weight: 500; color: var(--ink-soft); }
|
|
939
939
|
.thread-quote-dot { opacity: .5; }
|
|
940
940
|
.thread-quote-date { color: var(--muted); }
|
|
941
|
+
/* Audience block (To / Cc / Bcc) below the quote header — surfaces
|
|
942
|
+
* who was on the previous round of the thread, not just the sender.
|
|
943
|
+
* Optional: missing values are simply omitted, no row rendered. */
|
|
944
|
+
.thread-quote-audience {
|
|
945
|
+
margin: -4px 0 8px;
|
|
946
|
+
padding-left: 30px; /* line up with sender name (past avatar) */
|
|
947
|
+
font-size: 12px;
|
|
948
|
+
color: var(--muted);
|
|
949
|
+
line-height: 1.5;
|
|
950
|
+
}
|
|
951
|
+
.thread-quote-audience-row { display: block; }
|
|
952
|
+
.thread-quote-audience-label {
|
|
953
|
+
display: inline-block;
|
|
954
|
+
min-width: 28px;
|
|
955
|
+
color: var(--muted);
|
|
956
|
+
font-weight: 500;
|
|
957
|
+
text-transform: uppercase;
|
|
958
|
+
letter-spacing: 0.04em;
|
|
959
|
+
font-size: 10.5px;
|
|
960
|
+
}
|
|
961
|
+
.thread-quote-audience-value {
|
|
962
|
+
color: var(--ink-soft);
|
|
963
|
+
word-break: break-word;
|
|
964
|
+
}
|
|
941
965
|
.thread-quote-body {
|
|
942
966
|
color: var(--ink-soft);
|
|
943
967
|
}
|