@songsid/agend 2.2.0-beta.3 → 2.2.0-beta.5

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 (139) hide show
  1. package/dist/agent-cli.js +6 -2
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/backend/claude-code.d.ts +46 -2
  4. package/dist/backend/claude-code.js +125 -31
  5. package/dist/backend/claude-code.js.map +1 -1
  6. package/dist/backend/credential-profile.d.ts +33 -3
  7. package/dist/backend/credential-profile.js +107 -5
  8. package/dist/backend/credential-profile.js.map +1 -1
  9. package/dist/backend/types.d.ts +1 -1
  10. package/dist/cache-service.d.ts +2 -0
  11. package/dist/cache-service.js +2 -1
  12. package/dist/cache-service.js.map +1 -1
  13. package/dist/channel/adapters/discord.d.ts +13 -0
  14. package/dist/channel/adapters/discord.js +42 -0
  15. package/dist/channel/adapters/discord.js.map +1 -1
  16. package/dist/channel/adapters/telegram.d.ts +6 -1
  17. package/dist/channel/adapters/telegram.js +27 -2
  18. package/dist/channel/adapters/telegram.js.map +1 -1
  19. package/dist/channel/types.d.ts +22 -1
  20. package/dist/cli.js +35 -0
  21. package/dist/cli.js.map +1 -1
  22. package/dist/config-validator.js +7 -0
  23. package/dist/config-validator.js.map +1 -1
  24. package/dist/daemon.d.ts +96 -0
  25. package/dist/daemon.js +339 -16
  26. package/dist/daemon.js.map +1 -1
  27. package/dist/delivery-outbox.d.ts +18 -5
  28. package/dist/delivery-outbox.js +121 -14
  29. package/dist/delivery-outbox.js.map +1 -1
  30. package/dist/fleet-context.d.ts +1 -0
  31. package/dist/fleet-manager.d.ts +89 -3
  32. package/dist/fleet-manager.js +541 -86
  33. package/dist/fleet-manager.js.map +1 -1
  34. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +43 -10
  35. package/dist/host-memory.d.ts +10 -0
  36. package/dist/host-memory.js +33 -1
  37. package/dist/host-memory.js.map +1 -1
  38. package/dist/instance-lifecycle.d.ts +25 -6
  39. package/dist/instance-lifecycle.js +73 -26
  40. package/dist/instance-lifecycle.js.map +1 -1
  41. package/dist/locale.js +48 -18
  42. package/dist/locale.js.map +1 -1
  43. package/dist/login-controller.d.ts +20 -3
  44. package/dist/login-controller.js +71 -19
  45. package/dist/login-controller.js.map +1 -1
  46. package/dist/login-flows.d.ts +5 -2
  47. package/dist/login-flows.js +5 -4
  48. package/dist/login-flows.js.map +1 -1
  49. package/dist/memory-pressure.d.ts +13 -1
  50. package/dist/memory-pressure.js +50 -10
  51. package/dist/memory-pressure.js.map +1 -1
  52. package/dist/outbound-handlers.d.ts +61 -0
  53. package/dist/outbound-handlers.js +100 -35
  54. package/dist/outbound-handlers.js.map +1 -1
  55. package/dist/outbound-schemas.d.ts +1 -0
  56. package/dist/outbound-schemas.js +2 -0
  57. package/dist/outbound-schemas.js.map +1 -1
  58. package/dist/public-preview.d.ts +100 -0
  59. package/dist/public-preview.js +233 -0
  60. package/dist/public-preview.js.map +1 -0
  61. package/dist/public-web-gateway.js +6 -4
  62. package/dist/public-web-gateway.js.map +1 -1
  63. package/dist/public-web-link.d.ts +20 -0
  64. package/dist/public-web-link.js +26 -1
  65. package/dist/public-web-link.js.map +1 -1
  66. package/dist/restart-progress.d.ts +29 -0
  67. package/dist/restart-progress.js +23 -0
  68. package/dist/restart-progress.js.map +1 -1
  69. package/dist/settings-api.js +10 -4
  70. package/dist/settings-api.js.map +1 -1
  71. package/dist/spawn-gate.js +5 -1
  72. package/dist/spawn-gate.js.map +1 -1
  73. package/dist/spawn-hold-reply.d.ts +12 -0
  74. package/dist/spawn-hold-reply.js +39 -0
  75. package/dist/spawn-hold-reply.js.map +1 -0
  76. package/dist/topic-commands.js +9 -2
  77. package/dist/topic-commands.js.map +1 -1
  78. package/dist/tunnel/purpose-lane.d.ts +5 -1
  79. package/dist/tunnel/purpose-lane.js +14 -7
  80. package/dist/tunnel/purpose-lane.js.map +1 -1
  81. package/dist/turn-resume.d.ts +2 -0
  82. package/dist/turn-resume.js +1 -1
  83. package/dist/turn-resume.js.map +1 -1
  84. package/dist/types.d.ts +8 -0
  85. package/dist/ui/chat-mentions.js +63 -0
  86. package/dist/ui/chat-render.js +25 -0
  87. package/dist/ui/chat-strings.js +14 -2
  88. package/dist/ui/chat-thread.js +28 -10
  89. package/dist/ui/panel-chat.js +26 -11
  90. package/dist/ui/panel-details.js +2 -0
  91. package/dist/ui/panel-fleet.js +199 -57
  92. package/dist/ui/panel-needs.js +4 -3
  93. package/dist/ui/panel-settings.js +4 -2
  94. package/dist/ui/preview-panel.js +48 -14
  95. package/dist/ui/preview-status.js +28 -0
  96. package/dist/ui/preview.js +40 -3
  97. package/dist/ui/settings-token.js +1 -1
  98. package/dist/ui/shared/app-i18n.js +4 -0
  99. package/dist/ui/shared/app-prefs.js +1 -0
  100. package/dist/ui/shared/app-shell.js +146 -2
  101. package/dist/ui/shared/app-store.js +3 -1
  102. package/dist/ui/shared/app.css +47 -20
  103. package/dist/ui/shared/instance-nav.js +79 -23
  104. package/dist/ui/shared/view-strings.js +10 -0
  105. package/dist/unrecognised-prompt.d.ts +23 -0
  106. package/dist/unrecognised-prompt.js +74 -0
  107. package/dist/unrecognised-prompt.js.map +1 -0
  108. package/dist/update-install.d.ts +8 -0
  109. package/dist/update-install.js +9 -3
  110. package/dist/update-install.js.map +1 -1
  111. package/dist/update-marker.d.ts +40 -0
  112. package/dist/update-marker.js +49 -0
  113. package/dist/update-marker.js.map +1 -1
  114. package/dist/usage/i18n-keys.d.ts +1 -1
  115. package/dist/usage/i18n-keys.js +1 -1
  116. package/dist/usage/i18n-keys.js.map +1 -1
  117. package/dist/usage/providers.d.ts +34 -5
  118. package/dist/usage/providers.js +160 -27
  119. package/dist/usage/providers.js.map +1 -1
  120. package/dist/usage/statusline-usage.d.ts +6 -2
  121. package/dist/usage/statusline-usage.js +10 -4
  122. package/dist/usage/statusline-usage.js.map +1 -1
  123. package/dist/web-api.d.ts +20 -0
  124. package/dist/web-api.js +216 -30
  125. package/dist/web-api.js.map +1 -1
  126. package/dist/web-auth.js +3 -2
  127. package/dist/web-auth.js.map +1 -1
  128. package/dist/web-mentions.d.ts +44 -0
  129. package/dist/web-mentions.js +75 -0
  130. package/dist/web-mentions.js.map +1 -0
  131. package/dist/web-prefs.js +2 -0
  132. package/dist/web-prefs.js.map +1 -1
  133. package/dist/web-preview.d.ts +14 -1
  134. package/dist/web-preview.js +35 -13
  135. package/dist/web-preview.js.map +1 -1
  136. package/dist/web-schedules.d.ts +12 -0
  137. package/dist/web-schedules.js +34 -0
  138. package/dist/web-schedules.js.map +1 -0
  139. package/package.json +1 -1
