@songsid/agend 2.3.2 → 2.3.3-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. package/dist/agent-cli-instructions.md +2 -2
  2. package/dist/agent-cli.js +13 -112
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.d.ts +0 -1
  5. package/dist/agent-endpoint.js +1 -9
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-whats-new.d.ts +23 -5
  8. package/dist/agent-whats-new.js +58 -40
  9. package/dist/agent-whats-new.js.map +1 -1
  10. package/dist/auth-api.d.ts +0 -7
  11. package/dist/auth-api.js +1 -15
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/authority/authority-state.js +1 -1
  14. package/dist/authority/authority-state.js.map +1 -1
  15. package/dist/authority/web-authority.d.ts +9 -4
  16. package/dist/authority/web-authority.js +10 -5
  17. package/dist/authority/web-authority.js.map +1 -1
  18. package/dist/backend/antigravity.js +6 -1
  19. package/dist/backend/antigravity.js.map +1 -1
  20. package/dist/backend/claude-code.d.ts +20 -1
  21. package/dist/backend/claude-code.js +54 -29
  22. package/dist/backend/claude-code.js.map +1 -1
  23. package/dist/backend/claude-transcript-errors.d.ts +18 -0
  24. package/dist/backend/claude-transcript-errors.js +19 -0
  25. package/dist/backend/claude-transcript-errors.js.map +1 -0
  26. package/dist/backend/codex-session-lookup.d.ts +16 -3
  27. package/dist/backend/codex-session-lookup.js +45 -11
  28. package/dist/backend/codex-session-lookup.js.map +1 -1
  29. package/dist/backend/codex.d.ts +16 -0
  30. package/dist/backend/codex.js +184 -35
  31. package/dist/backend/codex.js.map +1 -1
  32. package/dist/backend/grok.js +58 -12
  33. package/dist/backend/grok.js.map +1 -1
  34. package/dist/backend/kiro-reconcile-store.js +8 -1
  35. package/dist/backend/kiro-reconcile-store.js.map +1 -1
  36. package/dist/backend/kiro.d.ts +0 -5
  37. package/dist/backend/kiro.js +2 -10
  38. package/dist/backend/kiro.js.map +1 -1
  39. package/dist/backend/muse.js +2 -1
  40. package/dist/backend/muse.js.map +1 -1
  41. package/dist/backend/types.d.ts +53 -4
  42. package/dist/backend/types.js.map +1 -1
  43. package/dist/bundled-skills.d.ts +4 -6
  44. package/dist/bundled-skills.js +10 -14
  45. package/dist/bundled-skills.js.map +1 -1
  46. package/dist/channel/adapters/discord.js +60 -17
  47. package/dist/channel/adapters/discord.js.map +1 -1
  48. package/dist/channel/mcp-server.js +5 -5
  49. package/dist/channel/mcp-server.js.map +1 -1
  50. package/dist/channel/mcp-tools.d.ts +1 -3
  51. package/dist/channel/mcp-tools.js +4 -58
  52. package/dist/channel/mcp-tools.js.map +1 -1
  53. package/dist/classic-channel-manager.d.ts +4 -1
  54. package/dist/classic-channel-manager.js +21 -7
  55. package/dist/classic-channel-manager.js.map +1 -1
  56. package/dist/cli.js +23 -6
  57. package/dist/cli.js.map +1 -1
  58. package/dist/config-history.d.ts +50 -0
  59. package/dist/config-history.js +263 -0
  60. package/dist/config-history.js.map +1 -0
  61. package/dist/config-validator.js +10 -8
  62. package/dist/config-validator.js.map +1 -1
  63. package/dist/config.d.ts +3 -0
  64. package/dist/config.js +11 -4
  65. package/dist/config.js.map +1 -1
  66. package/dist/cross-instance-envelope.d.ts +10 -5
  67. package/dist/cross-instance-envelope.js +20 -17
  68. package/dist/cross-instance-envelope.js.map +1 -1
  69. package/dist/daemon.d.ts +130 -64
  70. package/dist/daemon.js +603 -428
  71. package/dist/daemon.js.map +1 -1
  72. package/dist/daily-summary.d.ts +8 -1
  73. package/dist/daily-summary.js +8 -5
  74. package/dist/daily-summary.js.map +1 -1
  75. package/dist/dashboard-live.d.ts +1 -1
  76. package/dist/dashboard-live.js +3 -3
  77. package/dist/dashboard-live.js.map +1 -1
  78. package/dist/dashboard-permissions.d.ts +1 -1
  79. package/dist/dashboard-permissions.js +1 -3
  80. package/dist/dashboard-permissions.js.map +1 -1
  81. package/dist/delivery-outbox.d.ts +31 -22
  82. package/dist/delivery-outbox.js +119 -161
  83. package/dist/delivery-outbox.js.map +1 -1
  84. package/dist/delivery-outbox.test.js +10 -25
  85. package/dist/delivery-outbox.test.js.map +1 -1
  86. package/dist/doctor.d.ts +4 -0
  87. package/dist/doctor.js +16 -1
  88. package/dist/doctor.js.map +1 -1
  89. package/dist/fleet-level-config.js +1 -1
  90. package/dist/fleet-level-config.js.map +1 -1
  91. package/dist/fleet-manager.d.ts +74 -66
  92. package/dist/fleet-manager.js +285 -414
  93. package/dist/fleet-manager.js.map +1 -1
  94. package/dist/fleet-process-handlers.d.ts +5 -0
  95. package/dist/fleet-process-handlers.js +21 -0
  96. package/dist/fleet-process-handlers.js.map +1 -1
  97. package/dist/fleet-service-owner.d.ts +11 -0
  98. package/dist/fleet-service-owner.js +3 -0
  99. package/dist/fleet-service-owner.js.map +1 -0
  100. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
  101. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +1 -1
  102. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  103. package/dist/general-knowledge/skills/session-management/SKILL.md +7 -1
  104. package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
  105. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +30 -68
  106. package/dist/instance-lifecycle.d.ts +19 -0
  107. package/dist/instance-lifecycle.js +73 -15
  108. package/dist/instance-lifecycle.js.map +1 -1
  109. package/dist/instructions.js +6 -7
  110. package/dist/instructions.js.map +1 -1
  111. package/dist/kiro-reconcile.js +3 -3
  112. package/dist/kiro-reconcile.js.map +1 -1
  113. package/dist/locale.js +96 -58
  114. package/dist/locale.js.map +1 -1
  115. package/dist/log-service.js +1 -1
  116. package/dist/log-service.js.map +1 -1
  117. package/dist/needs-you-hub.d.ts +1 -0
  118. package/dist/needs-you-hub.js +6 -3
  119. package/dist/needs-you-hub.js.map +1 -1
  120. package/dist/needs-you-live.d.ts +3 -1
  121. package/dist/needs-you-live.js +24 -2
  122. package/dist/needs-you-live.js.map +1 -1
  123. package/dist/needs-you.d.ts +13 -2
  124. package/dist/needs-you.js +11 -1
  125. package/dist/needs-you.js.map +1 -1
  126. package/dist/notify-socket.d.ts +10 -0
  127. package/dist/notify-socket.js +45 -0
  128. package/dist/notify-socket.js.map +1 -0
  129. package/dist/outbound-handlers.js +20 -8
  130. package/dist/outbound-handlers.js.map +1 -1
  131. package/dist/outbound-schemas.d.ts +0 -62
  132. package/dist/outbound-schemas.js +4 -43
  133. package/dist/outbound-schemas.js.map +1 -1
  134. package/dist/parked-notices.d.ts +87 -0
  135. package/dist/parked-notices.js +194 -0
  136. package/dist/parked-notices.js.map +1 -0
  137. package/dist/public-link-progress.d.ts +5 -2
  138. package/dist/public-link-progress.js +4 -3
  139. package/dist/public-link-progress.js.map +1 -1
  140. package/dist/public-web-gateway.js +0 -9
  141. package/dist/public-web-gateway.js.map +1 -1
  142. package/dist/public-web-link.d.ts +12 -0
  143. package/dist/public-web-link.js +29 -5
  144. package/dist/public-web-link.js.map +1 -1
  145. package/dist/quota-notice.d.ts +23 -0
  146. package/dist/quota-notice.js +22 -0
  147. package/dist/quota-notice.js.map +1 -0
  148. package/dist/scheduler/task-board.d.ts +17 -25
  149. package/dist/scheduler/task-board.js +29 -39
  150. package/dist/scheduler/task-board.js.map +1 -1
  151. package/dist/sd-notify.d.ts +23 -15
  152. package/dist/sd-notify.js +88 -29
  153. package/dist/sd-notify.js.map +1 -1
  154. package/dist/search/search-docs.d.ts +1 -1
  155. package/dist/search/search-docs.js +2 -2
  156. package/dist/search/search-docs.js.map +1 -1
  157. package/dist/settings-api.d.ts +16 -1
  158. package/dist/settings-api.js +56 -16
  159. package/dist/settings-api.js.map +1 -1
  160. package/dist/settings-change.js +3 -1
  161. package/dist/settings-change.js.map +1 -1
  162. package/dist/settings-confirmation.d.ts +17 -3
  163. package/dist/settings-confirmation.js +8 -5
  164. package/dist/settings-confirmation.js.map +1 -1
  165. package/dist/settings-effect.js +0 -2
  166. package/dist/settings-effect.js.map +1 -1
  167. package/dist/settings-http-confirmation.js +31 -1
  168. package/dist/settings-http-confirmation.js.map +1 -1
  169. package/dist/setup-wizard.d.ts +3 -0
  170. package/dist/setup-wizard.js +4 -2
  171. package/dist/setup-wizard.js.map +1 -1
  172. package/dist/task-board-waker.d.ts +43 -1
  173. package/dist/task-board-waker.js +121 -9
  174. package/dist/task-board-waker.js.map +1 -1
  175. package/dist/tmux-control.d.ts +1 -1
  176. package/dist/tmux-control.js +3 -2
  177. package/dist/tmux-control.js.map +1 -1
  178. package/dist/tmux-manager.js +18 -16
  179. package/dist/tmux-manager.js.map +1 -1
  180. package/dist/tmux-read.d.ts +2 -0
  181. package/dist/tmux-read.js +7 -1
  182. package/dist/tmux-read.js.map +1 -1
  183. package/dist/tool-permissions.js +3 -10
  184. package/dist/tool-permissions.js.map +1 -1
  185. package/dist/topic-commands.d.ts +29 -7
  186. package/dist/topic-commands.js +97 -37
  187. package/dist/topic-commands.js.map +1 -1
  188. package/dist/transcript-monitor.d.ts +7 -0
  189. package/dist/transcript-monitor.js +48 -3
  190. package/dist/transcript-monitor.js.map +1 -1
  191. package/dist/tunnel/cloudflared.d.ts +5 -0
  192. package/dist/tunnel/cloudflared.js +32 -1
  193. package/dist/tunnel/cloudflared.js.map +1 -1
  194. package/dist/tunnel/named-diagnostics.d.ts +2 -1
  195. package/dist/tunnel/named-diagnostics.js +4 -1
  196. package/dist/tunnel/named-diagnostics.js.map +1 -1
  197. package/dist/tunnel/named-link-test.d.ts +90 -0
  198. package/dist/tunnel/named-link-test.js +270 -0
  199. package/dist/tunnel/named-link-test.js.map +1 -0
  200. package/dist/tunnel/purpose-lane.d.ts +3 -1
  201. package/dist/tunnel/purpose-lane.js +2 -0
  202. package/dist/tunnel/purpose-lane.js.map +1 -1
  203. package/dist/types.d.ts +9 -11
  204. package/dist/ui/chat-ambient.js +4 -4
  205. package/dist/ui/chat-commands.js +3 -2
  206. package/dist/ui/chat-store.js +4 -1
  207. package/dist/ui/chat-strings.js +12 -46
  208. package/dist/ui/chat-thread.js +51 -59
  209. package/dist/ui/fleet-office-skin.js +37 -14
  210. package/dist/ui/fleet-office.js +31 -7
  211. package/dist/ui/html-attachment.js +22 -8
  212. package/dist/ui/panel-chat.js +21 -50
  213. package/dist/ui/panel-log.js +2 -2
  214. package/dist/ui/panel-needs.js +11 -9
  215. package/dist/ui/panel-settings.js +5 -3
  216. package/dist/ui/preview-panel.js +12 -21
  217. package/dist/ui/preview.js +10 -3
  218. package/dist/ui/search-dialog.js +30 -14
  219. package/dist/ui/settings-appearance.js +3 -37
  220. package/dist/ui/settings-confirm.js +18 -0
  221. package/dist/ui/settings-guests.js +13 -4
  222. package/dist/ui/settings-learn.js +1 -1
  223. package/dist/ui/settings-mods.js +5 -12
  224. package/dist/ui/settings-named-test.js +144 -0
  225. package/dist/ui/settings-registry.js +0 -2
  226. package/dist/ui/settings-strings.js +16 -16
  227. package/dist/ui/settings-web-authority.js +4 -3
  228. package/dist/ui/shared/app-i18n.js +15 -4
  229. package/dist/ui/shared/app-needs.js +5 -1
  230. package/dist/ui/shared/app-prefs.js +17 -17
  231. package/dist/ui/shared/app-shell.js +47 -9
  232. package/dist/ui/shared/app-store.js +6 -2
  233. package/dist/ui/shared/app.css +33 -39
  234. package/dist/ui/shared/app.js +5 -4
  235. package/dist/ui/shared/instance-nav.js +9 -9
  236. package/dist/ui/shared/instance-switch.js +8 -2
  237. package/dist/ui/shared/panel-view.js +22 -8
  238. package/dist/ui/shared/view-strings.js +6 -0
  239. package/dist/ui/shared/web-authorize.js +35 -13
  240. package/dist/update-check.d.ts +13 -4
  241. package/dist/update-check.js +17 -4
  242. package/dist/update-check.js.map +1 -1
  243. package/dist/usage/usage-api.d.ts +3 -0
  244. package/dist/usage/usage-api.js +5 -1
  245. package/dist/usage/usage-api.js.map +1 -1
  246. package/dist/view-api.d.ts +2 -0
  247. package/dist/view-api.js +64 -0
  248. package/dist/view-api.js.map +1 -1
  249. package/dist/web-api.d.ts +9 -17
  250. package/dist/web-api.js +257 -371
  251. package/dist/web-api.js.map +1 -1
  252. package/dist/web-appearance-tools.d.ts +0 -2
  253. package/dist/web-appearance-tools.js +2 -5
  254. package/dist/web-appearance-tools.js.map +1 -1
  255. package/dist/web-appearance-view.d.ts +0 -6
  256. package/dist/web-appearance-view.js +0 -1
  257. package/dist/web-appearance-view.js.map +1 -1
  258. package/dist/web-appearance.d.ts +14 -7
  259. package/dist/web-appearance.js +5 -6
  260. package/dist/web-appearance.js.map +1 -1
  261. package/dist/web-asset-parts.d.ts +1 -17
  262. package/dist/web-asset-parts.js +1 -15
  263. package/dist/web-asset-parts.js.map +1 -1
  264. package/dist/web-auth.js +1 -7
  265. package/dist/web-auth.js.map +1 -1
  266. package/dist/web-chat-history.d.ts +3 -7
  267. package/dist/web-chat-history.js +10 -17
  268. package/dist/web-chat-history.js.map +1 -1
  269. package/dist/web-commands.d.ts +9 -1
  270. package/dist/web-commands.js +13 -0
  271. package/dist/web-commands.js.map +1 -1
  272. package/dist/web-experimental.d.ts +3 -8
  273. package/dist/web-experimental.js +9 -33
  274. package/dist/web-experimental.js.map +1 -1
  275. package/dist/web-guest-access.d.ts +40 -2
  276. package/dist/web-guest-access.js +50 -14
  277. package/dist/web-guest-access.js.map +1 -1
  278. package/dist/web-mod-bundle.d.ts +2 -45
  279. package/dist/web-mod-bundle.js +15 -63
  280. package/dist/web-mod-bundle.js.map +1 -1
  281. package/dist/web-mod-store.d.ts +3 -48
  282. package/dist/web-mod-store.js +30 -97
  283. package/dist/web-mod-store.js.map +1 -1
  284. package/dist/web-prefs.d.ts +13 -0
  285. package/dist/web-prefs.js +38 -0
  286. package/dist/web-prefs.js.map +1 -1
  287. package/dist/web-preview.d.ts +6 -13
  288. package/dist/web-preview.js +12 -23
  289. package/dist/web-preview.js.map +1 -1
  290. package/dist/web-session.d.ts +11 -3
  291. package/dist/web-session.js +22 -11
  292. package/dist/web-session.js.map +1 -1
  293. package/dist/web-settings-patch.d.ts +1 -1
  294. package/dist/web-settings-patch.js +9 -7
  295. package/dist/web-settings-patch.js.map +1 -1
  296. package/dist/web-settings-tools.d.ts +0 -13
  297. package/dist/web-settings-tools.js +12 -84
  298. package/dist/web-settings-tools.js.map +1 -1
  299. package/dist/workflow-templates/default.md +2 -2
  300. package/dist/working-recovery.d.ts +0 -2
  301. package/dist/working-recovery.js +2 -23
  302. package/dist/working-recovery.js.map +1 -1
  303. package/launcher/agend-install.cjs +7 -1
  304. package/launcher/fleet-service-owner.cjs +150 -0
  305. package/package.json +1 -1
  306. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
  307. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -673
  308. package/dist/task-board-summary.d.ts +0 -101
  309. package/dist/task-board-summary.js +0 -177
  310. package/dist/task-board-summary.js.map +0 -1
  311. package/dist/ui/app-effects.js +0 -251
  312. package/dist/ui/chat-effect-posters.js +0 -42
  313. package/dist/ui/chat-effects.js +0 -167
  314. package/dist/ui/effect-trial-controller.js +0 -124
  315. package/dist/ui/effect-trial-offer.js +0 -2
  316. package/dist/ui/effect-trial-panel.js +0 -40
  317. package/dist/ui/effects-waker.js +0 -2
  318. package/dist/ui/settings-effects.js +0 -202
  319. package/dist/ui/settings-owner-effects.js +0 -82
  320. package/dist/ui/shared/effect-app-runtime.js +0 -299
  321. package/dist/ui/shared/effect-baseline.js +0 -64
  322. package/dist/ui/shared/effect-chat-runtime.js +0 -162
  323. package/dist/ui/shared/effect-cursor.js +0 -126
  324. package/dist/ui/shared/effect-display-metadata.js +0 -39
  325. package/dist/ui/shared/effect-frame.js +0 -146
  326. package/dist/ui/shared/effect-geometry.js +0 -137
  327. package/dist/ui/shared/effect-host-style.js +0 -10
  328. package/dist/ui/shared/effect-owner-regions.js +0 -74
  329. package/dist/ui/shared/effect-owner-source.js +0 -122
  330. package/dist/ui/shared/effect-policy.js +0 -151
  331. package/dist/ui/shared/effect-poster-reader.js +0 -98
  332. package/dist/ui/shared/effect-posters.js +0 -117
  333. package/dist/ui/shared/effect-profiler.js +0 -184
  334. package/dist/ui/shared/effect-profiles.js +0 -65
  335. package/dist/ui/shared/effect-registry.js +0 -545
  336. package/dist/ui/shared/effect-runtime-loader.js +0 -236
  337. package/dist/ui/shared/effect-safety.js +0 -153
  338. package/dist/ui/shared/effect-stock-snapshot.js +0 -43
  339. package/dist/ui/shared/effect-stock-trust.js +0 -217
  340. package/dist/ui/shared/effect-trial-shadow.js +0 -62
  341. package/dist/ui/shared/effect-trial-source.js +0 -103
  342. package/dist/ui/shared/effect-trial-stage.js +0 -170
  343. package/dist/ui/shared/effect-trial.css +0 -40
  344. package/dist/ui/shared/feature-flags.js +0 -127
  345. package/dist/ui/shared/owner-effect-prefs.js +0 -117
  346. package/dist/upgrade-notices.d.ts +0 -17
  347. package/dist/upgrade-notices.js +0 -93
  348. package/dist/upgrade-notices.js.map +0 -1
  349. package/dist/web-effect-bootstrap.d.ts +0 -12
  350. package/dist/web-effect-bootstrap.js +0 -49
  351. package/dist/web-effect-bootstrap.js.map +0 -1
  352. package/dist/web-effect-bundle.d.ts +0 -28
  353. package/dist/web-effect-bundle.js +0 -171
  354. package/dist/web-effect-bundle.js.map +0 -1
  355. package/dist/web-effect-canonical.d.ts +0 -8
  356. package/dist/web-effect-canonical.js +0 -27
  357. package/dist/web-effect-canonical.js.map +0 -1
  358. package/dist/web-effect-library.d.ts +0 -114
  359. package/dist/web-effect-library.js +0 -530
  360. package/dist/web-effect-library.js.map +0 -1
  361. package/dist/web-effect-playback.d.ts +0 -30
  362. package/dist/web-effect-playback.js +0 -71
  363. package/dist/web-effect-playback.js.map +0 -1
  364. package/dist/web-effect-poster.d.ts +0 -8
  365. package/dist/web-effect-poster.js +0 -52
  366. package/dist/web-effect-poster.js.map +0 -1
  367. package/dist/web-effect-profiles.d.ts +0 -118
  368. package/dist/web-effect-profiles.js +0 -181
  369. package/dist/web-effect-profiles.js.map +0 -1
  370. package/dist/web-effect-service.d.ts +0 -93
  371. package/dist/web-effect-service.js +0 -335
  372. package/dist/web-effect-service.js.map +0 -1
  373. package/dist/web-effect-tools.d.ts +0 -48
  374. package/dist/web-effect-tools.js +0 -129
  375. package/dist/web-effect-tools.js.map +0 -1
  376. package/dist/web-effect-trials.d.ts +0 -86
  377. package/dist/web-effect-trials.js +0 -204
  378. package/dist/web-effect-trials.js.map +0 -1
  379. package/dist/web-effect-upload.d.ts +0 -27
  380. package/dist/web-effect-upload.js +0 -185
  381. package/dist/web-effect-upload.js.map +0 -1
  382. package/dist/web-effect-worker.d.ts +0 -8
  383. package/dist/web-effect-worker.js +0 -170
  384. package/dist/web-effect-worker.js.map +0 -1
  385. package/dist/web-effect.d.ts +0 -103
  386. package/dist/web-effect.js +0 -153
  387. package/dist/web-effect.js.map +0 -1
  388. package/dist/web-experimental-http.d.ts +0 -2
  389. package/dist/web-experimental-http.js +0 -12
  390. package/dist/web-experimental-http.js.map +0 -1
  391. package/dist/web-mod-agent.d.ts +0 -50
  392. package/dist/web-mod-agent.js +0 -207
  393. package/dist/web-mod-agent.js.map +0 -1
