@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
@@ -8,11 +8,11 @@
8
8
  // #1665 first: the host's preferences go into this browser's cache before any module below reads its key.
9
9
  import { bootPrefs } from "./app-prefs.js";
10
10
  import { html, render } from "./app-html.js";
11
- import { Shell, handleKey, showDialog, closeDrawer, openDrawer, shellStore, retryable, setNewInstanceOpener } from "./app-shell.js";
11
+ import { Shell, handleKey, handleSearchKey, showDialog, closeDrawer, openDrawer, shellStore, retryable, setNewInstanceOpener } from "./app-shell.js";
12
12
  import { installSwipe } from "./app-swipe.js";
13
13
  import { startRouter, navStore, navigate } from "./app-nav.js";
14
14
  import { createStream } from "./app-stream.js";
15
- import { appStore, applyStatus, applyActivity } from "./app-store.js";
15
+ import { appStore, applyStatus, applyActivity, guestPage } from "./app-store.js";
16
16
  import { chatPath } from "./app-route.js";
17
17
  import { startNeedsNotifier } from "./app-needs.js";
18
18
  import { toast } from "./ui-toast.js";
@@ -29,6 +29,9 @@ initUsageRail();
29
29
  // session-only panel, nothing under /ui/ is ever imported or read (#1408 §3).
30
30
  const mode = boot.mode === "view-only" ? "view-only" : "full";
31
31
  appStore.set({ viewOnly: mode === "view-only" });
32
+ // #1920 phase 3: a guest's page has its chats and nothing else — no panel, notifier, theme list or owner CSS a guest
33
+ // would be refused.
34
+ const guest = guestPage();
32
35
 
33
36
  // A failed load can be retried (the panel's Retry): each retry asks for a fresh URL, since a browser may keep a failed
34
37
  // module import cached under the first one. boot() runs once, whichever attempt succeeds.
