@songsid/agend 2.3.0-beta.2 → 2.3.0-beta.20

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 (502) hide show
  1. package/README.md +7 -1
  2. package/README.zh-TW.md +7 -1
  3. package/dist/agent-cli-instructions.md +9 -2
  4. package/dist/agent-cli.js +63 -2
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +4 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-file-read.d.ts +4 -1
  9. package/dist/agent-file-read.js +189 -16
  10. package/dist/agent-file-read.js.map +1 -1
  11. package/dist/agent-whats-new.d.ts +11 -0
  12. package/dist/agent-whats-new.js +29 -5
  13. package/dist/agent-whats-new.js.map +1 -1
  14. package/dist/auth-api.d.ts +30 -1
  15. package/dist/auth-api.js +45 -13
  16. package/dist/auth-api.js.map +1 -1
  17. package/dist/authority/authority-state.d.ts +38 -0
  18. package/dist/authority/authority-state.js +114 -0
  19. package/dist/authority/authority-state.js.map +1 -0
  20. package/dist/authority/session-authorizations.d.ts +43 -0
  21. package/dist/authority/session-authorizations.js +102 -0
  22. package/dist/authority/session-authorizations.js.map +1 -0
  23. package/dist/authority/web-authority.d.ts +120 -0
  24. package/dist/authority/web-authority.js +157 -0
  25. package/dist/authority/web-authority.js.map +1 -0
  26. package/dist/backend/claude-code.d.ts +2 -2
  27. package/dist/backend/claude-code.js +18 -8
  28. package/dist/backend/claude-code.js.map +1 -1
  29. package/dist/backend/codex-metadata.d.ts +8 -0
  30. package/dist/backend/codex-metadata.js +74 -17
  31. package/dist/backend/codex-metadata.js.map +1 -1
  32. package/dist/backend/codex.js +15 -2
  33. package/dist/backend/codex.js.map +1 -1
  34. package/dist/backend/credential-profile.d.ts +2 -0
  35. package/dist/backend/credential-profile.js +2 -0
  36. package/dist/backend/credential-profile.js.map +1 -1
  37. package/dist/backend/types.d.ts +6 -0
  38. package/dist/backend/types.js +15 -0
  39. package/dist/backend/types.js.map +1 -1
  40. package/dist/backend-availability.d.ts +120 -0
  41. package/dist/backend-availability.js +396 -0
  42. package/dist/backend-availability.js.map +1 -0
  43. package/dist/backend-command.d.ts +23 -3
  44. package/dist/backend-command.js +25 -4
  45. package/dist/backend-command.js.map +1 -1
  46. package/dist/bug-report-cli.d.ts +30 -0
  47. package/dist/bug-report-cli.js +138 -0
  48. package/dist/bug-report-cli.js.map +1 -0
  49. package/dist/bug-report.d.ts +47 -0
  50. package/dist/bug-report.js +136 -0
  51. package/dist/bug-report.js.map +1 -0
  52. package/dist/bundled-skills.d.ts +14 -0
  53. package/dist/bundled-skills.js +29 -0
  54. package/dist/bundled-skills.js.map +1 -0
  55. package/dist/cache-ledger.d.ts +16 -3
  56. package/dist/cache-ledger.js +33 -13
  57. package/dist/cache-ledger.js.map +1 -1
  58. package/dist/cache-service.d.ts +55 -3
  59. package/dist/cache-service.js +152 -16
  60. package/dist/cache-service.js.map +1 -1
  61. package/dist/channel/adapters/discord-client-retirement.d.ts +5 -0
  62. package/dist/channel/adapters/discord-client-retirement.js +57 -0
  63. package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
  64. package/dist/channel/adapters/discord.d.ts +9 -0
  65. package/dist/channel/adapters/discord.js +49 -4
  66. package/dist/channel/adapters/discord.js.map +1 -1
  67. package/dist/channel/mcp-tools.js +22 -11
  68. package/dist/channel/mcp-tools.js.map +1 -1
  69. package/dist/channel/types.d.ts +6 -0
  70. package/dist/classic-channel-manager.d.ts +10 -5
  71. package/dist/classic-channel-manager.js +12 -9
  72. package/dist/classic-channel-manager.js.map +1 -1
  73. package/dist/cli.js +224 -111
  74. package/dist/cli.js.map +1 -1
  75. package/dist/config-validator.d.ts +6 -0
  76. package/dist/config-validator.js +129 -3
  77. package/dist/config-validator.js.map +1 -1
  78. package/dist/config.d.ts +13 -1
  79. package/dist/config.js +14 -5
  80. package/dist/config.js.map +1 -1
  81. package/dist/cross-instance-notice.d.ts +7 -0
  82. package/dist/cross-instance-notice.js +10 -0
  83. package/dist/cross-instance-notice.js.map +1 -1
  84. package/dist/daemon.d.ts +24 -1
  85. package/dist/daemon.js +151 -19
  86. package/dist/daemon.js.map +1 -1
  87. package/dist/decision-list.d.ts +25 -0
  88. package/dist/decision-list.js +30 -0
  89. package/dist/decision-list.js.map +1 -0
  90. package/dist/delivery-outbox.d.ts +10 -0
  91. package/dist/delivery-outbox.js +17 -1
  92. package/dist/delivery-outbox.js.map +1 -1
  93. package/dist/diagnostic-redaction.d.ts +10 -0
  94. package/dist/diagnostic-redaction.js +167 -0
  95. package/dist/diagnostic-redaction.js.map +1 -0
  96. package/dist/fleet-context.d.ts +10 -0
  97. package/dist/fleet-manager.d.ts +505 -15
  98. package/dist/fleet-manager.js +1870 -204
  99. package/dist/fleet-manager.js.map +1 -1
  100. package/dist/fleet-process-handlers.d.ts +18 -0
  101. package/dist/fleet-process-handlers.js +69 -0
  102. package/dist/fleet-process-handlers.js.map +1 -0
  103. package/dist/fleet-traffic.d.ts +74 -0
  104. package/dist/fleet-traffic.js +196 -0
  105. package/dist/fleet-traffic.js.map +1 -0
  106. package/dist/general-knowledge/skills/bug-report/SKILL.md +31 -0
  107. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +17 -6
  108. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
  109. package/dist/general-knowledge/skills/fleet-config/SKILL.md +2 -0
  110. package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
  111. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
  112. package/dist/general-knowledge/skills/scheduling/SKILL.md +4 -0
  113. package/dist/general-knowledge/skills/web-appearance/SKILL.md +190 -47
  114. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +545 -0
  115. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  116. package/dist/human-delivery-receipts.d.ts +24 -1
  117. package/dist/human-delivery-receipts.js +64 -11
  118. package/dist/human-delivery-receipts.js.map +1 -1
  119. package/dist/instance-config-impact.js +7 -1
  120. package/dist/instance-config-impact.js.map +1 -1
  121. package/dist/instance-lifecycle.d.ts +10 -1
  122. package/dist/instance-lifecycle.js +79 -18
  123. package/dist/instance-lifecycle.js.map +1 -1
  124. package/dist/instance-names.d.ts +4 -0
  125. package/dist/instance-names.js +12 -0
  126. package/dist/instance-names.js.map +1 -0
  127. package/dist/instructions.d.ts +12 -1
  128. package/dist/instructions.js +13 -2
  129. package/dist/instructions.js.map +1 -1
  130. package/dist/locale.js +173 -12
  131. package/dist/locale.js.map +1 -1
  132. package/dist/log-steps.d.ts +1 -0
  133. package/dist/log-steps.js +15 -0
  134. package/dist/log-steps.js.map +1 -1
  135. package/dist/login-controller.d.ts +45 -0
  136. package/dist/login-controller.js +101 -5
  137. package/dist/login-controller.js.map +1 -1
  138. package/dist/ls-attach-resolve.d.ts +96 -0
  139. package/dist/ls-attach-resolve.js +180 -0
  140. package/dist/ls-attach-resolve.js.map +1 -0
  141. package/dist/ls-rows.d.ts +12 -0
  142. package/dist/ls-rows.js +82 -3
  143. package/dist/ls-rows.js.map +1 -1
  144. package/dist/memory-hogs.d.ts +27 -0
  145. package/dist/memory-hogs.js +124 -0
  146. package/dist/memory-hogs.js.map +1 -0
  147. package/dist/needs-you.d.ts +10 -0
  148. package/dist/needs-you.js +2 -1
  149. package/dist/needs-you.js.map +1 -1
  150. package/dist/outbound-handlers.d.ts +11 -0
  151. package/dist/outbound-handlers.js +139 -11
  152. package/dist/outbound-handlers.js.map +1 -1
  153. package/dist/outbound-schemas.d.ts +200 -4
  154. package/dist/outbound-schemas.js +124 -11
  155. package/dist/outbound-schemas.js.map +1 -1
  156. package/dist/public-web-gateway.js +17 -4
  157. package/dist/public-web-gateway.js.map +1 -1
  158. package/dist/public-web-link.d.ts +9 -0
  159. package/dist/public-web-link.js +8 -0
  160. package/dist/public-web-link.js.map +1 -1
  161. package/dist/quickstart-api.d.ts +19 -1
  162. package/dist/quickstart-api.js +94 -4
  163. package/dist/quickstart-api.js.map +1 -1
  164. package/dist/reply-buttons-controller.d.ts +4 -0
  165. package/dist/reply-buttons-controller.js +2 -2
  166. package/dist/reply-buttons-controller.js.map +1 -1
  167. package/dist/restart-progress.d.ts +80 -1
  168. package/dist/restart-progress.js +306 -21
  169. package/dist/restart-progress.js.map +1 -1
  170. package/dist/rollout-index.d.ts +5 -0
  171. package/dist/rollout-index.js +26 -5
  172. package/dist/rollout-index.js.map +1 -1
  173. package/dist/scheduler/db.d.ts +40 -13
  174. package/dist/scheduler/db.js +123 -89
  175. package/dist/scheduler/db.js.map +1 -1
  176. package/dist/scheduler/task-board-text.d.ts +26 -0
  177. package/dist/scheduler/task-board-text.js +135 -0
  178. package/dist/scheduler/task-board-text.js.map +1 -0
  179. package/dist/scheduler/task-board.d.ts +650 -0
  180. package/dist/scheduler/task-board.js +1938 -0
  181. package/dist/scheduler/task-board.js.map +1 -0
  182. package/dist/scheduler/types.d.ts +6 -1
  183. package/dist/search/search-docs.d.ts +58 -0
  184. package/dist/search/search-docs.js +63 -0
  185. package/dist/search/search-docs.js.map +1 -0
  186. package/dist/search/search-index.d.ts +141 -0
  187. package/dist/search/search-index.js +440 -0
  188. package/dist/search/search-index.js.map +1 -0
  189. package/dist/search/search-store.d.ts +122 -0
  190. package/dist/search/search-store.js +236 -0
  191. package/dist/search/search-store.js.map +1 -0
  192. package/dist/search/search-worker.d.ts +1 -0
  193. package/dist/search/search-worker.js +163 -0
  194. package/dist/search/search-worker.js.map +1 -0
  195. package/dist/service-installer.js +12 -0
  196. package/dist/service-installer.js.map +1 -1
  197. package/dist/settings-api.d.ts +23 -0
  198. package/dist/settings-api.js +164 -3
  199. package/dist/settings-api.js.map +1 -1
  200. package/dist/settings-change.js +104 -0
  201. package/dist/settings-change.js.map +1 -1
  202. package/dist/settings-confirmation.d.ts +3 -1
  203. package/dist/settings-confirmation.js +2 -0
  204. package/dist/settings-confirmation.js.map +1 -1
  205. package/dist/settings-effect.js +34 -3
  206. package/dist/settings-effect.js.map +1 -1
  207. package/dist/settings-http-confirmation.d.ts +11 -0
  208. package/dist/settings-http-confirmation.js +10 -0
  209. package/dist/settings-http-confirmation.js.map +1 -1
  210. package/dist/setup-form.d.ts +1 -1
  211. package/dist/setup-form.js +23 -7
  212. package/dist/setup-form.js.map +1 -1
  213. package/dist/spawn-gate.d.ts +1 -0
  214. package/dist/spawn-gate.js +13 -3
  215. package/dist/spawn-gate.js.map +1 -1
  216. package/dist/task-board-tool.d.ts +25 -0
  217. package/dist/task-board-tool.js +154 -0
  218. package/dist/task-board-tool.js.map +1 -0
  219. package/dist/task-board-waker.d.ts +44 -0
  220. package/dist/task-board-waker.js +128 -0
  221. package/dist/task-board-waker.js.map +1 -0
  222. package/dist/tmux-exec-helper.d.ts +94 -0
  223. package/dist/tmux-exec-helper.js +407 -0
  224. package/dist/tmux-exec-helper.js.map +1 -0
  225. package/dist/tmux-manager.d.ts +5 -0
  226. package/dist/tmux-manager.js +42 -11
  227. package/dist/tmux-manager.js.map +1 -1
  228. package/dist/tool-permissions.d.ts +0 -7
  229. package/dist/tool-permissions.js +44 -5
  230. package/dist/tool-permissions.js.map +1 -1
  231. package/dist/topic-commands.d.ts +17 -1
  232. package/dist/topic-commands.js +45 -19
  233. package/dist/topic-commands.js.map +1 -1
  234. package/dist/tunnel/cloudflared.js +37 -2
  235. package/dist/tunnel/cloudflared.js.map +1 -1
  236. package/dist/tunnel/types.d.ts +3 -1
  237. package/dist/tunnel/types.js.map +1 -1
  238. package/dist/types.d.ts +63 -1
  239. package/dist/ui/app-effects.js +90 -32
  240. package/dist/ui/backend-signin.js +297 -0
  241. package/dist/ui/chat-ambient.js +307 -0
  242. package/dist/ui/chat-effect-posters.js +8 -8
  243. package/dist/ui/chat-effects.js +110 -15
  244. package/dist/ui/chat-msg-actions.js +176 -0
  245. package/dist/ui/chat-render.js +21 -3
  246. package/dist/ui/chat-store.js +64 -12
  247. package/dist/ui/chat-strings.js +85 -8
  248. package/dist/ui/chat-thread.js +204 -21
  249. package/dist/ui/effect-trial-controller.js +13 -8
  250. package/dist/ui/effect-trial-panel.js +19 -16
  251. package/dist/ui/effects-waker.js +70 -0
  252. package/dist/ui/fleet-canvas-layout.js +117 -0
  253. package/dist/ui/fleet-canvas.js +555 -0
  254. package/dist/ui/fleet-org.js +27 -12
  255. package/dist/ui/fleet-route-scene.js +207 -0
  256. package/dist/ui/fleet-route-worker.js +21 -0
  257. package/dist/ui/fleet-route.js +88 -0
  258. package/dist/ui/fleet-task-card.js +436 -0
  259. package/dist/ui/fleet-task-common.js +448 -0
  260. package/dist/ui/fleet-tasks.js +535 -0
  261. package/dist/ui/fleet-traffic.js +359 -0
  262. package/dist/ui/fleet-usage.js +435 -0
  263. package/dist/ui/panel-chat.js +264 -66
  264. package/dist/ui/panel-details.js +2 -1
  265. package/dist/ui/panel-fleet.js +65 -83
  266. package/dist/ui/panel-needs.js +4 -2
  267. package/dist/ui/panel-settings.js +56 -11
  268. package/dist/ui/preview.js +3 -2
  269. package/dist/ui/search-dialog.js +349 -0
  270. package/dist/ui/settings-appearance.js +44 -19
  271. package/dist/ui/settings-apply.js +3 -3
  272. package/dist/ui/settings-catalog.js +92 -0
  273. package/dist/ui/settings-confirm.js +7 -2
  274. package/dist/ui/settings-dialogs.js +40 -12
  275. package/dist/ui/settings-effects.js +4 -3
  276. package/dist/ui/settings-guests.js +117 -0
  277. package/dist/ui/settings-learn.js +25 -0
  278. package/dist/ui/settings-model.js +15 -6
  279. package/dist/ui/settings-mods.js +23 -9
  280. package/dist/ui/settings-owner-effects.js +15 -13
  281. package/dist/ui/settings-strings.js +122 -42
  282. package/dist/ui/settings-web-authority.js +91 -0
  283. package/dist/ui/shared/agend-auth.js +1 -1
  284. package/dist/ui/shared/app-i18n.js +11 -2
  285. package/dist/ui/shared/app-prefs.js +17 -4
  286. package/dist/ui/shared/app-route.js +19 -1
  287. package/dist/ui/shared/app-session.js +2 -0
  288. package/dist/ui/shared/app-shell.js +174 -30
  289. package/dist/ui/shared/app-store.js +26 -1
  290. package/dist/ui/shared/app-stream.js +5 -1
  291. package/dist/ui/shared/app-themes.js +140 -7
  292. package/dist/ui/shared/app.css +704 -20
  293. package/dist/ui/shared/app.js +26 -15
  294. package/dist/ui/shared/card-strings.js +8 -0
  295. package/dist/ui/shared/effect-app-runtime.js +68 -19
  296. package/dist/ui/shared/effect-chat-runtime.js +16 -3
  297. package/dist/ui/shared/effect-frame.js +26 -7
  298. package/dist/ui/shared/effect-geometry.js +24 -9
  299. package/dist/ui/shared/effect-owner-source.js +8 -1
  300. package/dist/ui/shared/effect-policy.js +21 -7
  301. package/dist/ui/shared/effect-profiler.js +31 -9
  302. package/dist/ui/shared/effect-registry.js +97 -50
  303. package/dist/ui/shared/effect-stock-snapshot.js +9 -0
  304. package/dist/ui/shared/effect-stock-trust.js +62 -10
  305. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  306. package/dist/ui/shared/effect-trial-source.js +1 -1
  307. package/dist/ui/shared/effect-trial-stage.js +54 -31
  308. package/dist/ui/shared/effect-trial.css +40 -0
  309. package/dist/ui/shared/fx-motion.js +67 -0
  310. package/dist/ui/shared/identity-card.js +100 -11
  311. package/dist/ui/shared/identity-color.js +14 -10
  312. package/dist/ui/shared/instance-nav.js +179 -23
  313. package/dist/ui/shared/owner-css.js +30 -1
  314. package/dist/ui/shared/panel-view.js +24 -4
  315. package/dist/ui/shared/tokens.css +9 -3
  316. package/dist/ui/shared/ui-icons.js +2 -0
  317. package/dist/ui/shared/ui-menu.js +20 -7
  318. package/dist/ui/shared/ui-toast.js +16 -3
  319. package/dist/ui/shared/usage-dialog.js +34 -3
  320. package/dist/ui/shared/view-strings.js +72 -0
  321. package/dist/ui/shared/web-authorize.js +198 -0
  322. package/dist/ui/web-terminal/terminal.css +4 -4
  323. package/dist/ui/web-terminal/terminal.html +1 -1
  324. package/dist/update-launch.d.ts +9 -0
  325. package/dist/update-launch.js +38 -12
  326. package/dist/update-launch.js.map +1 -1
  327. package/dist/usage/format-rich.js +6 -3
  328. package/dist/usage/format-rich.js.map +1 -1
  329. package/dist/usage/i18n-keys.d.ts +1 -1
  330. package/dist/usage/i18n-keys.js +2 -1
  331. package/dist/usage/i18n-keys.js.map +1 -1
  332. package/dist/usage/i18n.d.ts +13 -1
  333. package/dist/usage/i18n.js +29 -0
  334. package/dist/usage/i18n.js.map +1 -1
  335. package/dist/usage/providers.d.ts +81 -2
  336. package/dist/usage/providers.js +240 -38
  337. package/dist/usage/providers.js.map +1 -1
  338. package/dist/usage/usage-api.d.ts +5 -1
  339. package/dist/usage/usage-api.js +184 -58
  340. package/dist/usage/usage-api.js.map +1 -1
  341. package/dist/usage-report.d.ts +158 -0
  342. package/dist/usage-report.js +226 -0
  343. package/dist/usage-report.js.map +1 -0
  344. package/dist/usage-rollup.d.ts +51 -0
  345. package/dist/usage-rollup.js +168 -0
  346. package/dist/usage-rollup.js.map +1 -0
  347. package/dist/user-systemd.d.ts +24 -0
  348. package/dist/user-systemd.js +75 -0
  349. package/dist/user-systemd.js.map +1 -0
  350. package/dist/view-api.d.ts +39 -0
  351. package/dist/view-api.js +69 -6
  352. package/dist/view-api.js.map +1 -1
  353. package/dist/web-ambient.d.ts +72 -0
  354. package/dist/web-ambient.js +161 -0
  355. package/dist/web-ambient.js.map +1 -0
  356. package/dist/web-api.d.ts +98 -14
  357. package/dist/web-api.js +1027 -83
  358. package/dist/web-api.js.map +1 -1
  359. package/dist/web-appearance-policy.d.ts +4 -6
  360. package/dist/web-appearance-policy.js +5 -12
  361. package/dist/web-appearance-policy.js.map +1 -1
  362. package/dist/web-appearance-tools.d.ts +3 -0
  363. package/dist/web-appearance-tools.js +22 -3
  364. package/dist/web-appearance-tools.js.map +1 -1
  365. package/dist/web-appearance-view.d.ts +27 -2
  366. package/dist/web-appearance-view.js +129 -14
  367. package/dist/web-appearance-view.js.map +1 -1
  368. package/dist/web-appearance.d.ts +17 -3
  369. package/dist/web-appearance.js +85 -8
  370. package/dist/web-appearance.js.map +1 -1
  371. package/dist/web-asset-parts.d.ts +2 -2
  372. package/dist/web-asset-parts.js +3 -3
  373. package/dist/web-asset-parts.js.map +1 -1
  374. package/dist/web-auth.js +3 -1
  375. package/dist/web-auth.js.map +1 -1
  376. package/dist/web-backend-login.d.ts +130 -0
  377. package/dist/web-backend-login.js +225 -0
  378. package/dist/web-backend-login.js.map +1 -0
  379. package/dist/web-card.d.ts +14 -0
  380. package/dist/web-card.js +40 -6
  381. package/dist/web-card.js.map +1 -1
  382. package/dist/web-channel-echo.d.ts +7 -1
  383. package/dist/web-channel-echo.js +6 -2
  384. package/dist/web-channel-echo.js.map +1 -1
  385. package/dist/web-chat-history.d.ts +83 -0
  386. package/dist/web-chat-history.js +137 -5
  387. package/dist/web-chat-history.js.map +1 -1
  388. package/dist/web-commands.d.ts +6 -1
  389. package/dist/web-commands.js +1 -1
  390. package/dist/web-commands.js.map +1 -1
  391. package/dist/web-effect-bootstrap.d.ts +12 -0
  392. package/dist/web-effect-bootstrap.js +49 -0
  393. package/dist/web-effect-bootstrap.js.map +1 -0
  394. package/dist/web-effect-bundle.js +46 -1
  395. package/dist/web-effect-bundle.js.map +1 -1
  396. package/dist/web-effect-playback.d.ts +30 -0
  397. package/dist/web-effect-playback.js +71 -0
  398. package/dist/web-effect-playback.js.map +1 -0
  399. package/dist/web-effect-profiles.d.ts +4 -4
  400. package/dist/web-effect-service.d.ts +11 -4
  401. package/dist/web-effect-service.js +25 -4
  402. package/dist/web-effect-service.js.map +1 -1
  403. package/dist/web-effect-tools.d.ts +1 -0
  404. package/dist/web-effect-tools.js +4 -4
  405. package/dist/web-effect-tools.js.map +1 -1
  406. package/dist/web-effect-trials.d.ts +2 -2
  407. package/dist/web-effect-trials.js +3 -3
  408. package/dist/web-effect-trials.js.map +1 -1
  409. package/dist/web-effect-upload.js +1 -1
  410. package/dist/web-effect-upload.js.map +1 -1
  411. package/dist/web-effect-worker.d.ts +1 -1
  412. package/dist/web-effect-worker.js +2 -2
  413. package/dist/web-effect-worker.js.map +1 -1
  414. package/dist/web-effect.d.ts +1 -1
  415. package/dist/web-guest-access.d.ts +126 -0
  416. package/dist/web-guest-access.js +238 -0
  417. package/dist/web-guest-access.js.map +1 -0
  418. package/dist/web-guest-rooms.d.ts +82 -0
  419. package/dist/web-guest-rooms.js +135 -0
  420. package/dist/web-guest-rooms.js.map +1 -0
  421. package/dist/web-host-guard.d.ts +11 -0
  422. package/dist/web-host-guard.js +11 -0
  423. package/dist/web-host-guard.js.map +1 -1
  424. package/dist/web-identity-color.d.ts +1 -1
  425. package/dist/web-identity-color.js +2 -2
  426. package/dist/web-identity-color.js.map +1 -1
  427. package/dist/web-identity-palette.d.ts +1 -1
  428. package/dist/web-identity-palette.js +2 -2
  429. package/dist/web-identity-palette.js.map +1 -1
  430. package/dist/web-image-meta.d.ts +13 -0
  431. package/dist/web-image-meta.js +62 -0
  432. package/dist/web-image-meta.js.map +1 -1
  433. package/dist/web-libavoid.d.ts +14 -0
  434. package/dist/web-libavoid.js +46 -0
  435. package/dist/web-libavoid.js.map +1 -0
  436. package/dist/web-login.d.ts +47 -0
  437. package/dist/web-login.js +115 -0
  438. package/dist/web-login.js.map +1 -1
  439. package/dist/web-mod-agent.d.ts +1 -1
  440. package/dist/web-mod-agent.js +7 -5
  441. package/dist/web-mod-agent.js.map +1 -1
  442. package/dist/web-mod-bundle.d.ts +9 -1
  443. package/dist/web-mod-bundle.js +62 -3
  444. package/dist/web-mod-bundle.js.map +1 -1
  445. package/dist/web-mod-export.d.ts +9 -0
  446. package/dist/web-mod-export.js +21 -0
  447. package/dist/web-mod-export.js.map +1 -1
  448. package/dist/web-mod-store.d.ts +7 -1
  449. package/dist/web-mod-store.js +19 -6
  450. package/dist/web-mod-store.js.map +1 -1
  451. package/dist/web-owner-css-store.d.ts +44 -1
  452. package/dist/web-owner-css-store.js +70 -9
  453. package/dist/web-owner-css-store.js.map +1 -1
  454. package/dist/web-prefs.d.ts +14 -0
  455. package/dist/web-prefs.js +37 -4
  456. package/dist/web-prefs.js.map +1 -1
  457. package/dist/web-preview.d.ts +6 -2
  458. package/dist/web-preview.js +4 -2
  459. package/dist/web-preview.js.map +1 -1
  460. package/dist/web-returning-device.d.ts +41 -0
  461. package/dist/web-returning-device.js +57 -5
  462. package/dist/web-returning-device.js.map +1 -1
  463. package/dist/web-session-authorization.d.ts +132 -0
  464. package/dist/web-session-authorization.js +193 -0
  465. package/dist/web-session-authorization.js.map +1 -0
  466. package/dist/web-session.d.ts +70 -10
  467. package/dist/web-session.js +137 -22
  468. package/dist/web-session.js.map +1 -1
  469. package/dist/web-settings-patch.d.ts +49 -2
  470. package/dist/web-settings-patch.js +145 -1
  471. package/dist/web-settings-patch.js.map +1 -1
  472. package/dist/web-settings-tools.d.ts +94 -0
  473. package/dist/web-settings-tools.js +301 -0
  474. package/dist/web-settings-tools.js.map +1 -0
  475. package/dist/web-shell-routes.d.ts +1 -1
  476. package/dist/web-shell-routes.js +1 -1
  477. package/dist/web-shell-routes.js.map +1 -1
  478. package/dist/web-terminal-http.js +15 -2
  479. package/dist/web-terminal-http.js.map +1 -1
  480. package/dist/web-theme-store.d.ts +39 -0
  481. package/dist/web-theme-store.js +122 -11
  482. package/dist/web-theme-store.js.map +1 -1
  483. package/dist/web-theme.d.ts +31 -0
  484. package/dist/web-theme.js +99 -2
  485. package/dist/web-theme.js.map +1 -1
  486. package/dist/web-upload.d.ts +13 -0
  487. package/dist/web-upload.js +39 -3
  488. package/dist/web-upload.js.map +1 -1
  489. package/dist/windows-shortcuts.d.ts +78 -0
  490. package/dist/windows-shortcuts.js +203 -0
  491. package/dist/windows-shortcuts.js.map +1 -0
  492. package/dist/workflow-templates/default.md +4 -1
  493. package/dist/yaml-shared-nodes.d.ts +25 -0
  494. package/dist/yaml-shared-nodes.js +185 -0
  495. package/dist/yaml-shared-nodes.js.map +1 -0
  496. package/launcher/agend-install.cjs +553 -0
  497. package/launcher/install-admission.cjs +4 -39
  498. package/launcher/postinstall.cjs +14 -12
  499. package/package.json +4 -2
  500. package/scripts/preinstall-guard.cjs +2 -5
  501. package/templates/windows-shortcuts.ps1 +181 -0
  502. package/launcher/old-updater-note.cjs +0 -48