@@ -1,167 +0,0 @@
1
- // #1629: Chat contributes a committed lease to the one app owner; no private pool or server polling.
2
- import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
3
- import { register, t } from "/assets/app-i18n.js";
4
- import { appStore, useStore } from "/assets/app-store.js";
5
- import { ownerCssLifecycle } from "/assets/owner-css.js";
6
- import { chatEffectState, chatEffectIdentity } from "/assets/effect-chat-runtime.js";
7
- import { Dialog } from "/assets/ui-dialog.js";
8
- import { navigate } from "/assets/app-nav.js";
9
- import { toast } from "/assets/ui-toast.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";
13
-
14
- register("chatFx", {
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 Settings → Look", 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: "效果已關閉。" },
31
- });
32
-
33
- /** Read/admin dashboard sessions keep management controls; public/guest Chat playback has no owner identity. */
34
- export function chatEffectsMenuAvailable() { return chatEffectIdentity(window) !== null; }
35
-
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/look", 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 }) {
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());
93
- const runtime = useRef(null), [state, setState] = useState(null), [canWrite, setCanWrite] = useState(false), [busy, setBusy] = useState(false);
94
- useLayoutEffect(() => {
95
- let live = true, api = null;
96
- const releases = [];
97
- // Effects are optional: a failed setup must release its partial ownership without breaking Chat.
98
- const cleanup = () => {
99
- if (!live) return;
100
- live = false;
101
- try { api?.dispose?.(); } catch { /* continue releasing partial setup */ }
102
- for (const off of releases) { try { if (typeof off === "function") off(); } catch { /* release the rest */ } }
103
- if (runtime.current === api) runtime.current = null;
104
- };
105
- lease.hold(cleanup);
106
- try {
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
110
- const host = window, owner = getAppEffects(host);
111
- const current = () => live && lease.current();
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; }
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
- };
124
- const changed = value => {
125
- if (!current()) return;
126
- track(value?.rows);
127
- const g = owner.gates();
128
- setState({ ...value, unavailable: !g.enabled || !g.show || !g.transportReady || g.safe || g.reducedMotion });
129
- };
130
- api = owner.attachChat({ name, element: element.current, current,
131
- observe: () => ({ state: chatEffectState(appStore.get(), name) }), onChange: changed });
132
- runtime.current = api;
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(() => {});
135
- api.sync();
136
- return cleanup;
137
- } catch {
138
- cleanup();
139
- if (lease.current()) setState({ error: "runtime_unavailable", rows: [], effects: [] });
140
- return cleanup;
141
- }
142
- }, [name, lease, active, awake]);
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;
150
- const css = cssLife.state !== "off" && effects.some(p => p.by !== "human" && p.manifest.layer === "front");
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); };
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}>
157
- ${state?.error ? html`<p>${t("chatFx.failed")}</p><button type="button" class="btn" onClick=${() => location.reload()}>${t("chatFx.reload")}</button>` : null}
158
- ${css ? html`<p>${t("chatFx.css")}</p><button type="button" class="btn" onClick=${() => { onClose(); navigate("/settings/look"); }}>${t("chatFx.setting")}</button>` : null}
159
- ${!effects.length && !state?.error ? html`<p>${t(state?.unavailable ? "chatFx.unknown" : state?.loading || !state ? "chatFx.loading" : "chatFx.empty")}</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}
163
- <button type="button" class="btn" disabled=${busy} onClick=${() => runtime.current?.pause?.(p.id)}>${t("chatFx.pause")}</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>
165
- ${canWrite ? html`<button type="button" class="btn" disabled=${busy} onClick=${() => remove(p.id)}>${t("chatFx.remove")}</button>` : null}</section>`; })}
166
- </${Dialog}>` : null}`;
167
- }
@@ -1,124 +0,0 @@
1
- /** #1629: explicit-click control lane. Responses never grant authority; Start reauthorizes on the server. */
2
- import { effectTrialMessage } from "./effect-trial-offer.js";
3
- import { checkEffectTrialStart } from "/assets/effect-runtime-loader.js";
4
- import { chatEffectIdentity } from "/assets/effect-chat-runtime.js";
5
- const owners = new WeakSet(), safe = fn => { try { return fn(); } catch { return null; } };
6
- const LIMIT = 128 * 1024;
7
- export function createEffectTrialController({ window: host, current, allowed, onStart, onChange = () => {} }) {
8
- let dead = false, active = null, physical = null, previous = null, broken = false, clockBusy = false, endedReason = null, endedOffer = null;
9
- const cleanup = [], releases = [], identity = safe(() => chatEffectIdentity(host));
10
- const tell = () => { if (!dead) safe(onChange); };
11
- function clock() {
12
- if (broken || clockBusy) { broken = true; return null; }
13
- clockBusy = true; const t = safe(() => host.performance.now()); clockBusy = false;
14
- if (broken || typeof t !== "number" || !Number.isFinite(t) || t < 0 || t > Number.MAX_SAFE_INTEGER || previous !== null && t < previous) { broken = true; return null; }
15
- return previous = t;
16
- }
17
- function eligible(op) {
18
- if (dead || broken || !op || active !== op || op.dead || op.checking || host.AgendAuth?.sessionEnded?.() !== false) return false;
19
- op.checking = true;
20
- try {
21
- const t = clock();
22
- if (t === null || t < op.began || t >= op.until || safe(() => current(op.instance) === true && allowed() === true) !== true) return false;
23
- const after = clock();
24
- return !dead && !op.dead && active === op && after !== null && after >= op.began && after < op.until;
25
- } finally { op.checking = false; }
26
- }
27
- function end(op, reason = "stopped") {
28
- if (!op || op.dead) return false;
29
- op.dead = true; if (active === op) active = null;
30
- endedReason = typeof reason === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(reason) ? reason : "trial_failed";
31
- endedOffer = op.offer.offer;
32
- safe(() => op.resolve?.(false));
33
- safe(() => host.clearTimeout(op.timer)); safe(() => op.controller.abort());
34
- if (op.reply && !op.stopQueued) { op.stopQueued = true; cleanup.push(op); }
35
- // Physical paint is removed before callbacks, cleanup I/O or another explicit click.
36
- safe(() => op.dispose?.()); op.dispose = null;
37
- if (!["stopped", "replaced"].includes(endedReason)) safe(() => host.console?.warn("[AgEnD effects] Try stopped:", endedReason));
38
- tell(); drain(); return true;
39
- }
40
- function arm(op) {
41
- safe(() => host.clearTimeout(op.timer));
42
- if (!eligible(op)) { end(op, "trial_ended"); return; }
43
- const t = clock(); if (t === null || t >= op.until) { end(op, "trial_expired"); return; }
44
- op.timer = host.setTimeout(() => { if (!op.dead) arm(op); }, Math.max(1, Math.ceil(op.until - t)));
45
- }
46
- async function read(response) {
47
- let reader = null;
48
- try {
49
- if (!response || response.redirected || response.status !== 200 || !/^application\/json(?:\s*;|$)/i.test(response.headers.get("content-type") || "")) return null;
50
- const length = response.headers.get("content-length"); if (length !== null && (!/^\d+$/.test(length) || Number(length) > LIMIT)) return null;
51
- reader = response.body?.getReader(); if (!reader) return null;
52
- const chunks = []; let size = 0;
53
- for (;;) { const r = await reader.read(); if (r.done) break; if (!(r.value instanceof Uint8Array) || (size += r.value.byteLength) > LIMIT) return null; chunks.push(r.value); }
54
- const bytes = new Uint8Array(size); let offset = 0; for (const c of chunks) { bytes.set(c, offset); offset += c.byteLength; }
55
- return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
56
- } catch { return null; }
57
- finally { try { if (reader) await reader.cancel(); else await response?.body?.cancel(); } catch { /* retained until cancellation settles */ } }
58
- }
59
- function drain() {
60
- if (physical || !cleanup.length) return;
61
- const op = cleanup.shift(), token = {}; physical = token; const controller = new host.AbortController();
62
- const timer = host.setTimeout(() => controller.abort(), 10000);
63
- // Stop remains permitted after CSS/gate/session changes. It cannot mount or mutate appearance.
64
- Promise.resolve().then(async () => {
65
- const response = await host.fetch(`/ui/effect-trials/${op.reply.trial}/stop?ownerEpoch=${identity.epoch}`, {
66
- method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" }, body: "{}", signal: controller.signal,
67
- });
68
- try { await response.body?.cancel(); } catch { /* no response fields are consumed */ }
69
- }).catch(() => {}).finally(() => { safe(() => host.clearTimeout(timer)); if (physical === token) physical = null; tell(); drain(); });
70
- }
71
- function dispose() {
72
- if (dead) return; dead = true; end(active); for (const off of releases.splice(0)) safe(off); tell();
73
- }
74
- const api = { ok: false, start: async () => false, stop: () => end(active), sync() { const op = active; if (op && !eligible(op)) end(op, "trial_ended"); },
75
- snapshot: () => Object.freeze({ busy: !!physical || cleanup.length > 0, phase: active ? active.reply ? "running" : "starting" : "idle", reason: active ? null : endedReason, offer: active ? active.offer.offer : endedOffer }), dispose };
76
- if (!identity || owners.has(host) || typeof current !== "function" || typeof allowed !== "function" || typeof onStart !== "function"
77
- || typeof host.AgendAuth?.trialSession !== "function" || typeof host.AgendAuth?.onSessionEnd !== "function") return Object.freeze(api);
78
- owners.add(host);
79
- try {
80
- const off = host.AgendAuth.onSessionEnd(dispose);
81
- if (dead) { safe(off); return Object.freeze(api); }
82
- releases.push(off);
83
- for (const [target, type, fn] of [[host, "blur", () => end(active, "trial_window_blur")], [host, "pagehide", dispose], [host.document, "visibilitychange", () => { if (host.document.visibilityState !== "visible") end(active, "hidden"); }]]) {
84
- target.addEventListener(type, fn); releases.push(() => target.removeEventListener(type, fn));
85
- }
86
- } catch { dispose(); return Object.freeze(api); }
87
- if (dead) return Object.freeze(api);
88
- api.ok = true;
89
- api.start = async message => {
90
- if (dead || active || physical || cleanup.length) return false;
91
- const offer = safe(() => effectTrialMessage(message)), instance = safe(() => message.instance), began = clock();
92
- if (!offer || began === null || began + 10000 > Number.MAX_SAFE_INTEGER || safe(() => host.Date.now() < offer.expiresAt) !== true) return false;
93
- const op = { instance, offer, began, until: began + 10000, controller: new host.AbortController(), timer: null, reply: null, dead: false, stopQueued: false, dispose: null };
94
- endedReason = null; endedOffer = null; active = op; const token = {}; physical = token;
95
- const cancelled = new Promise(resolve => { op.resolve = resolve; });
96
- const work = Promise.resolve().then(async () => {
97
- if (!eligible(op)) return false;
98
- arm(op); const session = await host.AgendAuth.trialSession();
99
- if (!eligible(op) || !session || session.localAdmin !== true || session.epoch !== identity.epoch) return false;
100
- const response = await host.fetch(`/ui/effect-trials/start?ownerEpoch=${identity.epoch}`, {
101
- method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" },
102
- body: JSON.stringify({ offer: offer.offer, instance }), signal: op.controller.signal,
103
- });
104
- const reply = checkEffectTrialStart(await read(response), offer.offer, offer.id);
105
- if (!reply) return false;
106
- op.reply = reply;
107
- if (op.dead && !op.stopQueued) { op.stopQueued = true; cleanup.push(op); }
108
- if (reply.offer.title !== offer.title || reply.offer.expiresAt !== offer.expiresAt) return false;
109
- if (!eligible(op)) { if (!op.stopQueued) { op.stopQueued = true; cleanup.push(op); } return false; }
110
- op.until = began + reply.remainingMs;
111
- if (op.until > Number.MAX_SAFE_INTEGER || !eligible(op)) return false;
112
- arm(op);
113
- const handle = Object.freeze({ reply, startedAt: began, instance, current: () => eligible(op), stop: (reason = "stopped") => end(op, reason), });
114
- const disposer = onStart(handle);
115
- if (typeof disposer !== "function") return false;
116
- if (!eligible(op)) { safe(disposer); return false; }
117
- op.dispose = disposer; tell(); return eligible(op);
118
- }).catch(() => false).then(ok => { if (!ok) end(op, "trial_start_failed"); return ok; }).finally(() => { if (physical === token) physical = null; tell(); drain(); });
119
- // The logical caller may stop immediately, while the physical request/cancel still owns the lane.
120
- tell();
121
- return Promise.race([work, cancelled]);
122
- };
123
- return Object.freeze(api);
124
- }
@@ -1,2 +0,0 @@
1
- // On-only Canvas Worker display validation; ordinary chat boot imports no parser.
2
- export { checkEffectTrialOffer, effectTrialMessage } from "/assets/effect-display-metadata.js";
@@ -1,40 +0,0 @@
1
- /** #1629: fixed host producer. Creates no frame or request; the admitted app controller owns its lease. */
2
- import { html, useLayoutEffect, useRef } from "/assets/app-html.js";
3
- import { register, t } from "/assets/app-i18n.js";
4
- import { createEffectTrialShadow } from "/assets/effect-trial-shadow.js";
5
- import { createEffectTrialStage } from "/assets/effect-trial-stage.js";
6
- register("effectTrial", {
7
- en: { label: "Trial", stop: "Stop trial", close: "Close", note: "A bounded sample. Nothing is applied or saved." },
8
- "zh-TW": { label: "試播", stop: "停止試播", close: "關閉", note: "僅在此區塊試播,不會套用或儲存。" },
9
- });
10
- /** The caller owns authenticated admission, lease/deadline and shared pool. No script/HTML is rendered here. */
11
- export function EffectTrialPanel({ window: host = globalThis.window, binding, title, current, onStage, onStop, onClose }) {
12
- const panel = useRef(null), life = useRef(null), latest = useRef(null);
13
- latest.current = { current, onStage, onStop, onClose };
14
- const stop = close => {
15
- const entry = life.current; if (!entry || (close ? entry.closed : entry.dead)) return;
16
- if (close) entry.closed = true;
17
- if (!entry.dead) { entry.dead = true; entry.api?.dispose(); }
18
- try { (close ? entry.onClose : entry.onStop)?.("stopped"); } catch { /* lease already ended */ }
19
- };
20
- useLayoutEffect(() => {
21
- const entry = { dead: false, closed: false, api: null, shadow: null, onStop, onClose }; life.current = entry;
22
- const ended = reason => { if (entry.dead) return; entry.dead = true; try { entry.onStop?.(reason); } catch { /* never revive */ } };
23
- try {
24
- entry.shadow = createEffectTrialShadow({ window: host, panel: panel.current, title,
25
- labels: { label: t("effectTrial.label"), close: t("effectTrial.close"), stop: t("effectTrial.stop"), note: t("effectTrial.note") },
26
- onStop: () => stop(false), onClose: () => stop(true), onError: ended,
27
- onReady(shell) {
28
- if (entry.dead || life.current !== entry) return;
29
- entry.api = createEffectTrialStage({ window: host, panel: panel.current, stage: shell.stage, binding,
30
- current: own => !entry.dead && life.current === entry && latest.current.current?.(own) === true, onStop: ended });
31
- if (!entry.api) ended("stage_unavailable");
32
- else { try { latest.current.onStage?.(entry.api); } catch { entry.api.dispose(); ended("stage_unavailable"); } }
33
- } });
34
- if (!entry.shadow) ended("stage_unavailable");
35
- } catch { entry.api?.dispose(); ended("stage_unavailable"); }
36
- return () => { entry.api?.dispose(); ended("stopped"); entry.shadow?.dispose(); entry.dead = true; if (life.current === entry) life.current = null; };
37
- }, [host, binding]);
38
- useLayoutEffect(() => { life.current?.shadow?.text(title, { label: t("effectTrial.label"), close: t("effectTrial.close"), stop: t("effectTrial.stop"), note: t("effectTrial.note") }); });
39
- return html`<aside ref=${panel} data-trust="preview" class="pv-panel effect-trial-panel" aria-label=${t("effectTrial.label")}></aside>`;
40
- }
@@ -1,2 +0,0 @@
1
- // Compatibility entry for opted-in effect consumers. Product boot uses the neutral feature policy.
2
- export { EMPTY_CHAT_EFFECTS, chatHasEffects, getAppEffects, effectsAwake, effectsLoaded, onEffectsAwake, wakeEffects, startEffectsWaker } from "/assets/feature-flags.js";
@@ -1,202 +0,0 @@
1
- // #1629 B2: library management only. This module never loads Worker code or creates a preview/frame.
2
- import { html, useEffect, useRef, useState } from "/assets/app-html.js";
3
- import { register, t } from "/assets/app-i18n.js";
4
- import { appStore, useStore } from "/assets/app-store.js";
5
- import { confirmDialog } from "/assets/ui-confirm.js";
6
- import { OwnerEffects } from "./settings-owner-effects.js";
7
- import { CardTitle } from "./settings-learn.js";
8
-
9
- register("effectSettings", {
10
- en: {
11
- title: "Effects", lead: "Your effect files, and which one a chat uses.", folder: "Upload an effect folder", files: "effect.json + worker.js (and pictures), up to 1 MiB.",
12
- inUse: "This effect is still in use and cannot be deleted.", quota: "Your library reached its 10-effect or 8 MiB limit. Delete unused copies first.", rateLimited: "Too many changes. Wait a moment, then try again.",
13
- loading: "Loading effects…", retry: "Reload library", empty: "No effects in your library.", failed: "Couldn't load the library. Try again.", actionFailed: "Couldn't save this change. Try again.", savedRefresh: "Saved. Reload the library to see the result.",
14
- uploadFailed: "Couldn't upload this folder. Check its files and size, then try again.", public: "Effect management isn't available on a temporary public link.", readOnly: "Read-only session: sign in as an admin to change effects.",
15
- yours: "Your copy", shared: "Shared with you", share: "Share your copy with agents and people on this fleet", delete: "Delete your copy", deleteConfirm: "Delete your unused copy of this effect? Other people's copies are kept.",
16
- chat: "Chat", chooseChat: "Choose a chat", apply: "Save for this chat", remove: "Remove from this chat", saved: "Saved.", history: "Profile history", unmeasured: "Not measured yet", unavailable: "Profile unavailable", reported: "Reported host observations; they don't prove an effect's cost.",
17
- fps: "Host FPS p50 / p10", tasks: "Host long task p95", noTasks: "Not observed", standard: "Standard device", constrained: "Constrained device", params: "Parameters", badParams: "Check the parameter values.", more: "Show more", count: "{0} effects", one: "1 effect", profileFailed: "Couldn't read profile history. Try again.",
18
- },
19
- "zh-TW": {
20
- title: "效果", lead: "你的效果檔案,以及每個聊天使用哪一個。", folder: "上傳效果資料夾", files: "effect.json + worker.js(可加圖片),最多 1 MiB。",
21
- inUse: "這個效果仍在使用中,無法刪除。", quota: "你的程式庫已達 10 個效果或 8 MiB 上限,請先刪除未使用的副本。", rateLimited: "變更太頻繁,請稍候再試。",
22
- loading: "正在載入效果…", retry: "重新載入程式庫", empty: "程式庫中還沒有效果。", failed: "無法載入程式庫,請重試。", actionFailed: "無法儲存變更,請重試。", savedRefresh: "已儲存,請重新載入程式庫查看結果。",
23
- uploadFailed: "無法上傳這個資料夾,請檢查檔案與大小後重試。", public: "臨時公開連結不提供效果管理。", readOnly: "目前為唯讀登入,請以管理員身分登入才能修改效果。",
24
- yours: "你持有的副本", shared: "他人與你共享", share: "將你的副本共享給這個 fleet 的 agent 與使用者", delete: "刪除你的副本", deleteConfirm: "刪除你未使用的效果副本?其他人的副本會保留。",
25
- chat: "聊天", chooseChat: "選擇聊天", apply: "儲存到這個聊天", remove: "從這個聊天移除", saved: "已儲存。", history: "效能回報紀錄", unmeasured: "尚未測量", unavailable: "無法取得效能回報", reported: "這是主機觀測回報,不能證明效果本身的資源用量。",
26
- fps: "主機 FPS p50/p10", tasks: "主機長任務 p95", noTasks: "未觀測到", standard: "一般裝置", constrained: "受限裝置", params: "參數", badParams: "請檢查參數值。", more: "顯示更多", count: "{0} 個效果", one: "1 個效果", profileFailed: "無法讀取效能回報紀錄,請重試。",
27
- },
28
- });
29
- const tn = (key, ...args) => t(`effectSettings.${key}`, ...args);
30
- const REFUSALS = Object.freeze({ effect_in_use: "inUse", effect_quota: "quota", rate_limited: "rateLimited" });
31
- const refusal = (body, fallback) => tn(typeof body?.error === "string" && Object.hasOwn(REFUSALS, body.error) ? REFUSALS[body.error] : fallback);
32
- const ID = /^[0-9a-f]{32}$/;
33
- const PATH = /^(?:effect\.json|worker\.js|poster\.png|cursor\.png|assets\/[a-z0-9][a-z0-9_-]{0,63}\.(?:png|jpe?g|webp|gif))$/;
34
- const RAW_MAX = 8 * 1024 * 1024, WIRE_MAX = 12 * 1024 * 1024;
35
- const plain = (v) => !!v && typeof v === "object" && Object.getPrototypeOf(v) === Object.prototype;
36
- const publicLink = () => typeof document !== "undefined" && document.body?.dataset.publicLink === "1";
37
-
38
- /** File metadata bounds are checked before reading. No directory path reaches the server. */
39
- export async function encodeEffectFolder(input, signal) {
40
- const files = Array.from(input || []);
41
- if (files.length < 2 || files.length > 20) throw Error("effect_files");
42
- const nested = files.some(f => !!f?.webkitRelativePath), paths = new Set(); let root = null, raw = 0, assets = 0;
43
- const rows = files.map(file => {
44
- if (!file || typeof file.name !== "string" || !Number.isSafeInteger(file.size) || file.size < 0 || (raw += file.size) > RAW_MAX || typeof file.arrayBuffer !== "function") throw Error("effect_files");
45
- let path = file.name;
46
- if (nested) {
47
- const relative = file.webkitRelativePath;
48
- if (typeof relative !== "string" || relative.startsWith("/") || relative.includes("\\")) throw Error("effect_files");
49
- const at = relative.indexOf("/"); if (at < 1) throw Error("effect_files");
50
- const prefix = relative.slice(0, at); if (root === null) root = prefix; if (root !== prefix) throw Error("effect_files");
51
- path = relative.slice(at + 1);
52
- }
53
- if (!PATH.test(path) || paths.has(path) || (path.startsWith("assets/") && ++assets > 16)) throw Error("effect_files");
54
- paths.add(path); return { file, path };
55
- });
56
- if (!paths.has("effect.json") || !paths.has("worker.js")) throw Error("effect_files");
57
- const encoded = [];
58
- for (const { file, path } of rows) {
59
- if (signal?.aborted) throw Error("cancelled");
60
- const buffer = await file.arrayBuffer(); if (signal?.aborted) throw Error("cancelled");
61
- if (!(buffer instanceof ArrayBuffer) || buffer.byteLength !== file.size) throw Error("effect_files");
62
- const bytes = new Uint8Array(buffer), chunks = [];
63
- for (let i = 0; i < bytes.length; i += 8192) chunks.push(String.fromCharCode(...bytes.subarray(i, i + 8192)));
64
- encoded.push({ path, data_b64: btoa(chunks.join("")) });
65
- }
66
- const body = JSON.stringify(encoded); if (new TextEncoder().encode(body).byteLength > WIRE_MAX) throw Error("effect_files");
67
- return body;
68
- }
69
- export function effectCatalog(body) {
70
- if (!plain(body) || typeof body.canWrite !== "boolean" || !Array.isArray(body.effects)) return null;
71
- const seen = new Set(), effects = [];
72
- for (const e of body.effects) {
73
- if (!plain(e) || typeof e.id !== "string" || !ID.test(e.id) || seen.has(e.id) || !plain(e.manifest) || typeof e.manifest.title !== "string" || e.manifest.title.length < 1 || e.manifest.title.length > 40 || !Number.isInteger(e.bytes) || e.bytes < 1 || e.bytes > 1048576 || typeof e.ownClaim !== "boolean" || typeof e.sharedByYou !== "boolean" || !e.ownClaim && e.sharedByYou) return null;
74
- if (effectParamOverrides(e.manifest) === null) return null;
75
- seen.add(e.id); effects.push(e);
76
- }
77
- return { canWrite: body.canWrite, effects };
78
- }
79
- /** UI draft conversion only; the host independently checks the manifest and every override on apply. */
80
- export function effectParamOverrides(manifest, draft = {}) {
81
- if (!plain(manifest) || !plain(draft) || manifest.params !== undefined && !plain(manifest.params)) return null;
82
- const defs = manifest.params || {}, out = Object.create(null);
83
- if (Object.keys(defs).length > 16 || Object.keys(draft).some(k => !Object.hasOwn(defs, k))) return null;
84
- for (const [key, d] of Object.entries(defs)) {
85
- if (!/^[a-z][a-z0-9_]{0,31}$/.test(key) || !plain(d)) return null;
86
- const supplied = Object.hasOwn(draft, key); let v = supplied ? draft[key] : d.default;
87
- if (d.type === "int" || d.type === "number") {
88
- if (supplied && typeof v === "string" && v.trim() !== "") v = Number(v);
89
- if (typeof v !== "number" || !Number.isFinite(v) || !Number.isFinite(d.min) || !Number.isFinite(d.max) || v < d.min || v > d.max || d.type === "int" && !Number.isSafeInteger(v)) return null;
90
- if (d.type === "number" && d.step !== undefined) {
91
- const n = (v - d.min) / d.step;
92
- if (!(d.step > 0) || !Number.isFinite(n) || Math.abs(n - Math.round(n)) > 1e-8) return null;
93
- }
94
- } else if (d.type === "bool") { if (typeof v !== "boolean") return null; }
95
- else if (d.type === "enum") { if (!Array.isArray(d.values) || d.values.length > 16 || typeof v !== "string" || !d.values.includes(v)) return null; }
96
- else if (d.type === "color") { if (typeof v !== "string" || !/^#[0-9a-fA-F]{6}$/.test(v)) return null; v = v.toLowerCase(); }
97
- else return null;
98
- if (v !== d.default) out[key] = v;
99
- }
100
- return out;
101
- }
102
- function Params({ manifest, draft, update, disabled }) {
103
- const defs = plain(manifest.params) ? Object.entries(manifest.params).slice(0, 16) : [];
104
- if (!defs.length) return null;
105
- return html`<fieldset class="s-fx-params" disabled=${disabled}><legend>${tn("params")}</legend>${defs.map(([key, d]) => {
106
- const value = Object.hasOwn(draft, key) ? draft[key] : d.default;
107
- if (d.type === "bool") return html`<label key=${key} class="check"><input data-param=${key} type="checkbox" checked=${value} onChange=${e => update(key, e.target.checked)} />${key}</label>`;
108
- if (d.type === "enum") return html`<label key=${key}>${key}<select data-param=${key} value=${value} onChange=${e => update(key, e.target.value)}>${d.values.map(v => html`<option key=${v} value=${v}>${v}</option>`)}</select></label>`;
109
- return html`<label key=${key}>${key}<input data-param=${key} type=${d.type === "color" ? "color" : "number"} min=${d.min} max=${d.max} step=${d.type === "int" ? 1 : d.step || "any"} value=${value} onInput=${e => update(key, e.target.value)} /></label>`;
110
- })}</fieldset>`;
111
- }
112
- async function call(path, init, signal) {
113
- try {
114
- const res = await fetch(path, { credentials: "same-origin", ...init, signal });
115
- let body = null; try { body = await res.json(); } catch { /* unknown response refuses */ }
116
- return { ok: res.ok && !body?.error, body };
117
- } catch { return { ok: false, body: null }; }
118
- }
119
-
120
- function Profile({ value }) {
121
- if (!value || value.error || !Array.isArray(value.records)) return html`<p class="sub">${tn("unavailable")}</p>`;
122
- if (value.status === "unmeasured" || !value.records.length) return html`<p class="sub">${tn("unmeasured")}</p>`;
123
- return html`<div class="s-fx-profile"><p class="sub">${tn("reported")}</p>${value.records.map((p, i) => {
124
- const valid = p && ["standard", "constrained"].includes(p.device) && [p.fps_p50, p.fps_p10].every(n => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 1000);
125
- if (!valid) return html`<p key=${i} class="sub">${tn("unavailable")}</p>`;
126
- return html`<div key=${i}><strong>${tn(p.device)}</strong><p class="sub">${tn("fps")}: ${p.fps_p50} / ${p.fps_p10} · ${tn("tasks")}: ${typeof p.long_task_p95 === "number" && Number.isFinite(p.long_task_p95) ? `${p.long_task_p95} ms` : tn("noTasks")}</p></div>`;
127
- })}</div>`;
128
- }
129
-
130
- /** Admin changes remain authoritative on the host; canWrite is a display hint only. */
131
- export function EffectsCard() {
132
- const app = useStore(appStore), [view, setView] = useState(null), [error, setError] = useState(null), [busy, setBusy] = useState(false), [message, setMessage] = useState(null);
133
- const [target, setTarget] = useState(""), [limit, setLimit] = useState(50), [history, setHistory] = useState({}), [drafts, setDrafts] = useState({});
134
- const uploadInput = useRef(null);
135
- const life = useRef({ alive: false, seq: 0, controller: null, pending: false });
136
- const current = (seq) => life.current.alive && seq === life.current.seq;
137
- const begin = () => { if (!life.current.alive || life.current.pending || publicLink()) return null; life.current.pending = true; life.current.controller?.abort(); const controller = new AbortController(), seq = ++life.current.seq; life.current.controller = controller; return { seq, signal: controller.signal }; };
138
- const finish = seq => { if (current(seq)) { life.current.pending = false; setBusy(false); } };
139
- const load = async (op) => {
140
- const r = await call("/ui/effects", {}, op.signal); if (!current(op.seq)) return false;
141
- const data = r.ok ? effectCatalog(r.body) : null; setView(data); setHistory({}); setError(data ? null : tn("failed")); return !!data;
142
- };
143
- const reload = async () => { const op = begin(); if (!op) return; setBusy(true); setMessage(null); await load(op); finish(op.seq); };
144
- useEffect(() => {
145
- life.current.alive = true;
146
- if (!publicLink()) reload();
147
- return () => { life.current.alive = false; life.current.seq++; life.current.controller?.abort(); };
148
- }, []);
149
- if (publicLink()) return html`<section class="card s-appearance s-effects"><h3>${tn("title")}</h3><p class="sub">${tn("public")}</p></section>`;
150
- const names = app.ready ? [...new Set((app.instances || []).map(i => i?.name).filter(n => typeof n === "string" && n.length > 0 && n.length <= 128))] : [];
151
- const selected = names.includes(target) ? target : "";
152
- const change = async (path, body, method = "POST") => {
153
- if (busy || !view?.canWrite) return;
154
- const op = begin(); if (!op) return; setBusy(true); setMessage(null);
155
- const r = await call(path, { method, ...(body === undefined ? {} : { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }) }, op.signal);
156
- if (!current(op.seq)) return;
157
- if (!r.ok) setMessage(refusal(r.body, "actionFailed")); else { const refreshed = await load(op); if (current(op.seq)) setMessage(tn(refreshed ? "saved" : "savedRefresh")); }
158
- finish(op.seq);
159
- };
160
- const upload = async (event) => {
161
- const input = event.currentTarget, files = Array.from(input.files || []); input.value = "";
162
- if (busy || !view?.canWrite || !files.length) return;
163
- const op = begin(); if (!op) return; setBusy(true); setMessage(null);
164
- try {
165
- const body = await encodeEffectFolder(files, op.signal); if (!current(op.seq)) return;
166
- const r = await call("/ui/effects/upload", { method: "POST", headers: { "Content-Type": "application/json" }, body }, op.signal);
167
- if (!current(op.seq)) return;
168
- if (!r.ok) setMessage(refusal(r.body, "uploadFailed")); else { const refreshed = await load(op); if (current(op.seq)) setMessage(tn(refreshed ? "saved" : "savedRefresh")); }
169
- } catch { if (current(op.seq)) setMessage(tn("uploadFailed")); }
170
- finish(op.seq);
171
- };
172
- const profile = async (id) => {
173
- if (busy) return; const op = begin(); if (!op) return; setBusy(true);
174
- const r = await call(`/ui/effects/profile/${id}`, {}, op.signal); if (!current(op.seq)) return;
175
- if (r.ok && r.body && Array.isArray(r.body.records) && r.body.records.length <= 40) setHistory(h => ({ ...h, [id]: r.body })); else setMessage(tn("profileFailed"));
176
- finish(op.seq);
177
- };
178
- const askDelete = async id => {
179
- if (!view?.canWrite) return; const op = begin(); if (!op) return; setBusy(true);
180
- const yes = await confirmDialog({ message: tn("deleteConfirm"), confirmLabel: tn("delete"), danger: true });
181
- if (!current(op.seq)) return; finish(op.seq);
182
- if (yes) await change(`/ui/effects/${id}`, undefined, "DELETE");
183
- };
184
- return html`<section class="card s-appearance s-effects"><${CardTitle} title=${tn("title")} topic="effects" /><p class="sub">${tn("lead")}</p>
185
- <div class="s-fx-actions"><button type="button" class="btn" disabled=${busy} onClick=${reload}>${tn("retry")}</button>
186
- ${view?.canWrite ? html`<button class="btn s-fx-choose" type="button" disabled=${busy} onClick=${() => uploadInput.current?.click()}>${tn("folder")}</button><input ref=${uploadInput} class="offscreen s-fx-upload" aria-label=${tn("folder")} tabindex="-1" type="file" webkitdirectory=${true} directory=${true} multiple disabled=${busy} onChange=${upload} />` : null}</div>
187
- ${busy ? html`<p class="sub" role="status">${tn("loading")}</p>` : null}${error ? html`<p class="err" role="status">${error}</p>` : null}${message ? html`<p class="sub" role="status">${message}</p>` : null}
188
- ${view ? html`<p class="sub">${tn(view.effects.length === 1 ? "one" : "count", view.effects.length)}${!view.canWrite ? ` · ${tn("readOnly")}` : ""}</p>` : null}
189
- ${view?.canWrite ? html`<p class="sub">${tn("files")}</p><label class="s-fx-chat">${tn("chat")}<select class="s-fx-target" value=${selected} disabled=${busy} onChange=${e => setTarget(e.target.value)}><option value="">${tn("chooseChat")}</option>${names.map(n => html`<option key=${n} value=${n}>${n}</option>`)}</select></label>` : null}
190
- ${view && !view.effects.length ? html`<p class="sub">${tn("empty")}</p>` : null}
191
- <${OwnerEffects} catalog=${view} busy=${busy} />
192
- <div class="s-fx-list">${(view?.effects || []).slice(0, limit).map(e => html`<article class="s-fx-item" key=${e.id} data-effect-id=${e.id}><h4>${e.manifest.title}</h4><p class="sub"><code>${e.id.slice(0, 8)}</code> · ${Math.ceil(e.bytes / 1024)} KiB · ${tn(e.ownClaim ? "yours" : "shared")}</p>
193
- ${e.manifest.poster === "poster.png" ? html`<img class="s-fx-poster" src=${`/ui/effects/image/${e.id}/poster.png`} alt="" loading="lazy" />` : null}
194
- <${Profile} value=${history[e.id] || e.profiles} />
195
- ${view.canWrite ? html`<${Params} manifest=${e.manifest} draft=${drafts[e.id] || {}} disabled=${busy} update=${(key, value) => setDrafts(s => ({ ...s, [e.id]: { ...(s[e.id] || {}), [key]: value } }))} />` : null}<div class="s-fx-actions"><button class="btn" type="button" disabled=${busy} onClick=${() => profile(e.id)}>${tn("history")}</button>
196
- ${view.canWrite ? html`<button class="btn s-fx-apply" type="button" disabled=${busy || !selected} onClick=${() => { const params = effectParamOverrides(e.manifest, drafts[e.id] || {}); if (params === null) setMessage(tn("badParams")); else change("/ui/effects/apply", { instance: selected, id: e.id, params }); }}>${tn("apply")}</button><button class="btn s-fx-remove" type="button" disabled=${busy || !selected} onClick=${() => change("/ui/effects/remove", { instance: selected, id: e.id })}>${tn("remove")}</button>` : null}
197
- ${view.canWrite && e.ownClaim ? html`<button class="btn s-fx-delete" type="button" disabled=${busy} onClick=${() => askDelete(e.id)}>${tn("delete")}</button>` : null}</div>
198
- ${view.canWrite && e.ownClaim ? html`<label class="check"><input class="s-fx-share" type="checkbox" checked=${e.sharedByYou} disabled=${busy} onChange=${event => change("/ui/effects/share", { id: e.id, shared: event.target.checked })} />${tn("share")}</label>` : null}
199
- </article>`)}</div>
200
- ${view && limit < view.effects.length ? html`<button class="btn" type="button" onClick=${() => setLimit(n => n + 50)}>${tn("more")}</button>` : null}
201
- </section>`;
202
- }