@songsid/agend 2.1.12 → 2.2.0-alpha.1

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 (358) hide show
  1. package/README.md +2 -2
  2. package/README.zh-TW.md +2 -2
  3. package/dist/auth/one-time-code.d.ts +19 -0
  4. package/dist/auth/one-time-code.js +49 -0
  5. package/dist/auth/one-time-code.js.map +1 -0
  6. package/dist/auth-api.d.ts +52 -0
  7. package/dist/auth-api.js +346 -0
  8. package/dist/auth-api.js.map +1 -0
  9. package/dist/backend/claude-code.d.ts +4 -0
  10. package/dist/backend/claude-code.js +97 -0
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.js +59 -30
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/kiro-agent.d.ts +66 -0
  15. package/dist/backend/kiro-agent.js +302 -0
  16. package/dist/backend/kiro-agent.js.map +1 -0
  17. package/dist/backend/kiro-identity.d.ts +80 -0
  18. package/dist/backend/kiro-identity.js +537 -0
  19. package/dist/backend/kiro-identity.js.map +1 -0
  20. package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
  21. package/dist/backend/kiro-v2-store-worker.js +87 -0
  22. package/dist/backend/kiro-v2-store-worker.js.map +1 -0
  23. package/dist/backend/kiro-v2-store.d.ts +54 -0
  24. package/dist/backend/kiro-v2-store.js +131 -0
  25. package/dist/backend/kiro-v2-store.js.map +1 -0
  26. package/dist/backend/kiro-v3-identity.d.ts +5 -0
  27. package/dist/backend/kiro-v3-identity.js +22 -0
  28. package/dist/backend/kiro-v3-identity.js.map +1 -1
  29. package/dist/backend/kiro.d.ts +83 -7
  30. package/dist/backend/kiro.js +418 -91
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +45 -3
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/channel/adapters/discord.d.ts +1 -0
  35. package/dist/channel/adapters/discord.js +29 -10
  36. package/dist/channel/adapters/discord.js.map +1 -1
  37. package/dist/channel/adapters/telegram.d.ts +1 -0
  38. package/dist/channel/adapters/telegram.js +13 -1
  39. package/dist/channel/adapters/telegram.js.map +1 -1
  40. package/dist/channel/types.d.ts +9 -1
  41. package/dist/classic-channel-manager.d.ts +18 -10
  42. package/dist/classic-channel-manager.js +45 -18
  43. package/dist/classic-channel-manager.js.map +1 -1
  44. package/dist/cli.js +225 -127
  45. package/dist/cli.js.map +1 -1
  46. package/dist/command-table.d.ts +5 -3
  47. package/dist/command-table.js +29 -27
  48. package/dist/command-table.js.map +1 -1
  49. package/dist/config-validator.js +46 -0
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.js +12 -0
  52. package/dist/config.js.map +1 -1
  53. package/dist/connection-secrets.d.ts +6 -0
  54. package/dist/connection-secrets.js +4 -0
  55. package/dist/connection-secrets.js.map +1 -1
  56. package/dist/daemon.d.ts +89 -4
  57. package/dist/daemon.js +661 -77
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/darwin-memory.d.ts +6 -1
  60. package/dist/darwin-memory.js +17 -6
  61. package/dist/darwin-memory.js.map +1 -1
  62. package/dist/delivery-consumed-watch.d.ts +85 -0
  63. package/dist/delivery-consumed-watch.js +244 -0
  64. package/dist/delivery-consumed-watch.js.map +1 -0
  65. package/dist/delivery-outbox.d.ts +61 -0
  66. package/dist/delivery-outbox.js +120 -2
  67. package/dist/delivery-outbox.js.map +1 -1
  68. package/dist/delivery-reconciliation.d.ts +14 -5
  69. package/dist/delivery-reconciliation.js +27 -4
  70. package/dist/delivery-reconciliation.js.map +1 -1
  71. package/dist/delivery-reconciliation.test.js +35 -1
  72. package/dist/delivery-reconciliation.test.js.map +1 -1
  73. package/dist/event-loop-watch.d.ts +28 -0
  74. package/dist/event-loop-watch.js +75 -3
  75. package/dist/event-loop-watch.js.map +1 -1
  76. package/dist/fleet-context.d.ts +22 -2
  77. package/dist/fleet-level-config.d.ts +2 -2
  78. package/dist/fleet-level-config.js +6 -1
  79. package/dist/fleet-level-config.js.map +1 -1
  80. package/dist/fleet-manager.d.ts +293 -12
  81. package/dist/fleet-manager.js +2364 -449
  82. package/dist/fleet-manager.js.map +1 -1
  83. package/dist/host-memory.d.ts +4 -0
  84. package/dist/host-memory.js.map +1 -1
  85. package/dist/instance-config-impact.js +5 -0
  86. package/dist/instance-config-impact.js.map +1 -1
  87. package/dist/instance-lifecycle.d.ts +10 -5
  88. package/dist/instance-lifecycle.js +162 -53
  89. package/dist/instance-lifecycle.js.map +1 -1
  90. package/dist/locale.js +150 -16
  91. package/dist/locale.js.map +1 -1
  92. package/dist/login-controller.d.ts +2 -0
  93. package/dist/login-controller.js +17 -5
  94. package/dist/login-controller.js.map +1 -1
  95. package/dist/memory-pressure.d.ts +7 -8
  96. package/dist/memory-pressure.js +76 -15
  97. package/dist/memory-pressure.js.map +1 -1
  98. package/dist/monotonic-budget.d.ts +2 -0
  99. package/dist/monotonic-budget.js +28 -0
  100. package/dist/monotonic-budget.js.map +1 -0
  101. package/dist/needs-you-hub.d.ts +114 -0
  102. package/dist/needs-you-hub.js +370 -0
  103. package/dist/needs-you-hub.js.map +1 -0
  104. package/dist/needs-you-live.d.ts +116 -0
  105. package/dist/needs-you-live.js +228 -0
  106. package/dist/needs-you-live.js.map +1 -0
  107. package/dist/needs-you.d.ts +132 -0
  108. package/dist/needs-you.js +164 -0
  109. package/dist/needs-you.js.map +1 -0
  110. package/dist/node-version-guard.d.ts +30 -0
  111. package/dist/node-version-guard.js +50 -0
  112. package/dist/node-version-guard.js.map +1 -0
  113. package/dist/operator-control-paths.d.ts +4 -0
  114. package/dist/operator-control-paths.js +24 -0
  115. package/dist/operator-control-paths.js.map +1 -0
  116. package/dist/outbound-handlers.d.ts +2 -0
  117. package/dist/outbound-handlers.js +9 -2
  118. package/dist/outbound-handlers.js.map +1 -1
  119. package/dist/outbound-schemas.d.ts +5 -5
  120. package/dist/profile-control.js +4 -20
  121. package/dist/profile-control.js.map +1 -1
  122. package/dist/public-web-gateway.d.ts +18 -0
  123. package/dist/public-web-gateway.js +141 -0
  124. package/dist/public-web-gateway.js.map +1 -0
  125. package/dist/public-web-link.d.ts +53 -0
  126. package/dist/public-web-link.js +175 -0
  127. package/dist/public-web-link.js.map +1 -0
  128. package/dist/quickstart-api.d.ts +8 -1
  129. package/dist/quickstart-api.js +86 -48
  130. package/dist/quickstart-api.js.map +1 -1
  131. package/dist/quickstart.d.ts +1 -0
  132. package/dist/quickstart.js +6 -2
  133. package/dist/quickstart.js.map +1 -1
  134. package/dist/scheduler/db.d.ts +7 -1
  135. package/dist/scheduler/db.js +38 -0
  136. package/dist/scheduler/db.js.map +1 -1
  137. package/dist/scheduler/index.d.ts +1 -1
  138. package/dist/scheduler/retry-label.d.ts +11 -0
  139. package/dist/scheduler/retry-label.js +24 -0
  140. package/dist/scheduler/retry-label.js.map +1 -0
  141. package/dist/scheduler/scheduler.d.ts +55 -2
  142. package/dist/scheduler/scheduler.js +220 -8
  143. package/dist/scheduler/scheduler.js.map +1 -1
  144. package/dist/scheduler/types.d.ts +22 -0
  145. package/dist/scheduler/types.js.map +1 -1
  146. package/dist/secret-store.d.ts +5 -0
  147. package/dist/secret-store.js +20 -0
  148. package/dist/secret-store.js.map +1 -1
  149. package/dist/service-activation.d.ts +110 -0
  150. package/dist/service-activation.js +302 -0
  151. package/dist/service-activation.js.map +1 -0
  152. package/dist/service-installer.d.ts +7 -0
  153. package/dist/service-installer.js +7 -4
  154. package/dist/service-installer.js.map +1 -1
  155. package/dist/service-restart-selection.d.ts +6 -5
  156. package/dist/service-restart-selection.js +6 -5
  157. package/dist/service-restart-selection.js.map +1 -1
  158. package/dist/settings-api.d.ts +10 -2
  159. package/dist/settings-api.js +175 -72
  160. package/dist/settings-api.js.map +1 -1
  161. package/dist/settings-baseline.d.ts +20 -0
  162. package/dist/settings-baseline.js +104 -0
  163. package/dist/settings-baseline.js.map +1 -0
  164. package/dist/settings-change.d.ts +21 -0
  165. package/dist/settings-change.js +133 -0
  166. package/dist/settings-change.js.map +1 -0
  167. package/dist/settings-confirmation.d.ts +105 -0
  168. package/dist/settings-confirmation.js +232 -0
  169. package/dist/settings-confirmation.js.map +1 -0
  170. package/dist/settings-control.d.ts +41 -0
  171. package/dist/settings-control.js +294 -0
  172. package/dist/settings-control.js.map +1 -0
  173. package/dist/settings-effect.d.ts +24 -0
  174. package/dist/settings-effect.js +169 -0
  175. package/dist/settings-effect.js.map +1 -0
  176. package/dist/settings-http-confirmation.d.ts +29 -0
  177. package/dist/settings-http-confirmation.js +123 -0
  178. package/dist/settings-http-confirmation.js.map +1 -0
  179. package/dist/settings-instance-patch.d.ts +4 -0
  180. package/dist/settings-instance-patch.js +26 -0
  181. package/dist/settings-instance-patch.js.map +1 -0
  182. package/dist/settings-request-capability.d.ts +16 -0
  183. package/dist/settings-request-capability.js +32 -0
  184. package/dist/settings-request-capability.js.map +1 -0
  185. package/dist/settings-transaction.d.ts +65 -0
  186. package/dist/settings-transaction.js +223 -0
  187. package/dist/settings-transaction.js.map +1 -0
  188. package/dist/setup-auth.d.ts +10 -18
  189. package/dist/setup-auth.js +14 -38
  190. package/dist/setup-auth.js.map +1 -1
  191. package/dist/setup-form.d.ts +1 -1
  192. package/dist/setup-form.js +75 -18
  193. package/dist/setup-form.js.map +1 -1
  194. package/dist/setup-guide.d.ts +13 -0
  195. package/dist/setup-guide.js +25 -0
  196. package/dist/setup-guide.js.map +1 -0
  197. package/dist/setup-host.d.ts +16 -0
  198. package/dist/setup-host.js +197 -32
  199. package/dist/setup-host.js.map +1 -1
  200. package/dist/setup-wizard.js +3 -0
  201. package/dist/setup-wizard.js.map +1 -1
  202. package/dist/slash-authz.d.ts +17 -3
  203. package/dist/slash-authz.js +5 -0
  204. package/dist/slash-authz.js.map +1 -1
  205. package/dist/spawn-gate.js +3 -3
  206. package/dist/spawn-gate.js.map +1 -1
  207. package/dist/statusline-watcher.d.ts +5 -0
  208. package/dist/statusline-watcher.js +2 -0
  209. package/dist/statusline-watcher.js.map +1 -1
  210. package/dist/steer-capability.d.ts +6 -0
  211. package/dist/steer-capability.js +8 -0
  212. package/dist/steer-capability.js.map +1 -1
  213. package/dist/sync-work-attribution.d.ts +14 -0
  214. package/dist/sync-work-attribution.js +43 -8
  215. package/dist/sync-work-attribution.js.map +1 -1
  216. package/dist/tips.js +10 -5
  217. package/dist/tips.js.map +1 -1
  218. package/dist/tmux-control.d.ts +35 -5
  219. package/dist/tmux-control.js +291 -57
  220. package/dist/tmux-control.js.map +1 -1
  221. package/dist/tmux-manager.d.ts +20 -6
  222. package/dist/tmux-manager.js +90 -47
  223. package/dist/tmux-manager.js.map +1 -1
  224. package/dist/tmux-read.d.ts +59 -0
  225. package/dist/tmux-read.js +213 -0
  226. package/dist/tmux-read.js.map +1 -0
  227. package/dist/topic-commands.d.ts +21 -10
  228. package/dist/topic-commands.js +191 -58
  229. package/dist/topic-commands.js.map +1 -1
  230. package/dist/transcript-delta-reader.js +2 -1
  231. package/dist/transcript-delta-reader.js.map +1 -1
  232. package/dist/tunnel/cloudflared-install.d.ts +2 -0
  233. package/dist/tunnel/cloudflared-install.js +1 -1
  234. package/dist/tunnel/cloudflared-install.js.map +1 -1
  235. package/dist/tunnel/purpose-lane.d.ts +14 -0
  236. package/dist/tunnel/purpose-lane.js +43 -0
  237. package/dist/tunnel/purpose-lane.js.map +1 -0
  238. package/dist/turn-reply-guard.d.ts +2 -0
  239. package/dist/turn-reply-guard.js +10 -1
  240. package/dist/turn-reply-guard.js.map +1 -1
  241. package/dist/types.d.ts +33 -0
  242. package/dist/ui/app.html +18 -0
  243. package/dist/ui/chat-render.js +478 -0
  244. package/dist/ui/chat-store.js +289 -0
  245. package/dist/ui/chat-strings.js +69 -0
  246. package/dist/ui/chat-thread.js +209 -0
  247. package/dist/ui/chat-tour.js +116 -0
  248. package/dist/ui/fleet-org.js +142 -0
  249. package/dist/ui/panel-chat.js +418 -0
  250. package/dist/ui/panel-fleet.js +413 -0
  251. package/dist/ui/panel-needs.js +202 -0
  252. package/dist/ui/panel-settings.js +750 -0
  253. package/dist/ui/preview.js +266 -0
  254. package/dist/ui/settings-apply.js +239 -0
  255. package/dist/ui/settings-confirm.js +186 -0
  256. package/dist/ui/settings-dialogs.js +546 -0
  257. package/dist/ui/settings-model.js +277 -0
  258. package/dist/ui/settings-strings.js +652 -0
  259. package/dist/ui/settings-wizard.js +140 -0
  260. package/dist/ui/shared/agend-auth.js +93 -0
  261. package/dist/ui/shared/app-ctx.js +80 -0
  262. package/dist/ui/shared/app-html.js +9 -0
  263. package/dist/ui/shared/app-i18n.js +95 -0
  264. package/dist/ui/shared/app-nav.js +73 -0
  265. package/dist/ui/shared/app-needs.js +114 -0
  266. package/dist/ui/shared/app-route.js +73 -0
  267. package/dist/ui/shared/app-session.js +66 -0
  268. package/dist/ui/shared/app-shell.js +344 -0
  269. package/dist/ui/shared/app-store.js +51 -0
  270. package/dist/ui/shared/app-stream.js +201 -0
  271. package/dist/ui/shared/app.css +676 -0
  272. package/dist/ui/shared/app.js +88 -0
  273. package/dist/ui/shared/fonts/OFL.txt +92 -0
  274. package/dist/ui/shared/fonts/README.md +21 -0
  275. package/dist/ui/shared/fonts/inter.woff2 +0 -0
  276. package/dist/ui/shared/htm.module.js +1 -0
  277. package/dist/ui/shared/panel-view.js +655 -0
  278. package/dist/ui/shared/preact-hooks.module.js +2 -0
  279. package/dist/ui/shared/preact.module.js +2 -0
  280. package/dist/ui/shared/signin.js +130 -0
  281. package/dist/ui/shared/theme.js +29 -0
  282. package/dist/ui/shared/tokens.css +153 -0
  283. package/dist/ui/shared/ui-confirm.js +43 -0
  284. package/dist/ui/shared/ui-dialog.js +45 -0
  285. package/dist/ui/shared/ui-icons.js +61 -0
  286. package/dist/ui/shared/ui-menu.js +39 -0
  287. package/dist/ui/shared/ui-states.js +20 -0
  288. package/dist/ui/shared/ui-toast.js +18 -0
  289. package/dist/ui/shared/vendor/htm.LICENSE +202 -0
  290. package/dist/ui/shared/vendor/preact.LICENSE +21 -0
  291. package/dist/ui/shared/vendor/vendor.json +39 -0
  292. package/dist/ui/shared/view-strings.js +309 -0
  293. package/dist/ui/signin.html +51 -0
  294. package/dist/update-check.d.ts +6 -2
  295. package/dist/update-check.js +12 -3
  296. package/dist/update-check.js.map +1 -1
  297. package/dist/update-install.d.ts +71 -0
  298. package/dist/update-install.js +162 -0
  299. package/dist/update-install.js.map +1 -0
  300. package/dist/upgrade-notices.d.ts +17 -0
  301. package/dist/upgrade-notices.js +93 -0
  302. package/dist/upgrade-notices.js.map +1 -0
  303. package/dist/usage/statusline-usage.d.ts +2 -0
  304. package/dist/usage/statusline-usage.js +14 -10
  305. package/dist/usage/statusline-usage.js.map +1 -1
  306. package/dist/view-api.d.ts +6 -1
  307. package/dist/view-api.js +62 -51
  308. package/dist/view-api.js.map +1 -1
  309. package/dist/web-api.d.ts +71 -14
  310. package/dist/web-api.js +492 -77
  311. package/dist/web-api.js.map +1 -1
  312. package/dist/web-auth.d.ts +71 -27
  313. package/dist/web-auth.js +169 -69
  314. package/dist/web-auth.js.map +1 -1
  315. package/dist/web-body.d.ts +3 -0
  316. package/dist/web-body.js +55 -0
  317. package/dist/web-body.js.map +1 -0
  318. package/dist/web-channel-echo.d.ts +9 -0
  319. package/dist/web-channel-echo.js +55 -0
  320. package/dist/web-channel-echo.js.map +1 -0
  321. package/dist/web-chat-history.d.ts +125 -0
  322. package/dist/web-chat-history.js +184 -0
  323. package/dist/web-chat-history.js.map +1 -0
  324. package/dist/web-continuation.d.ts +7 -0
  325. package/dist/web-continuation.js +22 -0
  326. package/dist/web-continuation.js.map +1 -0
  327. package/dist/web-host-guard.d.ts +34 -0
  328. package/dist/web-host-guard.js +57 -1
  329. package/dist/web-host-guard.js.map +1 -1
  330. package/dist/web-login.d.ts +72 -0
  331. package/dist/web-login.js +132 -0
  332. package/dist/web-login.js.map +1 -0
  333. package/dist/web-org.d.ts +55 -0
  334. package/dist/web-org.js +67 -0
  335. package/dist/web-org.js.map +1 -0
  336. package/dist/web-preview.d.ts +77 -0
  337. package/dist/web-preview.js +240 -0
  338. package/dist/web-preview.js.map +1 -0
  339. package/dist/web-request-context.d.ts +10 -0
  340. package/dist/web-request-context.js +13 -0
  341. package/dist/web-request-context.js.map +1 -0
  342. package/dist/web-session.d.ts +167 -0
  343. package/dist/web-session.js +389 -0
  344. package/dist/web-session.js.map +1 -0
  345. package/dist/web-shell-routes.d.ts +59 -0
  346. package/dist/web-shell-routes.js +96 -0
  347. package/dist/web-shell-routes.js.map +1 -0
  348. package/dist/web-upload.d.ts +183 -0
  349. package/dist/web-upload.js +468 -0
  350. package/dist/web-upload.js.map +1 -0
  351. package/dist/website-tips-renderer.d.ts +2 -2
  352. package/dist/website-tips-renderer.js +3 -3
  353. package/dist/website-tips-renderer.js.map +1 -1
  354. package/package.json +8 -5
  355. package/scripts/preinstall-guard.cjs +36 -0
  356. package/dist/ui/dashboard.html +0 -774
  357. package/dist/ui/settings.html +0 -2165
  358. package/dist/ui/view.html +0 -1139