@@ -0,0 +1,307 @@
1
+ // #2117: an agent's ambient effect on its own chat — falling petals, rising lanterns, twinkles, stars on a click. The
2
+ // agent sends data only (web-ambient.ts checks it; this checks the bounds again); the page draws it with plain nodes
3
+ // animated by the Web Animations API: transform and opacity only, positions through the CSSOM (no style attribute,
4
+ // #1300), every node aria-hidden and pointer-events: none. The looping layer sits behind the thread (.amb-layer, under
5
+ // the scroller and the dock); a burst shows over the messages for a moment (.amb-burst) and never takes the click.
6
+ //
7
+ // Its own gates (not the Canvas Worker runtime's, #2115): the viewer's "Show agents' themes, effects and widgets", safe
8
+ // mode, Web Animations support, and reduced motion only when this device chose to follow the system's setting
9
+ // (fx-motion.js; the user's direction: effects play by default). A hidden tab pauses. Nothing else gates it: it runs
10
+ // no agent code and fetches nothing but its own glyph pictures (fleet decision: no gate without a threat it stops).
11
+ // Caps: 24 looping particles, bursts of at most 16, at most 3 bursts alive — 72 nodes and animations per chat.
12
+ import { html, useEffect, useRef, useState } from "/assets/app-html.js";
13
+ import { t } from "/assets/app-i18n.js";
14
+ import { useStore } from "/assets/app-store.js";
15
+ import { onPrefsChanged } from "/assets/app-prefs.js";
16
+ import { safeMode } from "/assets/owner-css.js";
17
+ import { setThemesShown, themeImageUrl, themesShown, themesStore } from "/assets/app-themes.js";
18
+ import { markMotionNoteShown, motionBlocked, motionNoteDue, onMotionChange, setFollowsReducedMotion } from "/assets/fx-motion.js";
19
+ import "./chat-strings.js";
20
+
21
+ export const AMBIENT_CAPS = { loop: 24, burst: 16, bursts: 3 };
22
+ const LOOP_PRESETS = ["fall", "rise", "twinkle"];
23
+ const RULES = {
24
+ fall: { count: [1, 24], duration: [4, 30] }, rise: { count: [1, 24], duration: [4, 30] },
25
+ twinkle: { count: [1, 24], duration: [1, 10] }, "burst-on-click": { count: [4, 16], duration: [0.3, 2] },
26
+ };
27
+ const COLOR = /^#[0-9a-f]{6}$/;
28
+ const ASSET = /^[0-9a-f]{32}$/;
29
+ const own = (o, k) => !!o && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
30
+ const num = (v, lo, hi) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi;
31
+ const pair = (v, lo, hi) => Array.isArray(v) && v.length === 2 && num(v[0], lo, hi) && num(v[1], lo, hi) && v[0] <= v[1];
32
+
33
+ /** A spec as the host keeps it, checked again; null when anything is off (it is then not drawn at all). */
34
+ export function sanitizeAmbient(spec) {
35
+ if (!spec || typeof spec !== "object" || !own(RULES, spec.preset)) return null;
36
+ const r = RULES[spec.preset];
37
+ if (!Array.isArray(spec.glyphs) || spec.glyphs.length < 1 || spec.glyphs.length > 4) return null;
38
+ const glyphs = [];
39
+ for (const g of spec.glyphs) {
40
+ if (typeof g === "string" && g.length >= 1 && g.length <= 32) glyphs.push(g);
41
+ else if (g && typeof g === "object" && typeof g.asset === "string" && ASSET.test(g.asset)) glyphs.push({ asset: g.asset });
42
+ else return null;
43
+ }
44
+ const colors = Array.isArray(spec.colors) ? spec.colors.filter((c) => typeof c === "string" && COLOR.test(c)).slice(0, 5) : [];
45
+ if (!Number.isInteger(spec.count) || !num(spec.count, r.count[0], r.count[1])) return null;
46
+ if (!pair(spec.duration, r.duration[0], r.duration[1]) || !pair(spec.size, 10, 40) || !pair(spec.spread, 20, 120)) return null;
47
+ if (!num(spec.sway, 0, 80) || !num(spec.rotate, 0, 720)) return null;
48
+ if (!Array.isArray(spec.opacity) || spec.opacity.length !== 3 || !spec.opacity.every((o) => num(o, 0, 1))) return null;
49
+ return { preset: spec.preset, glyphs, colors, count: spec.count, duration: spec.duration, size: spec.size, sway: spec.sway, spread: spec.spread, rotate: spec.rotate, opacity: spec.opacity };
50
+ }
51
+
52
+ /** `name`'s ambients from GET /ui/themes, checked: { loop, click } (either null). Null when it has none. */
53
+ export function ambientFor(name) {
54
+ const a = themesStore.get().ambient;
55
+ if (!a || typeof a !== "object" || !own(a, name)) return null;
56
+ const mine = a[name];
57
+ const loop = own(mine, "loop") ? sanitizeAmbient(mine.loop && mine.loop.spec) : null;
58
+ const click = own(mine, "click") ? sanitizeAmbient(mine.click && mine.click.spec) : null;
59
+ if (loop && !LOOP_PRESETS.includes(loop.preset)) return null;
60
+ if (click && click.preset !== "burst-on-click") return null;
61
+ return loop || click ? { loop, click } : null;
62
+ }
63
+
64
+ /** Why this page does not draw ambients now, or null. */
65
+ export function ambientBlock(win = globalThis) {
66
+ if (safeMode()) return "safe";
67
+ if (!themesShown()) return "hidden";
68
+ if (motionBlocked(win)) return "reducedMotion";
69
+ const El = win.Element;
70
+ if (!El || !El.prototype || typeof El.prototype.animate !== "function") return "unsupported";
71
+ return null;
72
+ }
73
+
74
+ /** The gate as state: moves with the switches, the themes and the device's reduced-motion answer. */
75
+ function useAmbientBlock() {
76
+ useStore(themesStore);
77
+ const [, bump] = useState(0);
78
+ useEffect(() => {
79
+ const again = () => bump((n) => n + 1);
80
+ const offPrefs = onPrefsChanged((changed) => { if (changed.has("fx.show")) again(); });
81
+ const offMotion = onMotionChange(again);
82
+ return () => { if (typeof offPrefs === "function") offPrefs(); offMotion(); };
83
+ }, []);
84
+ return ambientBlock();
85
+ }
86
+
87
+ const rand = (lo, hi) => lo + Math.random() * (hi - lo);
88
+ const pick = (list) => list[Math.floor(Math.random() * list.length)];
89
+
90
+ /** One particle node: a text glyph (its colour, else the accent) or a glyph picture. */
91
+ function particle(doc, name, spec, cls) {
92
+ const g = pick(spec.glyphs), size = Math.round(rand(spec.size[0], spec.size[1]));
93
+ let el;
94
+ if (typeof g === "string") {
95
+ el = doc.createElement("span");
96
+ el.textContent = g;
97
+ el.style.setProperty("font-size", `${size}px`);
98
+ if (spec.colors.length) el.style.setProperty("color", pick(spec.colors));
99
+ } else {
100
+ el = doc.createElement("img");
101
+ el.src = themeImageUrl(name, g.asset);
102
+ el.alt = "";
103
+ el.decoding = "async";
104
+ el.draggable = false;
105
+ el.style.setProperty("width", `${size}px`);
106
+ el.style.setProperty("height", `${size}px`);
107
+ }
108
+ el.className = cls;
109
+ el.setAttribute("aria-hidden", "true");
110
+ return { el, size };
111
+ }
112
+
113
+ /**
114
+ * Draw `ambient` ({ loop, click }) for `name` into `layer` (behind) and `front` (bursts), listening on `surface` for
115
+ * clicks. Returns { stop() }: every node and animation it made goes, every listener it added is removed.
116
+ */
117
+ export function startAmbient({ name, ambient, layer, front, surface, doc = globalThis.document }) {
118
+ const anims = new Set();
119
+ const loopNodes = [];
120
+ let bursts = 0, stopped = false;
121
+ const track = (a) => { anims.add(a); if (doc.visibilityState === "hidden") a.pause(); return a; };
122
+
123
+ const drawLoop = () => {
124
+ for (const n of loopNodes.splice(0)) n.remove();
125
+ for (const a of [...anims]) if (a._amb === "loop") { a.cancel(); anims.delete(a); }
126
+ const spec = ambient.loop;
127
+ if (!spec) return;
128
+ const h = layer.clientHeight || 600, w = layer.clientWidth || 360;
129
+ const [o0, o1, o2] = spec.opacity;
130
+ for (let i = 0; i < Math.min(spec.count, AMBIENT_CAPS.loop); i++) {
131
+ const { el, size } = particle(doc, name, spec, "amb-p");
132
+ const dur = rand(spec.duration[0], spec.duration[1]) * 1000;
133
+ const turn = spec.rotate * (Math.random() < 0.5 ? -1 : 1);
134
+ let frames;
135
+ if (spec.preset === "twinkle") {
136
+ el.style.setProperty("left", `${rand(0, Math.max(0, w - size))}px`);
137
+ el.style.setProperty("top", `${rand(0, Math.max(0, h - size))}px`);
138
+ frames = [
139
+ { transform: "scale(0.6) rotate(0deg)", opacity: o0 },
140
+ { transform: `scale(1) rotate(${turn / 2}deg)`, opacity: o1, offset: 0.5 },
141
+ { transform: `scale(0.6) rotate(${turn}deg)`, opacity: o2 },
142
+ ];
143
+ } else {
144
+ el.style.setProperty("left", `${rand(0, Math.max(0, w - size))}px`);
145
+ el.style.setProperty("top", "0px");
146
+ const dx = rand(-spec.sway, spec.sway), down = spec.preset === "fall";
147
+ const from = down ? -size - 4 : h + 4, to = down ? h + 4 : -size - 4;
148
+ frames = [
149
+ { transform: `translate(0px, ${from}px) rotate(0deg)`, opacity: o0 },
150
+ { transform: `translate(${dx * 0.6}px, ${from + (to - from) * 0.15}px) rotate(${turn * 0.15}deg)`, opacity: o1, offset: 0.15 },
151
+ { transform: `translate(${-dx * 0.4}px, ${from + (to - from) * 0.6}px) rotate(${turn * 0.6}deg)`, opacity: (o1 + o2) / 2, offset: 0.6 },
152
+ { transform: `translate(${dx}px, ${to}px) rotate(${turn}deg)`, opacity: o2 },
153
+ ];
154
+ }
155
+ layer.appendChild(el);
156
+ loopNodes.push(el);
157
+ // A negative delay starts each one part-way through, so the layer is already full when it appears.
158
+ const a = el.animate(frames, { duration: dur, delay: -Math.random() * dur, iterations: Infinity, easing: "linear" });
159
+ a._amb = "loop";
160
+ track(a);
161
+ }
162
+ };
163
+
164
+ const burst = (x, y) => {
165
+ const spec = ambient.click;
166
+ if (!spec || bursts >= AMBIENT_CAPS.bursts || stopped) return;
167
+ bursts++;
168
+ const [, o1, o2] = spec.opacity;
169
+ let left = Math.min(spec.count, AMBIENT_CAPS.burst);
170
+ const done = (el, a) => { el.remove(); anims.delete(a); if (--left === 0) bursts--; };
171
+ for (let i = 0, n = left; i < n; i++) {
172
+ const { el, size } = particle(doc, name, spec, "amb-p");
173
+ el.style.setProperty("left", `${x - size / 2}px`);
174
+ el.style.setProperty("top", `${y - size / 2}px`);
175
+ const angle = (i / n) * Math.PI * 2 + rand(-0.3, 0.3), dist = rand(spec.spread[0], spec.spread[1]);
176
+ const turn = rand(-spec.rotate, spec.rotate) / 2;
177
+ front.appendChild(el);
178
+ const a = el.animate([
179
+ { transform: "translate(0px, 0px) scale(0.4) rotate(0deg)", opacity: o1 },
180
+ { transform: `translate(${Math.cos(angle) * dist}px, ${Math.sin(angle) * dist}px) scale(1) rotate(${turn}deg)`, opacity: Math.min(o2, o1) },
181
+ ], { duration: rand(spec.duration[0], spec.duration[1]) * 1000, easing: "cubic-bezier(.2,.7,.3,1)", fill: "forwards" });
182
+ a._amb = "burst";
183
+ track(a);
184
+ let gone = false;
185
+ const finish = () => { if (!gone) { gone = true; done(el, a); } };
186
+ a.onfinish = finish; a.oncancel = finish;
187
+ }
188
+ };
189
+
190
+ // Passive and never cancelling: the click still lands on what is under it. Only in the thread (not the composer),
191
+ // and not on a menu or dialog open over it: stars are for the conversation.
192
+ const onDown = (ev) => {
193
+ if (!ambient.click || ev.button > 0) return;
194
+ const target = ev.target;
195
+ if (!target || typeof target.closest !== "function" || !target.closest(".scroller") || target.closest("[role=menu], [role=dialog]")) return;
196
+ const box = front.getBoundingClientRect();
197
+ burst(ev.clientX - box.left, ev.clientY - box.top);
198
+ };
199
+ const onVisible = () => { for (const a of anims) { if (doc.visibilityState === "hidden") a.pause(); else a.play(); } };
200
+ // Size moves (a rotated phone, the preview panel opening): the loop is drawn again for the new box, a moment later.
201
+ let resizeTimer = null, lastW = layer.clientWidth, lastH = layer.clientHeight;
202
+ const ro = typeof globalThis.ResizeObserver === "function" ? new globalThis.ResizeObserver(() => {
203
+ if (layer.clientWidth === lastW && layer.clientHeight === lastH) return;
204
+ lastW = layer.clientWidth; lastH = layer.clientHeight;
205
+ clearTimeout(resizeTimer);
206
+ resizeTimer = setTimeout(() => { if (!stopped) drawLoop(); }, 200);
207
+ }) : null;
208
+
209
+ drawLoop();
210
+ if (ambient.click) surface.addEventListener("pointerdown", onDown, { passive: true });
211
+ doc.addEventListener("visibilitychange", onVisible);
212
+ if (ro && ambient.loop) ro.observe(layer);
213
+
214
+ return {
215
+ stop() {
216
+ stopped = true;
217
+ clearTimeout(resizeTimer);
218
+ if (ro) ro.disconnect();
219
+ surface.removeEventListener("pointerdown", onDown, { passive: true });
220
+ doc.removeEventListener("visibilitychange", onVisible);
221
+ for (const a of anims) a.cancel();
222
+ anims.clear();
223
+ for (const n of loopNodes.splice(0)) n.remove();
224
+ while (front.firstChild) front.firstChild.remove();
225
+ },
226
+ /** For tests and the smoke: what is alive now. */
227
+ stats() { return { nodes: layer.children.length + front.children.length, animations: anims.size, bursts }; },
228
+ };
229
+ }
230
+
231
+ /**
232
+ * The chat's ambient: renders its two layers inside .chat-main (`surface`) and runs them. `part` is "layer" (behind,
233
+ * right after the theme picture) or "front" (after the dock); the "layer" instance owns the run and finds its sibling.
234
+ */
235
+ export function ChatAmbient({ name, surface, part }) {
236
+ const block = useAmbientBlock();
237
+ const ambient = block ? null : ambientFor(name);
238
+ const key = ambient ? JSON.stringify(ambient) : "";
239
+ const ref = useRef(null);
240
+ useEffect(() => {
241
+ if (part !== "layer" || !ambient) return undefined;
242
+ const layer = ref.current, host = surface.current;
243
+ const front = host && host.querySelector(".amb-burst");
244
+ if (!layer || !host || !front) return undefined;
245
+ host.classList.add("has-amb");
246
+ const run = startAmbient({ name, ambient, layer, front, surface: host });
247
+ if (typeof globalThis !== "undefined") globalThis.__agendAmbient = run; // the smoke reads stats(); nothing else
248
+ return () => { run.stop(); host.classList.remove("has-amb"); if (globalThis.__agendAmbient === run) delete globalThis.__agendAmbient; };
249
+ }, [name, key, part]);
250
+ if (!ambient) return null;
251
+ return part === "layer" ? html`<div ref=${ref} class="amb-layer" aria-hidden="true"></div>` : html`<div class="amb-burst" aria-hidden="true"></div>`;
252
+ }
253
+
254
+ /**
255
+ * The one-time note (the user's direction): the system asks for reduced motion and this chat's ambient or Canvas Worker plays anyway —
256
+ * say so once on this device, and turn it off right here (this device's switch: the same on the public link, where
257
+ * Settings is not). Shown only while an ambient or Canvas Worker is playing.
258
+ */
259
+ export function AmbientMotionNote({ name, canvasPlaying = false }) {
260
+ const block = useAmbientBlock();
261
+ const shown = canvasPlaying || !block && !!ambientFor(name);
262
+ const noteDue = motionNoteDue();
263
+ const [open, setOpen] = useState(false);
264
+ useEffect(() => {
265
+ if (!shown || open || !noteDue) return;
266
+ markMotionNoteShown();
267
+ setOpen(true);
268
+ }, [shown, noteDue]);
269
+ if (!open || !shown) return null;
270
+ const off = () => { setOpen(false); setFollowsReducedMotion(true); };
271
+ return html`<div class="amb-note" data-trust="thread-control" role="status">
272
+ <span>${t("chat.ambientMotionNote")}</span>
273
+ <button type="button" class="amb-note-off" onClick=${off}>${t("chat.ambientMotionNoteLink")}</button>
274
+ <button type="button" class="amb-note-x" aria-label=${t("chat.ambientMotionNoteClose")} onClick=${() => setOpen(false)}>×</button>
275
+ </div>`;
276
+ }
277
+
278
+ /**
279
+ * The fix for a block, done right here: both switches are this viewer's own (the show switch is a local choice on the
280
+ * public link; the reduced-motion one is per device), so it works on the public link too; safe mode is the URL. Both
281
+ * stay in Settings → Web appearance as well.
282
+ */
283
+ const FIX = {
284
+ hidden: () => setThemesShown(true),
285
+ reducedMotion: () => setFollowsReducedMotion(false),
286
+ safe: () => { const u = new URL(globalThis.location.href); u.searchParams.delete("safe"); globalThis.location.assign(u.toString()); },
287
+ };
288
+
289
+ /**
290
+ * The chat ⋯ entries for `name`'s ambient: why it is not showing here (a visible reason; choosing it fixes it when the
291
+ * page can), and — for an admin, never over the public link — Clear for each kind.
292
+ */
293
+ export function ambientMenuItems(name, { publicLink, clear }) {
294
+ const a = themesStore.get().ambient;
295
+ const mine = a && own(a, name) ? a[name] : null;
296
+ if (!mine || typeof mine !== "object" || !(own(mine, "loop") || own(mine, "click"))) return [];
297
+ const items = [];
298
+ const block = ambientBlock();
299
+ if (block) {
300
+ items.push({
301
+ key: "ambient-blocked", label: t(`chat.ambientBlocked_${block}`), icon: "info",
302
+ onSelect: () => { if (own(FIX, block)) FIX[block](); },
303
+ });
304
+ }
305
+ if (!publicLink) for (const k of ["loop", "click"]) if (own(mine, k)) items.push({ key: `ambient-clear-${k}`, label: t(k === "loop" ? "chat.ambientClearLoop" : "chat.ambientClearClick"), icon: "trash", onSelect: () => clear(name, k) });
306
+ return items;
307
+ }
@@ -1,10 +1,12 @@
1
1
  /** #1629: static Chat fallback. It requires no preview origin, device consent, Worker or personal identity. */
