@songsid/agend 2.2.0 → 2.2.5
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.
- package/dist/agent-cli-instructions.md +5 -2
- package/dist/agent-cli.js +284 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +29 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +38 -0
- package/dist/agent-file-read.js +317 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.d.ts +11 -0
- package/dist/agent-whats-new.js +34 -2
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +61 -12
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +38 -0
- package/dist/authority/authority-state.js +114 -0
- package/dist/authority/authority-state.js.map +1 -0
- package/dist/authority/session-authorizations.d.ts +43 -0
- package/dist/authority/session-authorizations.js +102 -0
- package/dist/authority/session-authorizations.js.map +1 -0
- package/dist/authority/web-authority.d.ts +120 -0
- package/dist/authority/web-authority.js +157 -0
- package/dist/authority/web-authority.js.map +1 -0
- package/dist/backend/antigravity.js +3 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -2
- package/dist/backend/claude-code.js +10 -5
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/launch-effort.js +1 -1
- package/dist/backend/launch-effort.js.map +1 -1
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +14 -0
- package/dist/backend/types.js +25 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +120 -0
- package/dist/backend-availability.js +396 -0
- package/dist/backend-availability.js.map +1 -0
- package/dist/backend-command.d.ts +21 -3
- package/dist/backend-command.js +23 -4
- package/dist/backend-command.js.map +1 -1
- package/dist/bundled-skills.d.ts +14 -0
- package/dist/bundled-skills.js +29 -0
- package/dist/bundled-skills.js.map +1 -0
- package/dist/cache-analysis.d.ts +47 -1
- package/dist/cache-analysis.js +68 -8
- package/dist/cache-analysis.js.map +1 -1
- package/dist/cache-ledger.d.ts +16 -3
- package/dist/cache-ledger.js +33 -13
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-report-tool.d.ts +17 -0
- package/dist/cache-report-tool.js +40 -0
- package/dist/cache-report-tool.js.map +1 -0
- package/dist/cache-service.d.ts +70 -3
- package/dist/cache-service.js +178 -17
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord-client-retirement.d.ts +5 -0
- package/dist/channel/adapters/discord-client-retirement.js +57 -0
- package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +66 -6
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +8 -1
- package/dist/channel/adapters/telegram.js +74 -4
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +48 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/classic-channel-manager.d.ts +10 -5
- package/dist/classic-channel-manager.js +12 -9
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +142 -110
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +119 -4
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +10 -0
- package/dist/daemon.js +327 -31
- package/dist/daemon.js.map +1 -1
- package/dist/decision-list.d.ts +25 -0
- package/dist/decision-list.js +30 -0
- package/dist/decision-list.js.map +1 -0
- package/dist/delivery-outbox.d.ts +10 -0
- package/dist/delivery-outbox.js +12 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +15 -0
- package/dist/fleet-manager.d.ts +514 -12
- package/dist/fleet-manager.js +1885 -163
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +18 -0
- package/dist/fleet-process-handlers.js +69 -0
- package/dist/fleet-process-handlers.js.map +1 -0
- package/dist/fleet-traffic.d.ts +74 -0
- package/dist/fleet-traffic.js +196 -0
- package/dist/fleet-traffic.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +492 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +482 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/human-delivery-receipts.d.ts +24 -1
- package/dist/human-delivery-receipts.js +64 -11
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/instance-avatar.js +3 -0
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +6 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +20 -10
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +12 -1
- package/dist/instructions.js +13 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +100 -8
- package/dist/locale.js.map +1 -1
- package/dist/ls-attach-resolve.d.ts +95 -0
- package/dist/ls-attach-resolve.js +174 -0
- package/dist/ls-attach-resolve.js.map +1 -0
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +15 -0
- package/dist/outbound-handlers.js +152 -11
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +228 -4
- package/dist/outbound-schemas.js +147 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +21 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -0
- package/dist/public-web-link.js +8 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +10 -1
- package/dist/quickstart-api.js +12 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +4 -0
- package/dist/reply-buttons-controller.js +2 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/restart-progress.d.ts +65 -1
- package/dist/restart-progress.js +236 -10
- package/dist/restart-progress.js.map +1 -1
- package/dist/rollout-index.d.ts +5 -0
- package/dist/rollout-index.js +26 -5
- package/dist/rollout-index.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +70 -1
- package/dist/scheduler/scheduler.js +256 -17
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/service-installer.d.ts +2 -1
- package/dist/service-installer.js +18 -3
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +23 -0
- package/dist/settings-api.js +233 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +112 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +32 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.js +16 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +4 -6
- package/dist/tool-permissions.js +100 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +17 -7
- package/dist/topic-commands.js +39 -21
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/types.d.ts +61 -1
- package/dist/ui/app-effects.js +245 -0
- package/dist/ui/chat-ambient.js +307 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +160 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +25 -7
- package/dist/ui/chat-store.js +41 -13
- package/dist/ui/chat-strings.js +86 -2
- package/dist/ui/chat-thread.js +204 -37
- package/dist/ui/chat-tour.js +1 -1
- package/dist/ui/effect-trial-controller.js +124 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +40 -0
- package/dist/ui/fleet-cache.js +23 -1
- package/dist/ui/fleet-canvas-layout.js +111 -0
- package/dist/ui/fleet-canvas.js +525 -0
- package/dist/ui/fleet-org.js +27 -12
- package/dist/ui/fleet-traffic.js +343 -0
- package/dist/ui/fleet-usage.js +401 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +252 -58
- package/dist/ui/panel-details.js +20 -6
- package/dist/ui/panel-fleet.js +45 -19
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +3 -3
- package/dist/ui/panel-settings.js +58 -13
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/preview.js +3 -2
- package/dist/ui/settings-appearance.js +349 -0
- package/dist/ui/settings-catalog.js +92 -0
- package/dist/ui/settings-dialogs.js +40 -12
- package/dist/ui/settings-effects.js +202 -0
- package/dist/ui/settings-guests.js +117 -0
- package/dist/ui/settings-learn.js +25 -0
- package/dist/ui/settings-model.js +15 -6
- package/dist/ui/settings-mods.js +271 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +335 -10
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/agend-auth.js +28 -4
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +6 -2
- package/dist/ui/shared/app-prefs.js +34 -13
- package/dist/ui/shared/app-route.js +42 -2
- package/dist/ui/shared/app-session.js +3 -1
- package/dist/ui/shared/app-shell.js +119 -41
- package/dist/ui/shared/app-store.js +28 -1
- package/dist/ui/shared/app-stream.js +14 -7
- package/dist/ui/shared/app-themes.js +206 -0
- package/dist/ui/shared/app.css +566 -5
- package/dist/ui/shared/app.js +23 -10
- package/dist/ui/shared/card-strings.js +32 -0
- package/dist/ui/shared/effect-app-runtime.js +297 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +167 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +146 -0
- package/dist/ui/shared/effect-geometry.js +137 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +122 -0
- package/dist/ui/shared/effect-policy.js +151 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +184 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +543 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +43 -0
- package/dist/ui/shared/effect-stock-trust.js +217 -0
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +170 -0
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/fx-motion.js +67 -0
- package/dist/ui/shared/header-tools.js +1 -1
- package/dist/ui/shared/identity-card.js +273 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +215 -41
- package/dist/ui/shared/owner-css.js +250 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +103 -10
- package/dist/ui/shared/signin.js +2 -0
- package/dist/ui/shared/tokens.css +9 -3
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/usage-dialog.js +22 -2
- package/dist/ui/shared/view-strings.js +98 -2
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/ui/signin.html +1 -0
- package/dist/ui/web-terminal/terminal.css +4 -4
- package/dist/usage/providers.d.ts +28 -0
- package/dist/usage/providers.js +126 -36
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage-report.d.ts +158 -0
- package/dist/usage-report.js +226 -0
- package/dist/usage-report.js.map +1 -0
- package/dist/usage-rollup.d.ts +51 -0
- package/dist/usage-rollup.js +168 -0
- package/dist/usage-rollup.js.map +1 -0
- package/dist/view-api.d.ts +68 -0
- package/dist/view-api.js +249 -8
- package/dist/view-api.js.map +1 -1
- package/dist/web-ambient.d.ts +72 -0
- package/dist/web-ambient.js +161 -0
- package/dist/web-ambient.js.map +1 -0
- package/dist/web-api.d.ts +117 -7
- package/dist/web-api.js +1283 -70
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +64 -0
- package/dist/web-appearance-policy.js +87 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +53 -0
- package/dist/web-appearance-tools.js +124 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +124 -0
- package/dist/web-appearance-view.js +278 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +93 -0
- package/dist/web-appearance.js +373 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +93 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +20 -2
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +110 -0
- package/dist/web-card.js +266 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-channel-echo.d.ts +7 -1
- package/dist/web-channel-echo.js +6 -2
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +63 -0
- package/dist/web-chat-history.js +109 -6
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +1 -0
- package/dist/web-effect-bootstrap.d.ts +12 -0
- package/dist/web-effect-bootstrap.js +49 -0
- package/dist/web-effect-bootstrap.js.map +1 -0
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +171 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-playback.d.ts +30 -0
- package/dist/web-effect-playback.js +71 -0
- package/dist/web-effect-playback.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +93 -0
- package/dist/web-effect-service.js +335 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +48 -0
- package/dist/web-effect-tools.js +129 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +27 -0
- package/dist/web-effect-upload.js +185 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +103 -0
- package/dist/web-effect.js +153 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-guest-access.d.ts +125 -0
- package/dist/web-guest-access.js +234 -0
- package/dist/web-guest-access.js.map +1 -0
- package/dist/web-guest-rooms.d.ts +82 -0
- package/dist/web-guest-rooms.js +135 -0
- package/dist/web-guest-rooms.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +38 -0
- package/dist/web-image-meta.js +262 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +47 -0
- package/dist/web-login.js +115 -0
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +207 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +157 -0
- package/dist/web-mod-bundle.js +529 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +68 -0
- package/dist/web-mod-export.js +100 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +289 -0
- package/dist/web-mod-store.js +638 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +222 -0
- package/dist/web-owner-css-store.js +440 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +62 -0
- package/dist/web-prefs.js +232 -36
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +15 -3
- package/dist/web-preview.js +28 -9
- package/dist/web-preview.js.map +1 -1
- package/dist/web-returning-device.d.ts +41 -0
- package/dist/web-returning-device.js +57 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session-authorization.d.ts +132 -0
- package/dist/web-session-authorization.js +193 -0
- package/dist/web-session-authorization.js.map +1 -0
- package/dist/web-session.d.ts +70 -10
- package/dist/web-session.js +137 -22
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +67 -2
- package/dist/web-settings-patch.js +196 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +89 -0
- package/dist/web-settings-tools.js +273 -0
- package/dist/web-settings-tools.js.map +1 -0
- package/dist/web-shell-routes.d.ts +2 -2
- package/dist/web-shell-routes.js +2 -2
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +257 -0
- package/dist/web-theme-store.js +627 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +123 -0
- package/dist/web-theme.js +416 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +13 -0
- package/dist/web-upload.js +39 -3
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +8 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/** #1629: one session-only app effect owner. Importing this module starts nothing. */
|
|
2
|
+
import { appStore } from "/assets/app-store.js";
|
|
3
|
+
import { motionBlocked, onMotionChange } from "/assets/fx-motion.js";
|
|
4
|
+
import { navStore } from "/assets/app-nav.js";
|
|
5
|
+
import { onPrefsChanged } from "/assets/app-prefs.js";
|
|
6
|
+
import { resolvedMode, themesStore, themesShown } from "/assets/app-themes.js";
|
|
7
|
+
import { ownerCssLifecycle, safeMode } from "/assets/owner-css.js";
|
|
8
|
+
import { constrainedDevice } from "/assets/effect-policy.js";
|
|
9
|
+
import { createAppEffectRuntime } from "/assets/effect-app-runtime.js";
|
|
10
|
+
import { effectBootstrapReady } from "/assets/effect-frame.js";
|
|
11
|
+
import { createEffectCursor } from "/assets/effect-cursor.js";
|
|
12
|
+
import { createOwnerEffectRegions } from "/assets/effect-owner-regions.js";
|
|
13
|
+
import { chatEffectIdentity, effectPlaybackIdentity } from "/assets/effect-chat-runtime.js";
|
|
14
|
+
import { createEffectTrialController } from "./effect-trial-controller.js";
|
|
15
|
+
import { effectTrialMessage } from "./effect-trial-offer.js";
|
|
16
|
+
import { claimPanel, panelVersion, panelStore, openTrialPanel, closePanel } from "./preview-panel.js";
|
|
17
|
+
const owners = new WeakMap();
|
|
18
|
+
export function getAppEffects(host = window) { const value = owners.get(host); return value && !value.dead && value.api?.ok ? value : null; }
|
|
19
|
+
|
|
20
|
+
/** Boot once with the page's Chat store, including when the first visible panel is Fleet. */
|
|
21
|
+
export function startAppEffects({ store, window: host = window, factories = {} } = {}) {
|
|
22
|
+
const existing = owners.get(host);
|
|
23
|
+
if (existing) { if (!existing.dead) existing.bindStore(store); return getAppEffects(host); }
|
|
24
|
+
const playbackIdentity = effectPlaybackIdentity(host), ownerIdentity = chatEffectIdentity(host);
|
|
25
|
+
if (!playbackIdentity || typeof host.AgendAuth?.onSessionEnd !== "function" || typeof host.AgendAuth?.sessionEnded !== "function") return null;
|
|
26
|
+
const doc = host.document, releases = [], ended = new Set(), updates = new Set(), trialUpdates = new Set(); let replyOff = null, replyStore = null, regions = null, cursor = null, cursorStamp = null, trialControl = null, trialIntent = null, trialHint = ownerIdentity ? null : false, trialSerial = 0, trialStamp = null, chatBinding = null;
|
|
27
|
+
const safe = fn => { try { return fn(); } catch { return null; } };
|
|
28
|
+
const hold = off => { if (entry.dead) safe(() => off?.()); else releases.push(off); };
|
|
29
|
+
const entry = { dead: false, api: null,
|
|
30
|
+
cursorSnapshot: () => cursor?.snapshot() ?? { status: "unavailable", id: null },
|
|
31
|
+
gates: () => context(),
|
|
32
|
+
subscribe(fn) { if (typeof fn !== "function" || entry.dead) return () => {}; updates.add(fn); return () => updates.delete(fn); },
|
|
33
|
+
subscribeTrial(fn) { if (typeof fn !== "function" || entry.dead) return () => {}; trialUpdates.add(fn); return () => trialUpdates.delete(fn); },
|
|
34
|
+
onEnd(fn) { if (typeof fn !== "function") return () => {}; if (entry.dead) { safe(fn); return () => {}; } ended.add(fn); return () => ended.delete(fn); },
|
|
35
|
+
bindStore(value) {
|
|
36
|
+
if (entry.dead || value === replyStore) return;
|
|
37
|
+
safe(() => replyOff?.()); replyOff = null; replyStore = value;
|
|
38
|
+
trialControl?.stop(); chatBinding = null; entry.api?.detachChat();
|
|
39
|
+
if (typeof value?.onAgentReply === "function") {
|
|
40
|
+
const off = value.onAgentReply(event => { if (!entry.dead && replyStore === value) entry.api?.reply(event); });
|
|
41
|
+
if (entry.dead || replyStore !== value) safe(() => off?.()); else replyOff = off;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
trialStatus(message) {
|
|
45
|
+
const offer = safe(() => effectTrialMessage(message)), nav = navStore.get();
|
|
46
|
+
const now = safe(() => host.Date.now());
|
|
47
|
+
if (!offer || !Number.isFinite(now) || now >= offer.expiresAt) return "expired";
|
|
48
|
+
if (trialHint === null && !entry.dead) return "checking";
|
|
49
|
+
if (entry.dead || !trialHint || trialControl?.ok !== true) return "localAdmin";
|
|
50
|
+
if (!chatBinding || safe(() => chatBinding.current()) !== true || message.instance !== nav.route?.instance || nav.route?.panel !== "chat") return "chatChanged";
|
|
51
|
+
const state = trialControl.snapshot();
|
|
52
|
+
if (state.phase !== "idle" || state.busy) return "busy";
|
|
53
|
+
return trialPlaybackStatus();
|
|
54
|
+
},
|
|
55
|
+
trialFailure(message) {
|
|
56
|
+
const offer = safe(() => effectTrialMessage(message)), state = trialControl?.snapshot();
|
|
57
|
+
return offer && state?.phase === "idle" && state.offer === offer.offer && !["stopped", "replaced"].includes(state.reason) ? state.reason : null;
|
|
58
|
+
},
|
|
59
|
+
canTry(message) { return entry.trialStatus(message) === "ready"; },
|
|
60
|
+
stopTrial() { trialControl?.stop(); },
|
|
61
|
+
startTrial(message) {
|
|
62
|
+
if (!entry.canTry(message)) return Promise.resolve(false);
|
|
63
|
+
const nav = navStore.get(); trialIntent = { name: message.instance, seq: nav.seq, claim: claimPanel(), handle: null, chat: chatBinding };
|
|
64
|
+
return trialControl.start(message);
|
|
65
|
+
},
|
|
66
|
+
attachChat(options) {
|
|
67
|
+
const nav = navStore.get(), name = options?.name;
|
|
68
|
+
if (entry.dead || !entry.api?.ok || nav.route?.panel !== "chat" || nav.route.instance !== name || typeof options.current !== "function") return null;
|
|
69
|
+
const binding = { current: () => !entry.dead && host.document === doc && navStore.get().seq === nav.seq && navStore.get().route?.panel === "chat" && navStore.get().route?.instance === name && options.current() === true };
|
|
70
|
+
chatBinding = binding; trialControl?.stop();
|
|
71
|
+
if (entry.dead || chatBinding !== binding) return null;
|
|
72
|
+
const handle = entry.api.attachChat({ ...options, current: () => chatBinding === binding && binding.current() });
|
|
73
|
+
if (entry.dead || chatBinding !== binding) { safe(() => handle?.dispose()); return null; }
|
|
74
|
+
if (!handle) chatBinding = null;
|
|
75
|
+
return handle;
|
|
76
|
+
},
|
|
77
|
+
dispose() {
|
|
78
|
+
if (entry.dead) return; entry.dead = true;
|
|
79
|
+
safe(() => trialControl?.dispose()); safe(() => entry.api?.dispose()); safe(() => replyOff?.()); replyOff = null; replyStore = null;
|
|
80
|
+
for (const off of releases.splice(0)) safe(off);
|
|
81
|
+
updates.clear(); trialUpdates.clear(); const listeners = [...ended]; ended.clear(); for (const fn of listeners) safe(fn);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
owners.set(host, entry); // Reentrant setup never constructs another owner.
|
|
85
|
+
let media, coarse;
|
|
86
|
+
function ownerCssForPlayback() {
|
|
87
|
+
if (doc.body.dataset.tier !== "guest") return ownerCssLifecycle.get().state;
|
|
88
|
+
// Guests never install owner CSS. Prove its absence rather than borrowing the uninstalled
|
|
89
|
+
// lifecycle's initial unknown, and refuse unexpected styles instead of trusting a missing id.
|
|
90
|
+
try {
|
|
91
|
+
if (doc.body.dataset.ownerCss || doc.querySelectorAll("#owner-css").length) return "unknown";
|
|
92
|
+
// A CSP-refused or disabled STYLE has no paint. Recheck on every proof so enabling
|
|
93
|
+
// it never borrows an earlier absence verdict (WebKit capture inserts a refused no-op).
|
|
94
|
+
for (const style of doc.querySelectorAll("style")) if (style.sheet && style.sheet.disabled !== true) return "unknown";
|
|
95
|
+
const allowed = new Set(["/assets/tokens.css", "/assets/app.css"]);
|
|
96
|
+
for (const link of doc.querySelectorAll('link[rel~="stylesheet"]')) {
|
|
97
|
+
const url = new URL(link.getAttribute("href"), host.location.origin);
|
|
98
|
+
if (url.origin !== host.location.origin || !allowed.has(url.pathname) || url.search || url.hash) return "unknown";
|
|
99
|
+
}
|
|
100
|
+
return "off";
|
|
101
|
+
} catch { return "unknown"; }
|
|
102
|
+
}
|
|
103
|
+
function guestChatVersion(value) {
|
|
104
|
+
if (doc.body.dataset.tier !== "guest") return null;
|
|
105
|
+
const name = navStore.get().route?.instance, v = value.instances?.find(i => i.name === name)?.effect_chat_v;
|
|
106
|
+
return typeof v === "string" && /^[a-f0-9]{32}$/.test(v) ? v : null;
|
|
107
|
+
}
|
|
108
|
+
// #2188: whether a dialog, menu or popover is open — a whole-document query when none is (the usual case), asked
|
|
109
|
+
// several times per sync. Read once per task: the cached value is dropped at the next microtask, and whenever the
|
|
110
|
+
// document changes (the MutationObserver below drops it before it reads).
|
|
111
|
+
let modalCache = null;
|
|
112
|
+
function modalOpen() {
|
|
113
|
+
if (modalCache === null) {
|
|
114
|
+
modalCache = !!doc.querySelector("dialog[open],.menu-list,.pop,[role=alertdialog],.oc-keep,.shell.sb-open");
|
|
115
|
+
Promise.resolve().then(() => { modalCache = null; });
|
|
116
|
+
}
|
|
117
|
+
return modalCache;
|
|
118
|
+
}
|
|
119
|
+
function context() {
|
|
120
|
+
return { safe: safeMode(), publicLink: doc.body.dataset.publicLink === "1", reducedMotion: motionBlocked(host), saveData: host.navigator?.connection?.saveData === true,
|
|
121
|
+
hidden: doc.visibilityState !== "visible", enabled: entry.api?.playbackAvailable !== false && typeof host.OffscreenCanvas === "function" && typeof host.Worker === "function" && typeof host.MutationObserver === "function" && typeof host.ResizeObserver === "function",
|
|
122
|
+
show: themesShown(), transportReady: effectBootstrapReady(host, host.location?.origin, doc.body.dataset.effectBoot),
|
|
123
|
+
constrained: constrainedDevice({ memory: host.navigator?.deviceMemory, cores: host.navigator?.hardwareConcurrency, coarse: coarse?.matches === true, width: host.innerWidth }),
|
|
124
|
+
modal: modalOpen(), openInstance: null };
|
|
125
|
+
}
|
|
126
|
+
function trialCurrent(name) {
|
|
127
|
+
const intent = trialIntent, nav = navStore.get();
|
|
128
|
+
return !entry.dead && host.document === doc && !!intent && !!intent.chat && chatBinding === intent.chat && intent.chat.current() === true && intent.name === name && nav.seq === intent.seq && nav.route?.panel === "chat" && nav.route.instance === name
|
|
129
|
+
&& (panelVersion() === intent.claim || intent.handle !== null && panelStore.get().open?.handle === intent.handle);
|
|
130
|
+
}
|
|
131
|
+
// One priority order feeds both presentation and redraw identity: false→different-false must redraw.
|
|
132
|
+
function trialPlaybackStatus() {
|
|
133
|
+
const c = context();
|
|
134
|
+
if (c.safe) return "safe";
|
|
135
|
+
if (!c.show) return "effectsOff";
|
|
136
|
+
if (c.reducedMotion) return "reducedMotion";
|
|
137
|
+
if (!c.enabled) return "unsupported";
|
|
138
|
+
if (!c.transportReady) return "transport";
|
|
139
|
+
if (c.publicLink) return "localAdmin";
|
|
140
|
+
if (c.hidden || doc.hasFocus?.() !== true) return "inactive";
|
|
141
|
+
if (c.modal) return "dialog";
|
|
142
|
+
return "ready";
|
|
143
|
+
}
|
|
144
|
+
function trialAllowed() {
|
|
145
|
+
const c = context();
|
|
146
|
+
return !entry.dead && doc.visibilityState === "visible" && doc.hasFocus?.() === true
|
|
147
|
+
&& c.enabled && c.show && c.transportReady && !c.safe && !c.publicLink && !c.reducedMotion && !c.hidden && !c.modal;
|
|
148
|
+
}
|
|
149
|
+
function mountTrial(handle) {
|
|
150
|
+
const intent = trialIntent; if (!intent || !trialCurrent(handle.instance) || !handle.current() || !Number.isSafeInteger(++trialSerial)) throw Error("trial_unavailable");
|
|
151
|
+
intent.handle = handle;
|
|
152
|
+
const binding = Object.freeze({ key: `trial:${trialSerial}:${handle.reply.trial}`, id: handle.reply.offer.id, run: handle.reply.trial, instance: handle.instance, generation: trialSerial });
|
|
153
|
+
let stage = null, source = null;
|
|
154
|
+
const cleanup = () => {
|
|
155
|
+
safe(() => source?.stop()); safe(() => stage?.dispose());
|
|
156
|
+
if (panelStore.get().open?.handle === handle) closePanel();
|
|
157
|
+
};
|
|
158
|
+
try {
|
|
159
|
+
openTrialPanel({ key: binding.key, instance: handle.instance, binding, handle,
|
|
160
|
+
onStage(value) {
|
|
161
|
+
stage = value;
|
|
162
|
+
if (!handle.current()) { cleanup(); handle.stop(); return; }
|
|
163
|
+
source = entry.api.attachTrial({ reply: handle.reply, offer: handle.reply.offer.offer, startedAt: handle.startedAt, stage,
|
|
164
|
+
current: handle.current, onStop: handle.stop });
|
|
165
|
+
if (!source) { cleanup(); handle.stop(); }
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
} catch (error) { cleanup(); throw error; }
|
|
169
|
+
return cleanup;
|
|
170
|
+
}
|
|
171
|
+
function notifyTrial() {
|
|
172
|
+
if (entry.dead) return;
|
|
173
|
+
const nav = navStore.get(), next = JSON.stringify([trialHint, safe(() => chatBinding?.current()) === true, trialPlaybackStatus(), nav.route?.panel, nav.route?.instance, trialControl?.snapshot()]);
|
|
174
|
+
if (next === trialStamp) return; trialStamp = next;
|
|
175
|
+
for (const fn of [...trialUpdates]) safe(fn);
|
|
176
|
+
}
|
|
177
|
+
const notify = () => { if (!entry.dead) { trialControl?.sync(); notifyTrial(); for (const fn of [...updates]) safe(fn); } };
|
|
178
|
+
const cursorDragging = () => doc.body.classList?.contains("side-resizing") === true || !!doc.querySelector(".chat-split.resizing");
|
|
179
|
+
function syncCursor() {
|
|
180
|
+
if (entry.dead || !cursor) return;
|
|
181
|
+
const c = context();
|
|
182
|
+
cursor.update(entry.api?.cursorSelection?.() ?? null, c.enabled && c.show && c.transportReady && !c.safe && !c.publicLink && !c.reducedMotion && !c.hidden && !c.modal && !cursorDragging());
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
if (appStore.get().connection === "ended") { entry.dispose(); return null; }
|
|
186
|
+
media = host.matchMedia?.("(prefers-reduced-motion: reduce)") ?? { matches: true };
|
|
187
|
+
coarse = host.matchMedia?.("(pointer: coarse)") ?? { matches: false };
|
|
188
|
+
entry.api = (factories.runtime ?? createAppEffectRuntime)({ window: host, current: () => !entry.dead && host.document === doc,
|
|
189
|
+
observe: () => ({ context: context(), ownerCss: ownerCssForPlayback(), mode: resolvedMode() }), onDispose: () => entry.dispose(),
|
|
190
|
+
resolveOwnerTarget: (slot, manifest, proof) => !entry.dead ? regions?.resolve?.(slot, manifest.box, proof) ?? null : null,
|
|
191
|
+
onChange: () => { syncCursor(); notify(); } });
|
|
192
|
+
if (entry.dead || !entry.api.ok) { entry.dispose(); return null; }
|
|
193
|
+
if (ownerIdentity) trialControl = (factories.trialController ?? createEffectTrialController)({ window: host, current: trialCurrent, allowed: trialAllowed, onStart: mountTrial, onChange: notify });
|
|
194
|
+
hold(() => trialControl?.dispose());
|
|
195
|
+
if (ownerIdentity && typeof host.AgendAuth.trialSession === "function") Promise.resolve().then(() => host.AgendAuth.trialSession()).then(value => {
|
|
196
|
+
if (entry.dead) return; trialHint = value?.localAdmin === true && value.epoch === chatEffectIdentity(host)?.epoch; notify();
|
|
197
|
+
}).catch(() => { if (!entry.dead) { trialHint = false; notify(); } });
|
|
198
|
+
hold(panelStore.subscribe(() => { trialControl?.sync(); notifyTrial(); }));
|
|
199
|
+
const ownRegions = ownerIdentity && (factories.regions ?? createOwnerEffectRegions)({ window: host, current: () => !entry.dead && host.document === doc });
|
|
200
|
+
regions = ownRegions?.ok ? ownRegions : null; hold(() => ownRegions?.dispose?.());
|
|
201
|
+
if (entry.dead) return null;
|
|
202
|
+
const ownCursor = ownerIdentity && safe(() => (factories.cursor ?? createEffectCursor)({ window: host, current: () => !entry.dead && host.document === doc,
|
|
203
|
+
readImage: (selection, signal) => entry.api?.cursorImageSettled?.(selection, signal) ?? Promise.resolve(null),
|
|
204
|
+
onChange(value) { const next = JSON.stringify(value); if (next !== cursorStamp) { cursorStamp = next; notify(); } },
|
|
205
|
+
}));
|
|
206
|
+
cursor = ownCursor?.ok ? ownCursor : null; hold(() => ownCursor?.dispose?.());
|
|
207
|
+
if (entry.dead) return null;
|
|
208
|
+
const sync = () => { if (!entry.dead) { trialControl?.sync(); entry.api.sync(); syncCursor(); notifyTrial(); } };
|
|
209
|
+
let revision = appStore.get().themesRev, prefsRevision = appStore.get().prefsRev, guestRevision = guestChatVersion(appStore.get()), navSeq = navStore.get().seq;
|
|
210
|
+
hold(navStore.subscribe(value => { if (entry.dead || value.seq === navSeq) return; navSeq = value.seq; trialControl?.stop(); chatBinding = null; entry.api.detachChat(); syncCursor(); }));
|
|
211
|
+
hold(appStore.subscribe(value => {
|
|
212
|
+
if (entry.dead) return;
|
|
213
|
+
if (value.connection === "ended") { entry.dispose(); return; }
|
|
214
|
+
const guestNext = guestChatVersion(value), chatChanged = value.themesRev !== revision || guestNext !== guestRevision, ownerChanged = value.prefsRev !== prefsRevision;
|
|
215
|
+
revision = value.themesRev; prefsRevision = value.prefsRev; guestRevision = guestNext;
|
|
216
|
+
if (chatChanged) entry.api.changedChat();
|
|
217
|
+
if (ownerChanged) entry.api.changedOwner();
|
|
218
|
+
if (!chatChanged && !ownerChanged) sync();
|
|
219
|
+
}));
|
|
220
|
+
hold(ownerCssLifecycle.subscribe(sync)); hold(themesStore.subscribe(sync)); hold(onPrefsChanged(sync)); hold(onMotionChange(sync));
|
|
221
|
+
if (typeof host.AgendTheme?.onChange === "function") hold(host.AgendTheme.onChange(sync));
|
|
222
|
+
for (const [target, type] of [[media, "change"], [coarse, "change"], [doc, "visibilitychange"], [host, "resize"], [host.navigator?.connection, "change"]]) {
|
|
223
|
+
if (target?.addEventListener) { target.addEventListener(type, sync); hold(() => target.removeEventListener(type, sync)); }
|
|
224
|
+
}
|
|
225
|
+
if (entry.dead) return null;
|
|
226
|
+
let modal = context().modal, dragging = cursorDragging(), queuedSurface = false;
|
|
227
|
+
// Panel commits can happen after navigation and contain no Chat lease. Reproject fixed owner targets
|
|
228
|
+
// once after stock DOM changes. Body-level effect roots/frames/rules do not schedule themselves.
|
|
229
|
+
const surfaceChanged = () => {
|
|
230
|
+
if (entry.dead || queuedSurface) return; queuedSurface = true;
|
|
231
|
+
Promise.resolve().then(() => { queuedSurface = false; sync(); });
|
|
232
|
+
};
|
|
233
|
+
const barriers = typeof host.MutationObserver === "function" ? new host.MutationObserver(records => {
|
|
234
|
+
if (entry.dead) return;
|
|
235
|
+
modalCache = null; // the document changed: a dialog may have opened or closed (read it again)
|
|
236
|
+
const value = context().modal, drag = cursorDragging(); if (value !== modal || drag !== dragging) { modal = value; dragging = drag; sync(); }
|
|
237
|
+
if (Array.isArray(records) && records.some(r => r.target?.closest?.("#app") || r.target === doc.getElementById?.("app"))) surfaceChanged();
|
|
238
|
+
}) : null;
|
|
239
|
+
hold(() => barriers?.disconnect());
|
|
240
|
+
if (entry.dead) return null;
|
|
241
|
+
barriers?.observe(doc.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["class", "open", "hidden", "role"] });
|
|
242
|
+
entry.bindStore(store); sync();
|
|
243
|
+
return getAppEffects(host);
|
|
244
|
+
} catch { entry.dispose(); return null; }
|
|
245
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// #2117: an agent's ambient effect on its own chat — falling petals, rising lanterns, twinkles, stars on a click. The
|
|
2
|
+
// agent sends data only (web-ambient.ts checks it; this checks the bounds again); the page draws it with plain nodes
|
|
3
|
+
// animated by the Web Animations API: transform and opacity only, positions through the CSSOM (no style attribute,
|
|
4
|
+
// #1300), every node aria-hidden and pointer-events: none. The looping layer sits behind the thread (.amb-layer, under
|
|
5
|
+
// the scroller and the dock); a burst shows over the messages for a moment (.amb-burst) and never takes the click.
|
|
6
|
+
//
|
|
7
|
+
// Its own gates (not the Canvas Worker runtime's, #2115): the viewer's "Show agents' themes, effects and widgets", safe
|
|
8
|
+
// mode, Web Animations support, and reduced motion only when this device chose to follow the system's setting
|
|
9
|
+
// (fx-motion.js; the user's direction: effects play by default). A hidden tab pauses. Nothing else gates it: it runs
|
|
10
|
+
// no agent code and fetches nothing but its own glyph pictures (fleet decision: no gate without a threat it stops).
|
|
11
|
+
// Caps: 24 looping particles, bursts of at most 16, at most 3 bursts alive — 72 nodes and animations per chat.
|
|
12
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
13
|
+
import { t } from "/assets/app-i18n.js";
|
|
14
|
+
import { useStore } from "/assets/app-store.js";
|
|
15
|
+
import { onPrefsChanged } from "/assets/app-prefs.js";
|
|
16
|
+
import { safeMode } from "/assets/owner-css.js";
|
|
17
|
+
import { setThemesShown, themeImageUrl, themesShown, themesStore } from "/assets/app-themes.js";
|
|
18
|
+
import { markMotionNoteShown, motionBlocked, motionNoteDue, onMotionChange, setFollowsReducedMotion } from "/assets/fx-motion.js";
|
|
19
|
+
import "./chat-strings.js";
|
|
20
|
+
|
|
21
|
+
export const AMBIENT_CAPS = { loop: 24, burst: 16, bursts: 3 };
|
|
22
|
+
const LOOP_PRESETS = ["fall", "rise", "twinkle"];
|
|
23
|
+
const RULES = {
|
|
24
|
+
fall: { count: [1, 24], duration: [4, 30] }, rise: { count: [1, 24], duration: [4, 30] },
|
|
25
|
+
twinkle: { count: [1, 24], duration: [1, 10] }, "burst-on-click": { count: [4, 16], duration: [0.3, 2] },
|
|
26
|
+
};
|
|
27
|
+
const COLOR = /^#[0-9a-f]{6}$/;
|
|
28
|
+
const ASSET = /^[0-9a-f]{32}$/;
|
|
29
|
+
const own = (o, k) => !!o && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
|
|
30
|
+
const num = (v, lo, hi) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi;
|
|
31
|
+
const pair = (v, lo, hi) => Array.isArray(v) && v.length === 2 && num(v[0], lo, hi) && num(v[1], lo, hi) && v[0] <= v[1];
|
|
32
|
+
|
|
33
|
+
/** A spec as the host keeps it, checked again; null when anything is off (it is then not drawn at all). */
|
|
34
|
+
export function sanitizeAmbient(spec) {
|
|
35
|
+
if (!spec || typeof spec !== "object" || !own(RULES, spec.preset)) return null;
|
|
36
|
+
const r = RULES[spec.preset];
|
|
37
|
+
if (!Array.isArray(spec.glyphs) || spec.glyphs.length < 1 || spec.glyphs.length > 4) return null;
|
|
38
|
+
const glyphs = [];
|
|
39
|
+
for (const g of spec.glyphs) {
|
|
40
|
+
if (typeof g === "string" && g.length >= 1 && g.length <= 32) glyphs.push(g);
|
|
41
|
+
else if (g && typeof g === "object" && typeof g.asset === "string" && ASSET.test(g.asset)) glyphs.push({ asset: g.asset });
|
|
42
|
+
else return null;
|
|
43
|
+
}
|
|
44
|
+
const colors = Array.isArray(spec.colors) ? spec.colors.filter((c) => typeof c === "string" && COLOR.test(c)).slice(0, 5) : [];
|
|
45
|
+
if (!Number.isInteger(spec.count) || !num(spec.count, r.count[0], r.count[1])) return null;
|
|
46
|
+
if (!pair(spec.duration, r.duration[0], r.duration[1]) || !pair(spec.size, 10, 40) || !pair(spec.spread, 20, 120)) return null;
|
|
47
|
+
if (!num(spec.sway, 0, 80) || !num(spec.rotate, 0, 720)) return null;
|
|
48
|
+
if (!Array.isArray(spec.opacity) || spec.opacity.length !== 3 || !spec.opacity.every((o) => num(o, 0, 1))) return null;
|
|
49
|
+
return { preset: spec.preset, glyphs, colors, count: spec.count, duration: spec.duration, size: spec.size, sway: spec.sway, spread: spec.spread, rotate: spec.rotate, opacity: spec.opacity };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `name`'s ambients from GET /ui/themes, checked: { loop, click } (either null). Null when it has none. */
|
|
53
|
+
export function ambientFor(name) {
|
|
54
|
+
const a = themesStore.get().ambient;
|
|
55
|
+
if (!a || typeof a !== "object" || !own(a, name)) return null;
|
|
56
|
+
const mine = a[name];
|
|
57
|
+
const loop = own(mine, "loop") ? sanitizeAmbient(mine.loop && mine.loop.spec) : null;
|
|
58
|
+
const click = own(mine, "click") ? sanitizeAmbient(mine.click && mine.click.spec) : null;
|
|
59
|
+
if (loop && !LOOP_PRESETS.includes(loop.preset)) return null;
|
|
60
|
+
if (click && click.preset !== "burst-on-click") return null;
|
|
61
|
+
return loop || click ? { loop, click } : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Why this page does not draw ambients now, or null. */
|
|
65
|
+
export function ambientBlock(win = globalThis) {
|
|
66
|
+
if (safeMode()) return "safe";
|
|
67
|
+
if (!themesShown()) return "hidden";
|
|
68
|
+
if (motionBlocked(win)) return "reducedMotion";
|
|
69
|
+
const El = win.Element;
|
|
70
|
+
if (!El || !El.prototype || typeof El.prototype.animate !== "function") return "unsupported";
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The gate as state: moves with the switches, the themes and the device's reduced-motion answer. */
|
|
75
|
+
function useAmbientBlock() {
|
|
76
|
+
useStore(themesStore);
|
|
77
|
+
const [, bump] = useState(0);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const again = () => bump((n) => n + 1);
|
|
80
|
+
const offPrefs = onPrefsChanged((changed) => { if (changed.has("fx.show")) again(); });
|
|
81
|
+
const offMotion = onMotionChange(again);
|
|
82
|
+
return () => { if (typeof offPrefs === "function") offPrefs(); offMotion(); };
|
|
83
|
+
}, []);
|
|
84
|
+
return ambientBlock();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rand = (lo, hi) => lo + Math.random() * (hi - lo);
|
|
88
|
+
const pick = (list) => list[Math.floor(Math.random() * list.length)];
|
|
89
|
+
|
|
90
|
+
/** One particle node: a text glyph (its colour, else the accent) or a glyph picture. */
|
|
91
|
+
function particle(doc, name, spec, cls) {
|
|
92
|
+
const g = pick(spec.glyphs), size = Math.round(rand(spec.size[0], spec.size[1]));
|
|
93
|
+
let el;
|
|
94
|
+
if (typeof g === "string") {
|
|
95
|
+
el = doc.createElement("span");
|
|
96
|
+
el.textContent = g;
|
|
97
|
+
el.style.setProperty("font-size", `${size}px`);
|
|
98
|
+
if (spec.colors.length) el.style.setProperty("color", pick(spec.colors));
|
|
99
|
+
} else {
|
|
100
|
+
el = doc.createElement("img");
|
|
101
|
+
el.src = themeImageUrl(name, g.asset);
|
|
102
|
+
el.alt = "";
|
|
103
|
+
el.decoding = "async";
|
|
104
|
+
el.draggable = false;
|
|
105
|
+
el.style.setProperty("width", `${size}px`);
|
|
106
|
+
el.style.setProperty("height", `${size}px`);
|
|
107
|
+
}
|
|
108
|
+
el.className = cls;
|
|
109
|
+
el.setAttribute("aria-hidden", "true");
|
|
110
|
+
return { el, size };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Draw `ambient` ({ loop, click }) for `name` into `layer` (behind) and `front` (bursts), listening on `surface` for
|
|
115
|
+
* clicks. Returns { stop() }: every node and animation it made goes, every listener it added is removed.
|
|
116
|
+
*/
|
|
117
|
+
export function startAmbient({ name, ambient, layer, front, surface, doc = globalThis.document }) {
|
|
118
|
+
const anims = new Set();
|
|
119
|
+
const loopNodes = [];
|
|
120
|
+
let bursts = 0, stopped = false;
|
|
121
|
+
const track = (a) => { anims.add(a); if (doc.visibilityState === "hidden") a.pause(); return a; };
|
|
122
|
+
|
|
123
|
+
const drawLoop = () => {
|
|
124
|
+
for (const n of loopNodes.splice(0)) n.remove();
|
|
125
|
+
for (const a of [...anims]) if (a._amb === "loop") { a.cancel(); anims.delete(a); }
|
|
126
|
+
const spec = ambient.loop;
|
|
127
|
+
if (!spec) return;
|
|
128
|
+
const h = layer.clientHeight || 600, w = layer.clientWidth || 360;
|
|
129
|
+
const [o0, o1, o2] = spec.opacity;
|
|
130
|
+
for (let i = 0; i < Math.min(spec.count, AMBIENT_CAPS.loop); i++) {
|
|
131
|
+
const { el, size } = particle(doc, name, spec, "amb-p");
|
|
132
|
+
const dur = rand(spec.duration[0], spec.duration[1]) * 1000;
|
|
133
|
+
const turn = spec.rotate * (Math.random() < 0.5 ? -1 : 1);
|
|
134
|
+
let frames;
|
|
135
|
+
if (spec.preset === "twinkle") {
|
|
136
|
+
el.style.setProperty("left", `${rand(0, Math.max(0, w - size))}px`);
|
|
137
|
+
el.style.setProperty("top", `${rand(0, Math.max(0, h - size))}px`);
|
|
138
|
+
frames = [
|
|
139
|
+
{ transform: "scale(0.6) rotate(0deg)", opacity: o0 },
|
|
140
|
+
{ transform: `scale(1) rotate(${turn / 2}deg)`, opacity: o1, offset: 0.5 },
|
|
141
|
+
{ transform: `scale(0.6) rotate(${turn}deg)`, opacity: o2 },
|
|
142
|
+
];
|
|
143
|
+
} else {
|
|
144
|
+
el.style.setProperty("left", `${rand(0, Math.max(0, w - size))}px`);
|
|
145
|
+
el.style.setProperty("top", "0px");
|
|
146
|
+
const dx = rand(-spec.sway, spec.sway), down = spec.preset === "fall";
|
|
147
|
+
const from = down ? -size - 4 : h + 4, to = down ? h + 4 : -size - 4;
|
|
148
|
+
frames = [
|
|
149
|
+
{ transform: `translate(0px, ${from}px) rotate(0deg)`, opacity: o0 },
|
|
150
|
+
{ transform: `translate(${dx * 0.6}px, ${from + (to - from) * 0.15}px) rotate(${turn * 0.15}deg)`, opacity: o1, offset: 0.15 },
|
|
151
|
+
{ transform: `translate(${-dx * 0.4}px, ${from + (to - from) * 0.6}px) rotate(${turn * 0.6}deg)`, opacity: (o1 + o2) / 2, offset: 0.6 },
|
|
152
|
+
{ transform: `translate(${dx}px, ${to}px) rotate(${turn}deg)`, opacity: o2 },
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
layer.appendChild(el);
|
|
156
|
+
loopNodes.push(el);
|
|
157
|
+
// A negative delay starts each one part-way through, so the layer is already full when it appears.
|
|
158
|
+
const a = el.animate(frames, { duration: dur, delay: -Math.random() * dur, iterations: Infinity, easing: "linear" });
|
|
159
|
+
a._amb = "loop";
|
|
160
|
+
track(a);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const burst = (x, y) => {
|
|
165
|
+
const spec = ambient.click;
|
|
166
|
+
if (!spec || bursts >= AMBIENT_CAPS.bursts || stopped) return;
|
|
167
|
+
bursts++;
|
|
168
|
+
const [, o1, o2] = spec.opacity;
|
|
169
|
+
let left = Math.min(spec.count, AMBIENT_CAPS.burst);
|
|
170
|
+
const done = (el, a) => { el.remove(); anims.delete(a); if (--left === 0) bursts--; };
|
|
171
|
+
for (let i = 0, n = left; i < n; i++) {
|
|
172
|
+
const { el, size } = particle(doc, name, spec, "amb-p");
|
|
173
|
+
el.style.setProperty("left", `${x - size / 2}px`);
|
|
174
|
+
el.style.setProperty("top", `${y - size / 2}px`);
|
|
175
|
+
const angle = (i / n) * Math.PI * 2 + rand(-0.3, 0.3), dist = rand(spec.spread[0], spec.spread[1]);
|
|
176
|
+
const turn = rand(-spec.rotate, spec.rotate) / 2;
|
|
177
|
+
front.appendChild(el);
|
|
178
|
+
const a = el.animate([
|
|
179
|
+
{ transform: "translate(0px, 0px) scale(0.4) rotate(0deg)", opacity: o1 },
|
|
180
|
+
{ transform: `translate(${Math.cos(angle) * dist}px, ${Math.sin(angle) * dist}px) scale(1) rotate(${turn}deg)`, opacity: Math.min(o2, o1) },
|
|
181
|
+
], { duration: rand(spec.duration[0], spec.duration[1]) * 1000, easing: "cubic-bezier(.2,.7,.3,1)", fill: "forwards" });
|
|
182
|
+
a._amb = "burst";
|
|
183
|
+
track(a);
|
|
184
|
+
let gone = false;
|
|
185
|
+
const finish = () => { if (!gone) { gone = true; done(el, a); } };
|
|
186
|
+
a.onfinish = finish; a.oncancel = finish;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Passive and never cancelling: the click still lands on what is under it. Only in the thread (not the composer),
|
|
191
|
+
// and not on a menu or dialog open over it: stars are for the conversation.
|
|
192
|
+
const onDown = (ev) => {
|
|
193
|
+
if (!ambient.click || ev.button > 0) return;
|
|
194
|
+
const target = ev.target;
|
|
195
|
+
if (!target || typeof target.closest !== "function" || !target.closest(".scroller") || target.closest("[role=menu], [role=dialog]")) return;
|
|
196
|
+
const box = front.getBoundingClientRect();
|
|
197
|
+
burst(ev.clientX - box.left, ev.clientY - box.top);
|
|
198
|
+
};
|
|
199
|
+
const onVisible = () => { for (const a of anims) { if (doc.visibilityState === "hidden") a.pause(); else a.play(); } };
|
|
200
|
+
// Size moves (a rotated phone, the preview panel opening): the loop is drawn again for the new box, a moment later.
|
|
201
|
+
let resizeTimer = null, lastW = layer.clientWidth, lastH = layer.clientHeight;
|
|
202
|
+
const ro = typeof globalThis.ResizeObserver === "function" ? new globalThis.ResizeObserver(() => {
|
|
203
|
+
if (layer.clientWidth === lastW && layer.clientHeight === lastH) return;
|
|
204
|
+
lastW = layer.clientWidth; lastH = layer.clientHeight;
|
|
205
|
+
clearTimeout(resizeTimer);
|
|
206
|
+
resizeTimer = setTimeout(() => { if (!stopped) drawLoop(); }, 200);
|
|
207
|
+
}) : null;
|
|
208
|
+
|
|
209
|
+
drawLoop();
|
|
210
|
+
if (ambient.click) surface.addEventListener("pointerdown", onDown, { passive: true });
|
|
211
|
+
doc.addEventListener("visibilitychange", onVisible);
|
|
212
|
+
if (ro && ambient.loop) ro.observe(layer);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
stop() {
|
|
216
|
+
stopped = true;
|
|
217
|
+
clearTimeout(resizeTimer);
|
|
218
|
+
if (ro) ro.disconnect();
|
|
219
|
+
surface.removeEventListener("pointerdown", onDown, { passive: true });
|
|
220
|
+
doc.removeEventListener("visibilitychange", onVisible);
|
|
221
|
+
for (const a of anims) a.cancel();
|
|
222
|
+
anims.clear();
|
|
223
|
+
for (const n of loopNodes.splice(0)) n.remove();
|
|
224
|
+
while (front.firstChild) front.firstChild.remove();
|
|
225
|
+
},
|
|
226
|
+
/** For tests and the smoke: what is alive now. */
|
|
227
|
+
stats() { return { nodes: layer.children.length + front.children.length, animations: anims.size, bursts }; },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The chat's ambient: renders its two layers inside .chat-main (`surface`) and runs them. `part` is "layer" (behind,
|
|
233
|
+
* right after the theme picture) or "front" (after the dock); the "layer" instance owns the run and finds its sibling.
|
|
234
|
+
*/
|
|
235
|
+
export function ChatAmbient({ name, surface, part }) {
|
|
236
|
+
const block = useAmbientBlock();
|
|
237
|
+
const ambient = block ? null : ambientFor(name);
|
|
238
|
+
const key = ambient ? JSON.stringify(ambient) : "";
|
|
239
|
+
const ref = useRef(null);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (part !== "layer" || !ambient) return undefined;
|
|
242
|
+
const layer = ref.current, host = surface.current;
|
|
243
|
+
const front = host && host.querySelector(".amb-burst");
|
|
244
|
+
if (!layer || !host || !front) return undefined;
|
|
245
|
+
host.classList.add("has-amb");
|
|
246
|
+
const run = startAmbient({ name, ambient, layer, front, surface: host });
|
|
247
|
+
if (typeof globalThis !== "undefined") globalThis.__agendAmbient = run; // the smoke reads stats(); nothing else
|
|
248
|
+
return () => { run.stop(); host.classList.remove("has-amb"); if (globalThis.__agendAmbient === run) delete globalThis.__agendAmbient; };
|
|
249
|
+
}, [name, key, part]);
|
|
250
|
+
if (!ambient) return null;
|
|
251
|
+
return part === "layer" ? html`<div ref=${ref} class="amb-layer" aria-hidden="true"></div>` : html`<div class="amb-burst" aria-hidden="true"></div>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The one-time note (the user's direction): the system asks for reduced motion and this chat's ambient or Canvas Worker plays anyway —
|
|
256
|
+
* say so once on this device, and turn it off right here (this device's switch: the same on the public link, where
|
|
257
|
+
* Settings is not). Shown only while an ambient or Canvas Worker is playing.
|
|
258
|
+
*/
|
|
259
|
+
export function AmbientMotionNote({ name, canvasPlaying = false }) {
|
|
260
|
+
const block = useAmbientBlock();
|
|
261
|
+
const shown = canvasPlaying || !block && !!ambientFor(name);
|
|
262
|
+
const noteDue = motionNoteDue();
|
|
263
|
+
const [open, setOpen] = useState(false);
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (!shown || open || !noteDue) return;
|
|
266
|
+
markMotionNoteShown();
|
|
267
|
+
setOpen(true);
|
|
268
|
+
}, [shown, noteDue]);
|
|
269
|
+
if (!open || !shown) return null;
|
|
270
|
+
const off = () => { setOpen(false); setFollowsReducedMotion(true); };
|
|
271
|
+
return html`<div class="amb-note" data-trust="thread-control" role="status">
|
|
272
|
+
<span>${t("chat.ambientMotionNote")}</span>
|
|
273
|
+
<button type="button" class="amb-note-off" onClick=${off}>${t("chat.ambientMotionNoteLink")}</button>
|
|
274
|
+
<button type="button" class="amb-note-x" aria-label=${t("chat.ambientMotionNoteClose")} onClick=${() => setOpen(false)}>×</button>
|
|
275
|
+
</div>`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The fix for a block, done right here: both switches are this viewer's own (the show switch is a local choice on the
|
|
280
|
+
* public link; the reduced-motion one is per device), so it works on the public link too; safe mode is the URL. Both
|
|
281
|
+
* stay in Settings → Web appearance as well.
|
|
282
|
+
*/
|
|
283
|
+
const FIX = {
|
|
284
|
+
hidden: () => setThemesShown(true),
|
|
285
|
+
reducedMotion: () => setFollowsReducedMotion(false),
|
|
286
|
+
safe: () => { const u = new URL(globalThis.location.href); u.searchParams.delete("safe"); globalThis.location.assign(u.toString()); },
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The chat ⋯ entries for `name`'s ambient: why it is not showing here (a visible reason; choosing it fixes it when the
|
|
291
|
+
* page can), and — for an admin, never over the public link — Clear for each kind.
|
|
292
|
+
*/
|
|
293
|
+
export function ambientMenuItems(name, { publicLink, clear }) {
|
|
294
|
+
const a = themesStore.get().ambient;
|
|
295
|
+
const mine = a && own(a, name) ? a[name] : null;
|
|
296
|
+
if (!mine || typeof mine !== "object" || !(own(mine, "loop") || own(mine, "click"))) return [];
|
|
297
|
+
const items = [];
|
|
298
|
+
const block = ambientBlock();
|
|
299
|
+
if (block) {
|
|
300
|
+
items.push({
|
|
301
|
+
key: "ambient-blocked", label: t(`chat.ambientBlocked_${block}`), icon: "info",
|
|
302
|
+
onSelect: () => { if (own(FIX, block)) FIX[block](); },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (!publicLink) for (const k of ["loop", "click"]) if (own(mine, k)) items.push({ key: `ambient-clear-${k}`, label: t(k === "loop" ? "chat.ambientClearLoop" : "chat.ambientClearClick"), icon: "trash", onSelect: () => clear(name, k) });
|
|
306
|
+
return items;
|
|
307
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** #1629: static Chat fallback. It requires no preview origin, device consent, Worker or personal identity. */
|
|
2
|
+
import { useLayoutEffect } from "/assets/app-html.js";
|
|
3
|
+
import { appStore } from "/assets/app-store.js";
|
|
4
|
+
import { onPrefsChanged } from "/assets/app-prefs.js";
|
|
5
|
+
import { themesShown, themesStore } from "/assets/app-themes.js";
|
|
6
|
+
import { ownerCssLifecycle } from "/assets/owner-css.js";
|
|
7
|
+
import { effectPlaybackIdentity } from "/assets/effect-chat-runtime.js";
|
|
8
|
+
import { effectBootstrapReady } from "/assets/effect-frame.js";
|
|
9
|
+
import { motionBlocked, onMotionChange } from "/assets/fx-motion.js";
|
|
10
|
+
import { createChatPosters } from "/assets/effect-posters.js";
|
|
11
|
+
|
|
12
|
+
export function ChatEffectPosters({ name, lease, element }) {
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
let live = true, api = null;
|
|
15
|
+
const releases = [], host = window;
|
|
16
|
+
const current = () => live && lease.current();
|
|
17
|
+
const cleanup = () => { if (!live) return; live = false; try { api?.dispose?.(); } catch { /* optional */ } for (const off of releases) { try { off?.(); } catch { /* release all */ } } };
|
|
18
|
+
lease.hold(cleanup);
|
|
19
|
+
try {
|
|
20
|
+
const motion = host.matchMedia?.("(prefers-reduced-motion: reduce)") ?? { matches: true };
|
|
21
|
+
const observe = () => {
|
|
22
|
+
// This is a surface fallback, not a replacement for an explicit Pause, queued effect or failed live read.
|
|
23
|
+
const playback = effectPlaybackIdentity(host) && typeof host.Worker === "function" && typeof host.OffscreenCanvas === "function" &&
|
|
24
|
+
effectBootstrapReady(host, host.location?.origin, host.document.body.dataset.effectBoot) && !motionBlocked(host);
|
|
25
|
+
return { shown: themesShown(), fallback: !playback, ownerCss: ownerCssLifecycle.get().state };
|
|
26
|
+
};
|
|
27
|
+
api = createChatPosters({ window: host, name, element: element.current, current, observe });
|
|
28
|
+
if (!api.ok) return cleanup;
|
|
29
|
+
const sync = () => { if (current()) api.sync(); };
|
|
30
|
+
let revision = appStore.get().themesRev;
|
|
31
|
+
releases.push(appStore.subscribe(s => {
|
|
32
|
+
if (!current()) return;
|
|
33
|
+
if (s.connection === "ended") { api.dispose(); return; }
|
|
34
|
+
if (s.themesRev !== revision) { revision = s.themesRev; api.changed(); } else sync();
|
|
35
|
+
}));
|
|
36
|
+
releases.push(ownerCssLifecycle.subscribe(sync)); releases.push(themesStore.subscribe(sync)); releases.push(onPrefsChanged(sync)); releases.push(onMotionChange(sync));
|
|
37
|
+
for (const target of [motion, host.navigator?.connection]) if (target?.addEventListener) { target.addEventListener("change", sync); releases.push(() => target.removeEventListener("change", sync)); }
|
|
38
|
+
sync(); return cleanup;
|
|
39
|
+
} catch { cleanup(); return cleanup; }
|
|
40
|
+
}, [name, lease]);
|
|
41
|
+
return null;
|
|
42
|
+
}
|