@songsid/agend 2.2.0-alpha.2 → 2.2.0-alpha.4

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 (257) hide show
  1. package/README.md +4 -4
  2. package/README.zh-TW.md +4 -4
  3. package/dist/agent-endpoint.d.ts +4 -1
  4. package/dist/agent-endpoint.js +14 -2
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/atomic-write.d.ts +21 -0
  7. package/dist/atomic-write.js +65 -0
  8. package/dist/atomic-write.js.map +1 -0
  9. package/dist/auth-api.js +2 -0
  10. package/dist/auth-api.js.map +1 -1
  11. package/dist/backend/codex.d.ts +7 -0
  12. package/dist/backend/codex.js +7 -0
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/muse.d.ts +7 -0
  15. package/dist/backend/muse.js +7 -0
  16. package/dist/backend/muse.js.map +1 -1
  17. package/dist/backend/types.d.ts +6 -0
  18. package/dist/backend/types.js.map +1 -1
  19. package/dist/binary-probe.d.ts +80 -0
  20. package/dist/binary-probe.js +182 -0
  21. package/dist/binary-probe.js.map +1 -0
  22. package/dist/cache-service.d.ts +17 -1
  23. package/dist/cache-service.js +39 -4
  24. package/dist/cache-service.js.map +1 -1
  25. package/dist/channel/adapters/discord.d.ts +5 -1
  26. package/dist/channel/adapters/discord.js +56 -11
  27. package/dist/channel/adapters/discord.js.map +1 -1
  28. package/dist/channel/adapters/telegram.d.ts +38 -1
  29. package/dist/channel/adapters/telegram.js +82 -0
  30. package/dist/channel/adapters/telegram.js.map +1 -1
  31. package/dist/channel/mcp-tools.js +1 -1
  32. package/dist/channel/mcp-tools.js.map +1 -1
  33. package/dist/channel/tool-router.d.ts +8 -1
  34. package/dist/channel/tool-router.js +4 -2
  35. package/dist/channel/tool-router.js.map +1 -1
  36. package/dist/channel/types.d.ts +21 -0
  37. package/dist/cli.js +133 -38
  38. package/dist/cli.js.map +1 -1
  39. package/dist/connection-secrets.d.ts +5 -0
  40. package/dist/connection-secrets.js.map +1 -1
  41. package/dist/cpu-profile.js +50 -0
  42. package/dist/cpu-profile.js.map +1 -1
  43. package/dist/daemon.d.ts +12 -0
  44. package/dist/daemon.js +166 -26
  45. package/dist/daemon.js.map +1 -1
  46. package/dist/delivery-outbox.d.ts +1 -0
  47. package/dist/delivery-outbox.js +12 -0
  48. package/dist/delivery-outbox.js.map +1 -1
  49. package/dist/discord-general.d.ts +15 -0
  50. package/dist/discord-general.js +24 -0
  51. package/dist/discord-general.js.map +1 -0
  52. package/dist/discord-permissions.d.ts +33 -0
  53. package/dist/discord-permissions.js +42 -0
  54. package/dist/discord-permissions.js.map +1 -0
  55. package/dist/env-file.d.ts +11 -0
  56. package/dist/env-file.js +27 -0
  57. package/dist/env-file.js.map +1 -0
  58. package/dist/fleet-context.d.ts +5 -0
  59. package/dist/fleet-context.js.map +1 -1
  60. package/dist/fleet-level-config.d.ts +8 -1
  61. package/dist/fleet-level-config.js +31 -0
  62. package/dist/fleet-level-config.js.map +1 -1
  63. package/dist/fleet-manager.d.ts +76 -7
  64. package/dist/fleet-manager.js +610 -142
  65. package/dist/fleet-manager.js.map +1 -1
  66. package/dist/fleet-stop-budget.d.ts +22 -0
  67. package/dist/fleet-stop-budget.js +95 -0
  68. package/dist/fleet-stop-budget.js.map +1 -0
  69. package/dist/kiro-transcript-lane.d.ts +8 -4
  70. package/dist/kiro-transcript-lane.js +66 -44
  71. package/dist/kiro-transcript-lane.js.map +1 -1
  72. package/dist/locale.js +88 -0
  73. package/dist/locale.js.map +1 -1
  74. package/dist/next-steps.d.ts +5 -0
  75. package/dist/next-steps.js +26 -0
  76. package/dist/next-steps.js.map +1 -0
  77. package/dist/outbound-handlers.js +8 -0
  78. package/dist/outbound-handlers.js.map +1 -1
  79. package/dist/outbound-schemas.d.ts +4 -0
  80. package/dist/outbound-schemas.js +6 -1
  81. package/dist/outbound-schemas.js.map +1 -1
  82. package/dist/outbox-open-error.d.ts +7 -0
  83. package/dist/outbox-open-error.js +15 -0
  84. package/dist/outbox-open-error.js.map +1 -0
  85. package/dist/pane-context-cache.d.ts +25 -0
  86. package/dist/pane-context-cache.js +73 -0
  87. package/dist/pane-context-cache.js.map +1 -0
  88. package/dist/paths.d.ts +0 -7
  89. package/dist/paths.js +8 -2
  90. package/dist/paths.js.map +1 -1
  91. package/dist/provider-probe.d.ts +15 -0
  92. package/dist/provider-probe.js +20 -0
  93. package/dist/provider-probe.js.map +1 -1
  94. package/dist/public-link-progress.d.ts +75 -0
  95. package/dist/public-link-progress.js +204 -0
  96. package/dist/public-link-progress.js.map +1 -0
  97. package/dist/public-web-gateway.js +4 -1
  98. package/dist/public-web-gateway.js.map +1 -1
  99. package/dist/public-web-link.d.ts +9 -2
  100. package/dist/public-web-link.js +89 -15
  101. package/dist/public-web-link.js.map +1 -1
  102. package/dist/quickstart-api.d.ts +93 -20
  103. package/dist/quickstart-api.js +198 -63
  104. package/dist/quickstart-api.js.map +1 -1
  105. package/dist/quickstart.d.ts +15 -0
  106. package/dist/quickstart.js +100 -29
  107. package/dist/quickstart.js.map +1 -1
  108. package/dist/reply-buttons-controller.d.ts +98 -0
  109. package/dist/reply-buttons-controller.js +174 -0
  110. package/dist/reply-buttons-controller.js.map +1 -0
  111. package/dist/reply-buttons.d.ts +113 -0
  112. package/dist/reply-buttons.js +208 -0
  113. package/dist/reply-buttons.js.map +1 -0
  114. package/dist/reply-dedup.js +3 -1
  115. package/dist/reply-dedup.js.map +1 -1
  116. package/dist/restart-guard.d.ts +1 -0
  117. package/dist/restart-guard.js +8 -3
  118. package/dist/restart-guard.js.map +1 -1
  119. package/dist/service-activation.d.ts +15 -2
  120. package/dist/service-activation.js +81 -4
  121. package/dist/service-activation.js.map +1 -1
  122. package/dist/service-installer.js +19 -1
  123. package/dist/service-installer.js.map +1 -1
  124. package/dist/settings-api.js +53 -12
  125. package/dist/settings-api.js.map +1 -1
  126. package/dist/settings-authority.d.ts +11 -0
  127. package/dist/settings-authority.js +128 -0
  128. package/dist/settings-authority.js.map +1 -0
  129. package/dist/settings-change.d.ts +5 -0
  130. package/dist/settings-change.js +4 -3
  131. package/dist/settings-change.js.map +1 -1
  132. package/dist/settings-confirmation.d.ts +3 -0
  133. package/dist/settings-confirmation.js +7 -1
  134. package/dist/settings-confirmation.js.map +1 -1
  135. package/dist/settings-effect.js +19 -7
  136. package/dist/settings-effect.js.map +1 -1
  137. package/dist/settings-http-confirmation.js +1 -1
  138. package/dist/settings-http-confirmation.js.map +1 -1
  139. package/dist/settings-redaction.d.ts +18 -0
  140. package/dist/settings-redaction.js +58 -0
  141. package/dist/settings-redaction.js.map +1 -0
  142. package/dist/setup-form.d.ts +9 -1
  143. package/dist/setup-form.js +25 -1
  144. package/dist/setup-form.js.map +1 -1
  145. package/dist/setup-host.js +13 -3
  146. package/dist/setup-host.js.map +1 -1
  147. package/dist/setup-wizard.js +26 -19
  148. package/dist/setup-wizard.js.map +1 -1
  149. package/dist/systemd-runtime.d.ts +15 -0
  150. package/dist/systemd-runtime.js +41 -0
  151. package/dist/systemd-runtime.js.map +1 -0
  152. package/dist/tmux-control.d.ts +22 -0
  153. package/dist/tmux-control.js +82 -17
  154. package/dist/tmux-control.js.map +1 -1
  155. package/dist/token-env-name.d.ts +37 -0
  156. package/dist/token-env-name.js +93 -0
  157. package/dist/token-env-name.js.map +1 -0
  158. package/dist/tool-permissions.js +5 -3
  159. package/dist/tool-permissions.js.map +1 -1
  160. package/dist/topic-commands.d.ts +7 -16
  161. package/dist/topic-commands.js +48 -84
  162. package/dist/topic-commands.js.map +1 -1
  163. package/dist/transcript-monitor.js +3 -0
  164. package/dist/transcript-monitor.js.map +1 -1
  165. package/dist/transcript-sources.d.ts +68 -0
  166. package/dist/transcript-sources.js +336 -29
  167. package/dist/transcript-sources.js.map +1 -1
  168. package/dist/transcript-turns.d.ts +39 -0
  169. package/dist/transcript-turns.js +58 -0
  170. package/dist/transcript-turns.js.map +1 -0
  171. package/dist/tunnel/cloudflared-install.d.ts +42 -0
  172. package/dist/tunnel/cloudflared-install.js +194 -9
  173. package/dist/tunnel/cloudflared-install.js.map +1 -1
  174. package/dist/tunnel/deb-extract.d.ts +9 -0
  175. package/dist/tunnel/deb-extract.js +94 -0
  176. package/dist/tunnel/deb-extract.js.map +1 -0
  177. package/dist/turn-reply-guard.d.ts +3 -0
  178. package/dist/turn-reply-guard.js.map +1 -1
  179. package/dist/ui/app.html +3 -0
  180. package/dist/ui/chat-render.js +32 -6
  181. package/dist/ui/chat-store.js +21 -1
  182. package/dist/ui/chat-strings.js +45 -0
  183. package/dist/ui/chat-thread.js +109 -52
  184. package/dist/ui/first-run.js +64 -0
  185. package/dist/ui/html-attachment.js +26 -0
  186. package/dist/ui/icons/apple-touch-icon.png +0 -0
  187. package/dist/ui/icons/favicon.ico +0 -0
  188. package/dist/ui/icons/favicon.svg +8 -0
  189. package/dist/ui/panel-chat.js +16 -7
  190. package/dist/ui/panel-fleet.js +5 -5
  191. package/dist/ui/panel-settings.js +18 -8
  192. package/dist/ui/preview-panel.js +47 -7
  193. package/dist/ui/preview.js +36 -13
  194. package/dist/ui/settings-apply.js +3 -1
  195. package/dist/ui/settings-dialogs.js +95 -25
  196. package/dist/ui/settings-discord.js +130 -0
  197. package/dist/ui/settings-model.js +21 -0
  198. package/dist/ui/settings-strings.js +46 -6
  199. package/dist/ui/settings-token.js +54 -0
  200. package/dist/ui/settings-wizard.js +63 -29
  201. package/dist/ui/shared/app-shell.js +19 -33
  202. package/dist/ui/shared/app-store.js +5 -3
  203. package/dist/ui/shared/app-stream.js +2 -1
  204. package/dist/ui/shared/app.css +34 -2
  205. package/dist/ui/shared/app.js +2 -1
  206. package/dist/ui/shared/instance-nav.js +310 -0
  207. package/dist/ui/shared/panel-view.js +12 -190
  208. package/dist/ui/shared/view-roster-store.js +5 -0
  209. package/dist/ui/shared/view-strings.js +12 -0
  210. package/dist/ui/signin.html +3 -0
  211. package/dist/update-dispatch.d.ts +1 -1
  212. package/dist/update-dispatch.js +1 -1
  213. package/dist/update-install.d.ts +15 -3
  214. package/dist/update-install.js +41 -13
  215. package/dist/update-install.js.map +1 -1
  216. package/dist/update-launch.d.ts +22 -0
  217. package/dist/update-launch.js +106 -0
  218. package/dist/update-launch.js.map +1 -0
  219. package/dist/update-marker.js +3 -8
  220. package/dist/update-marker.js.map +1 -1
  221. package/dist/usage/format-rich.js +7 -1
  222. package/dist/usage/format-rich.js.map +1 -1
  223. package/dist/usage/i18n-keys.d.ts +1 -1
  224. package/dist/usage/i18n-keys.js +1 -1
  225. package/dist/usage/i18n-keys.js.map +1 -1
  226. package/dist/usage/providers.d.ts +4 -0
  227. package/dist/usage/providers.js +2 -1
  228. package/dist/usage/providers.js.map +1 -1
  229. package/dist/usage/usage-api.js +48 -2
  230. package/dist/usage/usage-api.js.map +1 -1
  231. package/dist/view-api.d.ts +40 -0
  232. package/dist/view-api.js +28 -10
  233. package/dist/view-api.js.map +1 -1
  234. package/dist/web-api.d.ts +8 -0
  235. package/dist/web-api.js +53 -17
  236. package/dist/web-api.js.map +1 -1
  237. package/dist/web-auth.d.ts +8 -0
  238. package/dist/web-auth.js +34 -8
  239. package/dist/web-auth.js.map +1 -1
  240. package/dist/web-chat-history.d.ts +21 -0
  241. package/dist/web-chat-history.js +44 -0
  242. package/dist/web-chat-history.js.map +1 -1
  243. package/dist/web-host-guard.d.ts +18 -2
  244. package/dist/web-host-guard.js +30 -5
  245. package/dist/web-host-guard.js.map +1 -1
  246. package/dist/web-icons.d.ts +21 -0
  247. package/dist/web-icons.js +38 -0
  248. package/dist/web-icons.js.map +1 -0
  249. package/dist/web-preview.d.ts +9 -0
  250. package/dist/web-preview.js +15 -9
  251. package/dist/web-preview.js.map +1 -1
  252. package/dist/web-session.d.ts +7 -1
  253. package/dist/web-session.js +23 -2
  254. package/dist/web-session.js.map +1 -1
  255. package/launcher/postinstall.cjs +8 -4
  256. package/package.json +1 -1
  257. package/templates/systemd.service.ejs +3 -3