package/dist/types.d.ts CHANGED
@@ -333,6 +333,14 @@ export interface FleetTemplate {
333
333
  }
334
334
  /** Web UI feature toggles (fleet.yaml `web:` section). */
335
335
  export interface WebConfig {
336
+ /** Only menu availability; every exposure requires explicit admin consent. */
337
+ public_link?: {
338
+ allow_public?: boolean;
339
+ ttl_minutes?: number;
340
+ protocol?: "http2" | "quic" | "auto";
341
+ };
342
+ /** Echo web chat to the owning fleet topic (default true); never ClassicBot rooms. */
343
+ echo_to_channel?: boolean;
336
344
  /** Show the AI subscription usage panel on /view and serve /api/ai-usage (default true). */
337
345
  usage_panel?: boolean;
338
346
  /** Expose provider API-key verify/apply endpoints (default false until dark-shipped). */
@@ -343,6 +351,26 @@ export interface WebConfig {
343
351
  * Names only (a port is ignored); anything not listed is refused with 403.
344
352
  */
345
353
  allowed_hosts?: string[];
354
+ /**
355
+ * Who may read /view (the page, the terminal capture, the roster, usage).
356
+ * `open` (default): anyone who can reach the listener — the page is a read-only dashboard on loopback.
357
+ * `session`: a signed-in web session or the CLI header token. Writes need a credential either way.
358
+ */
359
+ view_access?: "open" | "session";
360
+ /** Tell the admin channel when someone signs in to the web panel (default true). */
361
+ notify_login?: boolean;
362
+ /**
363
+ * #1306: HTML previews in the web chat (default true). false: no preview listener; cards show Source and
364
+ * Download only. Even when true, a browser runs a preview only after that device opted in.
365
+ */
366
+ preview?: boolean;
367
+ /** The preview listener's port on 127.0.0.1 (default health_port + 1, i.e. 19281). */
368
+ preview_port?: number;
369
+ /**
370
+ * A separate host name a proxy maps to the preview listener, as a bare origin (`https://preview.example.net`):
371
+ * previews through a tunnel or proxy need one. It must not be a name the dashboard answers to.
372
+ */
373
+ preview_origin?: string;
346
374
  }
347
375
  /** `login:` section — remote /login behaviour. */
348
376
  export interface LoginConfig {
@@ -392,6 +420,11 @@ export interface FleetConfig {
392
420
  profiles?: Record<string, ProfileConfig>;
393
421
  health_port?: number;
394
422
  web?: WebConfig;
423
+ /** #1386 "Needs you": the live list in each world's General (default on) and DMs to that world's admins (default off). */
424
+ needs_you?: {
425
+ live_message?: boolean;
426
+ dm?: boolean;
427
+ };
395
428
  /** Host name used when building URLs handed to users (/dashboard, web terminal). Default "localhost". */
396
429
  hostname?: string;
397
430
  /**
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
6
+ <meta name="referrer" content="no-referrer">
7
+ <title>AgEnD</title>
8
+ <script src="/assets/theme.js"></script>
9
+ <link rel="stylesheet" href="/assets/tokens.css">
10
+ <link rel="stylesheet" href="/assets/app.css">
11
+ <script src="/assets/agend-auth.js"></script>
12
+ <script type="module" src="/assets/app.js"></script>
13
+ </head>
14
+ <body>
15
+ <div id="app" class="app-root"></div>
16
+ <noscript><p class="noscript">AgEnD's web app needs JavaScript.</p></noscript>
17
+ </body>
18
+ </html>
@@ -0,0 +1,478 @@
1
+ /*
2
+ * Markdown for the web chat, safe by construction: the text is HTML-escaped FIRST, and only then are a few
3
+ * Markdown forms turned into a fixed set of tags. Nothing in a message can produce a tag, an attribute or an
4
+ * event handler of its own; the only attribute that carries message text is a link's href, and only an
5
+ * http(s)/mailto URL gets one (rendered with target=_blank rel="noopener noreferrer").
6
+ *
7
+ * Imported by the chat panel (#1408: the import sets globalThis.AgendChatRender); also exports itself for the tests.
8
+ */
9
+ (function (root, factory) {
10
+ "use strict";
11
+ var api = factory();
12
+ if (typeof module === "object" && module && module.exports) module.exports = api;
13
+ // The global as well: the chat imports this file for its side effect (#1408), in the browser and under the tests.
14
+ if (root) root.AgendChatRender = api;
15
+ })(typeof globalThis !== "undefined" ? globalThis : this, function () {
16
+ "use strict";
17
+
18
+ function escapeHtml(s) {
19
+ return String(s == null ? "" : s)
20
+ .replace(/&/g, "&amp;")
21
+ .replace(/</g, "&lt;")
22
+ .replace(/>/g, "&gt;")
23
+ .replace(/"/g, "&quot;")
24
+ .replace(/'/g, "&#39;");
25
+ }
26
+
27
+ /** An escaped URL is linkable only with an explicit http(s)/mailto scheme; anything else stays text. */
28
+ function safeHref(escapedUrl) {
29
+ var u = escapedUrl.trim();
30
+ if (!/^(https?:\/\/|mailto:)[^\s]+$/i.test(u)) return null;
31
+ // A quote or an angle bracket is never part of a URL we link: such text stays text (it could only be an attempt).
32
+ if (/&(?:quot|#39|lt|gt);/.test(u)) return null;
33
+ // A placeholder in a URL would be expanded into markup inside the attribute: never trust one there.
34
+ if (u.indexOf("\u0000") !== -1) return null;
35
+ return u;
36
+ }
37
+
38
+ function link(href, label) {
39
+ return '<a href="' + href + '" target="_blank" rel="noopener noreferrer">' + label + "</a>";
40
+ }
41
+
42
+ // Placeholders survive the later passes untouched (they contain no Markdown characters).
43
+ function stash(store, html) { store.push(html); return "\u0000" + (store.length - 1) + "\u0000"; }
44
+ function unstash(store, s) {
45
+ // A stashed fragment can hold another placeholder (a `code` span inside a link label): expand until
46
+ // none is left. Every fragment is the renderer's own output, so this cannot introduce message markup;
47
+ // bounded so a malformed store cannot loop.
48
+ for (var pass = 0; pass < 4 && s.indexOf("\u0000") !== -1; pass++) {
49
+ s = s.replace(/\u0000(\d+)\u0000/g, function (_m, i) { var f = store[Number(i)]; return f === undefined ? "" : f; });
50
+ }
51
+ return s;
52
+ }
53
+
54
+ /** Inline Markdown over ALREADY-ESCAPED text. */
55
+ function inline(escaped, store) {
56
+ var s = escaped;
57
+ // `code` first: nothing inside it is Markdown.
58
+ s = s.replace(/`([^`\n]+)`/g, function (_m, code) { return stash(store, "<code>" + code + "</code>"); });
59
+ // [label](url)
60
+ s = s.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, function (m, label, url) {
61
+ var href = safeHref(url);
62
+ return href ? stash(store, link(href, emphasis(label))) : m;
63
+ });
64
+ // bare URLs
65
+ s = s.replace(/(^|[\s(])((?:https?:\/\/)[^\s<]+[^\s<.,;:!?)\]'"&])/g, function (m, pre, url) {
66
+ var href = safeHref(url);
67
+ return href ? pre + stash(store, link(href, url)) : m;
68
+ });
69
+ return emphasis(s);
70
+ }
71
+
72
+ /** Bold, strike-through and italics, over escaped text (a link's label gets them too). */
73
+ function emphasis(s) {
74
+ s = s.replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>");
75
+ s = s.replace(/~~([^~\n]+)~~/g, "<del>$1</del>");
76
+ s = s.replace(/(^|[^*\w])\*([^*\s][^*\n]*?)\*(?![*\w])/g, "$1<em>$2</em>");
77
+ s = s.replace(/(^|[^_\w])_([^_\s][^_\n]*?)_(?![_\w])/g, "$1<em>$2</em>");
78
+ return s;
79
+ }
80
+
81
+ /** How deep a list may nest: deeper items stay at the deepest level (a message cannot build an unbounded tree). */
82
+ var MAX_LIST_DEPTH = 6;
83
+ /** The widest table rendered as a table; a wider one stays text. */
84
+ var MAX_TABLE_COLUMNS = 20;
85
+
86
+ /** Leading whitespace as columns: a tab counts four. */
87
+ function indentOf(ws) { var n = 0; for (var i = 0; i < ws.length; i++) n += ws[i] === "\t" ? 4 : 1; return n; }
88
+
89
+ /**
90
+ * A run of list items as nested lists. An item indented deeper than the one before opens a list inside that
91
+ * item; a shallower one closes lists back to its level; at the same level a change of kind (- vs 1.) starts a
92
+ * new list. Every tag is the renderer's own; item text goes through inline() like any other text.
93
+ */
94
+ function renderList(items, store) {
95
+ var html = "";
96
+ var stack = []; // { indent, tag }
97
+ for (var i = 0; i < items.length; i++) {
98
+ var it = items[i];
99
+ while (stack.length && it.indent < stack[stack.length - 1].indent) html += "</li></" + stack.pop().tag + ">";
100
+ var top = stack[stack.length - 1];
101
+ if (!top || (it.indent > top.indent && stack.length < MAX_LIST_DEPTH)) {
102
+ html += "<" + it.tag + ">";
103
+ stack.push({ indent: it.indent, tag: it.tag });
104
+ } else if (top.tag !== it.tag) {
105
+ html += "</li></" + top.tag + "><" + it.tag + ">";
106
+ top.tag = it.tag;
107
+ } else {
108
+ html += "</li>";
109
+ }
110
+ html += "<li>" + inline(it.text, store);
111
+ }
112
+ while (stack.length) html += "</li></" + stack.pop().tag + ">";
113
+ return html;
114
+ }
115
+
116
+ /** The cells of one table row (escaped text). A leading/trailing pipe is optional; `\|` is a literal pipe. */
117
+ function tableCells(line) {
118
+ var t = line.trim();
119
+ if (t.charAt(0) === "|") t = t.slice(1);
120
+ if (t.charAt(t.length - 1) === "|" && t.charAt(t.length - 2) !== "\\") t = t.slice(0, -1);
121
+ var cells = [];
122
+ var cur = "";
123
+ for (var i = 0; i < t.length; i++) {
124
+ if (t[i] === "\\" && t[i + 1] === "|") { cur += "|"; i++; continue; }
125
+ if (t[i] === "|") { cells.push(cur.trim()); cur = ""; continue; }
126
+ cur += t[i];
127
+ }
128
+ cells.push(cur.trim());
129
+ return cells;
130
+ }
131
+
132
+ /**
133
+ * The alignments of a separator row (`|---|:--:|--:|`), or null when the line is not one. Checked cell by cell
134
+ * with linear tests only (#1287 review: one regex with adjacent optional whitespace backtracked for seconds on a
135
+ * few KB of spaces with a bad tail, blocking the page).
136
+ */
137
+ function tableAligns(line) {
138
+ if (line.indexOf("-") === -1 || !/^[\s|:-]*$/.test(line)) return null;
139
+ var cells = tableCells(line);
140
+ for (var i = 0; i < cells.length; i++) if (!/^:?-+:?$/.test(cells[i])) return null;
141
+ return cells.map(function (c) {
142
+ var l = c.charAt(0) === ":", r = c.charAt(c.length - 1) === ":";
143
+ return l && r ? "c" : r ? "r" : l ? "l" : "";
144
+ });
145
+ }
146
+
147
+ function renderTable(header, aligns, rows, store) {
148
+ function cell(tag, text, i) {
149
+ var a = aligns[i] ? ' class="al-' + aligns[i] + '"' : "";
150
+ return "<" + tag + a + ">" + inline(text, store) + "</" + tag + ">";
151
+ }
152
+ function row(tag, cells) {
153
+ var out = "";
154
+ for (var i = 0; i < aligns.length; i++) out += cell(tag, cells[i] === undefined ? "" : cells[i], i);
155
+ return "<tr>" + out + "</tr>";
156
+ }
157
+ return '<div class="tbl"><table><thead>' + row("th", header) + "</thead><tbody>" +
158
+ rows.map(function (r) { return row("td", r); }).join("") + "</tbody></table></div>";
159
+ }
160
+
161
+ /** Block Markdown (headings, lists, tables, quotes, rules, paragraphs) over one run of non-code text. */
162
+ function blocks(text, store) {
163
+ var lines = escapeHtml(text).split("\n");
164
+ var out = [];
165
+ var para = [];
166
+ var list = []; // [{ indent, tag, text }]
167
+ var quote = [];
168
+ function flushPara() { if (para.length) { out.push("<p>" + para.map(function (l) { return inline(l, store); }).join("<br>") + "</p>"); para = []; } }
169
+ function flushList() { if (list.length) { out.push(renderList(list, store)); list = []; } }
170
+ function flushQuote() { if (quote.length) { out.push("<blockquote>" + quote.map(function (l) { return inline(l, store); }).join("<br>") + "</blockquote>"); quote = []; } }
171
+ function flushAll() { flushPara(); flushList(); flushQuote(); }
172
+ for (var i = 0; i < lines.length; i++) {
173
+ var line = lines[i];
174
+ var m;
175
+ if (/^\s*$/.test(line)) { flushAll(); continue; }
176
+ // A table: a header row with a pipe, then a separator row with the same number of columns.
177
+ if (line.indexOf("|") !== -1 && i + 1 < lines.length) {
178
+ var aligns = tableAligns(lines[i + 1]);
179
+ var header = aligns && tableCells(line);
180
+ if (aligns && header.length === aligns.length && aligns.length <= MAX_TABLE_COLUMNS) {
181
+ flushAll();
182
+ var rows = [];
183
+ var j = i + 2;
184
+ for (; j < lines.length && lines[j].indexOf("|") !== -1 && !/^\s*$/.test(lines[j]); j++) rows.push(tableCells(lines[j]));
185
+ out.push(renderTable(header, aligns, rows, store));
186
+ i = j - 1;
187
+ continue;
188
+ }
189
+ }
190
+ if ((m = /^(#{1,6})\s+(.*)$/.exec(line))) { flushAll(); var n = Math.min(m[1].length + 2, 6); out.push("<h" + n + ">" + inline(m[2], store) + "</h" + n + ">"); continue; }
191
+ if (/^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { flushAll(); out.push("<hr>"); continue; }
192
+ if ((m = /^&gt;\s?(.*)$/.exec(line))) { flushPara(); flushList(); quote.push(m[1]); continue; }
193
+ if ((m = /^([ \t]*)([-*+]|\d{1,9}[.)])\s+(.*)$/.exec(line))) {
194
+ flushPara(); flushQuote();
195
+ list.push({ indent: indentOf(m[1]), tag: /\d/.test(m[2]) ? "ol" : "ul", text: m[3] });
196
+ continue;
197
+ }
198
+ flushList(); flushQuote();
199
+ para.push(line);
200
+ }
201
+ flushAll();
202
+ return out.join("");
203
+ }
204
+
205
+ // ── Code highlighting: a small tokenizer, never a library. Every token is escaped and wrapped in one of four
206
+ // fixed classes; what is not recognised stays plain escaped text. Unknown languages are not highlighted. ──
207
+ var KEYWORDS = {
208
+ js: "break case catch class const continue default delete do else export extends false finally for function if import in instanceof let new null of return static super switch this throw true try typeof undefined var void while yield async await",
209
+ py: "and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield",
210
+ sh: "if then else elif fi for while until do done case esac function in return local export set unset echo exit",
211
+ json: "true false null"
212
+ };
213
+ KEYWORDS.ts = KEYWORDS.js + " interface type enum implements private public protected readonly declare namespace keyof as";
214
+ var LANG_ALIAS = { javascript: "js", jsx: "js", mjs: "js", cjs: "js", typescript: "ts", tsx: "ts", python: "py", bash: "sh", shell: "sh", zsh: "sh", console: "sh" };
215
+ var TOKEN_RE = {
216
+ // An unclosed string or comment runs to the end of its line (or of the code): it matches once instead of
217
+ // failing and being re-scanned from every later quote, which made a run of unclosed quotes quadratic.
218
+ js: /(\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$))|("(?:[^"\\\n]|\\.)*"?|'(?:[^'\\\n]|\\.)*'?|`(?:[^`\\]|\\.)*`?)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][\w$]*)/g,
219
+ py: /(#[^\n]*)|("""[\s\S]*?(?:"""|$)|'''[\s\S]*?(?:'''|$)|"(?:[^"\\\n]|\\.)*"?|'(?:[^'\\\n]|\\.)*'?)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_]\w*)/g,
220
+ sh: /(#[^\n]*)|("(?:[^"\\]|\\.)*"?|'[^']*'?)|(\b\d+\b)|([A-Za-z_][\w-]*)/g,
221
+ json: /(\u0001)|("(?:[^"\\\n]|\\.)*"?)|(-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|([A-Za-z_]\w*)/g
222
+ };
223
+ TOKEN_RE.ts = TOKEN_RE.js;
224
+
225
+ /** Code as highlighted HTML for a known language, or null (then the caller escapes it as before). */
226
+ function highlight(code, lang) {
227
+ var key = String(lang || "").toLowerCase();
228
+ if (Object.prototype.hasOwnProperty.call(LANG_ALIAS, key)) key = LANG_ALIAS[key];
229
+ if (!Object.prototype.hasOwnProperty.call(TOKEN_RE, key)) return null;
230
+ var words = {};
231
+ KEYWORDS[key].split(" ").forEach(function (w) { words[w] = true; });
232
+ var re = new RegExp(TOKEN_RE[key].source, "g");
233
+ var out = "";
234
+ var last = 0;
235
+ var m;
236
+ while ((m = re.exec(code)) !== null) {
237
+ if (m[0] === "") { re.lastIndex++; continue; }
238
+ out += escapeHtml(code.slice(last, m.index));
239
+ var cls = m[1] ? "tk-c" : m[2] ? "tk-s" : m[3] ? "tk-n" : (m[4] && Object.prototype.hasOwnProperty.call(words, m[4])) ? "tk-k" : null;
240
+ out += cls ? '<span class="' + cls + '">' + escapeHtml(m[0]) + "</span>" : escapeHtml(m[0]);
241
+ last = re.lastIndex;
242
+ }
243
+ return out + escapeHtml(code.slice(last));
244
+ }
245
+
246
+ // A fenced code block: ```lang …``` — or unterminated to the end of the text (what a length cut leaves).
247
+ var FENCE_RE = /```([A-Za-z0-9_+.-]{0,32})[^\n]*\n([\s\S]*?)(?:```|$)/g;
248
+ function isHtmlLang(lang) { return String(lang || "").toLowerCase() === "html"; }
249
+
250
+ /**
251
+ * The ```html fences of a message, in order (#1306): each one's code, and whether it was closed. The n-th entry is
252
+ * the card renderMarkdown(text, { htmlCards: true }) numbered n. Only `html` exactly (any case) — not html5, xhtml, htm.
253
+ */
254
+ function htmlFences(text) {
255
+ var src = String(text == null ? "" : text).replace(/\r\n?/g, "\n").replace(/\u0000/g, "");
256
+ var re = new RegExp(FENCE_RE.source, "g");
257
+ var out = [];
258
+ var m;
259
+ while ((m = re.exec(src)) !== null) {
260
+ if (isHtmlLang(m[1])) out.push({ code: m[2].replace(/\n$/, ""), terminated: m[0].slice(-3) === "```" });
261
+ if (m[0].length === 0) break;
262
+ }
263
+ return out;
264
+ }
265
+
266
+ /**
267
+ * The whole message: fenced code blocks verbatim (escaped), everything else as Markdown. With { htmlCards: true }
268
+ * (only ever for a message the server marked `agent` — #1306), each ```html fence is followed by an empty card
269
+ * placeholder <div class="html-card" data-card="n">, or — for a fence a cut left open — one marked "truncated",
270
+ * which gets no Preview. No text of the HTML goes into an attribute: the page reads it from the message, by n.
271
+ */
272
+ function renderMarkdown(text, opts) {
273
+ var cards = !!(opts && opts.htmlCards);
274
+ var src = String(text == null ? "" : text).replace(/\r\n?/g, "\n").replace(/\u0000/g, ""); // \u0000 is the placeholder mark
275
+ var store = [];
276
+ var html = "";
277
+ var re = new RegExp(FENCE_RE.source, "g");
278
+ var last = 0;
279
+ var n = 0;
280
+ var m;
281
+ while ((m = re.exec(src)) !== null) {
282
+ html += blocks(src.slice(last, m.index), store);
283
+ var lang = m[1] ? ' data-lang="' + escapeHtml(m[1]) + '"' : "";
284
+ var code = m[2].replace(/\n$/, "");
285
+ var lit = highlight(code, m[1]);
286
+ html += "<pre" + lang + "><code>" + (lit === null ? escapeHtml(code) : lit) + "</code></pre>";
287
+ if (cards && isHtmlLang(m[1])) {
288
+ html += '<div class="html-card' + (m[0].slice(-3) === "```" ? "" : " truncated") + '" data-card="' + n + '"></div>';
289
+ n++;
290
+ }
291
+ last = re.lastIndex;
292
+ if (m[0].length === 0) break;
293
+ }
294
+ html += blocks(src.slice(last), store);
295
+ return unstash(store, html);
296
+ }
297
+
298
+ /**
299
+ * The chat's message list after `incoming` arrives (history, a live SSE message or an SSE replay): one entry per
300
+ * id, in id order, the newest `cap` kept. The same message can arrive twice — from /ui/history and from the
301
+ * stream — and must show once.
302
+ */
303
+ function mergeMessages(existing, incoming, cap) {
304
+ var seen = {};
305
+ var out = [];
306
+ // Ids restart with every fleet process; the boot generation makes them an identity again.
307
+ function key(m) { return String(m.boot || "") + ":" + m.id; }
308
+ function add(m) {
309
+ if (!m || typeof m.id !== "number") return;
310
+ var k = key(m);
311
+ if (seen[k] !== undefined) {
312
+ // The same message again (history after the stream): the copy that got further shows its ticks.
313
+ var had = out[seen[k]];
314
+ var next = nextDeliveryState(had.delivery, m.delivery);
315
+ if (next !== had.delivery) out[seen[k]] = Object.assign({}, had, { delivery: next });
316
+ return;
317
+ }
318
+ seen[k] = out.length;
319
+ out.push(m);
320
+ }
321
+ (existing || []).forEach(add);
322
+ (incoming || []).forEach(add);
323
+ // Within one boot the id is the order; across boots (a restart) the time is.
324
+ out.sort(function (a, b) {
325
+ if ((a.boot || "") === (b.boot || "")) return a.id - b.id;
326
+ var ta = String(a.ts || ""), tb = String(b.ts || "");
327
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
328
+ });
329
+ var limit = typeof cap === "number" && cap > 0 ? cap : 500;
330
+ return out.length > limit ? out.slice(out.length - limit) : out;
331
+ }
332
+
333
+ /**
334
+ * The delivery ticks of a web user's message (web track C3) — the same order the fleet keeps
335
+ * (web-chat-history.ts nextDeliveryState): a report that arrives late or twice never moves a tick back;
336
+ * delivered and failed are final, cancelled gives way to what the agent actually got.
337
+ */
338
+ var DELIVERY_RANK = { queued: 1, processing: 2, cancelled: 3, delivered: 4, failed: 4 };
339
+ function nextDeliveryState(prev, next) {
340
+ if (!Object.prototype.hasOwnProperty.call(DELIVERY_RANK, next)) return prev;
341
+ if (prev === undefined || !Object.prototype.hasOwnProperty.call(DELIVERY_RANK, prev)) return next;
342
+ return DELIVERY_RANK[next] > DELIVERY_RANK[prev] ? next : prev;
343
+ }
344
+
345
+ /** The list after a delivery report for `messageId`: the same list when nothing moved, else a copy. */
346
+ function applyDelivery(list, messageId, delivery) {
347
+ var msgs = list || [];
348
+ for (var i = msgs.length - 1; i >= 0; i--) {
349
+ if (!msgs[i] || msgs[i].messageId !== messageId) continue;
350
+ var next = nextDeliveryState(msgs[i].delivery, delivery);
351
+ if (next === msgs[i].delivery) return msgs;
352
+ var out = msgs.slice();
353
+ out[i] = Object.assign({}, msgs[i], { delivery: next });
354
+ return out;
355
+ }
356
+ return msgs;
357
+ }
358
+
359
+ var TICK_GLYPH = { queued: "◷", processing: "✓", delivered: "✓✓", failed: "!", cancelled: "⊘" };
360
+ /** The ticks for one state, labelled for a screen reader as well as a pointer; "" when there is none. */
361
+ function deliveryHtml(state, labels) {
362
+ if (!Object.prototype.hasOwnProperty.call(TICK_GLYPH, state)) return "";
363
+ var label = escapeHtml((labels && labels[state]) || state);
364
+ return '<span class="tick tick-' + state + '" role="img" aria-label="' + label + '" title="' + label + '">' + TICK_GLYPH[state] + "</span>";
365
+ }
366
+
367
+ /**
368
+ * Whether a scrolled list counts as "at the bottom" (#1307): within `slack` px of it. Only then does a new message
369
+ * pull the view down; a reader who scrolled up stays where they are.
370
+ */
371
+ function isNearBottom(scrollTop, clientHeight, scrollHeight, slack) {
372
+ return Number(scrollHeight) - Number(scrollTop) - Number(clientHeight) <= (slack == null ? 48 : slack);
373
+ }
374
+
375
+ /** A paste longer than this many characters becomes a text file instead of filling the composer (#1307). */
376
+ var LONG_PASTE_CHARS = 10000;
377
+ function isLongPaste(text) { return typeof text === "string" && text.length > LONG_PASTE_CHARS; }
378
+
379
+ /** How long a turn has run, as m:ss (h:mm:ss past an hour); never negative. */
380
+ function formatElapsed(ms) {
381
+ var t = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
382
+ var h = Math.floor(t / 3600), m = Math.floor((t % 3600) / 60), sec = t % 60;
383
+ var two = function (n) { return (n < 10 ? "0" : "") + n; };
384
+ return h ? h + ":" + two(m) + ":" + two(sec) : m + ":" + two(sec);
385
+ }
386
+
387
+ /** A code block longer than this many lines is folded until it is opened. */
388
+ var CODE_FOLD_LINES = 30;
389
+ /** Lines in a block of code (a trailing newline does not start another). */
390
+ function lineCount(code) {
391
+ var s = String(code == null ? "" : code).replace(/\n$/, "");
392
+ return s === "" ? 0 : s.split("\n").length;
393
+ }
394
+
395
+ /** Whether the chat shows "working" and its Stop button for an instance in this execution state. */
396
+ function isBusy(state) { return state === "working" || state === "stuck"; }
397
+
398
+ /** What a key in the composer does: Enter sends, Shift+Enter is a new line, nothing happens mid-IME-composition. */
399
+ function composerKey(ev) {
400
+ if (!ev || ev.key !== "Enter" || ev.isComposing || ev.keyCode === 229) return "none";
401
+ if (ev.shiftKey || ev.altKey || ev.ctrlKey || ev.metaKey) return "newline";
402
+ return "send";
403
+ }
404
+
405
+ /**
406
+ * A send that failed: where does its text go? Back into the composer only if that composer is the same
407
+ * target's, still on screen and empty; otherwise it is kept aside for that target (never put into another
408
+ * target's composer, never over a draft someone is typing) and offered back when that chat is open.
409
+ */
410
+ function settleFailedSend(target, current, composerPresent, composerValue) {
411
+ return target === current && composerPresent && !composerValue ? "restore" : "keep";
412
+ }
413
+
414
+ /** "Put back" for a kept message: in front of whatever is being typed now, never replacing it. */
415
+ function putBack(failedText, draft) {
416
+ return draft ? failedText + "\n" + draft : failedText;
417
+ }
418
+
419
+ /** The upload limits the server enforces (web-upload.ts UPLOAD_LIMITS); checked here first only to say so early. */
420
+ var FILE_LIMITS = { maxFileBytes: 10 * 1024 * 1024, maxFiles: 5, maxTotalBytes: 25 * 1024 * 1024 };
421
+
422
+ function formatSize(n) {
423
+ var b = Number(n) || 0;
424
+ if (b < 1024) return b + " B";
425
+ if (b < 1024 * 1024) return (b / 1024).toFixed(b < 10 * 1024 ? 1 : 0) + " KB";
426
+ return (b / 1024 / 1024).toFixed(1) + " MB";
427
+ }
428
+
429
+ /**
430
+ * Which of `adding` can join `pending` on one message: within the count, per-file and total limits.
431
+ * Returns the files kept (pending first, then the accepted new ones) and, for each refused one, why.
432
+ */
433
+ function checkFiles(pending, adding) {
434
+ var kept = (pending || []).slice();
435
+ var total = 0;
436
+ kept.forEach(function (f) { total += Number(f.size) || 0; });
437
+ var rejected = [];
438
+ (adding || []).forEach(function (f) {
439
+ var size = Number(f.size) || 0;
440
+ if (size === 0) rejected.push({ name: f.name, reason: "empty" });
441
+ else if (size > FILE_LIMITS.maxFileBytes) rejected.push({ name: f.name, reason: "too-large" });
442
+ else if (kept.length >= FILE_LIMITS.maxFiles) rejected.push({ name: f.name, reason: "too-many" });
443
+ else if (total + size > FILE_LIMITS.maxTotalBytes) rejected.push({ name: f.name, reason: "too-much" });
444
+ else { kept.push(f); total += size; }
445
+ });
446
+ return { kept: kept, rejected: rejected };
447
+ }
448
+
449
+ /**
450
+ * The files shown with a message. Only an id the fleet issued (32 hex) becomes a URL, and only under
451
+ * /ui/file/; a name is text. Images are shown (and open full size), other files are download links.
452
+ */
453
+ function attachmentsHtml(attachments) {
454
+ if (!Array.isArray(attachments)) return "";
455
+ var out = [];
456
+ attachments.forEach(function (a) {
457
+ if (!a || typeof a.id !== "string" || !/^[0-9a-f]{32}$/.test(a.id)) return;
458
+ var url = "/ui/file/" + a.id;
459
+ var name = escapeHtml(a.name || "file");
460
+ if (a.kind === "photo") {
461
+ out.push('<a class="att-img" href="' + url + '" target="_blank" rel="noopener noreferrer"><img src="' + url + '" alt="' + name + '" loading="lazy"></a>');
462
+ } else {
463
+ out.push('<a class="att-file" href="' + url + '" download="' + name + '">📎 ' + name + ' <span class="att-size">' + escapeHtml(formatSize(a.size)) + "</span></a>");
464
+ }
465
+ });
466
+ return out.length ? '<div class="atts">' + out.join("") + "</div>" : "";
467
+ }
468
+
469
+ return {
470
+ renderMarkdown: renderMarkdown, escapeHtml: escapeHtml, mergeMessages: mergeMessages, composerKey: composerKey,
471
+ settleFailedSend: settleFailedSend, putBack: putBack,
472
+ FILE_LIMITS: FILE_LIMITS, formatSize: formatSize, checkFiles: checkFiles, attachmentsHtml: attachmentsHtml,
473
+ nextDeliveryState: nextDeliveryState, applyDelivery: applyDelivery, deliveryHtml: deliveryHtml, isBusy: isBusy,
474
+ isNearBottom: isNearBottom, CODE_FOLD_LINES: CODE_FOLD_LINES, lineCount: lineCount,
475
+ LONG_PASTE_CHARS: LONG_PASTE_CHARS, isLongPaste: isLongPaste, formatElapsed: formatElapsed,
476
+ htmlFences: htmlFences,
477
+ };
478
+ });