@songsid/agend 2.3.3 → 2.3.4-beta.10

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 (368) hide show
  1. package/README.md +3 -3
  2. package/README.zh-TW.md +3 -3
  3. package/THIRD_PARTY_NOTICES.md +37 -0
  4. package/dist/agend-home.d.ts +13 -0
  5. package/dist/agend-home.js +42 -0
  6. package/dist/agend-home.js.map +1 -1
  7. package/dist/agent-cli-instructions.md +2 -2
  8. package/dist/agent-credentials.d.ts +24 -0
  9. package/dist/agent-credentials.js +66 -0
  10. package/dist/agent-credentials.js.map +1 -0
  11. package/dist/agent-endpoint.d.ts +3 -1
  12. package/dist/agent-endpoint.js +13 -0
  13. package/dist/agent-endpoint.js.map +1 -1
  14. package/dist/auth-api.d.ts +16 -3
  15. package/dist/auth-api.js +32 -8
  16. package/dist/auth-api.js.map +1 -1
  17. package/dist/authority/session-authorizations.d.ts +28 -4
  18. package/dist/authority/session-authorizations.js +63 -17
  19. package/dist/authority/session-authorizations.js.map +1 -1
  20. package/dist/authority/web-authority.d.ts +15 -1
  21. package/dist/authority/web-authority.js +27 -10
  22. package/dist/authority/web-authority.js.map +1 -1
  23. package/dist/backend/antigravity.js +3 -1
  24. package/dist/backend/antigravity.js.map +1 -1
  25. package/dist/backend/claude-code.d.ts +3 -0
  26. package/dist/backend/claude-code.js +100 -5
  27. package/dist/backend/claude-code.js.map +1 -1
  28. package/dist/backend/codex-footer-hints.d.ts +4 -0
  29. package/dist/backend/codex-footer-hints.js +27 -0
  30. package/dist/backend/codex-footer-hints.js.map +1 -0
  31. package/dist/backend/codex-session-lookup.d.ts +35 -0
  32. package/dist/backend/codex-session-lookup.js +93 -0
  33. package/dist/backend/codex-session-lookup.js.map +1 -1
  34. package/dist/backend/codex.d.ts +27 -0
  35. package/dist/backend/codex.js +136 -23
  36. package/dist/backend/codex.js.map +1 -1
  37. package/dist/backend/kiro-identity.d.ts +2 -0
  38. package/dist/backend/kiro-identity.js +13 -6
  39. package/dist/backend/kiro-identity.js.map +1 -1
  40. package/dist/backend/kiro.d.ts +3 -0
  41. package/dist/backend/kiro.js +68 -5
  42. package/dist/backend/kiro.js.map +1 -1
  43. package/dist/backend/muse.js +5 -3
  44. package/dist/backend/muse.js.map +1 -1
  45. package/dist/backend/types.d.ts +13 -1
  46. package/dist/backend/types.js +3 -0
  47. package/dist/backend/types.js.map +1 -1
  48. package/dist/backend-availability.d.ts +6 -0
  49. package/dist/backend-availability.js +13 -5
  50. package/dist/backend-availability.js.map +1 -1
  51. package/dist/cache-ledger.d.ts +85 -0
  52. package/dist/cache-ledger.js +236 -1
  53. package/dist/cache-ledger.js.map +1 -1
  54. package/dist/cache-service.d.ts +5 -0
  55. package/dist/cache-service.js +16 -4
  56. package/dist/cache-service.js.map +1 -1
  57. package/dist/channel/adapters/discord.js +27 -4
  58. package/dist/channel/adapters/discord.js.map +1 -1
  59. package/dist/channel/adapters/telegram.d.ts +51 -0
  60. package/dist/channel/adapters/telegram.js +238 -18
  61. package/dist/channel/adapters/telegram.js.map +1 -1
  62. package/dist/channel/inbox-temp-name.d.ts +9 -0
  63. package/dist/channel/inbox-temp-name.js +13 -0
  64. package/dist/channel/inbox-temp-name.js.map +1 -0
  65. package/dist/channel/mcp-tools.js +3 -3
  66. package/dist/channel/mcp-tools.js.map +1 -1
  67. package/dist/channel/types.d.ts +2 -0
  68. package/dist/chat-log-ids.d.ts +48 -0
  69. package/dist/chat-log-ids.js +182 -0
  70. package/dist/chat-log-ids.js.map +1 -0
  71. package/dist/classic-channel-manager.d.ts +8 -1
  72. package/dist/classic-channel-manager.js +24 -2
  73. package/dist/classic-channel-manager.js.map +1 -1
  74. package/dist/cli.js +4 -6
  75. package/dist/cli.js.map +1 -1
  76. package/dist/config-validator.js +18 -1
  77. package/dist/config-validator.js.map +1 -1
  78. package/dist/cross-instance-notice.d.ts +10 -0
  79. package/dist/cross-instance-notice.js +67 -5
  80. package/dist/cross-instance-notice.js.map +1 -1
  81. package/dist/daemon.d.ts +52 -0
  82. package/dist/daemon.js +538 -35
  83. package/dist/daemon.js.map +1 -1
  84. package/dist/delivery-consumed-watch.d.ts +23 -1
  85. package/dist/delivery-consumed-watch.js +30 -3
  86. package/dist/delivery-consumed-watch.js.map +1 -1
  87. package/dist/delivery-reconciliation.d.ts +7 -0
  88. package/dist/delivery-reconciliation.js +9 -1
  89. package/dist/delivery-reconciliation.js.map +1 -1
  90. package/dist/dialog-screen-text.d.ts +16 -0
  91. package/dist/dialog-screen-text.js +51 -0
  92. package/dist/dialog-screen-text.js.map +1 -0
  93. package/dist/env-file.d.ts +2 -0
  94. package/dist/env-file.js +13 -8
  95. package/dist/env-file.js.map +1 -1
  96. package/dist/export-import.d.ts +7 -0
  97. package/dist/export-import.js +44 -10
  98. package/dist/export-import.js.map +1 -1
  99. package/dist/fleet-context.d.ts +2 -0
  100. package/dist/fleet-manager.d.ts +172 -12
  101. package/dist/fleet-manager.js +897 -139
  102. package/dist/fleet-manager.js.map +1 -1
  103. package/dist/fleet-yaml-patch.d.ts +3 -0
  104. package/dist/fleet-yaml-patch.js +103 -0
  105. package/dist/fleet-yaml-patch.js.map +1 -0
  106. package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
  107. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
  108. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
  109. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
  110. package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
  111. package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
  112. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +14 -4
  113. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  114. package/dist/install-stream.d.ts +53 -0
  115. package/dist/install-stream.js +134 -0
  116. package/dist/install-stream.js.map +1 -0
  117. package/dist/instance-config-impact.js +1 -0
  118. package/dist/instance-config-impact.js.map +1 -1
  119. package/dist/instance-lifecycle.d.ts +22 -2
  120. package/dist/instance-lifecycle.js +103 -19
  121. package/dist/instance-lifecycle.js.map +1 -1
  122. package/dist/instance-topic-delete.d.ts +77 -0
  123. package/dist/instance-topic-delete.js +123 -0
  124. package/dist/instance-topic-delete.js.map +1 -0
  125. package/dist/instructions.d.ts +2 -2
  126. package/dist/instructions.js +2 -2
  127. package/dist/instructions.js.map +1 -1
  128. package/dist/interaction-observation.d.ts +1 -0
  129. package/dist/interaction-observation.js +1 -1
  130. package/dist/interaction-observation.js.map +1 -1
  131. package/dist/kiro-engine-status.d.ts +11 -0
  132. package/dist/kiro-engine-status.js +6 -0
  133. package/dist/kiro-engine-status.js.map +1 -1
  134. package/dist/kiro-resume-summary.d.ts +37 -0
  135. package/dist/kiro-resume-summary.js +85 -0
  136. package/dist/kiro-resume-summary.js.map +1 -0
  137. package/dist/locale.js +130 -6
  138. package/dist/locale.js.map +1 -1
  139. package/dist/log-service.d.ts +30 -0
  140. package/dist/log-service.js +36 -3
  141. package/dist/log-service.js.map +1 -1
  142. package/dist/log-steps.d.ts +4 -3
  143. package/dist/log-steps.js +32 -7
  144. package/dist/log-steps.js.map +1 -1
  145. package/dist/logger.d.ts +12 -0
  146. package/dist/logger.js +43 -1
  147. package/dist/logger.js.map +1 -1
  148. package/dist/login-controller.js +9 -1
  149. package/dist/login-controller.js.map +1 -1
  150. package/dist/login-flows.d.ts +2 -0
  151. package/dist/login-flows.js +5 -2
  152. package/dist/login-flows.js.map +1 -1
  153. package/dist/login-manager.d.ts +6 -0
  154. package/dist/login-manager.js +10 -1
  155. package/dist/login-manager.js.map +1 -1
  156. package/dist/missed-offline.d.ts +25 -0
  157. package/dist/missed-offline.js +79 -0
  158. package/dist/missed-offline.js.map +1 -0
  159. package/dist/needs-you-hub.d.ts +6 -1
  160. package/dist/needs-you-hub.js +31 -8
  161. package/dist/needs-you-hub.js.map +1 -1
  162. package/dist/needs-you.d.ts +22 -2
  163. package/dist/needs-you.js +17 -3
  164. package/dist/needs-you.js.map +1 -1
  165. package/dist/next-steps.d.ts +12 -2
  166. package/dist/next-steps.js +19 -3
  167. package/dist/next-steps.js.map +1 -1
  168. package/dist/office-share-page.d.ts +1 -1
  169. package/dist/office-share-page.js +6 -2
  170. package/dist/office-share-page.js.map +1 -1
  171. package/dist/office-share-service.d.ts +57 -0
  172. package/dist/office-share-service.js +76 -11
  173. package/dist/office-share-service.js.map +1 -1
  174. package/dist/office-share.d.ts +8 -0
  175. package/dist/office-share.js +34 -0
  176. package/dist/office-share.js.map +1 -1
  177. package/dist/outbound-handlers.js +59 -3
  178. package/dist/outbound-handlers.js.map +1 -1
  179. package/dist/outbound-schemas.d.ts +1 -0
  180. package/dist/outbound-schemas.js +10 -4
  181. package/dist/outbound-schemas.js.map +1 -1
  182. package/dist/package-preimage.js +27 -2
  183. package/dist/package-preimage.js.map +1 -1
  184. package/dist/page-share.d.ts +122 -0
  185. package/dist/page-share.js +199 -0
  186. package/dist/page-share.js.map +1 -0
  187. package/dist/public-preview.d.ts +4 -0
  188. package/dist/public-preview.js +5 -4
  189. package/dist/public-preview.js.map +1 -1
  190. package/dist/public-web-gateway.js +5 -0
  191. package/dist/public-web-gateway.js.map +1 -1
  192. package/dist/queued-handoffs.d.ts +33 -0
  193. package/dist/queued-handoffs.js +75 -0
  194. package/dist/queued-handoffs.js.map +1 -0
  195. package/dist/quickstart-api.d.ts +5 -0
  196. package/dist/quickstart-api.js +1 -1
  197. package/dist/quickstart-api.js.map +1 -1
  198. package/dist/quickstart.d.ts +40 -0
  199. package/dist/quickstart.js +174 -28
  200. package/dist/quickstart.js.map +1 -1
  201. package/dist/remote-card-answer.js +2 -0
  202. package/dist/remote-card-answer.js.map +1 -1
  203. package/dist/remote-card-prompt.js +4 -2
  204. package/dist/remote-card-prompt.js.map +1 -1
  205. package/dist/reply-buttons-controller.js +10 -0
  206. package/dist/reply-buttons-controller.js.map +1 -1
  207. package/dist/scheduler/db.d.ts +2 -1
  208. package/dist/scheduler/db.js +23 -9
  209. package/dist/scheduler/db.js.map +1 -1
  210. package/dist/selection-card.d.ts +20 -0
  211. package/dist/selection-card.js +202 -0
  212. package/dist/selection-card.js.map +1 -0
  213. package/dist/settings-api.d.ts +13 -0
  214. package/dist/settings-api.js +16 -1
  215. package/dist/settings-api.js.map +1 -1
  216. package/dist/settings-change.js +5 -0
  217. package/dist/settings-change.js.map +1 -1
  218. package/dist/settings-effect.js +2 -0
  219. package/dist/settings-effect.js.map +1 -1
  220. package/dist/setup-host.js +2 -2
  221. package/dist/setup-host.js.map +1 -1
  222. package/dist/setup-wizard.js +2 -2
  223. package/dist/setup-wizard.js.map +1 -1
  224. package/dist/stall-reporter.d.ts +21 -0
  225. package/dist/stall-reporter.js +82 -0
  226. package/dist/stall-reporter.js.map +1 -0
  227. package/dist/tmux-control.d.ts +2 -2
  228. package/dist/tmux-control.js +4 -2
  229. package/dist/tmux-control.js.map +1 -1
  230. package/dist/tmux-exec-helper.d.ts +6 -3
  231. package/dist/tmux-exec-helper.js +115 -53
  232. package/dist/tmux-exec-helper.js.map +1 -1
  233. package/dist/tmux-manager.d.ts +6 -0
  234. package/dist/tmux-manager.js +17 -0
  235. package/dist/tmux-manager.js.map +1 -1
  236. package/dist/tool-permissions.d.ts +5 -2
  237. package/dist/tool-permissions.js +6 -5
  238. package/dist/tool-permissions.js.map +1 -1
  239. package/dist/tool-progress.d.ts +2 -0
  240. package/dist/tool-progress.js +19 -3
  241. package/dist/tool-progress.js.map +1 -1
  242. package/dist/topic-commands.js +4 -0
  243. package/dist/topic-commands.js.map +1 -1
  244. package/dist/tunnel/cloudflared-install.d.ts +4 -0
  245. package/dist/tunnel/cloudflared-install.js +20 -1
  246. package/dist/tunnel/cloudflared-install.js.map +1 -1
  247. package/dist/tunnel/cloudflared.d.ts +8 -1
  248. package/dist/tunnel/cloudflared.js +34 -6
  249. package/dist/tunnel/cloudflared.js.map +1 -1
  250. package/dist/tunnel/lease.d.ts +3 -3
  251. package/dist/tunnel/lease.js +23 -19
  252. package/dist/tunnel/lease.js.map +1 -1
  253. package/dist/tunnel/manager.js +6 -7
  254. package/dist/tunnel/manager.js.map +1 -1
  255. package/dist/types.d.ts +10 -0
  256. package/dist/ui/chat-store.js +28 -4
  257. package/dist/ui/chat-strings.js +40 -4
  258. package/dist/ui/chat-thread.js +52 -10
  259. package/dist/ui/fleet-canvas.js +4 -3
  260. package/dist/ui/fleet-office-model.js +55 -22
  261. package/dist/ui/fleet-office-overview.js +6 -38
  262. package/dist/ui/fleet-office-pack.js +4 -3
  263. package/dist/ui/fleet-office-skin.js +114 -21
  264. package/dist/ui/fleet-office-store.js +5 -0
  265. package/dist/ui/fleet-office-zones.js +2 -2
  266. package/dist/ui/fleet-office.js +41 -9
  267. package/dist/ui/fleet-usage.js +90 -27
  268. package/dist/ui/page-share-dialog.js +191 -0
  269. package/dist/ui/panel-chat.js +16 -0
  270. package/dist/ui/panel-log.js +7 -1
  271. package/dist/ui/panel-needs.js +17 -8
  272. package/dist/ui/panel-notices.js +98 -0
  273. package/dist/ui/panel-settings.js +9 -2
  274. package/dist/ui/preview-pages.js +41 -0
  275. package/dist/ui/preview-panel-actions.js +161 -0
  276. package/dist/ui/preview-panel.js +209 -28
  277. package/dist/ui/preview-versions.js +155 -0
  278. package/dist/ui/preview.js +88 -3
  279. package/dist/ui/settings-registry.js +1 -0
  280. package/dist/ui/settings-strings.js +6 -2
  281. package/dist/ui/shared/app-i18n.js +6 -6
  282. package/dist/ui/shared/app-needs.js +1 -1
  283. package/dist/ui/shared/app-route.js +4 -0
  284. package/dist/ui/shared/app-shell.js +9 -5
  285. package/dist/ui/shared/app-store.js +71 -1
  286. package/dist/ui/shared/app-stream.js +27 -1
  287. package/dist/ui/shared/app.css +94 -4
  288. package/dist/ui/shared/app.js +22 -1
  289. package/dist/ui/shared/office/guild.png +0 -0
  290. package/dist/ui/shared/office/school.png +0 -0
  291. package/dist/ui/shared/panel-view.js +339 -17
  292. package/dist/ui/shared/signin.js +12 -1
  293. package/dist/ui/shared/ui-icons.js +3 -0
  294. package/dist/ui/shared/usage-dialog.js +18 -1
  295. package/dist/ui/shared/view-history.css +17 -0
  296. package/dist/ui/shared/view-history.js +438 -0
  297. package/dist/ui/shared/view-strings.js +42 -0
  298. package/dist/ui/shared/web-authorize.js +3 -1
  299. package/dist/unattended-install.d.ts +6 -0
  300. package/dist/unattended-install.js +79 -0
  301. package/dist/unattended-install.js.map +1 -0
  302. package/dist/usage/capacity.d.ts +69 -0
  303. package/dist/usage/capacity.js +119 -0
  304. package/dist/usage/capacity.js.map +1 -0
  305. package/dist/usage/i18n-keys.d.ts +1 -1
  306. package/dist/usage/i18n-keys.js +3 -0
  307. package/dist/usage/i18n-keys.js.map +1 -1
  308. package/dist/usage/profile-timeline.d.ts +16 -0
  309. package/dist/usage/profile-timeline.js +58 -0
  310. package/dist/usage/profile-timeline.js.map +1 -0
  311. package/dist/usage/quota-history.d.ts +43 -0
  312. package/dist/usage/quota-history.js +168 -0
  313. package/dist/usage/quota-history.js.map +1 -0
  314. package/dist/usage/usage-api.d.ts +31 -0
  315. package/dist/usage/usage-api.js +80 -1
  316. package/dist/usage/usage-api.js.map +1 -1
  317. package/dist/usage-report.d.ts +25 -1
  318. package/dist/usage-report.js +55 -4
  319. package/dist/usage-report.js.map +1 -1
  320. package/dist/usage-rollup.d.ts +4 -1
  321. package/dist/usage-rollup.js +12 -3
  322. package/dist/usage-rollup.js.map +1 -1
  323. package/dist/view-api.d.ts +31 -0
  324. package/dist/view-api.js +186 -41
  325. package/dist/view-api.js.map +1 -1
  326. package/dist/view-history.d.ts +84 -0
  327. package/dist/view-history.js +116 -0
  328. package/dist/view-history.js.map +1 -0
  329. package/dist/web-api.d.ts +32 -0
  330. package/dist/web-api.js +195 -5
  331. package/dist/web-api.js.map +1 -1
  332. package/dist/web-auth.js +7 -1
  333. package/dist/web-auth.js.map +1 -1
  334. package/dist/web-chat-history.d.ts +20 -0
  335. package/dist/web-chat-history.js +21 -0
  336. package/dist/web-chat-history.js.map +1 -1
  337. package/dist/web-guest-access.js +5 -1
  338. package/dist/web-guest-access.js.map +1 -1
  339. package/dist/web-login.d.ts +6 -3
  340. package/dist/web-login.js +26 -15
  341. package/dist/web-login.js.map +1 -1
  342. package/dist/web-notices.d.ts +51 -0
  343. package/dist/web-notices.js +118 -0
  344. package/dist/web-notices.js.map +1 -0
  345. package/dist/web-office-pack-tools.d.ts +1 -1
  346. package/dist/web-office-pack-tools.js +1 -1
  347. package/dist/web-office-pack-tools.js.map +1 -1
  348. package/dist/web-only-fleet.d.ts +22 -0
  349. package/dist/web-only-fleet.js +13 -0
  350. package/dist/web-only-fleet.js.map +1 -0
  351. package/dist/web-preview.d.ts +32 -3
  352. package/dist/web-preview.js +52 -19
  353. package/dist/web-preview.js.map +1 -1
  354. package/dist/web-session-authorization.d.ts +13 -2
  355. package/dist/web-session-authorization.js +10 -3
  356. package/dist/web-session-authorization.js.map +1 -1
  357. package/dist/web-session.d.ts +12 -0
  358. package/dist/web-session.js +22 -8
  359. package/dist/web-session.js.map +1 -1
  360. package/dist/web-settings-patch.d.ts +3 -1
  361. package/dist/web-settings-patch.js +4 -1
  362. package/dist/web-settings-patch.js.map +1 -1
  363. package/dist/web-shell-routes.d.ts +2 -0
  364. package/dist/web-shell-routes.js +3 -0
  365. package/dist/web-shell-routes.js.map +1 -1
  366. package/launcher/agend-install.cjs +16 -2
  367. package/launcher/private-home.cjs +1 -1
  368. package/package.json +1 -1