2
2
  import { useLayoutEffect } from "/assets/app-html.js";
3
3
  import { appStore } from "/assets/app-store.js";
4
- import { onPrefsChanged, prefInForce } from "/assets/app-prefs.js";
4
+ import { onPrefsChanged } from "/assets/app-prefs.js";
5
5
  import { themesShown, themesStore } from "/assets/app-themes.js";
6
6
  import { ownerCssLifecycle } from "/assets/owner-css.js";
7
- import { chatEffectIdentity } from "/assets/effect-chat-runtime.js";
7
+ import { effectPlaybackIdentity } from "/assets/effect-chat-runtime.js";
8
+ import { effectBootstrapReady } from "/assets/effect-frame.js";
9
+ import { motionBlocked, onMotionChange } from "/assets/fx-motion.js";
8
10
  import { createChatPosters } from "/assets/effect-posters.js";
9
11
 
10
12
  export function ChatEffectPosters({ name, lease, element }) {
@@ -15,12 +17,11 @@ export function ChatEffectPosters({ name, lease, element }) {
15
17
  const cleanup = () => { if (!live) return; live = false; try { api?.dispose?.(); } catch { /* optional */ } for (const off of releases) { try { off?.(); } catch { /* release all */ } } };
16
18
  lease.hold(cleanup);
17
19
  try {
18
- const preview = host.AgendPreview, motion = host.matchMedia?.("(prefers-reduced-motion: reduce)") ?? { matches: true };
20
+ const motion = host.matchMedia?.("(prefers-reduced-motion: reduce)") ?? { matches: true };
19
21
  const observe = () => {
20
- let previewReady = false; try { previewReady = preview?.availability().ok === true; } catch { /* fallback */ }
21
22
  // This is a surface fallback, not a replacement for an explicit Pause, queued effect or failed live read.
22
- const playback = chatEffectIdentity(host) && typeof host.Worker === "function" && typeof host.OffscreenCanvas === "function" && previewReady &&
23
- prefInForce("preview.optIn")?.value === true && preview?.optedIn() === true && preview?.never() !== true && !motion.matches && host.navigator?.connection?.saveData !== true;
23
+ const playback = effectPlaybackIdentity(host) && typeof host.Worker === "function" && typeof host.OffscreenCanvas === "function" &&
24
+ effectBootstrapReady(host, host.location?.origin, host.document.body.dataset.effectBoot) && !motionBlocked(host);
24
25
  return { shown: themesShown(), fallback: !playback, ownerCss: ownerCssLifecycle.get().state };
25
26
  };
26
27
  api = createChatPosters({ window: host, name, element: element.current, current, observe });
@@ -32,8 +33,7 @@ export function ChatEffectPosters({ name, lease, element }) {
32
33
  if (s.connection === "ended") { api.dispose(); return; }
33
34
  if (s.themesRev !== revision) { revision = s.themesRev; api.changed(); } else sync();
34
35
  }));
35
- releases.push(ownerCssLifecycle.subscribe(sync)); releases.push(themesStore.subscribe(sync)); releases.push(onPrefsChanged(sync));
36
- if (typeof preview?.onChange === "function") releases.push(preview.onChange(sync));
36
+ releases.push(ownerCssLifecycle.subscribe(sync)); releases.push(themesStore.subscribe(sync)); releases.push(onPrefsChanged(sync)); releases.push(onMotionChange(sync));
37
37
  for (const target of [motion, host.navigator?.connection]) if (target?.addEventListener) { target.addEventListener("change", sync); releases.push(() => target.removeEventListener("change", sync)); }
38
38
  sync(); return cleanup;
39
39
  } catch { cleanup(); return cleanup; }