@@ -34,7 +34,7 @@ export const msgKey = (x) => `${x.boot}-${x.id}`;
34
34
  /**
35
35
  * Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
36
36
  * setPreviewOptIn(on), copyText(text) → Promise<boolean>, download(code), panel? }. panel (#1481): { shown() → the card
37
- * key the side panel shows, or null; open(spec, { run }) } — without it a card has no "Open in panel".
37
+ * key the side panel shows, or null; open(spec, { run }); version(); claim(); claims() } — without it a card has no "Open in panel".
38
38
  */
39
39
  export function createThread(list, scroller, opts) {
40
40
  const nodes = new Map(); // key → { html, node }
@@ -60,7 +60,7 @@ export function createThread(list, scroller, opts) {
60
60
  const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
61
61
  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
62
  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 })}</div>${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
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>`;
64
64
  }
65
65
  /**
66
66
  * #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
@@ -75,7 +75,7 @@ export function createThread(list, scroller, opts) {
75
75
  ? `<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
76
  : "";
77
77
  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${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>`;
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>`;
79
79
  }
80
80
  /**
81
81
  * #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
@@ -318,14 +318,32 @@ export function createThread(list, scroller, opts) {
318
318
  };
319
319
  const ui = { state: (name, reason) => { state = name === "starting" || name === "running" ? name : "idle"; refresh(reason || null); } };
320
320
  // Nothing is read for Preview on a device that does not allow previews: the button is not even shown there.
321
- run.onclick = () => { if (P().availability().ok) withCode((c) => P().start(key, holder, c, ui)); };
321
+ // A Preview click is newer than any Open in panel still reading (#1704): it claims the panel's turn too — and, its
322
+ // own HTML read, starts only if no newer click claimed meanwhile and no preview was started since (the panel's own
323
+ // Preview): a late read never undoes a newer choice. A close is neither, so it does not drop this click.
324
+ run.onclick = () => {
325
+ if (!P().availability().ok || reading) return; // this card already reading: that click stays the one
326
+ const mine = panel ? (panel.claim(), panel.claims()) : 0, started = P().startCount();
327
+ withCode((c) => {
328
+ if ((panel && panel.claims() !== mine) || P().startCount() !== started) { refresh(); return; }
329
+ P().start(key, holder, c, ui);
330
+ });
331
+ };
322
332
  stopB.onclick = () => { if (reading) { reading = 0; refresh(); } else P().stop(key, "stopped", ""); };
323
- // A running preview moves: it stops here and starts in the panel (the click that ran it). An idle one opens the
324
- // panel with its own Preview to click.
333
+ // #1704: this click runs it in the panel, as Preview there would — a running preview moves (it stops here and
334
+ // starts there), an idle one starts there. Whether it can run is the panel's to say: a device that has not allowed
335
+ // previews is offered Allow first (and allowing runs it); where previews are off, nothing runs.
336
+ // An attachment's HTML is read first. The click claims the panel at once, so a newer click (even one still reading)
337
+ // wins: if the panel changed (another click, block, close) or any preview started meanwhile, this one is dropped —
338
+ // a late read never undoes a newer click.
325
339
  if (toPanel) toPanel.onclick = () => {
326
- const moving = P().running(key);
327
- if (moving) P().stop(key, "stopped", "");
328
- withCode((c) => panel.open({ key, instance: x.instance, msgKey: msgKey(x), ...(att ? { att } : { n: source.n }), code: c, sender: x.sender, ts: x.ts }, { run: moving }));
340
+ if (reading) return; // this card already reading: that click stays the one
341
+ if (P().running(key)) P().stop(key, "stopped", "");
342
+ const at = panel.claim(), started = P().startCount();
343
+ withCode((c) => {
344
+ if (panel.version() !== at || P().startCount() !== started) { refresh(); return; }
345
+ panel.open({ key, instance: x.instance, msgKey: msgKey(x), ...(att ? { att } : { n: source.n }), code: c, sender: x.sender, ts: x.ts }, { run: true });
346
+ });
329
347
  };
330
348
  if (dl) dl.onclick = () => opts.download(code);
331
349
  ack.onclick = () => P().ackBanner(); // every card and the panel hear it (onChange)
@@ -445,7 +463,7 @@ export function createThread(list, scroller, opts) {
445
463
  if (act === "copyMsg") { const x = msgs.find(y => msgKey(y) === el.dataset.arg); if (x) copyFrom(el, x.text); }
446
464
  else if (act === "copyCode") { const pre = el.closest(".codeblock")?.querySelector("pre"); if (pre) copyFrom(el, pre.textContent); }
447
465
  else if (act === "toggleFold") {
448
- const box = el.closest(".codeblock, .msg.status"); if (!box) return;
466
+ const box = el.closest(".codeblock, .msg.status-line"); if (!box) return;
449
467
  const folded = box.classList.toggle("folded");
450
468
  el.textContent = !folded ? tr("chat.showLess") : el.dataset.lines ? trf("chat.showAll", el.dataset.lines) : tr("chat.showMore");
451
469
  } else if (act === "toggleWrap") opts.toggleWrap();
@@ -10,7 +10,7 @@ import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-
10
10
  import { t } from "/assets/app-i18n.js";
11
11
  import { appStore, useStore } from "/assets/app-store.js";
12
12
  import { useLease } from "/assets/app-ctx.js";
13
- import { labelOf } from "/assets/instance-nav.js";
13
+ import { namesOf, namesStore } from "/assets/instance-nav.js";
14
14
  import { PanelHeader, onPanelKey, setTitle, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
15
15
  import { detailsPath } from "/assets/app-route.js";
16
16
  import { InstanceSwitch } from "/assets/instance-switch.js";
@@ -28,7 +28,9 @@ import { confirmedWrite } from "./settings-confirm.js";
28
28
  import { createChatStore } from "./chat-store.js";
29
29
  import { createThread, msgKey } from "./chat-thread.js";
30
30
  import { createEmojiIndex } from "./chat-emojis.js";
31
- import { PreviewPanel, openPanel, panelStore, shownKey } from "./preview-panel.js";
31
+ import { pollPublicPreview } from "./preview-status.js";
32
+ import { createMentionIndex } from "./chat-mentions.js";
33
+ import { PreviewPanel, claimPanel, openPanel, panelClaims, panelStore, panelVersion, shownKey } from "./preview-panel.js";
32
34
  import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
33
35
  import { installTour, refreshTourSpot } from "./chat-tour.js";
34
36
  import { FirstRunCard } from "./first-run.js";
@@ -45,6 +47,7 @@ export let store = null;
45
47
  let announceTimer = null;
46
48
  /** #1630: which Discord server emoji this page may draw (created with the store, in boot). */
47
49
  let emojiIndex = null;
50
+ let mentionIndex = null; // #1682: the names of the mentions shown
48
51
  function announce(text) {
49
52
  let el = document.getElementById("announcer");
50
53
  if (!el) { el = document.createElement("div"); el.id = "announcer"; el.className = "sr-only"; el.setAttribute("role", "status"); el.setAttribute("aria-live", "polite"); el.setAttribute("aria-atomic", "true"); document.body.appendChild(el); }
@@ -56,11 +59,12 @@ function announce(text) {
56
59
 
57
60
  /**
58
61
  * This device's preview opt-in: asks once (the app's dialog) and says what it means; turning it off stops every
59
- * running preview. Resolves to the choice in force.
62
+ * running preview. Resolves to the choice in force. commit(apply): runs `apply` (the change itself) — the panel's Allow
63
+ * wraps it to know which change of the choice is its own (#1704).
60
64
  */
61
- export async function setPreviewOptIn(on) {
65
+ export async function setPreviewOptIn(on, commit = (apply) => apply()) {
62
66
  if (on && !P().optedIn() && !(await confirmDialog({ message: t("chat.pvConfirm"), confirmLabel: t("chat.pvAllowButton") }))) on = false;
63
- P().setOptIn(on);
67
+ commit(() => P().setOptIn(on));
64
68
  // #1665: kept on the host for every device (only a signed-in page writes it; never the public link).
65
69
  savePref("preview.optIn", P().optedIn());
66
70
  return P().optedIn();
@@ -73,11 +77,14 @@ export function boot({ stream, boot: bootData, deps = {} }) {
73
77
  fetch: (...a) => fetch(...a), toast, announce, t, setTimeout: (fn, ms) => setTimeout(fn, ms), confirm: confirmDialog, ...deps,
74
78
  });
75
79
  emojiIndex = createEmojiIndex((url) => (deps.fetch || fetch)(url, { credentials: "same-origin" }));
80
+ mentionIndex = createMentionIndex((url) => (deps.fetch || fetch)(url, { credentials: "same-origin" }));
76
81
  store.attach(stream);
77
82
  // #1306: what the server said about previews for this load. The page-wide message and storage listeners this adds
78
83
  // belong to the app, once (they are never per mount).
79
84
  // #1554: its words in the page's language (chat.pv*); a key the dictionary does not have keeps preview.js's English.
80
85
  if (P() && bootData) P().init(bootData, { text: (key, ...v) => { const k = `chat.${key}`, s = t(k, ...v); return s === k ? null : s; } });
86
+ // #1723: the public link's preview was still starting when this page loaded: ask until it is up (or failed).
87
+ if (P() && bootData) pollPublicPreview(P());
81
88
  // #1568: the banner's "I understand" lasts until the next sign-in — preview.js is told which sign-in this page is
82
89
  // (the session handle, from the one /auth/session read); none (no session, or no AgendAuth): this tab only.
83
90
  if (P() && typeof P().setBannerScope === "function") {
@@ -127,6 +134,9 @@ function useChatTick(name, kinds) {
127
134
 
128
135
  // ── The panel ──
129
136
 
137
+ /** #1722: the chat's title — the name that leads, the other on hover. */
138
+ const headerName = ({ primary, secondary }) => (secondary ? html`<span title=${secondary}>${primary}</span>` : primary);
139
+
130
140
  export function ChatPanel({ route, navKey }) {
131
141
  const lease = useLease(navKey);
132
142
  const app = useStore(appStore);
@@ -162,6 +172,7 @@ function NotFound({ name }) {
162
172
  }
163
173
 
164
174
  function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
175
+ const names = useStore(namesStore); // #1722: which name leads (the title)
165
176
  const [dialog, setDialog] = useState(null); // "delete" | null
166
177
  const [wrap, setWrap] = useState(codeWrap);
167
178
  const view = useRef(null), split = useRef(null), thread = useRef(null);
@@ -197,6 +208,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
197
208
  try { const res = await fetch(`/ui/${verb}/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" } }); r = await res.json(); }
198
209
  catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
199
210
  if (r && r.error) toast(r.error, false);
211
+ else if (r && r.held) toast(r.message);
200
212
  else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
201
213
  };
202
214
  const items = [
@@ -214,14 +226,14 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
214
226
  ${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", inst.model)} aria-label=${t("chat.chipModel", inst.model)} onClick=${() => pick("model")}>${inst.model}</button>` : null}
215
227
  ${inst.effort ? html`<button type="button" class="hd-chip" title=${t("chat.chipEffort", inst.effort)} aria-label=${t("chat.chipEffort", inst.effort)} onClick=${() => pick("effort")}>${inst.effort}</button>` : null}`;
216
228
  return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
217
- <${PanelHeader} title=${labelOf(inst) !== name ? html`<span title=${name}>${labelOf(inst)}</span>` : name} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
229
+ <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
218
230
  <div class="chat-split" ref=${split}>
219
231
  <div class="chat-main">
220
232
  <${Thread} name=${name} th=${thread} />
221
233
  <${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} />
222
234
  </div>
223
235
  <${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
224
- keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${() => setPreviewOptIn(true)} />
236
+ keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${(commit) => setPreviewOptIn(true, commit)} />
225
237
  </div>
226
238
  <${LightboxHost} />
227
239
  <div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
@@ -240,14 +252,17 @@ function Thread({ name, th }) {
240
252
  setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
241
253
  clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
242
254
  replyButtonBusy: (id) => store.state.rbBusy.has(id),
243
- panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o) }, // #1481
255
+ panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims }, // #1481, #1704
244
256
  openImage: openLightbox, // an image, full size, in the page
245
257
  emojis: (id) => !!emojiIndex && emojiIndex.known(id), // #1630: the fleet's server emoji
258
+ mentions: (kind, id) => (mentionIndex ? mentionIndex.name(kind, id) : null), // #1682: mention names
246
259
  });
247
260
  th.current = thread;
248
261
  // #1630: the server-emoji list is read once a message shows a tag; when it arrives, those messages are drawn again.
249
- const wantEmojis = (msgs) => { if (emojiIndex && emojiIndex.wants(msgs)) emojiIndex.ensure(); };
262
+ // #1682: the same for mention names — asked for the ids the messages carry; when names arrive, drawn again.
263
+ const wantEmojis = (msgs) => { if (emojiIndex && emojiIndex.wants(msgs)) emojiIndex.ensure(); if (mentionIndex) mentionIndex.ensure(msgs); };
250
264
  const offEmojis = emojiIndex ? emojiIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
265
+ const offMentions = mentionIndex ? mentionIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
251
266
  thread.render(store.state.msgs[name] || [], { restore: store.state.scrollMemo[name] ?? null });
252
267
  wantEmojis(store.state.msgs[name]);
253
268
  const off = store.subscribe((instance, kind) => {
@@ -262,7 +277,7 @@ function Thread({ name, th }) {
262
277
  sc.addEventListener("scroll", onScroll, { passive: true });
263
278
  ls.addEventListener("click", thread.onClick);
264
279
  return () => {
265
- off(); offPv(); offPanel(); offEmojis();
280
+ off(); offPv(); offPanel(); offEmojis(); offMentions();
266
281
  sc.removeEventListener("scroll", onScroll);
267
282
  ls.removeEventListener("click", thread.onClick);
268
283
  thread.dispose(); // the whole thread goes: every preview in it stops first
@@ -479,7 +494,7 @@ function DeleteDialog({ name, onClose }) {
479
494
  <button type="button" class="btn btn-danger" disabled=${typed !== want || busy} onClick=${go}>${t("chat.deleteGo")}</button>`}>
480
495
  <p>${t("chat.deleteHelp", name)}</p>
481
496
  <label class="field"><span class="sr-only">${t("chat.deleteHelp", name)}</span>
482
- <input type="text" value=${typed} autocomplete="off" spellcheck="false" placeholder=${want} onInput=${(e) => setTyped(e.target.value)}
497
+ <input type="text" value=${typed} autocomplete="off" spellcheck=${false} placeholder=${want} onInput=${(e) => setTyped(e.target.value)}
483
498
  onKeyDown=${(e) => { if (e.key === "Enter") go(); }} /></label>
484
499
  </${Dialog}>`;
485
500
  }
@@ -73,11 +73,13 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
73
73
  try { const res = await fetch(`/ui/${verb}/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" } }); r = await res.json(); }
74
74
  catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
75
75
  if (r && r.error) toast(r.error, false);
76
+ else if (r && r.held) toast(r.message);
76
77
  else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
77
78
  });
78
79
  const pauseWake = (verb) => act(verb, async () => {
79
80
  const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${verb}`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
80
81
  if (!res.ok) toast((res.body && res.body.error) || t("chat.dActionFailed"), false);
82
+ else if (res.body && res.body.held) toast(res.body.message);
81
83
  else toast(t(verb === "pause" ? "chat.dPaused" : "chat.dWoken", name));
82
84
  });
83
85
  // A ClassicBot room (classicBot.yaml) keeps Classic's split: its own Settings dialog, and only the actions Settings'