@songsid/agend 2.2.0-beta.6 → 2.2.0-beta.7

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.
Files changed (79) hide show
  1. package/dist/agent-cli-instructions.md +6 -2
  2. package/dist/agent-cli.js +9 -5
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/backend/muse.d.ts +26 -0
  5. package/dist/backend/muse.js +131 -0
  6. package/dist/backend/muse.js.map +1 -1
  7. package/dist/channel/adapters/discord.js +12 -1
  8. package/dist/channel/adapters/discord.js.map +1 -1
  9. package/dist/channel/adapters/telegram.js +6 -0
  10. package/dist/channel/adapters/telegram.js.map +1 -1
  11. package/dist/channel/mcp-tools.js +3 -3
  12. package/dist/channel/mcp-tools.js.map +1 -1
  13. package/dist/channel/types.d.ts +3 -0
  14. package/dist/cross-instance-envelope.js +2 -0
  15. package/dist/cross-instance-envelope.js.map +1 -1
  16. package/dist/daemon.d.ts +6 -0
  17. package/dist/daemon.js +124 -54
  18. package/dist/daemon.js.map +1 -1
  19. package/dist/delivery-outbox.d.ts +27 -0
  20. package/dist/delivery-outbox.js +175 -6
  21. package/dist/delivery-outbox.js.map +1 -1
  22. package/dist/fleet-manager.d.ts +28 -1
  23. package/dist/fleet-manager.js +203 -40
  24. package/dist/fleet-manager.js.map +1 -1
  25. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +14 -1
  26. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
  27. package/dist/human-delivery-receipts.d.ts +83 -0
  28. package/dist/human-delivery-receipts.js +194 -0
  29. package/dist/human-delivery-receipts.js.map +1 -0
  30. package/dist/instructions.d.ts +1 -1
  31. package/dist/instructions.js +1 -1
  32. package/dist/instructions.js.map +1 -1
  33. package/dist/locale.js +8 -0
  34. package/dist/locale.js.map +1 -1
  35. package/dist/outbound-handlers.d.ts +3 -0
  36. package/dist/outbound-handlers.js +20 -4
  37. package/dist/outbound-handlers.js.map +1 -1
  38. package/dist/outbound-schemas.d.ts +15 -13
  39. package/dist/outbound-schemas.js +5 -0
  40. package/dist/outbound-schemas.js.map +1 -1
  41. package/dist/public-link-progress.d.ts +1 -1
  42. package/dist/public-link-progress.js +8 -2
  43. package/dist/public-link-progress.js.map +1 -1
  44. package/dist/public-preview.d.ts +2 -1
  45. package/dist/public-preview.js +8 -5
  46. package/dist/public-preview.js.map +1 -1
  47. package/dist/public-web-link.d.ts +4 -1
  48. package/dist/public-web-link.js +27 -10
  49. package/dist/public-web-link.js.map +1 -1
  50. package/dist/reply-buttons-controller.d.ts +3 -0
  51. package/dist/reply-buttons-controller.js +3 -2
  52. package/dist/reply-buttons-controller.js.map +1 -1
  53. package/dist/topic-commands.d.ts +3 -2
  54. package/dist/topic-commands.js +14 -4
  55. package/dist/topic-commands.js.map +1 -1
  56. package/dist/tunnel/cloudflared.d.ts +7 -15
  57. package/dist/tunnel/cloudflared.js +36 -1
  58. package/dist/tunnel/cloudflared.js.map +1 -1
  59. package/dist/tunnel/types.d.ts +5 -1
  60. package/dist/tunnel/types.js.map +1 -1
  61. package/dist/ui/chat-strings.js +8 -2
  62. package/dist/ui/chat-thread.js +24 -4
  63. package/dist/ui/chat-time.js +80 -0
  64. package/dist/ui/preview-panel.js +7 -1
  65. package/dist/ui/preview.js +5 -1
  66. package/dist/ui/shared/app.css +4 -1
  67. package/dist/web-api.d.ts +2 -0
  68. package/dist/web-api.js +8 -1
  69. package/dist/web-api.js.map +1 -1
  70. package/dist/web-commands.d.ts +2 -2
  71. package/dist/web-commands.js +3 -2
  72. package/dist/web-commands.js.map +1 -1
  73. package/dist/web-reply-files.d.ts +45 -0
  74. package/dist/web-reply-files.js +223 -0
  75. package/dist/web-reply-files.js.map +1 -0
  76. package/dist/web-upload.d.ts +17 -4
  77. package/dist/web-upload.js +64 -13
  78. package/dist/web-upload.js.map +1 -1
  79. package/package.json +1 -1