@@ -8,6 +8,7 @@
8
8
  // Unrelated updates (a new message, another message's tick) stop nothing (#1306 §6.2, #1408 §4).
9
9
  import "./chat-render.js";
10
10
  import "./preview.js";
11
+ import { loadHtmlAttachment } from "./html-attachment.js";
11
12
 
12
13
  const R = () => globalThis.AgendChatRender;
13
14
  const P = () => globalThis.AgendPreview;
@@ -44,7 +45,23 @@ export function createThread(list, scroller, opts) {
44
45
  // renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
45
46
  const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
46
47
  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>`;
47
- 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, x.role === "agent" ? { htmlCards: true } : undefined)}</div>${R().attachmentsHtml(x.attachments)}</div>${tools}</div>`;
48
+ const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
49
+ 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, x.role === "agent" ? { htmlCards: true } : undefined)}</div>${R().attachmentsHtml(x.attachments, x.role === "agent" ? { htmlCards: true } : undefined)}${buttons}</div>${tools}</div>`;
50
+ }
51
+ /**
52
+ * #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
53
+ * nothing else. Ended (chosen / expired) or a click in flight: every button off; the chosen one marked, and who.
54
+ */
55
+ function replyButtonsHtml(b) {
56
+ if (!b || typeof b.id !== "string" || !Array.isArray(b.labels) || !b.labels.length) return "";
57
+ const busy = !!(opts.replyButtonBusy && opts.replyButtonBusy(b.id));
58
+ const off = b.state !== "open" || busy;
59
+ const items = b.labels.map((label, i) => {
60
+ const chosen = b.state === "chosen" && b.chosen === i;
61
+ return `<button type="button" class="btn btn-sm rb-btn${chosen ? " chosen" : ""}" data-act="replyButton" data-arg="${escAttr(`${b.id}:${i}`)}"${off ? " disabled" : ""}>${escAttr(chosen ? `✓ ${label}` : label)}</button>`;
62
+ }).join("");
63
+ const note = b.state === "chosen" ? trf("chat.rbChosen", b.by || "") : b.state === "expired" ? tr("chat.rbExpired") : "";
64
+ return `<div class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
48
65
  }