@@ -0,0 +1,155 @@
1
+ // #2911 (1): the versions of a page in the preview panel, and the changes from one to the next. Pure functions: the
2
+ // panel (preview-panel.js) draws them. Nothing here runs a preview — a version is shown as any block is, waiting for
3
+ // Preview (#1481 §14).
4
+ import "./chat-render.js";
5
+
6
+ const R = () => globalThis.AgendChatRender;
7
+
8
+ /** The text of a block's <title>, or "" — what says two blocks are versions of one thing. */
9
+ export function htmlTitle(code) {
10
+ const m = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(String(code || ""));
11
+ return m ? m[1].replace(/\s+/g, " ").trim() : "";
12
+ }
13
+
14
+ /**
15
+ * Every version of the shown block in the loaded thread, oldest first, and where the shown one is in it.
16
+ * - An attached file: the agent's attachments of the same name (case-insensitive). Their HTML is read when picked (code: null).
17
+ * - A titled block: the agent's complete ```html blocks with the same <title>.
18
+ * - An untitled block: the agent's complete untitled blocks — a titled one is another page.
19
+ * Only agent replies count, and only complete blocks, as for a card. `index` is -1 (and `list` is just the shown block)
20
+ * when the block is no longer in the thread.
21
+ */
22
+ export function versionsOf(open, msgs, keyOf) {
23
+ if (!open) return { list: [], index: -1 };
24
+ const list = [];
25
+ const all = Array.isArray(msgs) ? msgs : [];
26
+ if (open.att) {
27
+ const name = String(open.att.name || "").trim().toLowerCase();
28
+ for (const x of all) {
29
+ if (x.role !== "agent" || !Array.isArray(x.attachments)) continue;
30
+ const msgKey = keyOf(x);
31
+ for (const a of x.attachments) {
32
+ if (!R().isHtmlAttachment(a) || a.name.trim().toLowerCase() !== name) continue;
33
+ list.push({ key: `${msgKey}:a${a.id}`, instance: open.instance, msgKey, att: { id: a.id, name: a.name, size: a.size }, code: null, sender: x.sender, ts: x.ts });
34
+ }
35
+ }
36
+ } else {
37
+ const title = htmlTitle(open.code);
38
+ for (const x of all) {
39
+ if (x.role !== "agent") continue;
40
+ const msgKey = keyOf(x);
41
+ R().htmlFences(x.text).forEach((f, n) => {
42
+ if (!f.terminated || htmlTitle(f.code) !== title) return;
43
+ list.push({ key: `${msgKey}:f${n}`, instance: open.instance, msgKey, n, code: f.code, sender: x.sender, ts: x.ts });
44
+ });
45
+ }
46
+ }
47
+ const index = list.findIndex(v => v.key === open.key);
48
+ return index < 0 ? { list: [open], index: -1 } : { list, index };
49
+ }
50
+
51
+ // ── Changes: a line diff (Myers), bounded ──
52
+
53
+ /** Past these, "Changes" says the versions are too large or too different to compare here. */
54
+ export const DIFF_LIMITS = { maxChars: 5 * 1024 * 1024, maxInputLines: 50000, maxEdits: 2000, context: 3, maxLines: 3000 };
55
+
56
+ /**
57
+ * How many lines `lines()` makes of `s`, counted without splitting it (#2915 review: a budget before anything is
58
+ * built) — LF, CRLF and a lone CR each end a line, and a last line without one counts; it stops counting past `max`
59
+ * (anything over `max` is then reported as max + 1).
60
+ */
61
+ export function lineCount(s, max = Infinity) {
62
+ if (!s) return 0;
63
+ let breaks = 0;
64
+ for (let i = 0; i < s.length; i++) {
65
+ const c = s.charCodeAt(i);
66
+ if (c === 10 || c === 13) {
67
+ if (c === 13 && s.charCodeAt(i + 1) === 10) i++;
68
+ if (++breaks > max) return max + 1;
69
+ }
70
+ }
71
+ const last = s.charCodeAt(s.length - 1);
72
+ return Math.min(breaks + (last === 10 || last === 13 ? 0 : 1), max + 1);
73
+ }
74
+
75
+ const lines = (s) => { if (!s) return []; const v = String(s).replace(/\r\n?/g, "\n").split("\n"); if (v.length > 1 && v[v.length - 1] === "") v.pop(); return v; };
76
+
77
+ /**
78
+ * The line edits from `a` to `b`: [{ t: " " | "-" | "+", text }] in order, or null when it cannot be done within
79
+ * DIFF_LIMITS: either side over maxChars or maxInputLines (checked before anything is split or built: an op per line
80
+ * is made only under that budget), or more than maxEdits lines differ after the common head and tail.
81
+ */
82
+ export function lineDiff(a, b, limits = DIFF_LIMITS) {
83
+ const sa = String(a ?? ""), sb = String(b ?? "");
84
+ if (sa.length > limits.maxChars || sb.length > limits.maxChars) return null;
85
+ if (lineCount(sa, limits.maxInputLines) > limits.maxInputLines || lineCount(sb, limits.maxInputLines) > limits.maxInputLines) return null;
86
+ a = sa; b = sb;
87
+ const A = lines(a), B = lines(b);
88
+ let head = 0;
89
+ while (head < A.length && head < B.length && A[head] === B[head]) head++;
90
+ let tail = 0;
91
+ while (tail < A.length - head && tail < B.length - head && A[A.length - 1 - tail] === B[B.length - 1 - tail]) tail++;
92
+ const x = A.slice(head, A.length - tail), y = B.slice(head, B.length - tail);
93
+ const mid = myers(x, y, limits.maxEdits);
94
+ if (!mid) return null;
95
+ return [...A.slice(0, head).map(text => ({ t: " ", text })), ...mid, ...A.slice(A.length - tail).map(text => ({ t: " ", text }))];
96
+ }
97
+
98
+ /** Myers' O((N+M)·D) shortest edit script, with D capped (the trace is kept for the walk back). */
99
+ function myers(a, b, maxD) {
100
+ const n = a.length, m = b.length, max = n + m;
101
+ if (max === 0) return [];
102
+ const off = max + 1, v = new Int32Array(2 * max + 3), trace = [];
103
+ for (let d = 0; d <= Math.min(max, maxD); d++) {
104
+ trace.push(v.slice(off - d - 1, off + d + 2));
105
+ for (let k = -d; k <= d; k += 2) {
106
+ let i = k === -d || (k !== d && v[off + k - 1] < v[off + k + 1]) ? v[off + k + 1] : v[off + k - 1] + 1;
107
+ let j = i - k;
108
+ while (i < n && j < m && a[i] === b[j]) { i++; j++; }
109
+ v[off + k] = i;
110
+ if (i >= n && j >= m) return walkBack(a, b, trace, d);
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+
116
+ function walkBack(a, b, trace, dEnd) {
117
+ const out = [];
118
+ let i = a.length, j = b.length;
119
+ for (let d = dEnd; d >= 0; d--) {
120
+ const vd = trace[d], at = (k) => vd[k + d + 1]; // trace[d] holds v[-d-1 .. d+1] before step d
121
+ const k = i - j;
122
+ if (d === 0) { while (i > 0 && j > 0) { out.push({ t: " ", text: a[--i] }); j--; } break; }
123
+ const down = k === -d || (k !== d && at(k - 1) < at(k + 1));
124
+ const pk = down ? k + 1 : k - 1, pi = at(pk), pj = pi - pk;
125
+ while (i > pi && j > pj) { out.push({ t: " ", text: a[--i] }); j--; }
126
+ if (down) out.push({ t: "+", text: b[--j] }); else out.push({ t: "-", text: a[--i] });
127
+ }
128
+ return out.reverse();
129
+ }
130
+
131
+ /**
132
+ * The edits as hunks with `context` lines around each change, as a unified diff shows them:
133
+ * [{ oldStart, oldLines, newStart, newLines, lines: [{ t, text }] }] (1-based starts). [] when nothing changed.
134
+ */
135
+ export function diffHunks(ops, context = DIFF_LIMITS.context) {
136
+ // Each op's line numbers on either side (before it), then the changed spans widened by `context` and merged.
137
+ const oldAt = [], newAt = [];
138
+ let o = 1, n = 1;
139
+ for (const op of ops) { oldAt.push(o); newAt.push(n); if (op.t !== "+") o++; if (op.t !== "-") n++; }
140
+ const ranges = [];
141
+ ops.forEach((op, idx) => {
142
+ if (op.t === " ") return;
143
+ const from = Math.max(0, idx - context), to = Math.min(ops.length - 1, idx + context);
144
+ const last = ranges[ranges.length - 1];
145
+ if (last && from <= last.to + 1) last.to = Math.max(last.to, to); else ranges.push({ from, to });
146
+ });
147
+ return ranges.map(({ from, to }) => {
148
+ const part = ops.slice(from, to + 1);
149
+ return {
150
+ oldStart: oldAt[from], newStart: newAt[from],
151
+ oldLines: part.filter(x => x.t !== "+").length, newLines: part.filter(x => x.t !== "-").length,
152
+ lines: part,
153
+ };
154
+ });
155
+ }
@@ -249,6 +249,7 @@
249
249
  if (bytes > LIMITS.askBytes && !(opts && opts.anyway)) { ui.state("large", say("pvLarge", "This file is large ({0}). Preview anyway?", sizeLabel(bytes))); return false; }
250
250
  stopAll("another");
251
251
  starts++;
252
+ resetErrors(key);
252
253
  var doc = holder.ownerDocument;
253
254
  var iframe = mountPreview(doc);
254
255
  var card = {
@@ -285,6 +286,7 @@
285
286
  var card = live.get(key);
286
287
  if (!card) return false;
287
288
  live.delete(key);
289
+ if (card.picking) { card.picking = false; tellPick(key, null, false); }
288
290
  if (card.readyTimer) root.clearTimeout(card.readyTimer);
289
291
  if (card.watchdogTimer) root.clearTimeout(card.watchdogTimer);
290
292
  if (card.iframe.parentNode) card.iframe.parentNode.removeChild(card.iframe);
@@ -310,13 +312,93 @@
310
312
 
311
313
  // ── Messages from frames (design §4.2) ──────────────────
312
314
 
313
- var KEYS = { ready: "boot,ch,type,v", resize: "ch,height,type,v", heartbeat: "ch,type,v" };
315
+ var KEYS = { ready: "boot,ch,type,v", resize: "ch,height,type,v", heartbeat: "ch,type,v", error: "ch,col,kind,line,message,type,v",
316
+ pick: "ch,h,selector,text,type,v,w,x,y", pickend: "ch,type,v" };
317
+
318
+ // ── #2911: the errors a running preview reported (design §4.1 amendment) ──
319
+ // Recorded only: a count, and the first few distinct ones as one line of plain text each. Nothing here passes them on;
320
+ // a person's click in the panel (preview-panel-actions.js) puts them in the composer, quoted, for them to edit.
321
+ var ERRORS = { maxCount: 999, keep: 5, messageMax: 200, rawMax: 1000, notifyMs: 200 };
322
+ var ERROR_KINDS = { error: 1, rejection: 1, console: 1, blocked: 1 }; // blocked: #2911 (7), a CSP-blocked script/style/font URL
323
+ var errorLog = new Map(); // frame key → { count, items: [{ kind, message, line, col }] }
324
+ var errorListeners = new Set();
325
+ var errorNotify = new Map(); // frame key → a pending notification's timer
326
+ function oneLine(v, max) {
327
+ // Format characters (\p{Cf}: bidi overrides and isolates, zero-widths, U+FEFF…) go first: a quoted line must read
328
+ // as its bytes say (#2914 review — the Trojan Source reordering, text hidden between zero-widths).
329
+ var t = String(v).replace(/\p{Cf}+/gu, "").replace(/[\s\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, " ").trim();
330
+ var chars = Array.from(t);
331
+ return chars.length > max ? chars.slice(0, max - 1).join("") + "\u2026" : t;
332
+ }
333
+ function tellErrors(key) {
334
+ if (errorNotify.has(key)) return;
335
+ errorNotify.set(key, root.setTimeout(function () {
336
+ errorNotify.delete(key);
337
+ errorListeners.forEach(function (fn) { try { fn(key); } catch (e) { /* one listener's error stops no other */ } });
338
+ }, ERRORS.notifyMs));
339
+ }
340
+ // A new run: its chip goes at once (not after the notification delay), and a notification still pending is dropped.
341
+ function resetErrors(key) {
342
+ if (!errorLog.delete(key)) return;
343
+ if (errorNotify.has(key)) { root.clearTimeout(errorNotify.get(key)); errorNotify.delete(key); }
344
+ errorListeners.forEach(function (fn) { try { fn(key); } catch (e) { /* one listener's error stops no other */ } });
345
+ }
346
+ function recordError(card, d) {
347
+ if (!Object.prototype.hasOwnProperty.call(ERROR_KINDS, d.kind) || typeof d.message !== "string" || d.message.length > ERRORS.rawMax) return;
348
+ var pos = function (n) { return typeof n === "number" && isFinite(n) && Math.floor(n) === n && n >= 0 && n <= 1e7; };
349
+ if (!pos(d.line) || !pos(d.col)) return;
350
+ var log = errorLog.get(card.key);
351
+ if (!log) { log = { count: 0, items: [] }; errorLog.set(card.key, log); }
352
+ if (log.count >= ERRORS.maxCount) return;
353
+ log.count++;
354
+ var item = { kind: d.kind, message: oneLine(d.message, ERRORS.messageMax) || "(no message)", line: d.line, col: d.col };
355
+ if (log.items.length < ERRORS.keep && !log.items.some(function (x) { return x.kind === item.kind && x.message === item.message && x.line === item.line && x.col === item.col; })) log.items.push(item);
356
+ tellErrors(card.key);
357
+ }
358
+ /** What the preview under `key` reported since it started: { count, items } (copies), or null when nothing. */
359
+ function errors(key) {
360
+ var log = errorLog.get(key);
361
+ return log ? { count: log.count, items: log.items.map(function (x) { return { kind: x.kind, message: x.message, line: x.line, col: x.col }; }) } : null;
362
+ }
363
+ // ── #2911: pick mode — the person points at an element of a running preview (design §4.1 amendment) ──
364
+ // Only on a person's request (pick(key, true)), and only one answer per request: a `pick` from a frame that was not
365
+ // asked is dropped. The answer is the page's text — a selector it built and the element's words — kept one line each
366
+ // and handed to the listeners; the panel puts it in the composer for the person to edit. Nothing here sends it.
367
+ var PICK = { selectorMax: 300, textMax: 200 };
368
+ var pickListeners = new Set();
369
+ function tellPick(key, result, on) { pickListeners.forEach(function (fn) { try { fn(key, result, on); } catch (e) { /* one listener's error stops no other */ } }); }
370
+ /** Turn pick mode on or off for the running preview `key`; false when it is not running (rendered). */
371
+ function pick(key, on) {
372
+ var card = live.get(key);
373
+ if (!card || !card.rendered) return false;
374
+ card.picking = !!on;
375
+ try { card.iframe.contentWindow.postMessage({ v: 1, type: "pick", ch: card.ch, on: !!on }, "*"); } catch (e) { /* the frame went: its stop says so */ }
376
+ tellPick(key, null, card.picking);
377
+ return true;
378
+ }
379
+ function picking(key) { var c = live.get(key); return !!(c && c.picking); }
380
+ /** fn(key, result | null, on): a picked element ({ selector, text, x, y, w, h }), or the mode changing; returns the unsubscribe. */
381
+ function onPick(fn) { pickListeners.add(fn); return function () { pickListeners.delete(fn); }; }
382
+ function recordPick(card, d) {
383
+ if (!card.picking) return;
384
+ if (d.type === "pickend") { card.picking = false; tellPick(card.key, null, false); return; }
385
+ var box = function (n) { return typeof n === "number" && isFinite(n) && Math.floor(n) === n && n >= 0 && n < 1e6; };
386
+ if (typeof d.selector !== "string" || d.selector.length > 1000 || typeof d.text !== "string" || d.text.length > 1000) return;
387
+ if (!box(d.x) || !box(d.y) || !box(d.w) || !box(d.h)) return;
388
+ card.picking = false;
389
+ tellPick(card.key, { selector: oneLine(d.selector, PICK.selectorMax), text: oneLine(d.text, PICK.textMax), x: d.x, y: d.y, w: d.w, h: d.h }, false);
390
+ }
391
+
392
+ /** fn(key) after a preview's errors changed (at most every 200 ms per preview); returns the unsubscribe. */
393
+ function onErrors(fn) { errorListeners.add(fn); return function () { errorListeners.delete(fn); }; }
314
394
 
315
395
  /**
316
396
  * The only window message listener. A message counts only from one of OUR frames (event.source is that frame's
317
- * window, and its origin is the opaque "null"), with one of three exact shapes; each has one effect, on that frame
397
+ * window, and its origin is the opaque "null"), with one of six exact shapes; each has one effect, on that frame
318
398
  * only: ready → send the HTML once (boot checked, opt-in checked again), resize → its height, heartbeat → its
319
- * watchdog. It has no reference to the chat, the composer, the fleet's API, navigation or storage.
399
+ * watchdog, error (#2911) → its error log (recorded, never passed on), pick / pickend (#2911) → the answer to the
400
+ * person's own pick request, handed to the pick listeners (dropped when none was asked). It has no reference to the chat, the
401
+ * composer, the fleet's API, navigation or storage.
320
402
  */
321
403
  function onMessage(event) {
322
404
  var card = null;
@@ -344,6 +426,8 @@
344
426
  armWatchdog(card);
345
427
  return;
346
428
  }
429
+ if (d.type === "error") { recordError(card, d); return; }
430
+ if (d.type === "pick" || d.type === "pickend") { recordPick(card, d); return; }
347
431
  if (typeof d.height !== "number" || !isFinite(d.height) || Math.floor(d.height) !== d.height) return;
348
432
  requestHeight(card, d.height);
349
433
  }
@@ -377,6 +461,7 @@
377
461
 
378
462
  return {
379
463
  init: init, availability: availability, optedIn: optedIn, setOptIn: setOptIn, never: never, setNever: setNever, onChange: onChange, listeners: listeners, onStorage: onStorage,
464
+ errors: errors, onErrors: onErrors, ERRORS: ERRORS, pick: pick, picking: picking, onPick: onPick, PICK: PICK,
380
465
  start: start, startCount: startCount, stop: stop, stopAll: stopAll, stopIn: stopIn, running: running, liveCount: liveCount, liveFrame: liveFrame,
381
466
  mountPreview: mountPreview, onMessage: onMessage, BANNER: BANNER, LIMITS: LIMITS,
382
467
  /** The banner every running preview carries, in the page's language (#1554); BANNER is its English. */
@@ -65,6 +65,7 @@ const ROWS = [
65
65
  // Fleet · Web dashboard
66
66
  ["dash.log-tab", "web-dashboard", "#webLogTab", "settings.webLogTab", "web.log_tab", ["log tab", "steps", "log"], ["Log 分頁", "紀錄", "步驟"]],
67
67
  ["dash.usage-public", "web-dashboard", "#webUsagePublic", "settings.webUsagePublic", "web.usage_report_on_public_link", ["usage", "public link", "cost", "tokens"], ["用量", "公開連結", "費用"]],
68
+ ["dash.preview-cdn", "web-dashboard", "#webPreviewCdn", "settings.webPreviewCdn", "web.preview_cdn", ["cdn", "libraries", "html preview", "chart", "jsdelivr", "cdnjs"], ["CDN", "函式庫", "HTML 預覽", "圖表"]],
68
69
  ["dash.office-levels", "web-dashboard", "#webOfficeLevels", "settings.webOfficeLevels", "web.office_levels", ["office", "level", "badge", "profile card"], ["辦公室", "等級", "徽章", "名片"]],
69
70
  ["dash.app-icon", "web-dashboard", ".s-app-icon", "settings.appIconTitle", null, ["app icon", "favicon", "home screen", "icon"], ["App 圖示", "圖示", "主畫面"]],
70
71
  ["dash.card-limits", "web-dashboard", ".s-card-limits", "settings.cardLimitsTitle", "web.appearance", ["upload limits", "portrait", "banner", "card limits"], ["上傳限制", "身分卡上傳限制"]],
@@ -40,6 +40,8 @@ register("settings", {
40
40
  webEcho: "Web echo",
41
41
  webEchoHint: "Copies web messages to the bound Telegram or Discord topic. ClassicBot rooms follow it unless a room sets its own.",
42
42
  webOfficeLevels: "Office levels", webOfficeLevelsHint: "Each agent's level from its lifetime work, on its name tag in the Office and on its profile card. Off removes the badges and the card's level rows. The token counts behind a level follow the usage report's rules. Applies at once.",
43
+ webPreviewCdn: "HTML previews may load libraries from cdnjs / jsDelivr",
44
+ webPreviewCdnHint: "The page's scripts may load libraries from cdnjs (/ajax/libs/) and jsDelivr (/npm/); those requests leave this machine, so the CDN sees them, and a page could use them to send data out. Scripts, styles and fonts only — a preview still cannot fetch or post anything. Off by default; applies to the next preview started, on the public link too.",
43
45
  webUsagePublic: "Usage report over the public link", webUsagePublicHint: "Fleet → Usage (up to 400 days of each agent's tokens, requests, messages and API-equivalent amounts) is offered over the temporary public link too. Off by default; guests never see it. Applies at once.",
44
46
  webLogTab: "Log tab: each agent's steps", webLogTabHint: "Shows each instance's prompts, replies, tool calls and results, and fleet events in its Log tab, read from its own transcript only while the tab is open. Secret values are masked. Also over the public link. Turning it on waits for a fleet admin's confirmation.",
45
47
  general: "General Settings",
@@ -164,7 +166,7 @@ register("settings", {
164
166
  failed: "Failed",
165
167
  actionFailed: "{0} failed",
166
168
  noAgentMatch: "No agents match \"{0}\".",
167
- deleteAgent: "Delete agent \"{0}\"? This will be applied only after you confirm the pending changes.",
169
+ deleteAgent: "Delete agent \"{0}\" and its chat topic (on Discord, its channel and history)? This will be applied only after you confirm the pending changes.",
168
170
  nameRequired: "name required",
169
171
  tokenEnvRequired: "token env required",
170
172
  removeBot: "Remove bot \"{0}\"? (the token in .env is left untouched)",
@@ -721,6 +723,8 @@ register("settings", {
721
723
  webEcho: "Web 同步",
722
724
  webEchoHint: "將網頁訊息複製到綁定的 Telegram 或 Discord 主題。ClassicBot 聊天室也照這個設定,除非該聊天室另外設定。",
723
725
  webOfficeLevels: "辦公室等級", webOfficeLevelsHint: "每個 agent 依終身工作量得到的等級,顯示在辦公室的名牌與名片上。關閉會移除徽章與名片上的等級列。等級背後的 token 數字依用量報告的規則顯示。變更立即生效。",
726
+ webPreviewCdn: "HTML 預覽可從 cdnjs / jsDelivr 載入函式庫",
727
+ webPreviewCdnHint: "頁面的指令碼可以從 cdnjs(/ajax/libs/)和 jsDelivr(/npm/)載入函式庫;這些請求會離開這台電腦,CDN 會看到它們,頁面也可能藉此把資料傳出去。只限指令碼、樣式與字型——預覽仍然無法擷取或送出任何資料。預設關閉;之後開始的預覽才會套用,公開連結也一樣。",
724
728
  webUsagePublic: "透過公開連結提供用量報告", webUsagePublicHint: "Fleet → 用量(最多 400 天、每個 agent 的 token、請求、訊息與依牌價換算的 API 等值)也會透過臨時公開連結提供。預設關閉;訪客一律看不到。變更立即生效。",
725
729
  webLogTab: "紀錄分頁:每個 agent 的步驟", webLogTabHint: "在每個 instance 的「紀錄」分頁顯示提示、回覆、工具呼叫與結果,以及 fleet 事件;只在分頁開著時從它自己的對話紀錄讀取。機密值會遮蔽。公開連結也看得到。開啟需 fleet admin 確認。",
726
730
  general: "一般設定",
@@ -819,7 +823,7 @@ register("settings", {
819
823
  failed: "失敗",
820
824
  actionFailed: "{0}失敗",
821
825
  noAgentMatch: "沒有符合「{0}」的 Agent。",
822
- deleteAgent: "確定刪除 Agent「{0}」嗎?只有在確認套用待處理變更後才會生效。",
826
+ deleteAgent: "確定刪除 Agent「{0}」及它的聊天 topic(在 Discord 上是它的頻道與訊息記錄)嗎?只有在確認套用待處理變更後才會生效。",
823
827
  nameRequired: "必須填寫名稱",
824
828
  tokenEnvRequired: "必須填寫 token 環境變數",
825
829
  removeBot: "確定移除機器人「{0}」嗎?(.env 中的 token 會保留)",
@@ -78,7 +78,7 @@ register("app", {
78
78
  statusQueued: "queued", statusStarting: "starting", fleetStarting: "Fleet starting {0}/{1}…", statusRunning: "running", statusStopped: "stopped", statusPaused: "paused", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
79
79
  connReconnecting: "Can't reach AgEnD — it may be restarting. Reconnecting…", connReconnectingIn: "Can't reach AgEnD — it may be restarting. Trying again in {0} s.",
80
80
  hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
81
- skip: "Skip to the conversation", loadFailed: "This could not be loaded.", retry: "Try again", loading: "Loading…",
81
+ skip: "Skip to the conversation", loadFailed: "This could not be loaded.", retry: "Try again", reloadPage: "Reload the page", loading: "Loading…",
82
82
  noInstances: "No instances yet", noInstancesHint: "Create one to start chatting.",
83
83
  guestChip: "Guest", guestAs: "Guest · {0}", guestAsOn: "Guest · {0} ({1})", guestHint: "You see only the agents you can already use in chat.",
84
84
  session: "Session", signIn: "Sign in", signedIn: "Signed in", signedOut: "Not signed in", thisDevice: "This device", devicePrefs: "Theme and language",
@@ -105,13 +105,13 @@ register("app", {
105
105
  restartRefused: "Restart refused: {0}",
106
106
  pendingSent: "Sent for confirmation — a fleet admin confirms it in chat.",
107
107
  pendingSelfSent: "Confirmed by your account (this browser is web-verified) — applying.",
108
- needsNav: "Needs you", needsTab: "Needs you", needsCount: "{0} waiting",
108
+ needsNav: "Needs you", needsTab: "Needs you", needsCount: "{0} waiting", noticesNav: "Notices", noticesCount: "{0} unread",
109
109
  // #2293: global search — chats, cross-instance messages and tasks (not the sidebar's filter of agents).
110
110
  search: "Search", searchTitle: "Search chats, messages between agents and tasks ({0})",
111
111
  confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
112
112
  sidePanelLeave: "Leave {0} without saving? Your changes there will be lost.", sidePanelDiscard: "Discard changes",
113
113
  // The server's own wording (src/locale.ts needs.reason.*), so the web says what Discord says.
114
- needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_question: "Asking you a question", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_config_invalid: "Configuration file needs attention", needs_reason_config_fallback: "Running on stored configuration", config_history_edit: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. The next restart would use the stored valid copy from {4}. Correct the YAML error; the file has not been changed by AgEnD.", config_history_no_history: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. No stored valid copy is available, so the next restart cannot recover automatically. Correct the YAML error before restarting; the file has not been changed by AgEnD.", config_history_fallback: "{0}, line {1}, column {2}: {3}. AgEnD started using the stored valid copy from {4}. Correct the YAML error and reload the settings. The file on disk has not been changed by AgEnD.", needs_reason_crashed: "Crashed", needs_reason_usage_limit: "Usage limit reached · reset time unknown", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
114
+ needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_question: "Asking you a question", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_kiro_choice: "Kiro needs your decision", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_config_invalid: "Configuration file needs attention", needs_reason_config_fallback: "Running on stored configuration", config_history_edit: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. The next restart would use the stored valid copy from {4}. Correct the YAML error; the file has not been changed by AgEnD.", config_history_no_history: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. No stored valid copy is available, so the next restart cannot recover automatically. Correct the YAML error before restarting; the file has not been changed by AgEnD.", config_history_fallback: "{0}, line {1}, column {2}: {3}. AgEnD started using the stored valid copy from {4}. Correct the YAML error and reload the settings. The file on disk has not been changed by AgEnD.", needs_reason_crashed: "Crashed", needs_reason_usage_limit: "Usage limit reached · reset time unknown", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_missed_offline: "Missed while AgEnD was offline", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
115
115
  },
116
116
  "zh-TW": {
117
117
  brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
@@ -126,7 +126,7 @@ register("app", {
126
126
  statusQueued: "等待啟動", statusStarting: "啟動中", fleetStarting: "Fleet 啟動中 {0}/{1}…", statusRunning: "執行中", statusStopped: "已停止", statusPaused: "已暫停", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
127
127
  connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
128
128
  hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
129
- skip: "跳到對話", loadFailed: "無法載入。", retry: "再試一次", loading: "載入中…",
129
+ skip: "跳到對話", loadFailed: "無法載入。", retry: "再試一次", reloadPage: "重新載入頁面", loading: "載入中…",
130
130
  noInstances: "還沒有 instance", noInstancesHint: "建立一個就能開始聊天。",
131
131
  guestChip: "訪客", guestAs: "訪客 · {0}", guestAsOn: "訪客 · {0}({1})", guestHint: "你只會看到在聊天中本來就能用的 agent。",
132
132
  session: "登入狀態", signIn: "登入", signedIn: "已登入", signedOut: "尚未登入", thisDevice: "這台裝置", devicePrefs: "主題與語言",
@@ -152,10 +152,10 @@ register("app", {
152
152
  restartRefused: "重新啟動被拒絕:{0}",
153
153
  pendingSent: "已送出確認——由 fleet 管理員在聊天中確認。",
154
154
  pendingSelfSent: "已由你的帳號確認(這個瀏覽器已通過網頁驗證)——套用中。",
155
- needsNav: "等你處理", needsTab: "待處理", needsCount: "{0} 件待處理",
155
+ needsNav: "等你處理", needsTab: "待處理", needsCount: "{0} 件待處理", noticesNav: "通知", noticesCount: "{0} 則未讀",
156
156
  search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
157
157
  confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
158
158
  sidePanelLeave: "不儲存就離開「{0}」?你在那裡的修改會遺失。", sidePanelDiscard: "捨棄修改",
159
- needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_question: "正在問你問題", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_config_invalid: "設定檔需要處理", needs_reason_config_fallback: "正在使用儲存的設定副本", config_history_edit: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。下次重新啟動會改用 {4} 儲存的有效副本。請修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_no_history: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。沒有可用的有效副本,下次重新啟動將無法自動恢復。請在重新啟動前修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_fallback: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 已使用 {4} 儲存的有效副本啟動。請修正 YAML 錯誤並重新載入設定。AgEnD 未更動磁碟上的檔案。", needs_reason_crashed: "已崩潰", needs_reason_usage_limit: "用量已達上限 · 恢復時間未知", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
159
+ needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_question: "正在問你問題", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_kiro_choice: "Kiro 需要你決定", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_config_invalid: "設定檔需要處理", needs_reason_config_fallback: "正在使用儲存的設定副本", config_history_edit: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。下次重新啟動會改用 {4} 儲存的有效副本。請修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_no_history: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。沒有可用的有效副本,下次重新啟動將無法自動恢復。請在重新啟動前修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_fallback: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 已使用 {4} 儲存的有效副本啟動。請修正 YAML 錯誤並重新載入設定。AgEnD 未更動磁碟上的檔案。", needs_reason_crashed: "已崩潰", needs_reason_usage_limit: "用量已達上限 · 恢復時間未知", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_missed_offline: "AgEnD 離線時錯過", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
160
160
  },
161
161
  });
@@ -63,7 +63,7 @@ export const configDetail = (item) => {
63
63
  const issue = item.configIssue;
64
64
  return issue ? t(`app.config_history_${issue.mode === "fallback" ? "fallback" : issue.time ? "edit" : "no_history"}`, issue.file, issue.line, issue.column, issue.reason, issue.time || "") : item.detail;
65
65
  };
66
- export const reasonText = (item) => t(`app.needs_reason_${item.reason}`);
66
+ export const reasonText = (item) => t(`app.needs_reason_${item.kiroChoice ? "kiro_choice" : item.reason}`);
67
67
 
68
68
  let stop = null;
69
69
 
@@ -30,6 +30,7 @@ export function parseRoute(pathname) {
30
30
  if (pathname === "/ui") return { panel: "chat", instance: null };
31
31
  if (pathname === "/ui/fleet") return { panel: "fleet", tab: "tasks" };
32
32
  if (pathname === "/ui/needs") return { panel: "needs" };
33
+ if (pathname === NOTICES_PATH) return { panel: "notices" }; // #2910 D2: a web-only fleet's Notices
33
34
  // #1523 N2: one instance's Details (`agent` is no Fleet tab; a tab is one segment).
34
35
  let m = /^\/ui\/fleet\/agent\/([^/]+)$/.exec(pathname);
35
36
  if (m) return named("details", m[1]);
@@ -82,6 +83,7 @@ export function rememberFleetTab(tab) {
82
83
  try { localStorage.setItem(LAST_FLEET_TAB_KEY, tab); } catch { /* this page only */ }
83
84
  }
84
85
  export const NEEDS_PATH = "/ui/needs";
86
+ export const NOTICES_PATH = "/ui/notices";
85
87
  /** A section's address; an old name gives its new one (and the place in it: `chat#classicbot`). */
86
88
  export function settingsPath(name) {
87
89
  const section = (name && legacySection(name)) || name;
@@ -109,6 +111,7 @@ export function routePath(route) {
109
111
  if (route.panel === "fleet") return fleetPath(route.tab);
110
112
  if (route.panel === "settings") return settingsPath(route.section);
111
113
  if (route.panel === "needs") return NEEDS_PATH;
114
+ if (route.panel === "notices") return NOTICES_PATH;
112
115
  if (route.panel === "view") return viewPath(route.instance);
113
116
  if (route.panel === "details") return detailsPath(route.instance);
114
117
  if (route.panel === "log") return logPath(route.instance);
@@ -140,6 +143,7 @@ export function textLinkSegments(text, links) {
140
143
  export function routeKey(route) {
141
144
  if (!route) return "none";
142
145
  if (route.panel === "needs") return "needs:";
146
+ if (route.panel === "notices") return "notices:";
143
147
  return `${route.panel}:${route.panel === "fleet" ? route.tab : route.panel === "settings" ? route.section : route.instance ?? ""}`;
144
148
  }
145
149
 
@@ -10,7 +10,7 @@ import { t, lang, setLang, onLang } from "./app-i18n.js";
10
10
  import { appStore, createStore, guestBadge, guestPage, guestTrusted, useStore } from "./app-store.js";
11
11
  import { navStore } from "./app-nav.js";
12
12
  import { toast } from "./ui-toast.js";
13
- import { chatPath, viewPath, settingsPath, routeKey, NEEDS_PATH, fleetEntryPath } from "./app-route.js";
13
+ import { chatPath, viewPath, settingsPath, routeKey, NEEDS_PATH, NOTICES_PATH, fleetEntryPath } from "./app-route.js";
14
14
  import { SessionMenu } from "./app-session.js";
15
15
  import { onPrefsChanged, savePref } from "./app-prefs.js";
16
16
  import { applyPageTheme, pageNow, resolvedMode, themesStore } from "./app-themes.js";
@@ -328,8 +328,8 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
328
328
  const canCreate = useCanCreate();
329
329
  const on = (panel) => !!route && route.panel === panel;
330
330
  // A count badge is hidden at 0; its number is also in the link's accessible name.
331
- const navLink = (panel, href, icon, label, count = 0) => html`<a class=${`side-row${on(panel) ? " active" : ""}`} href=${href}
332
- aria-current=${on(panel) ? "page" : undefined} aria-label=${count ? `${label} (${t("app.needsCount", count)})` : undefined} onClick=${closeDrawer}>
331
+ const navLink = (panel, href, icon, label, count = 0, countText = "app.needsCount") => html`<a class=${`side-row${on(panel) ? " active" : ""}`} href=${href}
332
+ aria-current=${on(panel) ? "page" : undefined} aria-label=${count ? `${label} (${t(countText, count)})` : undefined} onClick=${closeDrawer}>
333
333
  <${Icon} name=${icon} /><span class="grow">${label}</span>${count ? html`<span class="nav-count" aria-hidden="true">${count}</span>` : null}</a>`;
334
334
  const needsCount = Array.isArray(app.needs) ? app.needs.length : 0;
335
335
  // View-only (an anonymous reader, #1408 §3): View and a way to sign in — nothing that needs a session is rendered.
@@ -379,6 +379,8 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
379
379
  <nav data-trust="navigation" class="side-nav" aria-label=${t("app.menu")}>
380
380
  ${overview}
381
381
  ${navLink("needs", NEEDS_PATH, "inbox", t("app.needsNav"), needsCount)}
382
+ ${/* #2910 D2: a web-only fleet (no chat connection) has Notices — what General would have said. */
383
+ app.connections === 0 && app.notices && app.notices.enabled ? navLink("notices", NOTICES_PATH, "bell", t("app.noticesNav"), app.notices.unread | 0, "app.noticesCount") : null}
382
384
  ${searchOffered(canCreate) ? searchRow() : null}
383
385
  ${canCreate && onNewInstance ? html`<button type="button" class="side-row side-new" onClick=${onNewInstance}><${Icon} name="plus" /><span class="grow">${t("app.newAgent")}</span></button>` : null}
384
386
  </nav>
@@ -506,12 +508,14 @@ function BottomTabs({ route, viewOnly }) {
506
508
  </nav>`;
507
509
  }
508
510
 
509
- function ConnectionLine() {
511
+ export function ConnectionLine() {
510
512
  const { connection, hydration, retryHydration } = useStore(appStore);
511
513
  // A late catch-up that is not done: the chat may be missing what was open before it loaded (#1425 review).
514
+ // M-2260: never a dead end — beside Try again, a reload reads everything current, whatever kept the catch-up from it.
512
515
  if (hydration === "retrying" || hydration === "failed") {
513
516
  return html`<div data-trust="status" class="conn" role="status">${hydration === "failed" ? t("app.hydrateFailed") : t("app.hydrating")}
514
- ${hydration === "failed" && retryHydration ? html` <button type="button" class="btn btn-sm" onClick=${() => retryHydration()}>${t("app.retry")}</button>` : null}</div>`;
517
+ ${hydration === "failed" && retryHydration ? html` <button type="button" class="btn btn-sm" onClick=${() => retryHydration()}>${t("app.retry")}</button>` : null}
518
+ ${hydration === "failed" ? html` <button type="button" class="btn btn-sm" onClick=${() => location.reload()}>${t("app.reloadPage")}</button>` : null}</div>`;
515
519
  }
516
520
  if (connection === "reconnecting") return html`<${Reconnecting} />`;
517
521
  // #1603: polling is a working transport (the public link polls by design; a buffering proxy makes a page poll), not
@@ -63,7 +63,77 @@ export function applyStatus(d) {
63
63
  // #1685: host memory pressure slowing or holding new agents ("elevated" | "critical"), else null.
64
64
  hostMemory: d.host_memory === "elevated" || d.host_memory === "critical" ? d.host_memory : null,
65
65
  // #2015: while it lasts, the largest processes in the fleet and whose they are (at most 3; anything else dropped).
66
- hostMemoryTop: hostMemoryTop(d.host_memory_top) });
66
+ hostMemoryTop: hostMemoryTop(d.host_memory_top),
67
+ // #2910 D2 (#2925 review): the newest Notice on a web-only fleet — a polling page reads the list again when it moves.
68
+ noticesNewest: Number.isSafeInteger(d.notices_newest) ? d.notices_newest : null });
69
+ }
70
+
71
+ /**
72
+ * #2910 D2 (#2925 review): a web-only fleet's Notices as the page keeps them. A list read (GET /ui/notices/list), a live
73
+ * one (SSE `notice`) and a read mark (POST /ui/notices/seen) can answer in any order, so each is merged, never
74
+ * replaced: the newest NOTICES_KEPT by id (a list read never drops one that arrived live after it was taken), and the
75
+ * read mark only moves forward. Unread is counted here from the two.
76
+ */
77
+ export const NOTICES_KEPT = 500;
78
+ function noticesState(enabled, byId, seen) {
79
+ const items = [...byId.values()].sort((a, b) => b.id - a.id).slice(0, NOTICES_KEPT);
80
+ return { enabled, items, seen, unread: items.filter((n) => n.id > seen).length };
81
+ }
82
+ const validNotice = (n) => !!n && Number.isSafeInteger(n.id) && n.id > 0 && typeof n.text === "string";
83
+ export function mergeNoticeList(cur, d) {
84
+ const byId = new Map();
85
+ for (const n of Array.isArray(d && d.items) ? d.items : []) if (validNotice(n)) byId.set(n.id, n);
86
+ const listNewest = Math.max(0, ...byId.keys());
87
+ for (const n of cur && Array.isArray(cur.items) ? cur.items : []) if (n.id > listNewest && !byId.has(n.id)) byId.set(n.id, n);
88
+ return noticesState(!!(d && d.enabled === true), byId, Math.max(cur ? cur.seen | 0 : 0, d ? d.seen | 0 : 0));
89
+ }
90
+ export function addNotice(cur, n) {
91
+ if (!cur || !cur.enabled || !validNotice(n) || cur.items.some((x) => x.id === n.id)) return cur;
92
+ return noticesState(true, new Map([...cur.items, n].map((x) => [x.id, x])), cur.seen | 0);
93
+ }
94
+ /** The newest Notice the status frame names that the page does not have yet (a polling page hears no `notice`). 0: none. */
95
+ export function noticesCatchUp(s) {
96
+ const have = s && s.notices && s.notices.items.length ? s.notices.items[0].id : 0;
97
+ return s && Number.isSafeInteger(s.noticesNewest) && s.noticesNewest > have ? s.noticesNewest : 0;
98
+ }
99
+ /** After a read that left the page still behind: try again this soon (a failed read waits longer). */
100
+ export const NOTICES_RETRY_MS = { stale: 1000, failed: 5000 };
101
+ /**
102
+ * #2910 D2 (#2925 review r2): the page's reads of GET /ui/notices/list. One at a time (a second ask joins the read in
103
+ * flight); once a read settles, the page asks again if it is still behind what the status frame names (an older read
104
+ * that was already in flight, or one that failed) — after NOTICES_RETRY_MS, and again until it catches up. Never a
105
+ * watermark set before a read succeeded.
106
+ */
107
+ export function createNoticesSync({ store, fetchList, setTimer = (fn, ms) => setTimeout(fn, ms), now = () => Date.now() }) {
108
+ let inFlight = null, retryAt = 0, timer = null, on = false;
109
+ const behind = () => on && noticesCatchUp(store.get()) > 0;
110
+ const recheck = () => { if (!inFlight && behind() && now() >= retryAt) read(); };
111
+ const later = (ms) => {
112
+ retryAt = now() + ms;
113
+ if (timer === null) timer = setTimer(() => { timer = null; recheck(); }, ms);
114
+ };
115
+ function read() {
116
+ return (inFlight ??= Promise.resolve().then(fetchList).then((d) => {
117
+ if (!d || !Array.isArray(d.items)) throw new Error("no list");
118
+ store.set({ notices: mergeNoticeList(store.get().notices, d) });
119
+ return true;
120
+ }).catch(() => false).then((ok) => {
121
+ inFlight = null;
122
+ if (behind()) later(ok ? NOTICES_RETRY_MS.stale : NOTICES_RETRY_MS.failed);
123
+ else retryAt = 0;
124
+ }));
125
+ }
126
+ return {
127
+ /** The fleet is web-only: start (the first read), and follow the status frame from now on. */
128
+ start() { if (!on) { on = true; read(); } },
129
+ read,
130
+ recheck,
131
+ started: () => on,
132
+ };
133
+ }
134
+ export function applyNoticesSeen(cur, seen) {
135
+ if (!cur || !Number.isSafeInteger(seen) || seen <= (cur.seen | 0)) return cur;
136
+ return noticesState(cur.enabled, new Map(cur.items.map((x) => [x.id, x])), seen);
67
137
  }
68
138
 
69
139
  /** #2015: the status frame's host_memory_top, as the dashboard keeps it: well-formed entries only, at most 3. */