@@ -80,19 +83,24 @@ if (mode === "full") {
80
83
  // a boot import that failed before the Outlet first asked would be retried at once, depending on timing.)
81
84
  let chatLoads = 0;
82
85
  panels.set("chat", { load: () => (chatLoads++ === 0 ? chatBoot : loadChat()).then((m) => m.ChatPanel) });
83
- panels.set("fleet", { load: () => loadFleet().then((m) => m.FleetPanel) });
84
- panels.set("settings", { load: () => loadSettings().then((m) => m.SettingsPanel) });
85
- panels.set("needs", { load: () => loadNeeds().then((m) => m.NeedsPanel) });
86
- // #1523 N2: one instance's Details (/ui/fleet/agent/<name>).
87
- panels.set("details", { load: () => loadDetails().then((m) => m.DetailsPanel) });
88
- // #1218: one instance's Log (/ui/log/<name>).
89
- panels.set("log", { load: () => loadLog().then((m) => m.LogPanel) });
90
- // #1386 §6.3: desktop notifications for new "Needs you" items, in any panel, while this signed-in page is open.
91
- startNeedsNotifier({ open: (instance) => navigate(chatPath(instance)) });
86
+ if (!guest) {
87
+ panels.set("fleet", { load: () => loadFleet().then((m) => m.FleetPanel) });
88
+ panels.set("settings", { load: () => loadSettings().then((m) => m.SettingsPanel) });
89
+ panels.set("needs", { load: () => loadNeeds().then((m) => m.NeedsPanel) });
90
+ // #1523 N2: one instance's Details (/ui/fleet/agent/<name>).
91
+ panels.set("details", { load: () => loadDetails().then((m) => m.DetailsPanel) });
92
+ // #1218: one instance's Log (/ui/log/<name>).
93
+ panels.set("log", { load: () => loadLog().then((m) => m.LogPanel) });
94
+ // #1386 §6.3: desktop notifications for new "Needs you" items, in any panel, while this signed-in page is open.
95
+ startNeedsNotifier({ open: (instance) => navigate(chatPath(instance)) });
96
+ // #2233: a backend sign-in running anywhere is shown on every admin page (the module lives behind the session gate).
97
+ import("/ui/js/backend-signin.js").then((m) => m.installSignIn(), () => {});
98
+ }
92
99
  // #1665: this device's first upload of its own choices (once), then another device's changes at each status frame.
100
+ // A guest's page only reads them (prefsWritable is false there).
93
101
  bootPrefs();
94
- // #1629: agents' themes, read when the status frame's themes_rev moves.
95
- startThemes();
102
+ // #1629: agents' themes, read when the status frame's themes_rev moves (a guest's frames never carry it).
103
+ if (!guest) startThemes();
96
104
  onNewInstance = async () => {
97
105
  closeDrawer(); // on a phone the dialog opens from the drawer: the drawer goes first
98
106
  let m;
@@ -103,8 +111,9 @@ if (mode === "full") {
103
111
  } else appStore.set({ connection: "none" });
104
112
 
105
113
  startRouter(window);
106
- // #1629 r4 §3: the person's owner CSS (full app only; never on Settings, in safe mode or where this device turned it off).
107
- installOwnerCss();
114
+ // #1629 r4 §3: the person's owner CSS (full app only; never on Settings, in safe mode or where this device turned it off;
115
+ // never a guest's — owner CSS is an admin's).
116
+ if (!guest) installOwnerCss();
108
117
  // /ui on its own opens the chat this browser had open last (an unknown name shows the panel's "not found").
109
118
  {
110
119
  const r = navStore.get().route;
@@ -114,6 +123,8 @@ installOwnerCss();
114
123
  }
115
124
 
116
125
  document.addEventListener("keydown", handleKey);
126
+ // #2293: Ctrl+K / ⌘K (and Ctrl+F in a chat) — an admin's full app only; the server refuses a guest's search anyway.
127
+ if (mode === "full" && !guest) document.addEventListener("keydown", handleSearchKey);
117
128
  // #1605: on a phone (the sidebar is a drawer), swipe right in the main area to open it, left to close it — the same
118
129
  // open/close as ☰ and the scrim. Nothing that owns a sideways drag (code, tables, the terminal, inputs, dialogs) and
119
130
  // never the left edge (iOS: "back").
@@ -11,8 +11,12 @@ register("card", {
11
11
  sizeSName: "Small", sizeMName: "Medium", sizeLName: "Large",
12
12
  byHuman: "set by a person",
13
13
  byAgent: "set by the agent",
14
+ menu: "Identity card",
14
15
  show: "Show card",
15
16
  hide: "Hide card",
17
+ showRow: "Show picture and bio",
18
+ bannerSafe: "Every screen",
19
+ bannerPhone: "Phone · M card",
16
20
  },
17
21
  "zh-TW": {
18
22
  label: "{0} 的身分卡",
@@ -22,7 +26,11 @@ register("card", {
22
26
  sizeSName: "小", sizeMName: "中", sizeLName: "大",
23
27
  byHuman: "由人設定",
24
28
  byAgent: "由 agent 設定",
29
+ menu: "身分卡",
25
30
  show: "顯示卡片",
26
31
  hide: "隱藏卡片",
32
+ showRow: "顯示小圖和介紹",
33
+ bannerSafe: "所有螢幕",
34
+ bannerPhone: "手機 · 中卡片",
27
35
  },
28
36
  });
@@ -1,4 +1,5 @@
1
1
  /** #1629: app-owned pool; Chat contributes a lease, never another registry/loader. */
2
+ import { effectPlaybackReason, EFFECT_BLOCK_REASONS } from "./effect-policy.js";
2
3
  import { createEffectTrialSource } from "./effect-trial-source.js";
3
4
  import { createOwnerEffectSource } from "./effect-owner-source.js";
4
5
  import { createEffectRuntimeLoader } from "./effect-runtime-loader.js";
@@ -6,7 +7,7 @@ import { createEffectRegistry } from "./effect-registry.js";
6
7
  import { createEffectSafety } from "./effect-safety.js";
7
8
  import { stockEffectTrust } from "./effect-stock-trust.js";
8
9
  import { createEffectProfileCollector } from "./effect-profiles.js";
9
- import { chatEffectIdentity, createEffectWriter } from "./effect-chat-runtime.js";
10
+ import { effectPlaybackIdentity, createEffectWriter } from "./effect-chat-runtime.js";
10
11
 
11
12
  const episodes = new WeakSet();
12
13
 
@@ -14,7 +15,8 @@ const episodes = new WeakSet();
14
15
  export function createAppEffectRuntime({ window: host, current, observe, onChange = () => {}, onDispose = () => {}, resolveOwnerTarget = null, onOwnerUnavailable = () => {}, factories = {} }) {
15
16
  let dead = false, error = null, loader = null, registry = null, writer = null, source = null, personal = null, trial = null, pending = null, lastMetadata = null, serial = 0, revision = 0, stamp = null, syncing = false, queued = false;
16
17
  const bindings = new Map(), profiles = new Map();
17
- const identity = chatEffectIdentity(host), auth = host?.AgendAuth;
18
+ let lastContext = null, playbackUnavailable = null;
19
+ const identity = effectPlaybackIdentity(host), auth = host?.AgendAuth;
18
20
  const safe = fn => { try { return fn(); } catch { return null; } };
19
21
  const alive = () => { if (dead) return false; const value = safe(() => current() === true && !auth.sessionEnded()); return !dead && value === true; };
20
22
  const sourceAlive = s => {
@@ -25,15 +27,49 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
25
27
  const placementIdentity = p => JSON.stringify([p.by, p.manifest.layer, p.params]);
26
28
  const clock = () => host.performance.now();
27
29
  let offAuth = null;
28
- const snapshot = s => ({ error: error ?? s?.error ?? null, rows: registry?.snapshot().filter(row => s?.bindings.get(row.id)?.key === row.key) ?? [], effects: s?.metadata?.effects ?? [], loading: !!s && !s.dead && s.metadata === null && !s.error });
30
+ const snapshot = s => {
31
+ const effects = s?.metadata?.effects ?? [], known = registry?.snapshot?.() ?? [];
32
+ const rows = effects.map(p => {
33
+ const b = s.bindings.get(p.id), row = known.find(r => b && r.key === b.key && r.id === p.id);
34
+ if (row && !s.error && !playbackUnavailable) return row.reason == null && row.phase == null ? { ...row, phase: "loading" } : row;
35
+ return { id: p.id, key: b?.key ?? null, verdict: "off", phase: null, quality: null,
36
+ reason: s.error ?? playbackUnavailable ?? effectPlaybackReason(lastContext) ?? (!p.manifest.regions.includes("chat") ? "region_unavailable" : "runtime_unavailable") };
37
+ });
38
+ return { error: error ?? s?.error ?? null, rows, effects, loading: !!s && !s.dead && s.metadata === null && !s.error };
39
+ };
29
40
  function tell() {
30
41
  const s = source, value = snapshot(s); if (dead) return;
31
- if (sourceAlive(s)) safe(() => s.onChange(value));
42
+ if (sourceAlive(s)) {
43
+ const next = JSON.stringify(value);
44
+ if (next !== s.lastSnapshot) { s.lastSnapshot = next; safe(() => s.onChange(value)); }
45
+ }
32
46
  if (!dead) safe(() => onChange(snapshot(source)));
33
47
  }
48
+ function reportPlayback(s) {
49
+ if (!sourceAlive(s) || identity.scope !== "owner") return;
50
+ const now = safe(clock);
51
+ if (!Number.isFinite(now) || now < 0 || s.reportClock !== null && now < s.reportClock) { s.reporter.dispose(); return; }
52
+ s.reportClock = now;
53
+ const value = snapshot(s);
54
+ for (const id of s.reported.keys()) if (!value.effects.some(p => p.id === id)) s.reported.delete(id);
55
+ for (const row of value.rows) {
56
+ if (!sourceAlive(s)) return;
57
+ const status = row.phase === "running" && row.reason == null ? "playing" : "blocked";
58
+ const reason = status === "playing" ? null : EFFECT_BLOCK_REASONS.includes(row.reason) ? row.reason : "unknown";
59
+ const device = lastContext?.constrained === true ? "constrained" : "standard";
60
+ const next = JSON.stringify([status, reason, device]), last = s.reported.get(row.id);
61
+ if (last && (now - last.at < 10000 || next === last.value && now - last.at < 60000)) continue;
62
+ s.reported.set(row.id, { at: now, value: next });
63
+ void s.reporter.post(`/ui/effects/profile/${row.id}?instance=${encodeURIComponent(s.name)}&ownerEpoch=${identity.epoch}`,
64
+ { v: 1, kind: "playback", device, status, reason });
65
+ }
66
+ // No server polling or unbounded retries: at most one observation per effect / 10s, a 60s heartbeat.
67
+ if (sourceAlive(s)) s.reportTimer = host.setTimeout(() => reportPlayback(s), 10000);
68
+ }
34
69
  function discard(s) {
35
70
  if (!s || s.dead) return;
36
71
  s.dead = true; s.generation++; s.metadata = null;
72
+ safe(() => host.clearTimeout(s.reportTimer)); safe(() => s.reporter?.dispose());
37
73
  if (source === s) source = null;
38
74
  safe(() => trial?.stopFor(s));
39
75
  for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear();
@@ -63,7 +99,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
63
99
  writer = createEffectWriter(host, alive);
64
100
  loader = make("loader", createEffectRuntimeLoader)({ window: host, ownerEpoch: identity.epoch, current: alive, onRevoke: () => fail("runtime_unavailable") });
65
101
  if (!loader.ok || typeof loader.placementsSettled !== "function" || typeof loader.resourceSettled !== "function") throw Error("runtime_unavailable");
66
- registry = make("registry", createEffectRegistry)({ window: host, origin: host.document.body.dataset.previewOrigin, boot: host.document.body.dataset.previewBoot,
102
+ registry = make("registry", createEffectRegistry)({ window: host, origin: host.location?.origin, boot: host.document.body.dataset.effectBoot,
67
103
  load: async (descriptor, signal) => {
68
104
  const b = bindingFor(descriptor.key, descriptor.id);
69
105
  if (!bindingAlive(b) || descriptor.id !== b.id || signal.aborted) return null;
@@ -78,7 +114,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
78
114
  stockSnapshot: ({ frameRoot }) => make("stock", stockEffectTrust)(host, { frameRoot, ownerCss: observe().ownerCss }),
79
115
  onChange() { try { tell(); emit(); } catch { fail("runtime_unavailable"); } },
80
116
  onMount(owner) {
81
- const b = bindingFor(owner.key, owner.id); if (!bindingAlive(b) || b.id !== owner.id || !owner.current()) return;
117
+ const b = bindingFor(owner.key, owner.id); if (identity.scope !== "owner" || !bindingAlive(b) || b.id !== owner.id || !owner.current()) return;
82
118
  const collector = make("collector", createEffectProfileCollector)({ key: owner.key, id: owner.id, device: owner.device, clock });
83
119
  const began = safe(clock);
84
120
  if (collector.ok && Number.isFinite(began) && began >= 0 && bindingAlive(b) && owner.current()) profiles.set(owner, { binding: b, collector, began, attempted: null }); else collector.dispose?.();
@@ -95,17 +131,23 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
95
131
  void writer.post(`/ui/effects/profile/${owner.id}?${query}ownerEpoch=${identity.epoch}`, report.profile);
96
132
  }, clock,
97
133
  });
98
- if (!registry.ok) throw Error("runtime_unavailable");
99
- trial = make("trialSource", createEffectTrialSource)({ window: host, current: () => {
134
+ if (!registry.ok) {
135
+ // Keep authenticated placement metadata available for explanations. This inert adapter cannot create,
136
+ // load, resume, send to, or authorize a frame; a failed paint subsystem is never permission.
137
+ playbackUnavailable = "unsupported_host";
138
+ safe(() => registry?.dispose?.());
139
+ registry = { ok: true, frameRoot: null, update() {}, snapshot: () => [], publish() {}, resume: () => false, dispose() {} };
140
+ }
141
+ if (identity.scope === "owner") trial = make("trialSource", createEffectTrialSource)({ window: host, current: () => {
100
142
  if (!alive()) return false; const v = observe(), c = v.context;
101
- return alive() && v.ownerCss === "off" && c.enabled === true && c.show === true && c.previewReady === true && c.optIn === true
102
- && c.safe === false && c.publicLink === false && c.reducedMotion === false && c.saveData === false && c.hidden === false;
143
+ return alive() && c.enabled === true && c.show === true && c.transportReady === true
144
+ && c.safe === false && c.publicLink === false && c.reducedMotion === false && c.hidden === false;
103
145
  }, ownerCurrent: (s, name) => sourceAlive(s) && s.name === name,
104
146
  stock: () => registry.frameRoot ? make("stock", stockEffectTrust)(host, { frameRoot: registry.frameRoot, ownerCss: observe().ownerCss }) : null,
105
147
  readResource: (spec, signal) => typeof loader.trialResourceSettled === "function" ? loader.trialResourceSettled(spec, signal) : Promise.resolve(null),
106
148
  onChange() { revision++; stamp = null; sync(); tell(); },
107
149
  });
108
- if (typeof resolveOwnerTarget === "function") {
150
+ if (identity.scope === "owner" && typeof resolveOwnerTarget === "function") {
109
151
  if (typeof loader.ownerSettled !== "function") throw Error("runtime_unavailable");
110
152
  personal = createOwnerEffectSource({ current: alive,
111
153
  readMetadata: signal => loader.ownerSettled(signal), readResource: (spec, signal) => loader.resourceSettled(spec, signal), resolveTarget: resolveOwnerTarget,
@@ -139,7 +181,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
139
181
  }
140
182
  function refuseSource(s) {
141
183
  if (!sourceAlive(s)) return;
142
- s.error = "resource_unavailable"; s.metadata = null; s.generation++;
184
+ s.error = "resource_unavailable"; s.generation++;
143
185
  for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear(); revision++; stamp = null; sync(); tell();
144
186
  }
145
187
  function queueSync() {
@@ -156,6 +198,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
156
198
  if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
157
199
  const entries = [...(personal?.descriptors() ?? []), ...(trial?.descriptors() ?? []), ...descriptors(s)];
158
200
  if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
201
+ lastContext = { ...value.context };
159
202
  const next = JSON.stringify([revision, value.context, entries.map(d => [d.key, d.paused])]);
160
203
  if (stamp !== next) {
161
204
  if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
@@ -164,12 +207,15 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
164
207
  if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
165
208
  emit();
166
209
  const c = value.context;
167
- if (!pending && c.enabled && c.show && c.previewReady && c.optIn && !c.safe && !c.publicLink && !c.reducedMotion && !c.saveData && !c.hidden) {
168
- const ownerNeeds = personal?.needsMetadata() === true, chatNeeds = sourceAlive(s) && !s.error && !s.metadata;
210
+ // Metadata describes placements, not executable resources. Its existing session/epoch/current and
211
+ // shared settled-lane fences apply even when this viewer has declined playback. No code is fetched here.
212
+ if (!pending) {
213
+ const ownerNeeds = identity.scope === "owner" && !c.publicLink && c.enabled && c.show && c.transportReady && !c.safe && !c.reducedMotion && !c.hidden && personal?.needsMetadata() === true, chatNeeds = sourceAlive(s) && !s.error && !s.metadata;
169
214
  if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
170
215
  if (ownerNeeds && (!chatNeeds || lastMetadata !== "owner")) void readPersonal();
171
216
  else if (chatNeeds) void read(s);
172
217
  }
218
+ tell();
173
219
  } catch { fail("runtime_unavailable"); }
174
220
  finally { syncing = false; }
175
221
  }
@@ -216,14 +262,17 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
216
262
  for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear();
217
263
  if (pending?.source === s) safe(() => pending.controller.abort()); revision++; stamp = null; sync(); tell();
218
264
  }
219
- function publishReply(s, event) { if (sourceAlive(s) && !s.error && event.instance === s.name) registry.publish("reply", {}, s.name); }
265
+ function publishReply(s, event) { if (host.document.body.dataset.webTransport !== "poll" && sourceAlive(s) && !s.error && event.instance === s.name) registry.publish("reply", {}, s.name); }
220
266
  function attachChat(options) {
221
267
  discard(source); const expected = revision; sync();
222
268
  if (!alive() || revision !== expected || source !== null) return null;
223
269
  const { name, element, current: ownCurrent, observe: ownObserve, onChange: ownChange = () => {} } = options ?? {};
224
270
  if (!alive() || typeof name !== "string" || name.length < 1 || name.length > 128 || !element || element.isConnected !== true || typeof ownCurrent !== "function" || typeof ownObserve !== "function" || safe(() => ownCurrent() === true) !== true || revision !== expected || source !== null) return null;
225
271
  const s = { name, element, current: ownCurrent, observe: ownObserve, onChange: ownChange, serial: ++serial, generation: 0, dead: false, error: null, metadata: null, bindings: new Map(), paused: new Map() };
226
- source = s; revision++; stamp = null;
272
+ source = s;
273
+ s.reporter = createEffectWriter(host, () => sourceAlive(s)); s.reported = new Map(); s.reportClock = null;
274
+ s.reportTimer = host.setTimeout(() => reportPlayback(s), 10000);
275
+ revision++; stamp = null;
227
276
  const placement = id => sourceAlive(s) && !s.error ? s.metadata?.effects.find(p => p.id === id) : null;
228
277
  const changed = () => changedSource(s);
229
278
  const api = { ok: true, sync,
@@ -231,14 +280,14 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
231
280
  reply(event) { publishReply(s, event); },
232
281
  pause(id) { const p = placement(id); if (!p) return false; s.paused.set(id, placementIdentity(p)); revision++; sync(); return true; },
233
282
  resume(id) { const p = placement(id); if (!p) return false; s.paused.delete(id); revision++; sync(); const b = s.bindings.get(id); return bindingAlive(b) && registry.resume(b.key); },
234
- async remove(id) { const p = placement(id); if (!p) return false; const generation = s.generation; const ok = await writer.post("/ui/effects/remove", { instance: s.name, id }); if (!sourceAlive(s) || generation !== s.generation) return false; if (ok) changed(); return ok; },
283
+ async remove(id) { const p = placement(id); if (identity.scope !== "owner" || !p) return false; const generation = s.generation; const ok = await writer.post("/ui/effects/remove", { instance: s.name, id }); if (!sourceAlive(s) || generation !== s.generation) return false; if (ok) changed(); return ok; },
235
284
  snapshot: () => sourceAlive(s) ? snapshot(s) : { error: null, rows: [], effects: [], loading: false },
236
285
  dispose() { if (source !== s) return; discard(s); sync(); },
237
286
  };
238
287
  sync(); tell(); return sourceAlive(s) ? api : null;
239
288
  }
240
- return { ok: true, sync, attachChat, changedOwner, ownerSnapshot,
241
- attachTrial(options) { const s = source; return sourceAlive(s) ? trial.attach(options, s) : null; },
289
+ return { ok: true, playbackAvailable: playbackUnavailable === null, sync, attachChat, changedOwner, ownerSnapshot,
290
+ attachTrial(options) { const s = source; return trial && !playbackUnavailable && sourceAlive(s) ? trial.attach(options, s) : null; },
242
291
  stopTrial() { trial?.stop(); },
243
292
  trialSnapshot() { return { rows: registry?.snapshot().filter(row => trial?.binding(row.key, row.id)) ?? [] }; },
244
293
  cursorSelection: () => personal?.cursorSelection() ?? null, cursorImageSettled,
@@ -46,10 +46,23 @@ export function createEffectWriter(host, current) {
46
46
  export function chatEffectIdentity(host) {
47
47
  try {
48
48
  const b = host.document.body.dataset, x = JSON.parse(host.document.documentElement.getAttribute("data-prefs") || "null");
49
- if (b.mode !== "full" || b.publicLink === "1" || b.webTransport === "poll" || !["person", "operator"].includes(x?.ownerScope) || x.unreadable === true || typeof x.ownerEpoch !== "string" || !/^[a-f0-9]{16}$/.test(x.ownerEpoch)) return null;
49
+ if (b.mode !== "full" || b.tier === "guest" || b.publicLink === "1" || b.webTransport === "poll" || !["person", "operator"].includes(x?.ownerScope) || x.unreadable === true || typeof x.ownerEpoch !== "string" || !/^[a-f0-9]{16}$/.test(x.ownerEpoch)) return null;
50
50
  return Object.freeze({ epoch: x.ownerEpoch, scope: x.ownerScope });
51
51
  } catch { return null; }
52
52
  }
53
+ /** Chat-only public/guest capability stays separate from personal preferences and management. */
54
+ export function effectPlaybackIdentity(host) {
55
+ try {
56
+ const b = host.document.body.dataset;
57
+ if (b.mode !== "full" || typeof b.effectEpoch !== "string" || !/^[a-f0-9]{16}$/.test(b.effectEpoch)) return null;
58
+ if (b.effectScope === "owner") {
59
+ const own = chatEffectIdentity(host);
60
+ return own?.epoch === b.effectEpoch ? Object.freeze({ epoch: b.effectEpoch, scope: "owner" }) : null;
61
+ }
62
+ if (b.effectScope === "chat" && (b.publicLink === "1" && b.webTransport === "poll" || b.tier === "guest")) return Object.freeze({ epoch: b.effectEpoch, scope: "chat" });
63
+ } catch { /* no session capability */ }
64
+ return null;
65
+ }
53
66
  export function chatEffectState(state, name) {
54
67
  const inst = state.instances?.find(i => i.name === name);
55
68
  if (!inst) return null;
@@ -81,7 +94,7 @@ export function createChatEffectRuntime({ window: host, name, element, current,
81
94
  writer = createEffectWriter(host, alive);
82
95
  loader = make("loader", createEffectRuntimeLoader)({ window: host, ownerEpoch: identity.epoch, current: alive });
83
96
  if (!loader.ok) throw Error("runtime_unavailable");
84
- registry = make("registry", createEffectRegistry)({ window: host, origin: host.document.body.dataset.previewOrigin, boot: host.document.body.dataset.previewBoot,
97
+ registry = make("registry", createEffectRegistry)({ window: host, origin: host.location?.origin, boot: host.document.body.dataset.effectBoot,
85
98
  load: async (d, signal) => {
86
99
  const mine = generation, value = await loader.resource({ id: d.id, instance: name }, signal);
87
100
  if (!alive() || mine !== generation || signal.aborted) return null;
@@ -125,7 +138,7 @@ export function createChatEffectRuntime({ window: host, name, element, current,
125
138
  const stamp = JSON.stringify(ctx);
126
139
  if (stamp !== lastContext) { lastContext = stamp; registry.update(descriptors(), ctx); }
127
140
  emit();
128
- if (!metadata && !active && ctx.enabled && ctx.show && ctx.previewReady && ctx.optIn && !ctx.safe && !ctx.publicLink && !ctx.reducedMotion && !ctx.saveData && !ctx.hidden) void read();
141
+ if (!metadata && !active && ctx.enabled && ctx.show && ctx.transportReady && !ctx.safe && !ctx.publicLink && !ctx.reducedMotion && !ctx.hidden) void read();
129
142
  }
130
143
  async function read() {
131
144
  if (!alive() || error || active) return;
@@ -30,16 +30,35 @@ function snapshotRender(raw) {
30
30
  return { code: raw.code, params: { ...params }, instance: raw.instance, width: raw.width, height: raw.height, dpr: raw.dpr, assets, wants: [...raw.wants] };
31
31
  }
32
32
 
33
+ /** Listener transport only; this does not grant HTML-preview or ordinary-effect consent. */
34
+ export function effectTransportReady(host, origin, boot) {
35
+ try {
36
+ const target = new URL(origin), page = new URL(host.location.origin);
37
+ return origin === target.origin && ["http:", "https:"].includes(target.protocol) && ["http:", "https:"].includes(page.protocol)
38
+ && target.origin !== page.origin && !(page.protocol === "https:" && target.protocol !== "https:") && typeof boot === "string" && /^[0-9a-f]{32}$/.test(boot);
39
+ } catch { return false; }
40
+ }
41
+
42
+ /** #2118: same URL origin, opaque realm. This helper does not alter product admission or HTML preview consent. */
43
+ export function effectBootstrapReady(host, origin, boot) {
44
+ try {
45
+ const target = new URL(origin), page = new URL(host.location.origin);
46
+ return origin === target.origin && target.origin === page.origin && ["http:", "https:"].includes(target.protocol)
47
+ && typeof boot === "string" && /^[0-9a-f]{32}$/.test(boot);
48
+ } catch { return false; }
49
+ }
50
+
33
51
  /**
34
52
  * All options come from the host, never from an agent event or manifest origin. The registry supplies an admitted
35
53
  * root and geometry in a later integration. This primitive alone does not implement caps, clipping or profiling.
36
54
  */
37
- export function createEffectFrame({ window: host, root, origin, boot, render, clock = () => performance.now(), onReady = () => {}, onRemove = () => {} } = {}) {
38
- let url;
39
- try { url = new URL(origin); } catch { return { ok: false, reason: "preview_unknown" }; }
40
- if (origin !== url.origin || !["http:", "https:"].includes(url.protocol) || typeof boot !== "string" || !/^[0-9a-f]{32}$/.test(boot)) return { ok: false, reason: "preview_unknown" };
41
- let hostOrigin; try { hostOrigin = new URL(host.location.origin); } catch { return { ok: false, reason: "preview_unknown" }; }
42
- if (!["http:", "https:"].includes(hostOrigin.protocol) || origin === hostOrigin.origin || hostOrigin.protocol === "https:" && url.protocol !== "https:") return { ok: false, reason: "preview_unknown" };
55
+ export function createEffectFrame(options = {}) { return ownedEffectFrame(options, false); }
56
+
57
+ /** Product transport: the same-listener trusted document still has an opaque sandboxed realm. */
58
+ export function createOpaqueEffectFrame(options = {}) { return ownedEffectFrame(options, true); }
59
+
60
+ function ownedEffectFrame({ window: host, root, origin, boot, render, clock = () => performance.now(), onReady = () => {}, onRemove = () => {} } = {}, sameListener) {
61
+ if (!(sameListener ? effectBootstrapReady : effectTransportReady)(host, origin, boot)) return { ok: false, reason: sameListener ? "bootstrap_unknown" : "preview_unknown" };
43
62
  const payload = snapshotRender(render);
44
63
  if (!payload || !host || !root || root.ownerDocument !== host.document || typeof root.append !== "function" || typeof host.MessageChannel !== "function" || typeof clock !== "function" || typeof onReady !== "function" || typeof onRemove !== "function") return { ok: false, reason: "unsupported" };
45
64
  let mounted;
@@ -117,7 +136,7 @@ export function createEffectFrame({ window: host, root, origin, boot, render, cl
117
136
  host.addEventListener("message", hello);
118
137
  frame.addEventListener("load", load);
119
138
  timer = host.setTimeout(deadline, 10000);
120
- try { frame.src = `${origin}/effect`; root.append(frame); } catch { remove("mount_failed"); }
139
+ try { frame.src = origin + (sameListener ? "/effect-bootstrap" : "/effect"); root.append(frame); } catch { remove("mount_failed"); }
121
140
  return { ok: true, element: frame, state, remove,
122
141
  send(event) {
123
142
  if (!ready || !current() || !maySendEffectEvent(event, { instance: payload.instance, wants: payload.wants })) return false;
@@ -70,36 +70,51 @@ export function createEffectGeometry({ window: host } = {}) {
70
70
  frame.remove();
71
71
  return { ok: false, reason: "geometry_unknown" };
72
72
  }
73
- let dead = false;
73
+ let dead = false, shown = false;
74
74
  const hide = () => {
75
+ shown = false;
75
76
  try {
76
77
  rule.style.setProperty("visibility", "hidden", "important");
77
- if (frame.isConnected === true && host.getComputedStyle(frame).visibility !== "hidden") frame.remove();
78
- } catch { frame.remove(); }
78
+ if (frame.isConnected === true && host.getComputedStyle(frame).visibility !== "hidden") { frame.remove(); return false; }
79
+ return true;
80
+ } catch { frame.remove(); return false; }
79
81
  };
80
82
  const handle = { ok: true, hide,
81
83
  /** Same-turn host paint snapshot is mandatory, including behind/human placements. No DOM fallback. */
82
84
  sync(rect, stock) {
83
- hide();
85
+ const refuse = () => { hide(); return false; };
84
86
  const proof = effectStockSnapshot(stock);
85
- if (!effectStockAllows(proof, { layer, by })) return false;
86
- if (dead || disposed || !hostEffectSpace(host, frame) || !rect || ![rect.x, rect.y, rect.width, rect.height].every(number) || ![rect.x, rect.y, rect.width, rect.height].every(Number.isInteger) || rect.width < 1 || rect.height < 1 || rect.width > 4096 || rect.height > 4096 || rect.width * rect.height > 8388608) { hide(); return false; }
87
+ if (!effectStockAllows(proof, { layer, by })) return refuse();
88
+ if (dead || disposed || !hostEffectSpace(host, frame) || !rect || ![rect.x, rect.y, rect.width, rect.height].every(number) || ![rect.x, rect.y, rect.width, rect.height].every(Number.isInteger) || rect.width < 1 || rect.height < 1 || rect.width > 4096 || rect.height > 4096 || rect.width * rect.height > 8388608) return refuse();
87
89
  let path = "none";
88
90
  if (layer === "front" && by !== "human") {
89
91
  const clipped = effectTrustClip(rect, proof.rects);
90
- if (!clipped.ok) return false;
92
+ if (!clipped.ok) return refuse();
91
93
  path = clipped.path;
92
94
  }
93
95
 
94
96
  try {
97
+ const matches = visibility => {
98
+ const style = host.getComputedStyle(frame), actual = frame.getBoundingClientRect();
99
+ return style.position === "fixed" && style.pointerEvents === "none" && style.visibility === visibility
100
+ && style.opacity === (layer === "front" && by !== "human" ? "0.85" : "1") && style.zIndex === (layer === "front" ? "20" : "0")
101
+ && sameClip(path, rule.style.getPropertyValue("clip-path")) && sameClip(path, style.clipPath)
102
+ && ["x", "y", "width", "height"].every(key => number(actual[key]) && Math.abs(actual[key] - rect[key]) < 0.02)
103
+ && frame.isConnected === true && frame.ownerDocument === host.document;
104
+ };
105
+ // A fresh proof and actual viewport/clip are still checked every frame. Rewriting an
106
+ // already verified presentation makes WebKit repeatedly invalidate the iframe's paint.
107
+ if (shown && matches("visible")) return true;
108
+ // Changed or unproven geometry is hidden before writes and verified before reveal.
109
+ if (!hide()) return false;
95
110
  for (const key of ["x", "y", "width", "height"]) rule.style.setProperty(key === "x" ? "left" : key === "y" ? "top" : key, `${rect[key]}px`, "important");
96
111
  rule.style.setProperty("clip-path", path, "important");
97
112
  if (!sameClip(path, rule.style.getPropertyValue("clip-path"))) return false;
98
113
  // The stylesheet can lose the cascade to another host rule. Verify the actual viewport and clip
99
114
  // before revealing: holes expressed in planned coordinates cannot protect a transformed frame.
100
- const style = host.getComputedStyle(frame), actual = frame.getBoundingClientRect();
101
- if (style.position !== "fixed" || style.pointerEvents !== "none" || style.visibility !== "hidden" || style.opacity !== (layer === "front" && by !== "human" ? "0.85" : "1") || style.zIndex !== (layer === "front" ? "20" : "0") || !sameClip(path, style.clipPath) || !["x", "y", "width", "height"].every(key => number(actual[key]) && Math.abs(actual[key] - rect[key]) < 0.02)) return false;
115
+ if (!matches("hidden")) return false;
102
116
  rule.style.setProperty("visibility", "visible", "important");
117
+ shown = true;
103
118
  return true;
104
119
  } catch { hide(); return false; }
105
120
  },
@@ -74,7 +74,14 @@ export function createOwnerEffectSource({ current, readMetadata, readResource, r
74
74
  },
75
75
  descriptors() {
76
76
  if (!alive() || !metadata || error) return [];
77
- const body = metadata, gen = generation, proof = safe(() => effectStockSnapshot(stock()));
77
+ const body = metadata, gen = generation;
78
+ // #2188: the stock snapshot walks the whole page (thousands of computed styles). With no slot holding an effect
79
+ // that could play, there is nothing for it to admit: skipped — every page change (a status, a chat switch) ran it.
80
+ if (!SLOTS.some(slot => body.slots[slot]?.status === "available" && failed.get(slot) !== body.slots[slot].id)) {
81
+ for (const slot of SLOTS) drop(slot);
82
+ return [];
83
+ }
84
+ const proof = safe(() => effectStockSnapshot(stock()));
78
85
  if (!fresh(body, gen)) return [];
79
86
  if (!proof) { clear(); return []; }
80
87
  const result = [];
@@ -58,23 +58,35 @@ export function constrainedDevice(signals) {
58
58
  return !!signals && (finite(signals.memory) && signals.memory > 0 && signals.memory <= 4 || finite(signals.cores) && signals.cores > 0 && signals.cores <= 4 || signals.coarse === true && finite(signals.width) && signals.width > 0 && signals.width <= 900);
59
59
  }
60
60
 
61
+ export const EFFECT_BLOCK_REASONS = Object.freeze([
62
+ "transport_unavailable", "preview_unavailable", "preview_not_consented", "unsupported_host", "safe_mode", "reduced_motion", "save_data", "hidden", "disabled", "public_link",
63
+ "baseline_unsupported", "baseline_unavailable", "baseline_timeout", "calibrating", "clock_unavailable", "gates_unknown", "owner_css", "stock_overlay", "modal",
64
+ "safety_off", "safety_unknown", "interrupted", "paused", "host_budget", "frame_limit", "front_limit", "offscreen", "region_unavailable", "resource_unavailable", "resource_timeout", "runtime_unavailable", "unknown",
65
+ ]);
66
+
67
+ /** Diagnostic names only; the admission policy below still validates every required flag. */
68
+ export function effectPlaybackReason(c) {
69
+ if (!c || ["safe", "publicLink", "reducedMotion", "hidden", "enabled", "show", "transportReady"].some(k => typeof c[k] !== "boolean")) return "gates_unknown";
70
+ return c.safe ? "safe_mode" : c.reducedMotion ? "reduced_motion"
71
+ : c.hidden ? "hidden" : !c.enabled ? "unsupported_host" : !c.show ? "disabled" : !c.transportReady ? "transport_unavailable" : null;
72
+ }
73
+
61
74
  const OWNER_ORDER = Object.freeze({ page: 0, header: 1, sidebar: 2, box: 3 });
62
75
  /** Recomputed from host descriptors; queued/off/unknown descriptors never get a frame reservation. */
63
76
  export function allocateEffectFrames(descriptors, context) {
64
77
  if (!Array.isArray(descriptors) || descriptors.length > 128 || !plain(context)) return [];
65
78
  const keys = new Set(), duplicate = new Set();
66
79
  for (const d of descriptors) if (d && typeof d.key === "string") { if (keys.has(d.key)) duplicate.add(d.key); keys.add(d.key); }
67
- const result = descriptors.map(d => ({ key: d?.key, verdict: "off", reason: "unknown", queuePos: null }));
68
- const gates = ["safe", "publicLink", "reducedMotion", "saveData", "hidden", "enabled", "show", "previewReady", "optIn", "constrained", "modal"];
69
- if (gates.some(k => typeof context[k] !== "boolean") || context.openInstance !== null && !instanceName(context.openInstance)) return result;
80
+ const result = descriptors.map(d => ({ key: d?.key, verdict: "off", reason: "descriptor_invalid", queuePos: null }));
81
+ const gates = ["safe", "publicLink", "reducedMotion", "hidden", "enabled", "show", "transportReady", "constrained", "modal"];
82
+ if (gates.some(k => typeof context[k] !== "boolean") || context.openInstance !== null && !instanceName(context.openInstance)) return result.map(r => ({ ...r, reason: "gates_unknown" }));
70
83
  let global = null;
71
84
  if (context.safe === true) global = "safe_mode";
72
- else if (context.publicLink === true) global = "public_link";
73
85
  else if (context.reducedMotion === true) global = "reduced_motion";
74
- else if (context.saveData === true) global = "save_data";
75
86
  else if (context.hidden === true) global = "hidden";
76
- else if (context.enabled !== true || context.show !== true) global = "disabled";
77
- else if (context.previewReady !== true || context.optIn !== true) global = "preview_unavailable";
87
+ else if (context.enabled !== true) global = "unsupported_host";
88
+ else if (context.show !== true) global = "disabled";
89
+
78
90
  const candidates = [];
79
91
  for (let i = 0; i < descriptors.length; i++) {
80
92
  const d = descriptors[i], r = result[i];
@@ -87,6 +99,8 @@ export function allocateEffectFrames(descriptors, context) {
87
99
  if (!owner && d.region !== "chat") { r.reason = "scope"; continue; }
88
100
  if (d.region === "chat" && (!instanceName(d.instance) || d.instance !== context.openInstance)) { r.reason = "closed_chat"; continue; }
89
101
  if (global) { r.reason = global; continue; }
102
+ if (context.publicLink && (d.region !== "chat" || trial)) { r.reason = "public_link"; continue; }
103
+ if (!context.transportReady) { r.reason = "transport_unavailable"; continue; }
90
104
  if (d.paused === true) { r.reason = "paused"; continue; }
91
105
  if (d.visible !== true) { r.reason = "offscreen"; continue; }
92
106
  if (d.layer === "front" && context.modal === true && !trial) { r.reason = "modal"; continue; }
@@ -12,10 +12,14 @@ const id = v => typeof v === "string" && /^[0-9a-f]{32}$/.test(v);
12
12
  /** One host-owned mount. The registry must remove the frame when remove is true, never merely hide it. */
13
13
  export class EffectBudget {
14
14
  constructor({ device, baselineFps, clock = () => performance.now(), longTasksSupported = false } = {}) {
15
- if (!deviceClass(device) || !finite(baselineFps) || baselineFps <= 0 || baselineFps > 1000 || typeof clock !== "function" || typeof longTasksSupported !== "boolean") throw new TypeError("effect budget options");
15
+ if (!deviceClass(device) || baselineFps !== undefined && (!finite(baselineFps) || baselineFps <= 0 || baselineFps > 1000) || typeof clock !== "function" || typeof longTasksSupported !== "boolean") throw new TypeError("effect budget options");
16
16
  this.device = device;
17
- // Measured by the host while NO effects are mounted; never author-supplied or learned from a running effect.
18
- this.fpsFloor = Math.min(EFFECT_BUDGETS[device].fps, baselineFps * 0.9);
17
+ // Legacy host measurements may still be supplied by pure probes. Product playback starts
18
+ // immediately and learns cadence concurrently from host rAF, never Worker timing.
19
+ // Freeze before the first report: a mount's profile floor must remain coherent.
20
+ this.fpsFloor = baselineFps === undefined ? null : Math.min(EFFECT_BUDGETS[device].fps, baselineFps * 0.9);
21
+ this.cadence = [];
22
+ this.cadenceAt = null;
19
23
  this.clock = clock;
20
24
  this.longTasksSupported = longTasksSupported;
21
25
  this.last = null;
@@ -58,7 +62,16 @@ export class EffectBudget {
58
62
  frame() {
59
63
  const now = this.time();
60
64
  if (now === null) return this.stop("clock_unavailable");
61
- if (this.verdict === "paused" || this.hiddenAt !== null || now < this.epoch + 1000) return this.snapshot();
65
+ if (this.verdict === "paused" || this.hiddenAt !== null) return this.snapshot();
66
+ if (this.fpsFloor === null) {
67
+ const interval = this.cadenceAt === null ? null : now - this.cadenceAt;
68
+ this.cadenceAt = now;
69
+ if (interval !== null && interval >= 1 && interval <= 1000) {
70
+ if (this.cadence.length === 256) this.cadence.shift();
71
+ this.cadence.push(interval);
72
+ }
73
+ }
74
+ if (now < this.epoch + 1000) return this.snapshot();
62
75
  this.trim(now);
63
76
  if (this.frames.at(-1) !== now) {
64
77
  if (this.frames.length >= 2048) this.overflow = true;
@@ -88,10 +101,10 @@ export class EffectBudget {
88
101
  const now = this.time();
89
102
  if (now === null) return this.stop("clock_unavailable");
90
103
  if (!plain(raw)) return this.stop("gates_unknown");
91
- const { hidden, reducedMotion, saveData, show } = raw;
92
- if ([hidden, reducedMotion, saveData, show].some(v => typeof v !== "boolean")) return this.stop("gates_unknown");
104
+ const { hidden, reducedMotion, show } = raw;
105
+ if ([hidden, reducedMotion, show].some(v => typeof v !== "boolean")) return this.stop("gates_unknown");
93
106
  if (this.verdict === "paused") return this.snapshot();
94
- if (reducedMotion || saveData || !show) return this.stop(reducedMotion ? "reduced_motion" : saveData ? "save_data" : "viewer_off");
107
+ if (reducedMotion || !show) return this.stop(reducedMotion ? "reduced_motion" : "viewer_off");
95
108
  if (hidden && this.hiddenAt === null) this.hiddenAt = now;
96
109
  if (hidden && now - this.hiddenAt >= 10000) return this.stop("hidden");
97
110
  if (!hidden && this.hiddenAt !== null) {
@@ -105,6 +118,7 @@ export class EffectBudget {
105
118
  this.overflow = false;
106
119
  this.badWindows = 0;
107
120
  this.metrics = null;
121
+ this.cadence = []; this.cadenceAt = null;
108
122
  }
109
123
  return this.snapshot();
110
124
  }
@@ -123,8 +137,16 @@ export class EffectBudget {
123
137
  const hostMs = this.longTasksSupported ? sum(null) : null;
124
138
  const ownMs = this.longTasksSupported && this.tasks.some(t => t.attributed) ? sum(true) : null;
125
139
  const limits = EFFECT_BUDGETS[this.device];
126
- const bad = this.overflow || fps < this.fpsFloor || hostMs !== null && hostMs > limits.longTaskMsPerSecond;
127
- this.metrics = { at: now, hostFps: fps, hostLongTaskMsPerSecond: hostMs, attributedLongTaskMsPerSecond: ownMs,
140
+ if (this.fpsFloor === null && this.cadence.length >= 3) {
141
+ // Faster observed intervals distinguish intermittent jank from a genuinely slower
142
+ // display. This is a live reference, not an empty-page health certification.
143
+ const sorted = [...this.cadence].sort((a, b) => a - b);
144
+ const interval = sorted[Math.ceil(sorted.length * (sorted.length < 8 ? 0.5 : 0.2)) - 1];
145
+ this.fpsFloor = Math.min(limits.fps, 900 / interval);
146
+ this.cadence = []; this.cadenceAt = null;
147
+ }
148
+ const bad = this.overflow || this.fpsFloor === null || fps < this.fpsFloor || hostMs !== null && hostMs > limits.longTaskMsPerSecond;
149
+ this.metrics = this.fpsFloor === null ? null : { at: now, hostFps: fps, hostLongTaskMsPerSecond: hostMs, attributedLongTaskMsPerSecond: ownMs,
128
150
  attribution: ownMs === null ? "unknown" : "observed", fpsFloor: this.fpsFloor, windowMs: 2000 };
129
151
  this.overflow = false;
130
152
  if (bad) {