49
66
  function msgNode(html, x, stoppedKeys) {
50
67
  const tpl = list.ownerDocument.createElement("template");
@@ -76,72 +93,108 @@ export function createThread(list, scroller, opts) {
76
93
  }
77
94
  }
78
95
 
79
- // HTML cards (#1306): each ```html fence of a server-marked agent message gets a card under its code block. Building
80
- // a card makes no frame; Preview does (AgendPreview.start → mountPreview).
96
+ // HTML cards (#1306): each ```html fence of a server-marked agent message gets a card under its code block, and each
97
+ // .html/.htm file it attached gets one under its files (§6.1, Q4). Building a card makes no frame and fetches
98
+ // nothing; Preview does (AgendPreview.start → mountPreview), after reading an attachment's HTML on that click.
81
99
  function decorateHtmlCards(node, x, stoppedKeys) {
82
100
  const doc = list.ownerDocument;
83
101
  const fences = R().htmlFences(x.text);
84
102
  for (const ph of node.querySelectorAll(".html-card[data-card]")) {
85
103
  const n = Number(ph.dataset.card), fence = fences[n];
86
104
  if (!fence) continue;
87
- const key = `${msgKey(x)}:f${n}`;
88
105
  ph.textContent = "";
89
106
  if (ph.classList.contains("truncated") || !fence.terminated) {
90
107
  const note = doc.createElement("div"); note.className = "pv-note"; note.textContent = tr("chat.pvTruncated"); ph.append(note);
91
108
  continue;
92
109
  }
93
- const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
94
- const head = el("div", "pv-head");
95
- const label = el("span", "pv-label", tr("chat.pvHtml"));
96
- const run = el("button", "btn btn-sm pv-run", tr("chat.pvPreview")); run.type = "button";
97
- const stopB = el("button", "btn btn-sm pv-stop", tr("chat.pvStop")); stopB.type = "button"; stopB.hidden = true;
98
- const dl = el("button", "btn btn-sm btn-ghost pv-dl", tr("chat.pvDownload")); dl.type = "button";
99
- const panel = opts.panel || null;
100
- const toPanel = panel ? el("button", "btn btn-sm btn-ghost pv-open", tr("chat.pvOpenPanel")) : null;
101
- if (toPanel) toPanel.type = "button";
102
- const menu = el("details", "pv-menu");
103
- const sum = el("summary", null, "⋯"); sum.title = tr("chat.pvMenu"); sum.setAttribute("aria-label", tr("chat.pvMenu"));
104
- const optB = el("button", "chip-btn pv-opt"); optB.type = "button";
105
- const neverB = el("button", "chip-btn pv-never"); neverB.type = "button";
106
- menu.append(sum, optB, neverB);
107
- head.append(label, run, stopB, ...(toPanel ? [toPanel] : []), dl, menu);
108
- const note = el("div", "pv-note");
109
- const banner = el("div", "pv-banner", P().BANNER); banner.hidden = true;
110
- const holder = el("div", "pv-holder");
111
- ph.append(head, note, banner, holder);
112
- let state = "idle";
113
- const refresh = (reason) => {
114
- const a = P().availability();
115
- const going = state === "starting" || state === "running";
116
- const inPanel = !!panel && panel.shown() === key; // #1481: the panel shows this block; the card points there
117
- run.hidden = !a.ok || going || inPanel;
118
- if (toPanel) toPanel.hidden = inPanel;
119
- stopB.hidden = !going;
120
- banner.hidden = !going;
121
- note.textContent = reason != null ? reason : going ? (state === "starting" ? tr("chat.pvStarting") : "") : inPanel ? tr("chat.pvInPanel") : a.ok ? "" : a.reason;
122
- optB.textContent = P().optedIn() ? tr("chat.pvDisallow") : tr("chat.pvAllow");
123
- neverB.textContent = P().never() ? tr("chat.pvNeverUndo") : tr("chat.pvNever");
124
- };
125
- const ui = { state: (name, reason) => { state = name === "starting" || name === "running" ? name : "idle"; refresh(reason || null); } };
126
- run.onclick = () => P().start(key, holder, fence.code, ui);
127
- stopB.onclick = () => P().stop(key, "stopped", "");
128
- // A running preview moves: it stops here and starts in the panel (the click that ran it). An idle one opens the
129
- // panel with its own Preview to click.
130
- if (toPanel) toPanel.onclick = () => {
131
- const moving = P().running(key);
132
- if (moving) P().stop(key, "stopped", "");
133
- panel.open({ key, instance: x.instance, msgKey: msgKey(x), n, code: fence.code, sender: x.sender, ts: x.ts }, { run: moving });
134
- };
135
- dl.onclick = () => opts.download(fence.code);
136
- optB.onclick = () => { menu.open = false; opts.setPreviewOptIn(!P().optedIn()); };
137
- neverB.onclick = () => { menu.open = false; P().setNever(!P().never()); refreshCards(); };
138
- cardRefresh.set(ph, refresh);
139
- refresh(stoppedKeys.includes(key) ? tr("chat.pvChanged") : null);
110
+ buildCard(ph, x, `${msgKey(x)}:f${n}`, { n, code: fence.code }, stoppedKeys);
111
+ }
112
+ const atts = Array.isArray(x.attachments) ? x.attachments : [];
113
+ for (const ph of node.querySelectorAll(".html-card[data-att]")) {
114
+ const att = atts.find(a => a && a.id === ph.dataset.att);
115
+ if (!att || !R().isHtmlAttachment(att)) continue;
116
+ ph.textContent = "";
117
+ buildCard(ph, x, `${msgKey(x)}:a${att.id}`, { att: { id: att.id, name: att.name, size: att.size } }, stoppedKeys);
140
118
  }
141
119
  }