@@ -9,6 +9,7 @@
9
9
  import "./chat-render.js";
10
10
  import "./preview.js";
11
11
  import { loadHtmlAttachment } from "./html-attachment.js";
12
+ import { chatTime, chatTimeFull, onDayChange } from "./chat-time.js";
12
13
 
13
14
  const R = () => globalThis.AgendChatRender;
14
15
  /** #1589: the public link shows attachments as downloads only (its page says so: <body data-public-link="1">). */
@@ -49,18 +50,33 @@ export function createThread(list, scroller, opts) {
49
50
  const cardRefresh = new WeakMap(); // card node → re-read availability (weak: a card that left takes its source with it)
50
51
  const tr = opts.t, trf = opts.tf;
51
52
  const stopIn = (node) => (P() ? P().stopIn(node) : []);
53
+ /** #1770: write a message node's time label for now; unchanged text is not touched. */
54
+ function timeLabel(node) {
55
+ const el = node.querySelector(".meta .time[data-ts]");
56
+ if (!el) return;
57
+ const v = chatTime(el.dataset.ts, { yesterday: (hm) => trf("chat.yesterdayAt", hm) });
58
+ if (el.textContent !== v) el.textContent = v;
59
+ }
60
+ // #1770: "today" ends at the fleet's midnight; the labels are rewritten then (and on every render), while there are any.
61
+ let offDay = null;
62
+ function armDay() {
63
+ if (nodes.size && !offDay) offDay = onDayChange(() => { for (const v of nodes.values()) timeLabel(v.node); });
64
+ else if (!nodes.size && offDay) { offDay(); offDay = null; }
65
+ }
52
66
 
53
67
  const TICKS = () => ({ queued: tr("chat.tick_queued"), processing: tr("chat.tick_processing"), delivered: tr("chat.tick_delivered"), failed: tr("chat.tick_failed"), cancelled: tr("chat.tick_cancelled") });
54
68
 
55
69
  function msgHtml(x) {
56
- const time = x.ts ? new Date(x.ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
70
+ // #1770: the label ("17:38", "Yesterday 17:38", "10/11 17:38") depends on today, so it is not part of the html a node
71
+ // is kept by: it is written into the node (timeLabels) — crossing midnight relabels in place and reloads no preview.
72
+ const time = x.ts ? `<span class="time" data-ts="${escAttr(x.ts)}" title="${escAttr(chatTimeFull(x.ts))}"></span>` : `<span class="time"></span>`;
57
73
  if (x.role === "status") return statusHtml(x, time);
58
74
  const u = opts.isUser(x);
59
75
  // renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
60
76
  const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
61
77
  const tools = u ? "" : `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
62
78
  const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
63
- return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span><span class="time">${time}</span>${ticks}</div><div class="body"><div class="md">${R().renderMarkdown(x.text, { htmlCards: x.role === "agent", emojis: opts.emojis, mentions: opts.mentions })}</div>${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
79
+ return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div><div class="body"><div class="md">${R().renderMarkdown(x.text, { htmlCards: x.role === "agent", emojis: opts.emojis, mentions: opts.mentions })}</div>${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
64
80
  }
65
81
  /**
66
82
  * #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
@@ -75,7 +91,7 @@ export function createThread(list, scroller, opts) {
75
91
  ? `<button type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
76
92
  : "";
77
93
  const tools = `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
78
- return `<div class="msg status-line${more ? " folded" : ""}"><div class="meta"><span class="time">${time}</span></div><div class="body"><div class="st-text">${escAttr(text)}</div>${more}</div>${tools}</div>`;
94
+ return `<div class="msg status-line${more ? " folded" : ""}"><div class="meta">${time}</div><div class="body"><div class="st-text">${escAttr(text)}</div>${more}</div>${tools}</div>`;
79
95
  }
80
96
  /**
81
97
  * #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
@@ -97,6 +113,7 @@ export function createThread(list, scroller, opts) {
97
113
  tpl.innerHTML = html;
98
114
  const node = tpl.content.firstElementChild;
99
115
  decorateCode(node);
116
+ timeLabel(node);
100
117
  if (x) decorateHtmlCards(node, x, stoppedKeys || []);
101
118
  if (x) decorateTextCards(node, x);
102
119
  return node;
@@ -378,6 +395,7 @@ export function createThread(list, scroller, opts) {
378
395
  if (!msgs.length) {
379
396
  for (const v of nodes.values()) stopIn(v.node);
380
397
  nodes.clear();
398
+ armDay();
381
399
  list.textContent = "";
382
400
  opts.onEmpty(true);
383
401
  return;
@@ -394,12 +412,13 @@ export function createThread(list, scroller, opts) {
394
412
  const node = msgNode(html, x, stopped);
395
413
  if (have) have.node.replaceWith(node); else added++;
396
414
  have = { html, node }; nodes.set(key, have);
397
- }
415
+ } else timeLabel(have.node);
398
416
  const want = prev ? prev.nextSibling : list.firstChild;
399
417
  // A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
400
418
  if (have.node !== want) { if (have.node.parentNode === list) stopIn(have.node); list.insertBefore(have.node, want); }
401
419
  prev = have.node;
402
420
  }
421
+ armDay();
403
422
  if ("restore" in renderOpts) {
404
423
  unseen = 0;
405
424
  if (renderOpts.restore == null) toBottom();
@@ -478,6 +497,7 @@ export function createThread(list, scroller, opts) {
478
497
  if (ro) ro.disconnect();
479
498
  if (P()) P().stopAll("leave");
480
499
  nodes.clear();
500
+ armDay();
481
501
  }
482
502
 
483
503
  return { render, onScroll, onClick, jumpLatest, reveal, refreshCards, dispose, nodes, get unseen() { return unseen; } };
@@ -0,0 +1,80 @@
1
+ // #1770: the time a chat line shows, read in the fleet's zone (the one chat-logs, fleet.log and the #1586 DMs use) —
2
+ // "17:38" today, "Yesterday 17:38", "10/11 17:38" earlier this year, "2025/10/11 17:38" before. The hour and minute keep
3
+ // the browser's own format (12- or 24-hour), as they always had. No imports: the thread, the preview panel and the
4
+ // tests read it as it is.
5
+
6
+ const cache = new Map();
7
+ /** One formatter per (kind, zone, locale): a thread may label 500 lines on every render. */
8
+ function fmt(kind, tz, locale) {
9
+ const key = `${kind}|${tz || ""}|${locale || ""}`;
10
+ let f = cache.get(key);
11
+ if (!f) {
12
+ const o = kind === "hm" ? { hour: "2-digit", minute: "2-digit" }
13
+ : kind === "ymd" ? { year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric", hourCycle: "h23" }
14
+ : { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", timeZoneName: "short" };
15
+ f = new Intl.DateTimeFormat(kind === "ymd" ? "en-US" : (locale || []), { ...o, timeZone: tz || undefined });
16
+ cache.set(key, f);
17
+ }
18
+ return f;
19
+ }
20
+ /** `tz` when this browser knows it, else undefined (the browser's own zone): a label must still be drawn. */
21
+ function known(tz) {
22
+ if (!tz) return undefined;
23
+ try { fmt("ymd", tz); return tz; } catch { return undefined; }
24
+ }
25
+ /** The fleet's zone, as the page was served it (<body data-fleet-tz>); undefined when absent or unknown here. */
26
+ export function fleetTz() { return known(globalThis.document?.body?.dataset?.fleetTz); }
27
+
28
+ const toMs = (ts) => (typeof ts === "number" ? ts : Date.parse(String(ts || "")));
29
+ function wall(ms, tz) {
30
+ const p = {};
31
+ for (const x of fmt("ymd", tz).formatToParts(ms)) p[x.type] = Number(x.value);
32
+ return p;
33
+ }
34
+
35
+ /**
36
+ * The label for `ts` (ISO string or epoch ms), "" when it cannot be read. opts: tz (default the fleet's), now (epoch ms),
37
+ * yesterday(hm) → the word for "yesterday" around hm (default: hm alone), locale (tests).
38
+ */
39
+ export function chatTime(ts, opts = {}) {
40
+ const ms = toMs(ts);
41
+ if (!Number.isFinite(ms)) return "";
42
+ const tz = "tz" in opts ? known(opts.tz) : fleetTz();
43
+ const hm = fmt("hm", tz, opts.locale).format(ms);
44
+ const d = wall(ms, tz), today = wall(opts.now ?? Date.now(), tz);
45
+ if (d.year === today.year && d.month === today.month && d.day === today.day) return hm;
46
+ const y = new Date(Date.UTC(today.year, today.month - 1, today.day - 1));
47
+ if (d.year === y.getUTCFullYear() && d.month === y.getUTCMonth() + 1 && d.day === y.getUTCDate()) return opts.yesterday ? opts.yesterday(hm) : hm;
48
+ return d.year === today.year ? `${d.month}/${d.day} ${hm}` : `${d.year}/${d.month}/${d.day} ${hm}`;
49
+ }
50
+
51
+ /** The full date, time and zone of `ts`, for a tooltip; "" when it cannot be read. */
52
+ export function chatTimeFull(ts, opts = {}) {
53
+ const ms = toMs(ts);
54
+ if (!Number.isFinite(ms)) return "";
55
+ return fmt("full", "tz" in opts ? known(opts.tz) : fleetTz(), opts.locale).format(ms);
56
+ }
57
+
58
+ /** Milliseconds until the fleet's next midnight (when "today" labels change), at most an hour (a DST day self-corrects). */
59
+ export function msToNextDay(now = Date.now(), tz = fleetTz()) {
60
+ const w = wall(now, known(tz));
61
+ const into = ((w.hour * 60 + w.minute) * 60 + w.second) * 1000 + (now % 1000);
62
+ return Math.max(1000, Math.min(3_600_000, 86_400_000 - into + 1000));
63
+ }
64
+
65
+ /**
66
+ * Call `fn` when the fleet's day may have changed — at its next midnight, then again for each one after (an hour at
67
+ * most between looks, so a DST day or a late timer corrects itself). Each subscriber holds its own timer, and no
68
+ * module-wide state: the unsubscribe it returns leaves nothing armed.
69
+ */
70
+ export function onDayChange(fn) {
71
+ let timer = null, live = true;
72
+ const arm = () => {
73
+ timer = setTimeout(() => {
74
+ timer = null;
75
+ try { fn(); } finally { if (live) arm(); }
76
+ }, msToNextDay());
77
+ };
78
+ arm();
79
+ return () => { live = false; if (timer !== null) { clearTimeout(timer); timer = null; } };
80
+ }
@@ -16,6 +16,7 @@ import { Icon } from "/assets/ui-icons.js";
16
16
  import "./chat-render.js";
17
17
  import "./preview.js";
18
18
  import { loadHtmlAttachment } from "./html-attachment.js";
19
+ import { chatTime, onDayChange } from "./chat-time.js";
19
20
 
20
21
  const R = () => globalThis.AgendChatRender;
21
22
  const P = () => globalThis.AgendPreview;
@@ -112,7 +113,8 @@ export function clampWidth(w, total) {
112
113
  return Math.round(Math.min(max, Math.max(MIN_PANEL, w)));
113
114
  }
114
115
 
115
- const timeOf = (ts) => (ts ? new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "");
116
+ /** #1770: as the thread shows it — the fleet's zone, with the day when it is not today. */
117
+ const timeOf = (ts) => (ts ? chatTime(ts, { yesterday: (hm) => t("chat.yesterdayAt", hm) }) : "");
116
118
 
117
119
  /**
118
120
  * The panel, inside the chat's split (`split`: that element — the divider sets its width there, through CSSOM).
@@ -140,6 +142,10 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
140
142
  useEffect(() => (P() ? P().onChange(() => { if (pending.current && ownAllow.current !== pending.current) pending.current = 0; choice(n => n + 1); }) : undefined), []);
141
143
  // Leaving the chat (another instance, another panel) closes the panel: its frame stops with the thread's.
142
144
  useEffect(() => () => closePanel(), []);
145
+ // #1770: the source and newer-version labels say the day as the thread does — drawn again when the fleet's day
146
+ // changes, while a block is open. A render: the frame and the run are left as they are.
147
+ const [, dayTick] = useState(0);
148
+ useEffect(() => (open ? onDayChange(() => dayTick(n => n + 1)) : undefined), [!!open]);
143
149
 
144
150
  const ui = { state: (s, reason) => setRun({ state: s === "starting" || s === "running" ? s : "idle", reason: reason || "" }) };
145
151
  const start = () => {
@@ -31,7 +31,11 @@
31
31
  var cfg = { dashboardOrigin: "", previewOrigin: "", boot: "", reason: "", reasonCode: "", detail: "" };
32
32
  // Why it failed, in English when the app's dictionary has no word for it (chat-strings.js pvFail_*).
33
33
  var FAIL_EN = { exited: "its address closed unexpectedly", "spawn-failed": "cloudflared could not start for it", "no-address": "no address was given for it",
34
- unreachable: "its address could not be reached", "lease-held": "an earlier preview tunnel is still being cleaned up", "gateway-failed": "its local listener could not start" };
34
+ unreachable: "its address could not be reached", "lease-held": "an earlier preview tunnel is still being cleaned up", "gateway-failed": "its local listener could not start",
35
+ // #1767: what cloudflared's exit before an address really was.
36
+ "rate-limited": "Cloudflare is limiting new temporary links from this machine — try again in a while",
37
+ "provisioning-failed": "Cloudflare refused to create its address right now", "process-exited": "cloudflared stopped before it gave an address",
38
+ "bad-address": "Cloudflare gave an address that could not be used" };
35
39
  // #1554: the page's words. `say(key, english, …values)` asks the app's dictionary (init's opts.text) and keeps the
36
40
  // English here when it has none — so this file still works, and reads the same, without the app.
37
41
  var text = null;
@@ -336,7 +336,10 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
336
336
  .msg .time { font-family: var(--font-mono); white-space: nowrap; flex: none; }
337
337
  .msg.user { align-self: flex-end; max-width: min(80%, 620px); }
338
338
  .msg.user .meta { justify-content: flex-end; }
339
- .msg.user .body { background: var(--user-bubble); border-radius: var(--r-xl); padding: 10px var(--s-4); }
339
+ /* #1773: the bubble hugs its text. The message is as wide as its widest child — usually the meta row (sender, a time
340
+ such as 「昨天 下午03:58」, ticks) — and a block body would stretch to it; fit-content keeps a short message short, on
341
+ the right under its meta, and a long one still wraps at the message's max-width. */
342
+ .msg.user .body { background: var(--user-bubble); border-radius: var(--r-xl); padding: 10px var(--s-4); width: fit-content; max-width: 100%; margin-left: auto; }
340
343
  .msg.agent { align-self: stretch; }
341
344
  /* #1672: a status line (another instance's message, a daemon notice): muted plain text, no bubble; a long one folds.
342
345
  Its class is `status-line`, never `status`: that is the header's inline-flex badge class, and on a message it turned
package/dist/web-api.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type HumanOrigin } from "./human-delivery-receipts.js";
1
2
  import type { CachedImage } from "./web-images.js";
2
3
  import type { SettingsExecution } from "./settings-transaction.js";
3
4
  import type { PaneContextSource } from "./pane-context-cache.js";
@@ -106,6 +107,7 @@ export interface WebApiContext {
106
107
  sendClassicWebEcho?(instance: string, text: string): Promise<number>;
107
108
  /** false: definitely not delivered (the instance's IPC is gone, or it was restarted meanwhile). */
108
109
  deliverToInstance(instanceName: string, payload: Record<string, unknown>): Promise<boolean | void>;
110
+ deliverHumanToInstance?(instanceName: string, payload: Record<string, unknown>, origin: HumanOrigin, originalBody: string): Promise<boolean | void>;
109
111
  getUiStatus(): unknown;
110
112
  /** Current owned pane reader shared with status; absent means context is unavailable. */
111
113
  getPaneContextSource?(name: string): PaneContextSource | null;
package/dist/web-api.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { webHumanOrigin } from "./human-delivery-receipts.js";
1
2
  import { withSpawnHoldReply } from "./spawn-hold-reply.js";
2
3
  import { binaryProbe } from "./binary-probe.js";
3
4
  import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
@@ -32,6 +33,7 @@ import { commitTeamCreate, commitTeamUpdate } from "./outbound-handlers.js";
32
33
  import { parseMentionQuery } from "./web-mentions.js";
33
34
  import { authorizeExplicitInstanceRemoval } from "./instance-removal.js";
34
35
  import { measureSyncWork } from "./sync-work-attribution.js";
36
+ import { resolveTz } from "./tz-utils.js";
35
37
  // ── Strict public-facing schemas ────────────────────────────────────────────
36
38
  // web-api endpoints must reject unknown fields so the dashboard cannot inject
37
39
  // internal-only flags that would reach handleCreate/scheduler/config writers.
@@ -1403,7 +1405,10 @@ function handleSendMessage(req, res, ctx) {
1403
1405
  }) : undefined;
1404
1406
  let delivered;
1405
1407
  try {
1406
- delivered = await ctx.deliverToInstance(instance, {
1408
+ const deliver = (name, payload) => ctx.deliverHumanToInstance
1409
+ ? ctx.deliverHumanToInstance(name, payload, webHumanOrigin(), typed)
1410
+ : ctx.deliverToInstance(name, payload);
1411
+ delivered = await deliver(instance, {
1407
1412
  type: "fleet_inbound",
1408
1413
  content: delivery.text,
1409
1414
  targetSession: instance,
@@ -1607,6 +1612,8 @@ function shellBodyTag(req, ctx, mode) {
1607
1612
  + `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
1608
1613
  // #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔.
1609
1614
  + ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"`
1615
+ // #1770: the fleet's zone (chat-logs' and fleet.log's), in which the chat reads its message times and days.
1616
+ + ` data-fleet-tz="${attr(resolveTz())}"`
1610
1617
  // #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.
1611
1618
  + `${gatewayRequestContext(req) ? ' data-public-link="1"' : ""}>`;
1612
1619
  }