@@ -1,5 +1,5 @@
1
1
  // #1629: Chat contributes a committed lease to the one app owner; no private pool or server polling.
2
- import { html, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
2
+ import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
3
3
  import { register, t } from "/assets/app-i18n.js";
4
4
  import { appStore, useStore } from "/assets/app-store.js";
5
5
  import { ownerCssLifecycle } from "/assets/owner-css.js";
@@ -7,18 +7,89 @@ import { chatEffectState, chatEffectIdentity } from "/assets/effect-chat-runtime
7
7
  import { Dialog } from "/assets/ui-dialog.js";
8
8
  import { navigate } from "/assets/app-nav.js";
9
9
  import { toast } from "/assets/ui-toast.js";
10
- import { getAppEffects } from "./app-effects.js";
10
+ import { setThemesShown, themesShown, themesStore } from "/assets/app-themes.js";
11
+ import { setFollowsReducedMotion } from "/assets/fx-motion.js";
12
+ import { getAppEffects, wakeEffects, effectsLoaded, onEffectsAwake } from "./effects-waker.js";
11
13
 
12
14
  register("chatFx", {
13
- en: { title: "Effects", loading: "Checking effects…", empty: "No effects in this chat.", failed: "Effects stopped because their resources could not be verified. Reload the page to try again.", reload: "Reload page", pause: "Pause", resume: "Resume", remove: "Remove from chat", removeFailed: "Could not remove this effect. Reload the page to check its state.", css: "Agent foreground effects are paused while your custom CSS is on or cannot be verified.", setting: "Open Web appearance settings", running: "Running", starting: "Starting", off: "Stopped", resumeFailed: "This effect cannot resume: the page or its safety history is not ready. Reload the page or choose another effect.", queued: "Waiting for a slot", unknown: "Playback is off or unavailable here.", budget: "Paused to protect page responsiveness. Resume keeps the three-pauses safety limit.", paused: "Paused", low: "Reduced quality" },
14
- "zh-TW": { title: "效果", loading: "正在確認效果…", empty: "這個聊天沒有放置效果。", failed: "無法確認效果資源,已停止播放。重新載入頁面後再試。", reload: "重新載入頁面", pause: "暫停", resume: "恢復", remove: "從聊天移除", removeFailed: "無法移除效果。請重新載入頁面確認狀態。", css: "自訂 CSS 開啟或無法確認時,代理的前景效果會暫停。", setting: "開啟網頁外觀設定", running: "播放中", starting: "啟動中", off: "已停止", resumeFailed: "頁面或安全紀錄尚未就緒,無法恢復此效果。請重新載入頁面,或選擇其他效果。", queued: "等待空位", unknown: "這裡已關閉播放,或無法播放效果。", budget: "為維持頁面順暢,已暫停效果。恢復仍保留三次暫停的安全上限。", paused: "已暫停", low: "降低畫質" },
15
+ en: { stock: "Off: the page layout could not be verified ({0}). Reload and report this code if it persists.", transport: "Off: the isolated effect frame could not start. Reload to try again.", enableHere: "Turn effects on here", playMotion: "Play effects anyway", leaveSafe: "Leave safe mode", viewerShow: "Show themes, effects and widgets here", clock: "Off: the page timing clock is unavailable. Reload to try again.", gatesUnknown: "Off: the playback conditions could not be verified.", resource: "Off: this effect could not be loaded or verified. Reload to try again.", frontQueue: "Waiting for a foreground slot.", consent: "This page has older playback information. Reload to use the isolated effect player.", previewMissing: "Off: the isolated effect frame is unavailable. Reload to try again.", unsupported: "Off: this browser lacks required Canvas Worker or layout support.", calibration: "Starting the effect.", baseline: "This page has older playback information. Reload to try again.", safetyUnknown: "Stopped: this device's safety history could not be verified. Reload to check it.", rawReason: "Playback blocked: {0}", missingRow: "Playback state is unavailable (runtime_unavailable).", region: "Off: this effect does not declare the chat region.", blocked: "Placed effects are blocked for this viewer:", deviceSetting: "Open device preferences", webSetting: "Open Web settings", reloadHint: "Reload page",
16
+ title: "Effects", loading: "Checking effects…", empty: "No effects in this chat.", failed: "Effects stopped because their resources could not be verified. Reload the page to try again.", reload: "Reload page", pause: "Pause", resume: "Resume", remove: "Remove from chat", removeFailed: "Could not remove this effect. Reload the page to check its state.", css: "Agent foreground effects are paused while your custom CSS is on or cannot be verified.", setting: "Open Web appearance settings", running: "Running", starting: "Starting", off: "Stopped", resumeFailed: "This effect cannot resume: the page or its safety history is not ready. Reload the page or choose another effect.", queued: "Waiting for a slot", unknown: "Playback is off or unavailable here.", budget: "Paused to protect page responsiveness. Resume keeps the three-pauses safety limit.", pauseHelp: "Resume keeps this device’s automatic-pause history and three-pause safety limit.", paused: "Paused", low: "Reduced quality",
17
+ typing: "Waiting while you type — it plays when you leave the message box.", covered: "Waiting while a menu or panel covers the chat.",
18
+ dialogOpen: "Paused while this dialog is open.", wasRunning: "Was running; paused while this dialog is open.", modal: "Waiting while a dialog is open.",
19
+ off3: "Turned off on this device after three automatic pauses. Resume cannot restart it here.", interrupted: "Stopped: the page closed while it was playing. Resume to play it again.",
20
+ cssOff: "Off while your custom CSS is on.", reducedMotion: "Off: you chose to follow the system's reduced motion setting.", saveData: "This page has older playback information. Reload to try again.",
21
+ hiddenTab: "Waiting until this tab is visible.", offscreen: "Waiting until the chat is on screen.",
22
+ safeMode: "Off in safe mode.", publicLink: "Off on a public link.", disabled: "Effects are switched off." },
23
+ "zh-TW": { stock: "已關閉:無法驗證頁面版面({0})。請重新載入;若持續發生,請回報此代碼。", transport: "已關閉:無法啟動隔離效果框架。請重新載入再試。", enableHere: "在這裡開啟效果", playMotion: "仍然播放效果", leaveSafe: "離開安全模式", viewerShow: "在這裡顯示主題、效果與小工具", clock: "已關閉:頁面計時無法使用。請重新載入再試。", gatesUnknown: "已關閉:無法確認播放條件。", resource: "已關閉:無法載入或驗證此效果。請重新載入再試。", frontQueue: "等待前景空位。", consent: "這個頁面的播放資訊已過期。請重新載入以使用隔離效果播放器。", previewMissing: "已關閉:隔離效果框架無法使用。請重新載入再試。", unsupported: "已關閉:這個瀏覽器缺少 Canvas Worker 或版面驗證支援。", calibration: "正在啟動效果。", baseline: "這個頁面的播放資訊已過期,請重新載入再試。", safetyUnknown: "已停止:無法確認這台裝置的安全紀錄。請重新載入確認。", rawReason: "播放受阻:{0}", missingRow: "無法取得播放狀態(runtime_unavailable)。", region: "已關閉:此效果未宣告聊天區域。", blocked: "放置的效果目前無法在這個畫面播放:", deviceSetting: "開啟裝置偏好設定", webSetting: "開啟網頁設定", reloadHint: "重新載入頁面",
24
+ title: "效果", loading: "正在確認效果…", empty: "這個聊天沒有放置效果。", failed: "無法確認效果資源,已停止播放。重新載入頁面後再試。", reload: "重新載入頁面", pause: "暫停", resume: "恢復", remove: "從聊天移除", removeFailed: "無法移除效果。請重新載入頁面確認狀態。", css: "自訂 CSS 開啟或無法確認時,代理的前景效果會暫停。", setting: "開啟網頁外觀設定", running: "播放中", starting: "啟動中", off: "已停止", resumeFailed: "頁面或安全紀錄尚未就緒,無法恢復此效果。請重新載入頁面,或選擇其他效果。", queued: "等待空位", unknown: "這裡已關閉播放,或無法播放效果。", budget: "為維持頁面順暢,已暫停效果。恢復仍保留三次暫停的安全上限。", pauseHelp: "恢復仍保留這台裝置的自動暫停紀錄與三次暫停安全上限。", paused: "已暫停", low: "降低畫質",
25
+ typing: "輸入時暫停,離開訊息輸入框就會播放。", covered: "選單或面板蓋住聊天時暫停。",
26
+ dialogOpen: "開著這個對話框時暫停。", wasRunning: "原本在播放;開著這個對話框時暫停。", modal: "有對話框開著時暫停。",
27
+ off3: "已自動暫停三次,這台裝置已關閉此效果,無法在這裡恢復。", interrupted: "播放時頁面被關閉而停止。按「恢復」再播放一次。",
28
+ cssOff: "自訂 CSS 開啟時關閉。", reducedMotion: "已關閉:效果跟隨這台裝置的「減少動態效果」設定。", saveData: "這個頁面的播放資訊已過期,請重新載入再試。",
29
+ hiddenTab: "等這個分頁顯示時再播放。", offscreen: "等聊天顯示在畫面上時再播放。",
30
+ safeMode: "安全模式下關閉。", publicLink: "公開連結上關閉。", disabled: "效果已關閉。" },
15
31
  });
16
32
 
17
- /** Local read/admin sessions keep controls; public, View, poll and header-only pages have no runtime identity. */
33
+ /** Read/admin dashboard sessions keep management controls; public/guest Chat playback has no owner identity. */
18
34
  export function chatEffectsMenuAvailable() { return chatEffectIdentity(window) !== null; }
19
35
 
20
- export function ChatEffects({ name, lease, element, store, open, onClose }) {
36
+ // #2055: what a row's state means, in words — never only "Stopped" when the runtime knows why.
37
+ const RECENT_MS = 5000;
38
+ const REASON_KEY = { __proto__: null, transport_unavailable: "chatFx.transport", owner_css: "chatFx.cssOff", reduced_motion: "chatFx.reducedMotion", save_data: "chatFx.saveData", preview_unavailable: "chatFx.previewMissing", preview_not_consented: "chatFx.consent", unsupported_host: "chatFx.unsupported", registry_unsupported: "chatFx.unsupported",
39
+ baseline_unsupported: "chatFx.baseline", baseline_unavailable: "chatFx.baseline", baseline_timeout: "chatFx.baseline", calibrating: "chatFx.calibration", safety_unknown: "chatFx.safetyUnknown", region_unavailable: "chatFx.region",
40
+ hidden: "chatFx.hiddenTab", offscreen: "chatFx.offscreen", safe_mode: "chatFx.safeMode", public_link: "chatFx.publicLink", disabled: "chatFx.disabled",
41
+ clock_unavailable: "chatFx.clock", gates_unknown: "chatFx.gatesUnknown", runtime_unavailable: "chatFx.missingRow", resource_unavailable: "chatFx.resource", resource_timeout: "chatFx.resource", front_limit: "chatFx.frontQueue",
42
+ safety_off: "chatFx.off3", interrupted: "chatFx.interrupted" };
43
+ /** `ownOpen`: this Effects dialog is open; `wasRunning`: the effect was running when it opened (shown as past, never as current). */
44
+ export function effectRowText(row, { ownOpen = false, wasRunning = false, doc = typeof document === "undefined" ? null : document } = {}) {
45
+ if (!row) return t("chatFx.missingRow");
46
+ if (typeof row.reason === "string" && row.reason !== "stock_overlay" && /^(stock_|trust_|geometry_)/.test(row.reason)) return t("chatFx.stock", row.reason.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 80));
47
+ // The sticky history first (it is what Resume answers), then a pause, then what is playing or queued.
48
+ if (row.reason === "safety_off" || row.reason === "interrupted") return t(REASON_KEY[row.reason]);
49
+ if (row.reason === "paused" || row.reason === "host_budget") return t("chatFx.paused");
50
+ if (REASON_KEY[row.reason]) return t(REASON_KEY[row.reason]);
51
+ if (row.reason != null && !["stock_overlay", "modal", "frame_limit"].includes(row.reason))
52
+ return t("chatFx.rawReason", typeof row.reason === "string" ? row.reason.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 80) : "invalid_reason_type");
53
+ if (row.phase === "running") return t("chatFx.running");
54
+ if (row.phase) return t("chatFx.starting");
55
+ if (row.verdict === "queued") return t("chatFx.queued");
56
+ if (row.reason === "stock_overlay" || row.reason === "modal") {
57
+ if (ownOpen) return t(wasRunning ? "chatFx.wasRunning" : "chatFx.dialogOpen");
58
+ if (row.reason === "modal") return t("chatFx.modal");
59
+ return t("chatFx.covered");
60
+ }
61
+ return t("chatFx.rawReason", typeof row.reason === "string" ? row.reason.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 80) : "invalid_reason_type");
62
+ }
63
+
64
+ /** Fixed local actions: no server/Worker-provided destination or raw reason in a URL. */
65
+ export function effectReasonAction(reason) {
66
+ if (reason === "preview_not_consented" || reason === "preview_unavailable") return { reload: true, label: "chatFx.reloadHint" };
67
+ if (["disabled", "owner_css"].includes(reason)) return { href: "/settings/appearance", label: "chatFx.setting" };
68
+ if (["transport_unavailable", "baseline_unsupported", "baseline_unavailable", "baseline_timeout", "runtime_unavailable", "resource_unavailable", "resource_timeout", "clock_unavailable", "safety_unknown"].includes(reason)) return { reload: true, label: "chatFx.reloadHint" };
69
+ return null;
70
+ }
71
+ const localViewer = () => document.body.dataset.publicLink === "1" || document.body.dataset.tier === "guest";
72
+ function ReasonAction({ reason, onClose = () => {} }) {
73
+ if (reason === "safe_mode") return html`<button type="button" class="btn" onClick=${() => { onClose(); const url = new URL(location.href); url.searchParams.delete("safe"); location.assign(url.href); }}>${t("chatFx.leaveSafe")}</button>`;
74
+ if (reason === "reduced_motion") return html`<button type="button" class="btn" onClick=${() => { onClose(); setFollowsReducedMotion(false); }}>${t("chatFx.playMotion")}</button>`;
75
+ if (reason === "disabled" && localViewer()) return html`<button type="button" class="btn" onClick=${() => { onClose(); setThemesShown(true); }}>${t("chatFx.enableHere")}</button>`;
76
+ const action = document.body.dataset.tier === "guest" && reason === "owner_css" ? { reload: true, label: "chatFx.reloadHint" } : effectReasonAction(reason);
77
+ return action ? html`<button type="button" class="btn" onClick=${() => { onClose(); if (action.reload) location.reload(); else navigate(action.href); }}>${t(action.label)}</button>` : null;
78
+ }
79
+
80
+ const NO_PLAYBACK = () => {};
81
+ export function ChatEffects({ name, lease, element, store, open, onClose, onPlayback = NO_PLAYBACK, active = true }) {
21
82
  const cssLife = useStore(ownerCssLifecycle);
83
+ // #2193: `active` — this chat has placements (its status row says so). While the runtime sleeps, a chat with none
84
+ // attaches and reads nothing (the dialog says "No effects in this chat"); one with some wakes it and attaches once it
85
+ // runs. Once it runs (woken by anything — a Try card needs its chat attached), every chat attaches as before.
86
+ const [awake, setAwake] = useState(() => getAppEffects() !== null || effectsLoaded());
87
+ useEffect(() => (awake ? undefined : onEffectsAwake(() => setAwake(true))), [awake]);
88
+ useStore(themesStore); // Viewer-local off/on is visible immediately, independent of runtime callbacks.
89
+ // Which effects were running just before this dialog opened ("was running" is said in the past, never shown as
90
+ // current). The ⋯ menu that opens it is a barrier too, so "just before" is within RECENT_MS of the observed stop.
91
+ // Tracked where updates arrive (an unchanged snapshot is not republished while an effect keeps running).
92
+ const before = useRef(null), wasOpen = useRef(false), playing = useRef(new Map());
22
93
  const runtime = useRef(null), [state, setState] = useState(null), [canWrite, setCanWrite] = useState(false), [busy, setBusy] = useState(false);
23
94
  useLayoutEffect(() => {
24
95
  let live = true, api = null;
@@ -34,20 +105,33 @@ export function ChatEffects({ name, lease, element, store, open, onClose }) {
34
105
  lease.hold(cleanup);
35
106
  try {
36
107
  setBusy(false); onClose(); setCanWrite(false); setState(null);
108
+ if (!active && !awake) { setState({ error: null, rows: [], effects: [], loading: false }); return cleanup; }
109
+ if (!awake) { void wakeEffects(); return cleanup; } // "Checking effects…" until it runs
37
110
  const host = window, owner = getAppEffects(host);
38
111
  const current = () => live && lease.current();
39
- if (!owner) { setState({ error: null, rows: [], effects: [], unavailable: true }); return cleanup; }
112
+ if (!owner) { setState({ error: null, rows: [], effects: [], unavailable: true, reason: typeof host.OffscreenCanvas !== "function" || typeof host.Worker !== "function" ? "unsupported_host" : "runtime_unavailable" }); return cleanup; }
40
113
  releases.push(owner.onEnd(() => { if (current()) setState({ error: "resource_unavailable", rows: [], effects: [], unavailable: true }); }));
114
+ playing.current = new Map(); // evidence belongs to this name and lease only
115
+ const track = rows => {
116
+ const now = performance.now(), seen = new Set();
117
+ for (const r of Array.isArray(rows) ? rows : []) {
118
+ seen.add(r.id);
119
+ if (r.phase === "running") playing.current.set(r.id, null);
120
+ else if (playing.current.get(r.id) === null) playing.current.set(r.id, now);
121
+ }
122
+ for (const [id, at] of playing.current) if (!seen.has(id) && at === null) playing.current.set(id, now);
123
+ };
41
124
  const changed = value => {
42
125
  if (!current()) return;
126
+ track(value?.rows);
43
127
  const g = owner.gates();
44
- setState({ ...value, unavailable: !g.enabled || !g.show || !g.previewReady || !g.optIn || g.safe || g.publicLink || g.reducedMotion || g.saveData });
128
+ setState({ ...value, unavailable: !g.enabled || !g.show || !g.transportReady || g.safe || g.reducedMotion });
45
129
  };
46
130
  api = owner.attachChat({ name, element: element.current, current,
47
131
  observe: () => ({ state: chatEffectState(appStore.get(), name) }), onChange: changed });
48
132
  runtime.current = api;
49
- if (!api?.ok) { setState({ error: null, rows: [], effects: [], unavailable: true }); return cleanup; }
50
- host.AgendAuth?.canWrite?.().then(value => { if (current()) setCanWrite(value === true); }).catch(() => {});
133
+ if (!api?.ok) { setState({ error: null, rows: [], effects: [], unavailable: true, reason: "runtime_unavailable" }); return cleanup; }
134
+ if (chatEffectIdentity(host)) host.AgendAuth?.canWrite?.().then(value => { if (current()) setCanWrite(value === true); }).catch(() => {});
51
135
  api.sync();
52
136
  return cleanup;
53
137
  } catch {
@@ -55,18 +139,29 @@ export function ChatEffects({ name, lease, element, store, open, onClose }) {
55
139
  if (lease.current()) setState({ error: "runtime_unavailable", rows: [], effects: [] });
56
140
  return cleanup;
57
141
  }
58
- }, [name, lease]);
142
+ }, [name, lease, active, awake]);
59
143
  const rows = state?.rows ?? [], effects = state?.effects ?? [];
144
+ if (open && !wasOpen.current) {
145
+ const now = performance.now();
146
+ before.current = new Map([...playing.current].map(([id, stoppedAt]) => [id, stoppedAt === null || now - stoppedAt < RECENT_MS]));
147
+ }
148
+ if (!open) before.current = null;
149
+ wasOpen.current = open;
60
150
  const css = cssLife.state !== "off" && effects.some(p => p.by !== "human" && p.manifest.layer === "front");
61
151
  const remove = async id => { if (busy) return; setBusy(true); const api = runtime.current; const ok = await api?.remove?.(id); if (!lease.current() || runtime.current !== api) return; setBusy(false); if (!ok) toast(t("chatFx.removeFailed"), false); };
62
- return html`${open ? html`<${Dialog} title=${t("chatFx.title")} onClose=${onClose} busy=${busy}>
152
+ const playingNow = !state?.unavailable && !state?.error && rows.some(row => row.phase === "running" && row.reason == null);
153
+ useEffect(() => { onPlayback({ name, playing: playingNow }); return () => onPlayback({ name, playing: false }); }, [name, playingNow, onPlayback]);
154
+ const blocked = effects.flatMap(p => { const row = rows.find(r => r.id === p.id); return row?.phase === "running" && row.reason == null ? [] : [{ p, row }]; });
155
+ return html`${!open && localViewer() && effects.length ? html`<div class="chat-fx-notice"><label><input type="checkbox" checked=${themesShown()} onChange=${e => setThemesShown(e.currentTarget.checked)} /> ${t("chatFx.viewerShow")}</label></div>` : null}${!open && state?.error && !effects.length ? html`<div class="chat-fx-notice" role="status">${effectRowText({ reason: state.error })} <${ReasonAction} reason=${state.error} /></div>` : null}${!open && state?.reason ? html`<div class="chat-fx-notice" role="status">${effectRowText({ reason: state.reason })} <${ReasonAction} reason=${state.reason} /></div>` : null}${!open && blocked.length ? html`<div class="chat-fx-notice" role="status"><strong>${t("chatFx.blocked")}</strong>
156
+ ${blocked.map(({ p, row }) => html`<span key=${p.id}>${p.manifest.title}: ${effectRowText(row)} ${["paused", "host_budget"].includes(row?.reason) ? html`<small>${t(row.reason === "host_budget" ? "chatFx.budget" : "chatFx.pauseHelp")}</small>` : null} <${ReasonAction} reason=${row?.reason} /></span>`)}</div>` : null}${open ? html`<${Dialog} title=${t("chatFx.title")} onClose=${onClose} busy=${busy}>
63
157
  ${state?.error ? html`<p>${t("chatFx.failed")}</p><button type="button" class="btn" onClick=${() => location.reload()}>${t("chatFx.reload")}</button>` : null}
64
158
  ${css ? html`<p>${t("chatFx.css")}</p><button type="button" class="btn" onClick=${() => { onClose(); navigate("/settings/appearance"); }}>${t("chatFx.setting")}</button>` : null}
65
159
  ${!effects.length && !state?.error ? html`<p>${t(state?.unavailable ? "chatFx.unknown" : state?.loading || !state ? "chatFx.loading" : "chatFx.empty")}</p>` : null}
66
- ${effects.map(p => { const row = rows.find(r => r.id === p.id), phase = row?.phase; return html`<section key=${p.id}><h3>${p.manifest.title}</h3><p>${t(row?.reason === "paused" || row?.reason === "host_budget" ? "chatFx.paused" : phase === "running" ? "chatFx.running" : phase ? "chatFx.starting" : row?.verdict === "queued" ? "chatFx.queued" : "chatFx.off")}${row?.quality === "low" ? ` · ${t("chatFx.low")}` : ""}</p>
67
- ${row?.reason === "host_budget" ? html`<p>${t("chatFx.budget")}</p>` : null}
160
+ ${effects.map(p => { const row = rows.find(r => r.id === p.id); return html`<section key=${p.id}><h3>${p.manifest.title}</h3><p>${effectRowText(row, { ownOpen: open, wasRunning: before.current?.get(p.id) === true })}${row?.quality === "low" ? ` · ${t("chatFx.low")}` : ""}</p>
161
+ <${ReasonAction} reason=${row?.reason} onClose=${onClose} />
162
+ ${["paused", "host_budget"].includes(row?.reason) ? html`<p>${t(row.reason === "host_budget" ? "chatFx.budget" : "chatFx.pauseHelp")}</p>` : null}
68
163
  <button type="button" class="btn" disabled=${busy} onClick=${() => runtime.current?.pause?.(p.id)}>${t("chatFx.pause")}</button>
69
- <button type="button" class="btn" disabled=${busy} onClick=${() => { if (!runtime.current?.resume?.(p.id)) toast(t("chatFx.resumeFailed"), false); }}>${t("chatFx.resume")}</button>
164
+ <button type="button" class="btn" disabled=${busy} onClick=${() => { if (!runtime.current?.resume?.(p.id)) toast(t(rows.find(r => r.id === p.id)?.reason === "safety_off" ? "chatFx.off3" : "chatFx.resumeFailed"), false); }}>${t("chatFx.resume")}</button>
70
165
  ${canWrite ? html`<button type="button" class="btn" disabled=${busy} onClick=${() => remove(p.id)}>${t("chatFx.remove")}</button>` : null}</section>`; })}
71
166
  </${Dialog}>` : null}`;
72
167
  }