120
+ /** Why an attachment's HTML could not be read, for the card's note. */
121
+ const attReason = (reason) => tr(reason === "over" ? "chat.pvAttOver" : reason === "gone" ? "chat.pvAttGone" : "chat.pvAttFailed");
122
+ /** One card: `source` is { n, code } for a fence or { att } for an attachment (its HTML read on the first click). */
123
+ function buildCard(ph, x, key, source, stoppedKeys) {
124
+ const doc = list.ownerDocument;
125
+ const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
126
+ const att = source.att || null;
127
+ let code = att ? null : source.code;
128
+ const head = el("div", "pv-head");
129
+ const label = el("span", "pv-label", att ? `${tr("chat.pvHtml")} · ${att.name}` : tr("chat.pvHtml"));
130
+ const run = el("button", "btn btn-sm pv-run", tr("chat.pvPreview")); run.type = "button";
131
+ const stopB = el("button", "btn btn-sm pv-stop", tr("chat.pvStop")); stopB.type = "button"; stopB.hidden = true;
132
+ // An attachment already has its download link (the file itself) right above the card.
133
+ const dl = att ? null : el("button", "btn btn-sm btn-ghost pv-dl", tr("chat.pvDownload"));
134
+ if (dl) dl.type = "button";
135
+ const panel = opts.panel || null;
136
+ const toPanel = panel ? el("button", "btn btn-sm btn-ghost pv-open", tr("chat.pvOpenPanel")) : null;
137
+ if (toPanel) toPanel.type = "button";
138
+ const menu = el("details", "pv-menu");
139
+ const sum = el("summary", null, "⋯"); sum.title = tr("chat.pvMenu"); sum.setAttribute("aria-label", tr("chat.pvMenu"));
140
+ const optB = el("button", "chip-btn pv-opt"); optB.type = "button";
141
+ const neverB = el("button", "chip-btn pv-never"); neverB.type = "button";
142
+ menu.append(sum, optB, neverB);
143
+ head.append(label, run, stopB, ...(toPanel ? [toPanel] : []), ...(dl ? [dl] : []), menu);
144
+ const note = el("div", "pv-note");
145
+ const banner = el("div", "pv-banner", P().banner()); banner.hidden = true;
146
+ const holder = el("div", "pv-holder");
147
+ ph.append(head, note, banner, holder);
148
+ let state = "idle";
149
+ // Reading an attachment: one read per card at a time (a second click while it reads does nothing), and a read
150
+ // whose card was stopped, replaced or left meanwhile is dropped (`reading` moved on, or the card left the page).
151
+ let reading = 0, reads = 0;
152
+ const refresh = (reason) => {
153
+ const a = P().availability();
154
+ const going = state === "starting" || state === "running";
155
+ const inPanel = !!panel && panel.shown() === key; // #1481: the panel shows this block; the card points there
156
+ run.hidden = !a.ok || going || inPanel || !!reading;
157
+ if (toPanel) toPanel.hidden = inPanel || !!reading;
158
+ stopB.hidden = !going && !reading;
159
+ banner.hidden = !going;
160
+ note.textContent = reason != null ? reason : reading ? tr("chat.pvLoading") : going ? (state === "starting" ? tr("chat.pvStarting") : "") : inPanel ? tr("chat.pvInPanel") : a.ok ? "" : a.reason;
161
+ optB.textContent = P().optedIn() ? tr("chat.pvDisallow") : tr("chat.pvAllow");
162
+ neverB.textContent = P().never() ? tr("chat.pvNeverUndo") : tr("chat.pvNever");
163
+ };
164
+ /** The HTML, then `then(code)`: at once for a fence or an attachment already read; otherwise read it first. */
165
+ const withCode = async (then) => {
166
+ if (code != null) { then(code); return; }
167
+ if (reading) return;
168
+ const mine = reading = ++reads;
169
+ refresh();
170
+ const r = await loadHtmlAttachment(att);
171
+ if (reading !== mine || !ph.isConnected) return;
172
+ reading = 0;
173
+ if (!r.ok) { refresh(attReason(r.reason)); return; }
174
+ code = r.code;
175
+ refresh();
176
+ then(code);
177
+ };
178
+ const ui = { state: (name, reason) => { state = name === "starting" || name === "running" ? name : "idle"; refresh(reason || null); } };
179
+ // Nothing is read for Preview on a device that does not allow previews: the button is not even shown there.
180
+ run.onclick = () => { if (P().availability().ok) withCode((c) => P().start(key, holder, c, ui)); };
181
+ stopB.onclick = () => { if (reading) { reading = 0; refresh(); } else P().stop(key, "stopped", ""); };
182
+ // A running preview moves: it stops here and starts in the panel (the click that ran it). An idle one opens the
183
+ // panel with its own Preview to click.
184
+ if (toPanel) toPanel.onclick = () => {
185
+ const moving = P().running(key);
186
+ if (moving) P().stop(key, "stopped", "");
187
+ 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 }));
188
+ };
189
+ if (dl) dl.onclick = () => opts.download(code);
190
+ optB.onclick = () => { menu.open = false; opts.setPreviewOptIn(!P().optedIn()); };
191
+ neverB.onclick = () => { menu.open = false; P().setNever(!P().never()); refreshCards(); };
192
+ cardRefresh.set(ph, refresh);
193
+ refresh(stoppedKeys.includes(key) ? tr("chat.pvChanged") : null);
194
+ }
142
195
  /** Re-read this device's preview choice on every card in this thread. */
143
196
  function refreshCards() {
144
- for (const node of list.querySelectorAll(".html-card[data-card]")) { const f = cardRefresh.get(node); if (f) f(); }
197
+ for (const node of list.querySelectorAll(".html-card[data-card], .html-card[data-att]")) { const f = cardRefresh.get(node); if (f) f(); }
145
198
  }
146
199
 
147
200
  const nearBottom = () => R().isNearBottom(scroller.scrollTop, scroller.clientHeight, scroller.scrollHeight);
@@ -233,6 +286,10 @@ export function createThread(list, scroller, opts) {
233
286
  const folded = box.classList.toggle("folded");
234
287
  el.textContent = folded ? trf("chat.showAll", el.dataset.lines) : tr("chat.showLess");
235
288
  } else if (act === "toggleWrap") opts.toggleWrap();
289
+ else if (act === "replyButton") {
290
+ const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
291
+ if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
292
+ }
236
293
  }
237
294
 
238
295
  /** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
@@ -0,0 +1,64 @@
1
+ // #1519 P7 (docs/design/ux-onboarding-walkthrough.md §5.1): the first-run card. A fleet with no chat connection (a
2
+ // web-only fleet) opens Chat and Settings with it: "Connect a chat app" opens the setup wizard (in Settings, where its
3
+ // progress is shown), "Use the web only" hides it in this browser. It stays until a connection exists or it is dismissed.
4
+ import { html } from "/assets/app-html.js";
5
+ import { register, t } from "/assets/app-i18n.js";
6
+ import { appStore, createStore, useStore } from "/assets/app-store.js";
7
+ import { navigate } from "/assets/app-nav.js";
8
+ import { settingsPath } from "/assets/app-route.js";
9
+ import { Icon } from "/assets/ui-icons.js";
10
+
11
+ register("firstRun", {
12
+ en: {
13
+ title: "Connect a chat app",
14
+ body: "AgEnD works in this browser on its own. To talk to your agents from Telegram or Discord too, add a bot: the setup wizard checks its token and connects it to an agent.",
15
+ connect: "Connect a chat app", webOnly: "Use the web only",
16
+ },
17
+ "zh-TW": {
18
+ title: "連接聊天應用程式",
19
+ body: "AgEnD 只用這個瀏覽器也能運作。想同時從 Telegram 或 Discord 跟你的 agent 對話,就加一個機器人:設定精靈會檢查它的 token,並把它接到一個 agent。",
20
+ connect: "連接聊天應用程式", webOnly: "只用網頁",
21
+ },
22
+ });
23
+
24
+ export const DISMISS_KEY = "agend_firstrun_dismissed";
25
+ function readDismissed() { try { return localStorage.getItem(DISMISS_KEY) === "1"; } catch { return false; } }
26
+ // `dismissed`: this page's own answer (it holds even when the browser keeps no storage); a stored one is read each time.
27
+ const local = createStore({ dismissed: false, wizard: false });
28
+
29
+ export function dismissFirstRun() {
30
+ try { localStorage.setItem(DISMISS_KEY, "1"); } catch { /* this page only */ }
31
+ local.set({ dismissed: true });
32
+ }
33
+
34
+ /** For the tests: a fresh page's state. */
35
+ export function resetFirstRun() { local.set({ dismissed: false, wizard: false }); }
36
+
37
+ /** Settings asks once, when it has its data: was it opened by "Connect a chat app" (open the wizard)? */
38
+ export function takeWizardRequest() {
39
+ if (!local.get().wizard) return false;
40
+ local.set({ wizard: false });
41
+ return true;
42
+ }
43
+
44
+ /** Shown on a known web-only fleet (no connection) until dismissed. */
45
+ export function useFirstRun() {
46
+ const app = useStore(appStore);
47
+ const { dismissed } = useStore(local);
48
+ return app.ready && app.connections === 0 && !dismissed && !readDismissed();
49
+ }
50
+
51
+ /** onConnect: Settings opens its wizard in place; elsewhere the card goes to Settings, which opens it. */
52
+ export function FirstRunCard({ onConnect }) {
53
+ const shown = useFirstRun();
54
+ if (!shown) return null;
55
+ const connect = onConnect || (() => { local.set({ wizard: true }); navigate(settingsPath("bots")); });
56
+ return html`<section class="card first-run" aria-labelledby="first-run-title">
57
+ <h3 id="first-run-title"><${Icon} name="plug" size=${14} /> ${t("firstRun.title")}</h3>
58
+ <p>${t("firstRun.body")}</p>
59
+ <div class="dlg-inline-actions">
60
+ <button type="button" class="btn btn-primary" onClick=${connect}>${t("firstRun.connect")}</button>
61
+ <button type="button" class="btn btn-ghost" onClick=${dismissFirstRun}>${t("firstRun.webOnly")}</button>
62
+ </div>
63
+ </section>`;
64
+ }
@@ -0,0 +1,26 @@
1
+ // #1306 §6.1 (Q4): an agent's .html / .htm attachment gets the same preview card as a ```html block. Its HTML is read
2
+ // from /ui/file/<id> — same origin, this session's cookie — only on a click (Preview, Open in panel), never to show a
3
+ // card. What runs is then handed to the preview frame exactly like a block's (AgendPreview.start): the file URL itself
4
+ // is never opened or framed on the dashboard's origin.
5
+
6
+ /** The same cap as a block's (preview.js LIMITS.maxBytes): UTF-8 bytes of the HTML. */
7
+ export const HTML_ATTACHMENT_MAX = 1024 * 1024;
8
+
9
+ /**
10
+ * Read an attachment's HTML: { ok: true, code } or { ok: false, reason } with reason "over" (the file is over the
11
+ * cap: its listed size is checked before anything is fetched, the bytes again after), "gone" (the fleet no longer
12
+ * serves that id — the file changed, or the fleet restarted), or "failed".
13
+ */
14
+ export async function loadHtmlAttachment(att, fetchImpl = globalThis.fetch) {
15
+ if (!att || typeof att.id !== "string" || !/^[0-9a-f]{32}$/.test(att.id)) return { ok: false, reason: "failed" };
16
+ if (typeof att.size === "number" && att.size > HTML_ATTACHMENT_MAX) return { ok: false, reason: "over" };
17
+ let res;
18
+ try { res = await fetchImpl(`/ui/file/${att.id}`, { credentials: "same-origin", cache: "no-store" }); }
19
+ catch { return { ok: false, reason: "failed" }; }
20
+ if (!res || !res.ok) return { ok: false, reason: res && res.status === 404 ? "gone" : "failed" };
21
+ let bytes;
22
+ try { bytes = await res.arrayBuffer(); } catch { return { ok: false, reason: "failed" }; }
23
+ if (bytes.byteLength > HTML_ATTACHMENT_MAX) return { ok: false, reason: "over" };
24
+ // UTF-8, as a block's text is; a BOM is dropped.
25
+ return { ok: true, code: new TextDecoder("utf-8").decode(bytes) };
26
+ }
Binary file
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2
+ <title>AgEnD — Ag</title>
3
+ <rect x="1" y="1" width="62" height="62" rx="12" fill="#e5eaf0" stroke="#617386" stroke-width="2"/>
4
+ <g transform="translate(0 -2)"><g fill="#14273d" fill-rule="evenodd">
5
+ <path d="M8.5 43 17 18h6l8.5 25H26l-2-6h-8l-2 6Zm9-10.5h5l-2.5-8Z"/>
6
+ <path transform="translate(2 0)" d="M49.5 27v18.8c0 5-3.1 7.2-8.2 7.2h-6.8v-4.8H41c2.6 0 3.7-.9 3.7-3.2v-3.3c-1.3 1.5-3.1 2.3-5.6 2.3-5.8 0-9-3.7-9-8.9 0-5.1 3.2-8.9 9-8.9 2.5 0 4.3.8 5.6 2.3V27Zm-9.7 3.8c-3 0-4.6 1.8-4.6 4.3s1.6 4.3 4.6 4.3 4.6-1.8 4.6-4.3-1.6-4.3-4.6-4.3Z"/>
7
+ </g></g>
8
+ </svg>
@@ -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 { PanelHeader, onPanelKey, setTitle, addFooterItem, statusClass, statusLabel, openDrawer } from "/assets/app-shell.js";
13
+ import { PanelHeader, onPanelKey, setTitle, addFooterItem, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
14
14
  import { navigate } from "/assets/app-nav.js";
15
15
  import { Menu } from "/assets/ui-menu.js";
16
16
  import { Dialog } from "/assets/ui-dialog.js";
@@ -25,6 +25,7 @@ import { createThread, msgKey } from "./chat-thread.js";
25
25
  import { PreviewPanel, openPanel, panelStore, shownKey } from "./preview-panel.js";
26
26
  import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
27
27
  import { installTour, refreshTourSpot, startTour } from "./chat-tour.js";
28
+ import { FirstRunCard } from "./first-run.js";
28
29
 
29
30
  const R = () => globalThis.AgendChatRender;
30
31
  const P = () => globalThis.AgendPreview;
@@ -63,7 +64,8 @@ export function boot({ stream, boot: bootData, deps = {} }) {
63
64
  store.attach(stream);
64
65
  // #1306: what the server said about previews for this load. The page-wide message and storage listeners this adds
65
66
  // belong to the app, once (they are never per mount).
66
- if (P() && bootData) P().init(bootData);
67
+ // #1554: its words in the page's language (chat.pv*); a key the dictionary does not have keeps preview.js's English.
68
+ if (P() && bootData) P().init(bootData, { text: (key, ...v) => { const k = `chat.${key}`, s = t(k, ...v); return s === k ? null : s; } });
67
69
  addFooterItem("previews", PreviewOptIn);
68
70
  addFooterItem("tour", TourButton);
69
71
  installTour();
@@ -99,9 +101,9 @@ async function copyText(text) {
99
101
  return ok;
100
102
  }
101
103
  /** The HTML as a file: a blob of the source as application/octet-stream, saved as reply.html — never opened. */
102
- function downloadHtml(code) {
104
+ function downloadHtml(code, name) {
103
105
  const url = URL.createObjectURL(new Blob([code], { type: "application/octet-stream" }));
104
- const a = document.createElement("a"); a.href = url; a.download = "reply.html"; a.className = "offscreen";
106
+ const a = document.createElement("a"); a.href = url; a.download = typeof name === "string" && name.trim() ? name.trim() : "reply.html"; a.className = "offscreen";
105
107
  document.body.appendChild(a); a.click(); a.remove();
106
108
  URL.revokeObjectURL(url);
107
109
  }
@@ -127,9 +129,14 @@ export function ChatPanel({ route, navKey }) {
127
129
  }, [name, !!inst]);
128
130
  useEffect(() => { setTitle(name || t("app.chat")); }, [name, navKey]);
129
131
  if (!name) {
132
+ // #1519 P7: a web-only fleet starts here with the first-run card; with no agent at all, the way to make one.
133
+ const none = app.ready && !app.instances.length;
130
134
  return html`<div class="panel p-chat"><${PanelHeader} title=${t("app.chat")} />
131
- <div class="panel-body center"><${Empty} icon="chat" title=${t("chat.pick")} hint=${t("chat.pickHint")}
132
- action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} /></div></div>`;
135
+ <div class="panel-body center"><div class="first-run-stack"><${FirstRunCard} />
136
+ ${none ? html`<${Empty} icon="bot" title=${t("app.noInstances")} hint=${t("app.noInstancesHint")}
137
+ action=${html`<button type="button" class="btn btn-primary" onClick=${requestNewInstance}><${Icon} name="plus" size=${16} />${t("app.newInstance")}</button>`} />`
138
+ : html`<${Empty} icon="chat" title=${t("chat.pick")} hint=${t("chat.pickHint")}
139
+ action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} />`}</div></div></div>`;
133
140
  }
134
141
  if (!app.ready) return html`<div class="panel p-chat"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${5} /></div></div>`;
135
142
  if (!inst) return html`<${NotFound} name=${name} />`;
@@ -204,7 +211,7 @@ function ChatView({ name, inst, lease, exec, awaiting }) {
204
211
  <${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} />
205
212
  </div>
206
213
  <${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
207
- keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} />
214
+ keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${() => setPreviewOptIn(true)} />
208
215
  </div>
209
216
  <div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
210
217
  ${dialog === "details" ? html`<${DetailsDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
@@ -221,6 +228,8 @@ function Thread({ name, th }) {
221
228
  const thread = createThread(list.current, scroller.current, {
222
229
  t, tf: t, isUser: (x) => store.isUser(x, name), onJump: setJump, onEmpty: setEmpty,
223
230
  setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
231
+ clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
232
+ replyButtonBusy: (id) => store.state.rbBusy.has(id),
224
233
  panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o) }, // #1481
225
234
  });
226
235
  th.current = thread;
@@ -36,8 +36,8 @@ register("fleet", {
36
36
  channel: "Channel", type: "Type", tokenEnv: "Bot token env", groupId: "Group ID", show: "Show", hide: "Hide", access: "Access control", mode: "Mode", locked: "Locked", open: "Open",
37
37
  allowedUsers: "Allowed users", allowedUsersNote: "Comma-separated user IDs", defaults: "Defaults", backend: "Backend", roots: "Project roots", addRoot: "Add root", removeRoot: "Remove",
38
38
  saveNote: "Saving rewrites fleet.yaml (its comments are lost).",
39
- newInstance: "New instance", directory: "Directory", directoryHint: "Optional — a workspace is created if empty", topic: "Topic name", topicHint: "From the directory name",
40
- topicRequired: "Topic name is required when Directory is empty", description: "Description", descriptionHint: "What this instance does", model: "Model", modelHint: "e.g. sonnet, opus, gpt-5",
39
+ newInstance: "New instance", directory: "Directory", directoryHint: "Optional — a workspace is created if empty", topic: "Name", topicHint: "From the directory name",
40
+ topicRequired: "Name is required when Directory is empty", description: "Description", descriptionHint: "What this instance does", model: "Model", modelHint: "e.g. sonnet, opus, gpt-5",
41
41
  branch: "Branch (git worktree)", branchHint: "e.g. feature-x", tags: "Tags", tagsHint: "comma-separated, e.g. dev, review", systemPrompt: "System prompt", systemPromptHint: "Custom instructions for this instance",
42
42
  fleetDefault: "Use the fleet default", notInstalled: "(not installed)", deprecated: "(deprecated)", instanceCreated: "Instance created",
43
43
  },
@@ -56,8 +56,8 @@ register("fleet", {
56
56
  channel: "頻道", type: "類型", tokenEnv: "Bot token 環境變數", groupId: "Group ID", show: "顯示", hide: "隱藏", access: "存取控制", mode: "模式", locked: "鎖定", open: "開放",
57
57
  allowedUsers: "允許的使用者", allowedUsersNote: "以逗號分隔的使用者 ID", defaults: "預設值", backend: "Backend", roots: "專案根目錄", addRoot: "新增根目錄", removeRoot: "移除",
58
58
  saveNote: "儲存會改寫 fleet.yaml(其中的註解會遺失)。",
59
- newInstance: "新增 instance", directory: "目錄", directoryHint: "可留空——留空會自動建立工作區", topic: "Topic 名稱", topicHint: "預設取自目錄名稱",
60
- topicRequired: "目錄留空時必須填寫 Topic 名稱", description: "說明", descriptionHint: "這個 instance 做什麼", model: "模型", modelHint: "例如 sonnet、opus、gpt-5",
59
+ newInstance: "新增 instance", directory: "目錄", directoryHint: "可留空——留空會自動建立工作區", topic: "名稱", topicHint: "預設取自目錄名稱",
60
+ topicRequired: "目錄留空時必須填寫名稱", description: "說明", descriptionHint: "這個 instance 做什麼", model: "模型", modelHint: "例如 sonnet、opus、gpt-5",
61
61
  branch: "分支(git worktree)", branchHint: "例如 feature-x", tags: "標籤", tagsHint: "以逗號分隔,例如 dev, review", systemPrompt: "System prompt", systemPromptHint: "這個 instance 的自訂指示",
62
62
  fleetDefault: "使用 fleet 預設", notInstalled: "(未安裝)", deprecated: "(已淘汰)", instanceCreated: "Instance 已建立",
63
63
  },
@@ -359,7 +359,7 @@ function CreateTeam({ onClose, onDone }) {
359
359
  </${FormDialog}>`;
360
360
  }
361
361
 
362
- /** "New instance": the sidebar's ✎ opens it from anywhere (#1408 step 3 merges it with Settings' create flow). */
362
+ /** "New instance": the sidebar's + opens it from anywhere (#1408 step 3 merges it with Settings' create flow). */
363
363
  export function CreateInstanceDialog({ onClose }) {
364
364
  const lease = useLease("create-instance");
365
365
  const [backends, setBackends] = useState(null);
@@ -25,7 +25,7 @@ import { api, attach, confirmedWrite, newKey } from "./settings-confirm.js";
25
25
  import { dismissOperation, operationActive, restartFleet, startOperation, takeLeftover } from "./settings-apply.js";
26
26
  import {
27
27
  ACCESS_MODES, AGENT_MODES, DEFAULT_SCHEMA, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS, VISIBILITY_MODES, batchImpact,
28
- changedFields, channelId, channelsOf, chLabel, effectiveSummary, fleetModel, fromYaml, fullModelRequests, groupAgents,
28
+ changedFields, channelId, channelsOf, chLabel, connectionState, effectiveSummary, fleetModel, fromYaml, fullModelRequests, groupAgents,
29
29
  hasOwn, impactOf, localeOptions, defaultsImpact, requestImpact, nonNegative, positive, replyGuardSupported, shortName, STATUS_ORDER, toYaml,
30
30
  visibilityDefault, worstImpact,
31
31
  } from "./settings-model.js";
@@ -35,6 +35,7 @@ import {
35
35
  } from "./settings-dialogs.js";
36
36
  import { SetupWizard } from "./settings-wizard.js";
37
37
  import { CreateInstanceDialog } from "./panel-fleet.js";
38
+ import { FirstRunCard, takeWizardRequest } from "./first-run.js";
38
39
 
39
40
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
40
41
  const ICONS = { agents: "bot", bots: "plug", classic: "room", general: "sliders", advanced: "code" };
@@ -197,6 +198,8 @@ export function SettingsPanel({ route, navKey }) {
197
198
  };
198
199
 
199
200
  const ctx = data && !data.error ? makeCtx(data, setData, stage, unstage, reload, reloadLive, () => mount.current()) : null;
201
+ // #1519 P7: "Connect a chat app" on the first-run card elsewhere came here for the wizard: it opens once the data is in.
202
+ useEffect(() => { if (ctx && takeWizardRequest()) setDialog({ kind: "wizard", key: navKey }); }, [!!ctx, navKey]);
200
203
  const sectionBody = !data ? html`<${Skeleton} lines=${6} />`
201
204
  : data.error ? html`<${ErrorState} message=${tn("configLoadFailed")} onRetry=${reload} />`
202
205
  : section === "agents" ? html`<${Agents} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
@@ -220,6 +223,7 @@ export function SettingsPanel({ route, navKey }) {
220
223
  <input type="search" value=${search} placeholder=${tn("searchAll")} onInput=${(e) => setSearch(e.target.value)} /></label></div>` : null}
221
224
  ${op ? html`<${OperationCard} op=${op} schema=${data && data.schema} />` : null}
222
225
  <${PendingChanges} inline=${true} />
226
+ ${ctx && (section === "agents" || section === "bots") ? html`<${FirstRunCard} onConnect=${() => setDialog({ kind: "wizard", key: navKey })} />` : null}
223
227
  ${sectionBody}
224
228
  </div></div>
225
229
  ${staged.size ? html`<${PendingBar} staged=${staged} busy=${opBusy(op)} onApply=${apply} onDiscard=${discard} />` : null}
@@ -378,7 +382,12 @@ function Bots({ ctx, search, openDialog }) {
378
382
  return html`${head}
379
383
  ${!rows.length ? html`<${Empty} icon="plug" title=${needle ? tn("noAgentMatch", search.trim()) : tn("noBots")} />` : html`<div class="s-list">${rows.map(({ ch, i }) => {
380
384
  const users = (ch.access && ch.access.allowed_users) || [];
381
- const token = (ctx.connections.find((c, j) => channelId(c, j) === channelId(ch, i)) || {}).token_present;
385
+ const meta = ctx.connections.find((c, j) => channelId(c, j) === channelId(ch, i)) || {};
386
+ const token = meta.token_present;
387
+ // #1519 P6: this connection's own state (not the fleet's "Connected") — a stopped one says so; a known problem says
388
+ // what to do (P3: Message Content intent; a rejected token).
389
+ const state = connectionState(meta, ctx.fleetUp);
390
+ const stateText = tn(state.key, state.bot);
382
391
  // #1408 step 5: one line on a desktop — who it is, its facts in one clipped line (all of them in its tooltip),
383
392
  // then the cluster; on a phone the facts go under it. It no longer wraps fact by fact.
384
393
  const facts = [ch.bot_token_env || tn("noTokenEnv"),
@@ -386,13 +395,13 @@ function Bots({ ctx, search, openDialog }) {
386
395
  tn("accessTag", (ch.access && ch.access.mode) || "locked"),
387
396
  users.length ? `${users.slice(0, 3).join(", ")}${users.length > 3 ? ` +${users.length - 3}` : ""}` : tn("noAllowedUsers")].filter(Boolean).join(" · ");
388
397
  return html`<div key=${channelId(ch, i)} class="s-row s-conn">
389
- <span class="s-ident"><span class=${`dot ${ctx.fleetUp ? "ok" : "bad"}`} title=${ctx.fleetUp ? tn("connected") : tn("problem")} aria-hidden="true"></span>
398
+ <span class="s-ident"><span class=${`dot ${state.cls}`} title=${stateText} aria-hidden="true"></span>
390
399
  <span class="s-name">${ch.type === "telegram" ? "Telegram" : "Discord"}</span>
391
400
  <span class=${`tag${i === 0 ? "" : " persona"}`}>${ch.id || chLabel(i)}</span></span>
392
401
  <span class="s-facts" title=${facts}>${facts}</span>
393
402
  <span class="s-actions">
394
403
  <span class=${`tag${token ? "" : " warn"}`}>${token ? tn("tokenConfigured") : tn("tokenMissing")}</span>
395
- <span class=${`s-state ${ctx.fleetUp ? "ok" : "bad"}`}>${ctx.fleetUp ? tn("connected") : tn("problem")}</span>
404
+ <span class=${`s-state ${state.cls}`} title=${stateText}>${stateText}</span>
396
405
  <button type="button" class="btn btn-sm" onClick=${() => openDialog({ kind: "bot", id: channelId(ch, i) })}>${tn("settingsButton")}</button></span>
397
406
  </div>`;
398
407
  })}</div>`}
@@ -671,7 +680,7 @@ function Developer({ ctx }) {
671
680
  ${["yaml", "json"].map((k) => html`<button key=${k} type="button" class=${`btn btn-sm${fmt === k ? " on" : ""}`} aria-pressed=${fmt === k ? "true" : "false"} onClick=${() => setFmt(k)}>${k.toUpperCase()}</button>`)}</div>
672
681
  <button type="button" class="btn btn-sm" onClick=${() => setEditing(!editing)}>${editing ? tn("viewMode") : tn("editMode")}</button>
673
682
  <button type="button" class="btn btn-sm" title=${tn("copy")} onClick=${copy}><${Icon} name="copy" size=${14} /><span class="lbl">${tn("copy")}</span></button>
674
- <button type="button" class="btn btn-sm" title="fleet.yaml" onClick=${download}><${Icon} name="download" size=${14} /><span class="lbl">fleet.yaml</span></button>
683
+ <button type="button" class="btn btn-sm" title=${tn("devDownload")} onClick=${download}><${Icon} name="download" size=${14} /><span class="lbl">${tn("devDownload")}</span></button>
675
684
  </div>
676
685
  ${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck="false" wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
677
686
  <div class="save-row"><button type="button" class="btn btn-primary" onClick=${save}>${tn("devApply")}</button><span class="note">${tn("devApplyHint")}</span></div>`
@@ -699,7 +708,8 @@ function OperationCard({ op, schema }) {
699
708
  const phaseBusy = ["writing", "posting", "watching"].includes(op.phase);
700
709
  const job = op.job;
701
710
  const rows = (job && job.targets) || [];
702
- const fleetRestarting = job && job.status === "running" && rows.some((r) => r.target === "fleet" && r.status === "running");
711
+ // A fleet row of kind "hot" is connections starting without a restart (#1519 P6), not AgEnD restarting.
712
+ const fleetRestarting = job && job.status === "running" && rows.some((r) => r.target === "fleet" && r.status === "running" && r.kind !== "hot");
703
713
  const needsRestart = rows.some((r) => r.status === "restart-required");
704
714
  const failedRows = rows.some((r) => r.status === "failed");
705
715
  const mismatch = schema && schema.fleet_signature_mismatch;
@@ -732,11 +742,11 @@ function OperationCard({ op, schema }) {
732
742
  ${op.steps.length ? html`<ul class="s-op-steps">${op.steps.map((s, i) => html`<li key=${i} class=${`s-step ${s.status}`}>
733
743
  <span class="s-step-icon" aria-hidden="true">${icon(s.status) ? html`<${Icon} name=${icon(s.status)} size=${14} />` : "·"}</span>
734
744
  <span class="grow">${s.label}</span>
735
- <span class="note">${s.status === "waiting" ? tn("stepWaiting") : s.status === "skipped" ? tn("stepSkipped") : s.status === "failed" ? (s.error || tn("applyFailed")) : impactText(s.impact)}</span></li>`)}</ul>` : null}
745
+ <span class="note">${s.status === "waiting" ? tn("stepWaiting") : s.status === "skipped" ? tn("stepSkipped") : s.status === "failed" ? (s.error || tn("applyFailed")) : s.note || impactText(s.impact)}</span></li>`)}</ul>` : null}
736
746
  ${rows.length ? html`<ul class="s-op-steps">${rows.map((r) => html`<li key=${r.target} class=${`s-step ${r.status}`}>
737
747
  <span class="s-step-icon" aria-hidden="true">${r.status === "done" ? html`<${Icon} name="check" size=${14} />` : r.status === "failed" ? html`<${Icon} name="alert" size=${14} />` : r.status === "restart-required" ? html`<${Icon} name="restart" size=${14} />` : "·"}</span>
738
748
  <span class="grow">${r.target === "fleet" ? "AgEnD" : shortName(r.target, {})}</span>
739
- <span class="note">${r.target === "fleet" && r.status === "running" ? tn("restartFleetTitle") : r.status === "restart-required" ? tn("applyRestartNeeded")
749
+ <span class="note">${r.target === "fleet" && r.status === "running" ? tn(r.kind === "hot" ? "connStartingNow" : "restartFleetTitle") : r.status === "restart-required" ? tn("applyRestartNeeded")
740
750
  : r.settled_by === "no-change" ? tn("applyNoChange") : r.settled_by === "fleet-restart" ? tn("viaRestart") : r.kind === "hot" ? tn("impactNow") : tn("impactAgent")}</span>
741
751
  ${r.error ? html`<span class="feedback error">${r.error}</span>` : null}</li>`)}</ul>` : null}
742
752
  ${job && job.error ? html`<p class="feedback error">${job.error}</p>` : null}