@songsid/agend 2.2.0-beta.9 → 2.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -24
- package/README.zh-TW.md +40 -24
- package/dist/agent-cli.js +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +28 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +20 -1
- 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 +3 -1
- package/dist/auth-api.js +32 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +33 -0
- package/dist/backend/antigravity.js +129 -21
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +9 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +3 -0
- package/dist/backend/grok.js +9 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -1
- package/dist/backend/kiro.js +7 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +24 -0
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +14 -0
- package/dist/channel/adapters/discord.js +66 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +26 -1
- package/dist/channel/adapters/telegram.js +98 -7
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +1 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +31 -0
- package/dist/cli.js +124 -24
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +5 -2
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.d.ts +86 -0
- package/dist/compact-watch.js +288 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/config-validator.js +55 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.js +394 -29
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -1
- package/dist/delivery-outbox.js +6 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.js +7 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +12 -0
- package/dist/fleet-manager.d.ts +247 -9
- package/dist/fleet-manager.js +1227 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/install-lock.d.ts +18 -1
- package/dist/install-lock.js +30 -3
- package/dist/install-lock.js.map +1 -1
- package/dist/instance-avatar.d.ts +5 -4
- package/dist/instance-avatar.js +15 -8
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +2 -2
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/launchd-handoff.d.ts +43 -0
- package/dist/launchd-handoff.js +56 -0
- package/dist/launchd-handoff.js.map +1 -0
- package/dist/locale.js +202 -30
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +10 -2
- package/dist/log-service.js.map +1 -1
- package/dist/login-shell.d.ts +21 -0
- package/dist/login-shell.js +98 -0
- package/dist/login-shell.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 +32 -0
- package/dist/outbound-handlers.js +150 -45
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +76 -0
- package/dist/outbound-schemas.js +65 -4
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +29 -0
- package/dist/provider-probe.js +198 -2
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +25 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +8 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +12 -0
- package/dist/quickstart.js +36 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +86 -5
- package/dist/reply-buttons-controller.js +271 -7
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- 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-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +0 -2
- package/dist/restart-progress.js +0 -2
- package/dist/restart-progress.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/sd-notify.d.ts +22 -1
- package/dist/sd-notify.js +63 -10
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +42 -26
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +24 -0
- package/dist/service-installer.js +49 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +11 -0
- package/dist/service-plan.js +23 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -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/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tool-permissions.js +53 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +72 -2
- package/dist/topic-commands.js +156 -38
- 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/transcript-jsonl.d.ts +7 -2
- package/dist/transcript-jsonl.js +13 -7
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-sources.js +6 -1
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +59 -14
- package/dist/ui/chat-store.js +78 -8
- package/dist/ui/chat-strings.js +43 -16
- package/dist/ui/chat-thread.js +175 -31
- package/dist/ui/chat-tour.js +18 -15
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +101 -27
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +18 -5
- package/dist/ui/panel-log.js +10 -10
- package/dist/ui/panel-needs.js +13 -10
- package/dist/ui/panel-settings.js +66 -12
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-dialogs.js +11 -6
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-model.js +3 -1
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +269 -10
- package/dist/ui/settings-token.js +7 -0
- package/dist/ui/settings-wizard.js +5 -3
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +3 -1
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +15 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +266 -11
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -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 +115 -0
- package/dist/ui/shared/effect-policy.js +137 -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 +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -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 +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +35 -15
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/instance-switch.js +5 -2
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +96 -12
- package/dist/ui/shared/signin.js +17 -9
- 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/view-strings.js +40 -2
- package/dist/ui/signin.html +9 -3
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-launch.d.ts +48 -1
- package/dist/update-launch.js +159 -5
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/providers.d.ts +28 -8
- package/dist/usage/providers.js +96 -41
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +214 -17
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +86 -2
- package/dist/web-api.js +1160 -92
- 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 +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +39 -1
- package/dist/web-chat-history.js +69 -9
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +39 -3
- package/dist/web-commands.js +22 -5
- package/dist/web-commands.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -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-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 +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -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 +29 -0
- package/dist/web-effect-upload.js +190 -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 +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.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 +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -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 +180 -0
- package/dist/web-owner-css-store.js +384 -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 +66 -0
- package/dist/web-prefs.js +243 -35
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-session.d.ts +14 -0
- package/dist/web-session.js +10 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +119 -4
- package/dist/web-upload.js +252 -39
- 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/launcher/install-admission.cjs +61 -6
- package/package.json +3 -3
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/** #1629: app-owned pool; Chat contributes a lease, never another registry/loader. */
|
|
2
|
+
import { createEffectTrialSource } from "./effect-trial-source.js";
|
|
3
|
+
import { createOwnerEffectSource } from "./effect-owner-source.js";
|
|
4
|
+
import { createEffectRuntimeLoader } from "./effect-runtime-loader.js";
|
|
5
|
+
import { createEffectRegistry } from "./effect-registry.js";
|
|
6
|
+
import { createEffectSafety } from "./effect-safety.js";
|
|
7
|
+
import { stockEffectTrust } from "./effect-stock-trust.js";
|
|
8
|
+
import { createEffectProfileCollector } from "./effect-profiles.js";
|
|
9
|
+
import { chatEffectIdentity, createEffectWriter } from "./effect-chat-runtime.js";
|
|
10
|
+
|
|
11
|
+
const episodes = new WeakSet();
|
|
12
|
+
|
|
13
|
+
/** `current` belongs to the page/session episode. Route cleanup only releases its contribution. */
|
|
14
|
+
export function createAppEffectRuntime({ window: host, current, observe, onChange = () => {}, onDispose = () => {}, resolveOwnerTarget = null, onOwnerUnavailable = () => {}, factories = {} }) {
|
|
15
|
+
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
|
+
const bindings = new Map(), profiles = new Map();
|
|
17
|
+
const identity = chatEffectIdentity(host), auth = host?.AgendAuth;
|
|
18
|
+
const safe = fn => { try { return fn(); } catch { return null; } };
|
|
19
|
+
const alive = () => { if (dead) return false; const value = safe(() => current() === true && !auth.sessionEnded()); return !dead && value === true; };
|
|
20
|
+
const sourceAlive = s => {
|
|
21
|
+
if (!s || !alive() || s !== source || s.dead) return false;
|
|
22
|
+
const value = safe(() => s.current() === true && s.element.isConnected === true);
|
|
23
|
+
return !dead && s === source && !s.dead && value === true;
|
|
24
|
+
};
|
|
25
|
+
const placementIdentity = p => JSON.stringify([p.by, p.manifest.layer, p.params]);
|
|
26
|
+
const clock = () => host.performance.now();
|
|
27
|
+
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 });
|
|
29
|
+
function tell() {
|
|
30
|
+
const s = source, value = snapshot(s); if (dead) return;
|
|
31
|
+
if (sourceAlive(s)) safe(() => s.onChange(value));
|
|
32
|
+
if (!dead) safe(() => onChange(snapshot(source)));
|
|
33
|
+
}
|
|
34
|
+
function discard(s) {
|
|
35
|
+
if (!s || s.dead) return;
|
|
36
|
+
s.dead = true; s.generation++; s.metadata = null;
|
|
37
|
+
if (source === s) source = null;
|
|
38
|
+
safe(() => trial?.stopFor(s));
|
|
39
|
+
for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear();
|
|
40
|
+
if (pending?.source === s) safe(() => pending.controller.abort());
|
|
41
|
+
revision++; stamp = null;
|
|
42
|
+
}
|
|
43
|
+
function dispose() {
|
|
44
|
+
if (dead) return; dead = true; discard(source);
|
|
45
|
+
safe(() => trial?.dispose()); safe(() => pending?.controller.abort()); safe(() => registry?.dispose?.()); safe(() => loader?.dispose?.()); safe(() => personal?.dispose()); safe(() => writer?.dispose()); safe(() => offAuth?.());
|
|
46
|
+
safe(() => host?.removeEventListener?.("pagehide", dispose));
|
|
47
|
+
for (const row of profiles.values()) safe(() => row.collector.dispose()); profiles.clear(); bindings.clear(); safe(() => onDispose({ error }));
|
|
48
|
+
}
|
|
49
|
+
function fail(reason) { if (dead) return; error = reason; dispose(); safe(() => onChange({ error, rows: [], effects: [], loading: false })); }
|
|
50
|
+
const unavailable = { ok: false, sync() {}, changedChat() {}, reply() {}, detachChat() {}, attachChat() { return null; }, attachTrial() { return null; }, stopTrial() {}, trialSnapshot: () => ({ rows: [] }), snapshot: () => snapshot(null), dispose };
|
|
51
|
+
if (!host || typeof host !== "object" || episodes.has(host) || !identity || typeof current !== "function" || typeof observe !== "function" || typeof auth?.onSessionEnd !== "function" || typeof auth?.sessionEnded !== "function") return unavailable;
|
|
52
|
+
episodes.add(host); // Claim before invoking current/auth callbacks: one attempt per Window episode.
|
|
53
|
+
if (!alive()) { dispose(); return unavailable; }
|
|
54
|
+
try {
|
|
55
|
+
offAuth = auth.onSessionEnd(dispose);
|
|
56
|
+
if (!alive()) { dispose(); safe(() => offAuth?.()); return unavailable; }
|
|
57
|
+
host.addEventListener?.("pagehide", dispose);
|
|
58
|
+
} catch { fail("runtime_unavailable"); return unavailable; }
|
|
59
|
+
const make = (key, fn) => factories[key] ?? fn;
|
|
60
|
+
function bindingAlive(b) { if (b?.kind === "trial") return trial?.current(b) === true; if (b?.kind === "owner") return personal?.current(b) === true; return !!b && sourceAlive(b.source) && b.source.generation === b.generation && bindings.get(b.key) === b && b.source.bindings.get(b.id) === b; }
|
|
61
|
+
const bindingFor = (key, id) => { const b = bindings.get(key) ?? trial?.binding(key, id) ?? personal?.binding(key, id); return b?.id === id && bindingAlive(b) ? b : null; };
|
|
62
|
+
try {
|
|
63
|
+
writer = createEffectWriter(host, alive);
|
|
64
|
+
loader = make("loader", createEffectRuntimeLoader)({ window: host, ownerEpoch: identity.epoch, current: alive, onRevoke: () => fail("runtime_unavailable") });
|
|
65
|
+
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,
|
|
67
|
+
load: async (descriptor, signal) => {
|
|
68
|
+
const b = bindingFor(descriptor.key, descriptor.id);
|
|
69
|
+
if (!bindingAlive(b) || descriptor.id !== b.id || signal.aborted) return null;
|
|
70
|
+
if (b.kind === "trial") return trial.load(b, signal);
|
|
71
|
+
if (b.kind === "owner") return personal.load(b, signal);
|
|
72
|
+
const value = await loader.resourceSettled({ id: b.id, instance: b.source.name }, signal);
|
|
73
|
+
if (!bindingAlive(b) || signal.aborted) return null;
|
|
74
|
+
if (!value) { refuseSource(b.source); return null; }
|
|
75
|
+
return value;
|
|
76
|
+
},
|
|
77
|
+
safety: make("safety", createEffectSafety)({ storage: host.localStorage, crypto: host.crypto }),
|
|
78
|
+
stockSnapshot: ({ frameRoot }) => make("stock", stockEffectTrust)(host, { frameRoot, ownerCss: observe().ownerCss }),
|
|
79
|
+
onChange() { try { tell(); emit(); } catch { fail("runtime_unavailable"); } },
|
|
80
|
+
onMount(owner) {
|
|
81
|
+
const b = bindingFor(owner.key, owner.id); if (!bindingAlive(b) || b.id !== owner.id || !owner.current()) return;
|
|
82
|
+
const collector = make("collector", createEffectProfileCollector)({ key: owner.key, id: owner.id, device: owner.device, clock });
|
|
83
|
+
const began = safe(clock);
|
|
84
|
+
if (collector.ok && Number.isFinite(began) && began >= 0 && bindingAlive(b) && owner.current()) profiles.set(owner, { binding: b, collector, began, attempted: null }); else collector.dispose?.();
|
|
85
|
+
},
|
|
86
|
+
onRetire(owner) { profiles.get(owner)?.collector.dispose(); profiles.delete(owner); },
|
|
87
|
+
onProfile(raw, owner) {
|
|
88
|
+
const row = profiles.get(owner); if (!row || !bindingAlive(row.binding) || !owner.current()) return;
|
|
89
|
+
row.collector.add(raw); if (row.binding.kind === "trial") return; // Diagnostic samples stay transient; shared safety still runs in registry.
|
|
90
|
+
const now = clock();
|
|
91
|
+
if (!Number.isFinite(now) || now < row.began || now - row.began < 10000 && raw.verdict !== "paused" || row.attempted !== null && now - row.attempted < 10000) return;
|
|
92
|
+
const report = row.collector.report(); if (!report || !bindingAlive(row.binding) || !owner.current()) return;
|
|
93
|
+
row.attempted = now;
|
|
94
|
+
const query = row.binding.kind === "owner" ? "" : `instance=${encodeURIComponent(row.binding.source.name)}&`;
|
|
95
|
+
void writer.post(`/ui/effects/profile/${owner.id}?${query}ownerEpoch=${identity.epoch}`, report.profile);
|
|
96
|
+
}, clock,
|
|
97
|
+
});
|
|
98
|
+
if (!registry.ok) throw Error("runtime_unavailable");
|
|
99
|
+
trial = make("trialSource", createEffectTrialSource)({ window: host, current: () => {
|
|
100
|
+
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;
|
|
103
|
+
}, ownerCurrent: (s, name) => sourceAlive(s) && s.name === name,
|
|
104
|
+
stock: () => registry.frameRoot ? make("stock", stockEffectTrust)(host, { frameRoot: registry.frameRoot, ownerCss: observe().ownerCss }) : null,
|
|
105
|
+
readResource: (spec, signal) => typeof loader.trialResourceSettled === "function" ? loader.trialResourceSettled(spec, signal) : Promise.resolve(null),
|
|
106
|
+
onChange() { revision++; stamp = null; sync(); tell(); },
|
|
107
|
+
});
|
|
108
|
+
if (typeof resolveOwnerTarget === "function") {
|
|
109
|
+
if (typeof loader.ownerSettled !== "function") throw Error("runtime_unavailable");
|
|
110
|
+
personal = createOwnerEffectSource({ current: alive,
|
|
111
|
+
readMetadata: signal => loader.ownerSettled(signal), readResource: (spec, signal) => loader.resourceSettled(spec, signal), resolveTarget: resolveOwnerTarget,
|
|
112
|
+
stock: () => registry.frameRoot ? make("stock", stockEffectTrust)(host, { frameRoot: registry.frameRoot, ownerCss: observe().ownerCss }) : null,
|
|
113
|
+
onChange() { revision++; stamp = null; sync(); tell(); }, onUnavailable: (slot, id) => safe(() => onOwnerUnavailable(slot, id)),
|
|
114
|
+
});
|
|
115
|
+
if (!personal.ok) throw Error("runtime_unavailable");
|
|
116
|
+
}
|
|
117
|
+
} catch { fail("runtime_unavailable"); return unavailable; }
|
|
118
|
+
function descriptors(s) {
|
|
119
|
+
if (!sourceAlive(s) || s.error) return [];
|
|
120
|
+
return (s.metadata?.effects ?? []).filter(p => p.manifest.regions.includes("chat")).map((p, order) => {
|
|
121
|
+
let b = s.bindings.get(p.id); const identity = placementIdentity(p);
|
|
122
|
+
if (!b || b.identity !== identity) {
|
|
123
|
+
if (b) bindings.delete(b.key);
|
|
124
|
+
b = Object.freeze({ key: `chat:${s.serial}:${p.id}`, id: p.id, source: s, generation: s.generation, identity });
|
|
125
|
+
s.bindings.set(p.id, b); bindings.set(b.key, b);
|
|
126
|
+
}
|
|
127
|
+
return { key: b.key, id: p.id, by: p.by, region: "chat", layer: p.manifest.layer, order, paused: s.paused.get(p.id) === identity, visible: true, instance: s.name, params: p.params, element: s.element };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function observation() {
|
|
131
|
+
const value = observe();
|
|
132
|
+
return { ...value, context: { ...value.context, openInstance: sourceAlive(source) ? source.name : null } };
|
|
133
|
+
}
|
|
134
|
+
function emit() {
|
|
135
|
+
if (!alive() || !registry?.ok) return;
|
|
136
|
+
const s = source, value = observation();
|
|
137
|
+
if (sourceAlive(s)) { const state = safe(() => s.observe().state); if (sourceAlive(s) && ["idle", "working", "needs_you", "stopped"].includes(state)) registry.publish("state", { state }, s.name); }
|
|
138
|
+
if (alive() && ["light", "dark"].includes(value.mode)) registry.publish("theme", { mode: value.mode });
|
|
139
|
+
}
|
|
140
|
+
function refuseSource(s) {
|
|
141
|
+
if (!sourceAlive(s)) return;
|
|
142
|
+
s.error = "resource_unavailable"; s.metadata = null; s.generation++;
|
|
143
|
+
for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear(); revision++; stamp = null; sync(); tell();
|
|
144
|
+
}
|
|
145
|
+
function queueSync() {
|
|
146
|
+
if (queued || dead) return; queued = true;
|
|
147
|
+
Promise.resolve().then(() => { queued = false; if (alive()) sync(); else dispose(); });
|
|
148
|
+
}
|
|
149
|
+
function sync() {
|
|
150
|
+
if (!alive()) { dispose(); return; } if (syncing) { queueSync(); return; }
|
|
151
|
+
syncing = true;
|
|
152
|
+
try {
|
|
153
|
+
const candidate = source;
|
|
154
|
+
if (candidate && !sourceAlive(candidate)) discard(candidate);
|
|
155
|
+
const before = revision, value = observation(), s = source;
|
|
156
|
+
if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
|
|
157
|
+
const entries = [...(personal?.descriptors() ?? []), ...(trial?.descriptors() ?? []), ...descriptors(s)];
|
|
158
|
+
if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
|
|
159
|
+
const next = JSON.stringify([revision, value.context, entries.map(d => [d.key, d.paused])]);
|
|
160
|
+
if (stamp !== next) {
|
|
161
|
+
if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
|
|
162
|
+
stamp = next; registry.update(entries, value.context);
|
|
163
|
+
}
|
|
164
|
+
if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
|
|
165
|
+
emit();
|
|
166
|
+
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;
|
|
169
|
+
if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
|
|
170
|
+
if (ownerNeeds && (!chatNeeds || lastMetadata !== "owner")) void readPersonal();
|
|
171
|
+
else if (chatNeeds) void read(s);
|
|
172
|
+
}
|
|
173
|
+
} catch { fail("runtime_unavailable"); }
|
|
174
|
+
finally { syncing = false; }
|
|
175
|
+
}
|
|
176
|
+
async function readPersonal() {
|
|
177
|
+
if (!personal || pending || !personal.needsMetadata() || !alive()) return;
|
|
178
|
+
const op = { source: personal, controller: new host.AbortController() }; pending = op; lastMetadata = "owner";
|
|
179
|
+
try { await personal.read(op.controller.signal); }
|
|
180
|
+
catch { fail("runtime_unavailable"); }
|
|
181
|
+
finally { if (pending === op) { pending = null; if (alive()) sync(); } }
|
|
182
|
+
}
|
|
183
|
+
function changedOwner() {
|
|
184
|
+
if (!personal || !alive()) return;
|
|
185
|
+
if (pending?.source === personal) safe(() => pending.controller.abort());
|
|
186
|
+
personal.invalidate();
|
|
187
|
+
}
|
|
188
|
+
function ownerSnapshot() {
|
|
189
|
+
return { ...(personal?.snapshot() ?? { error: null, rev: -1, loading: false, slots: {} }),
|
|
190
|
+
rows: registry?.snapshot().flatMap(row => { const b = personal?.binding(row.key, row.id); return b ? [{ ...row, slot: b.slot }] : []; }) ?? [] };
|
|
191
|
+
}
|
|
192
|
+
async function cursorImageSettled(selection, signal) {
|
|
193
|
+
const own = personal?.cursorSelection();
|
|
194
|
+
if (!alive() || !own || !personal.cursorCurrent(selection) || signal?.aborted) return null;
|
|
195
|
+
let value; try { value = await loader.cursorImageSettled(own.id, signal); } catch { value = null; }
|
|
196
|
+
if (!alive() || !personal.cursorCurrent(own) || !personal.cursorCurrent(selection) || signal?.aborted) return null;
|
|
197
|
+
if (!value) { personal.refuseCursor(own); return null; }
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
async function read(s) {
|
|
201
|
+
if (!sourceAlive(s) || pending || s.error) return;
|
|
202
|
+
const op = { source: s, generation: s.generation, controller: new host.AbortController() }; pending = op; lastMetadata = "chat";
|
|
203
|
+
try {
|
|
204
|
+
const answer = await loader.placementsSettled(s.name, op.controller.signal);
|
|
205
|
+
if (!sourceAlive(s) || op.generation !== s.generation || pending !== op || op.controller.signal.aborted) return;
|
|
206
|
+
if (!answer) { refuseSource(s); return; }
|
|
207
|
+
s.metadata = answer;
|
|
208
|
+
for (const [id, identity] of s.paused) if (!answer.effects.some(p => p.id === id && placementIdentity(p) === identity)) s.paused.delete(id);
|
|
209
|
+
revision++; stamp = null; sync(); tell();
|
|
210
|
+
} catch { if (sourceAlive(s) && op.generation === s.generation) refuseSource(s); }
|
|
211
|
+
finally { if (pending === op) { pending = null; if (alive()) sync(); } }
|
|
212
|
+
}
|
|
213
|
+
function changedSource(s) {
|
|
214
|
+
if (!sourceAlive(s) || s.error) return;
|
|
215
|
+
s.generation++; s.metadata = null;
|
|
216
|
+
for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear();
|
|
217
|
+
if (pending?.source === s) safe(() => pending.controller.abort()); revision++; stamp = null; sync(); tell();
|
|
218
|
+
}
|
|
219
|
+
function publishReply(s, event) { if (sourceAlive(s) && !s.error && event.instance === s.name) registry.publish("reply", {}, s.name); }
|
|
220
|
+
function attachChat(options) {
|
|
221
|
+
discard(source); const expected = revision; sync();
|
|
222
|
+
if (!alive() || revision !== expected || source !== null) return null;
|
|
223
|
+
const { name, element, current: ownCurrent, observe: ownObserve, onChange: ownChange = () => {} } = options ?? {};
|
|
224
|
+
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
|
+
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;
|
|
227
|
+
const placement = id => sourceAlive(s) && !s.error ? s.metadata?.effects.find(p => p.id === id) : null;
|
|
228
|
+
const changed = () => changedSource(s);
|
|
229
|
+
const api = { ok: true, sync,
|
|
230
|
+
changed,
|
|
231
|
+
reply(event) { publishReply(s, event); },
|
|
232
|
+
pause(id) { const p = placement(id); if (!p) return false; s.paused.set(id, placementIdentity(p)); revision++; sync(); return true; },
|
|
233
|
+
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; },
|
|
235
|
+
snapshot: () => sourceAlive(s) ? snapshot(s) : { error: null, rows: [], effects: [], loading: false },
|
|
236
|
+
dispose() { if (source !== s) return; discard(s); sync(); },
|
|
237
|
+
};
|
|
238
|
+
sync(); tell(); return sourceAlive(s) ? api : null;
|
|
239
|
+
}
|
|
240
|
+
return { ok: true, sync, attachChat, changedOwner, ownerSnapshot,
|
|
241
|
+
attachTrial(options) { const s = source; return sourceAlive(s) ? trial.attach(options, s) : null; },
|
|
242
|
+
stopTrial() { trial?.stop(); },
|
|
243
|
+
trialSnapshot() { return { rows: registry?.snapshot().filter(row => trial?.binding(row.key, row.id)) ?? [] }; },
|
|
244
|
+
cursorSelection: () => personal?.cursorSelection() ?? null, cursorImageSettled,
|
|
245
|
+
pauseOwner(slot) { return personal?.pause(slot) === true; },
|
|
246
|
+
resumeOwner(slot) { if (!personal?.unpause(slot)) return false; const b = personal.slotBinding(slot); return bindingAlive(b) && registry.resume(b.key); },
|
|
247
|
+
changedChat() { changedSource(source); }, reply(event) { publishReply(source, event); }, detachChat() { discard(source); sync(); }, snapshot: () => snapshot(source), dispose };
|
|
248
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** #1629: host cadence before ANY effect frame; never learns a baseline from an active effect. */
|
|
2
|
+
const finite = n => typeof n === "number" && Number.isFinite(n);
|
|
3
|
+
export function captureEffectBaseline({ window: host, empty, clock = () => host.performance.now() } = {}) {
|
|
4
|
+
let finish, settled = false, raf = null, timer = null, last = null, first = null, count = 0;
|
|
5
|
+
const promise = new Promise(resolve => { finish = resolve; });
|
|
6
|
+
const end = value => {
|
|
7
|
+
if (settled) return;
|
|
8
|
+
settled = true;
|
|
9
|
+
if (raf !== null) host.cancelAnimationFrame(raf);
|
|
10
|
+
if (timer !== null) host.clearTimeout(timer);
|
|
11
|
+
host.document.removeEventListener("visibilitychange", visibility);
|
|
12
|
+
finish(value);
|
|
13
|
+
};
|
|
14
|
+
const fail = reason => end({ ok: false, reason });
|
|
15
|
+
const gates = () => {
|
|
16
|
+
try { return host.document.visibilityState === "visible" && empty() === true; } catch { return false; }
|
|
17
|
+
};
|
|
18
|
+
const time = () => {
|
|
19
|
+
let now; try { now = clock(); } catch { return null; }
|
|
20
|
+
if (!finite(now) || now < 0 || last !== null && now < last) return null;
|
|
21
|
+
last = now; return now;
|
|
22
|
+
};
|
|
23
|
+
const visibility = () => { if (!gates()) fail("baseline_unavailable"); };
|
|
24
|
+
const step = () => {
|
|
25
|
+
if (settled) return;
|
|
26
|
+
raf = null;
|
|
27
|
+
if (!gates()) { fail("baseline_unavailable"); return; }
|
|
28
|
+
const now = time();
|
|
29
|
+
if (now === null) { fail("clock_unavailable"); return; }
|
|
30
|
+
if (now - began >= 10000) { fail("baseline_timeout"); return; }
|
|
31
|
+
if (first === null) { first = now; count = 1; }
|
|
32
|
+
else if (now > sample) count++;
|
|
33
|
+
sample = now;
|
|
34
|
+
if (now - first >= 2000) {
|
|
35
|
+
const fps = (count - 1) * 1000 / (now - first);
|
|
36
|
+
if (!finite(fps) || fps <= 0 || fps > 1000 || !gates()) fail("baseline_unavailable");
|
|
37
|
+
else end({ ok: true, baselineFps: fps });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
raf = host.requestAnimationFrame(step);
|
|
41
|
+
};
|
|
42
|
+
let began = null, sample = null;
|
|
43
|
+
if (!host || !host.document || typeof empty !== "function" || typeof clock !== "function" || ["requestAnimationFrame", "cancelAnimationFrame", "setTimeout", "clearTimeout"].some(k => typeof host[k] !== "function") || typeof host.document.addEventListener !== "function" || typeof host.document.removeEventListener !== "function") {
|
|
44
|
+
settled = true; finish({ ok: false, reason: "baseline_unsupported" });
|
|
45
|
+
return { promise, cancel() {} };
|
|
46
|
+
}
|
|
47
|
+
began = time();
|
|
48
|
+
if (began === null) fail("clock_unavailable");
|
|
49
|
+
else if (!gates()) fail("baseline_unavailable");
|
|
50
|
+
else {
|
|
51
|
+
host.document.addEventListener("visibilitychange", visibility);
|
|
52
|
+
const deadline = () => {
|
|
53
|
+
if (settled) return;
|
|
54
|
+
if (!gates()) { fail("baseline_unavailable"); return; }
|
|
55
|
+
const now = time();
|
|
56
|
+
if (now === null) fail("clock_unavailable");
|
|
57
|
+
else if (now - began >= 10000) fail("baseline_timeout");
|
|
58
|
+
else timer = host.setTimeout(deadline, Math.max(1, 10000 - (now - began)));
|
|
59
|
+
};
|
|
60
|
+
timer = host.setTimeout(deadline, 10000);
|
|
61
|
+
raf = host.requestAnimationFrame(step);
|
|
62
|
+
}
|
|
63
|
+
return { promise, cancel() { fail("baseline_cancelled"); } };
|
|
64
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/** #1629: one committed local Chat lease joins the reviewed host-only effect primitives. */
|
|
2
|
+
import { createEffectRuntimeLoader } from "./effect-runtime-loader.js";
|
|
3
|
+
import { createEffectRegistry } from "./effect-registry.js";
|
|
4
|
+
import { createEffectSafety } from "./effect-safety.js";
|
|
5
|
+
import { stockEffectTrust } from "./effect-stock-trust.js";
|
|
6
|
+
import { createEffectProfileCollector } from "./effect-profiles.js";
|
|
7
|
+
|
|
8
|
+
const writers = new WeakMap();
|
|
9
|
+
/** Physical write reservations outlive a canceled caller. Responses never supply runtime authority. */
|
|
10
|
+
export function createEffectWriter(host, current) {
|
|
11
|
+
let dead = false;
|
|
12
|
+
const own = new Set();
|
|
13
|
+
if (!writers.has(host)) writers.set(host, new Set());
|
|
14
|
+
const all = writers.get(host);
|
|
15
|
+
let previous = null;
|
|
16
|
+
function stop() { if (dead) return; dead = true; for (const op of own) op.cancel(); }
|
|
17
|
+
function time() {
|
|
18
|
+
let now; try { now = host.performance.now(); } catch { stop(); return null; }
|
|
19
|
+
if (!Number.isFinite(now) || now < 0 || previous !== null && now < previous) { stop(); return null; }
|
|
20
|
+
previous = now; return now;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
async post(path, body) {
|
|
24
|
+
if (dead || !current() || all.size >= 3) return false;
|
|
25
|
+
const began = time(); if (began === null) return false;
|
|
26
|
+
const text = JSON.stringify(body); if (new TextEncoder().encode(text).length > 16384) return false;
|
|
27
|
+
const controller = new host.AbortController(), op = { controller }; own.add(op); all.add(op);
|
|
28
|
+
let timer, cancel;
|
|
29
|
+
const aborted = new Promise(resolve => { cancel = () => { controller.abort(); resolve(false); }; }); op.cancel = cancel;
|
|
30
|
+
const work = Promise.resolve().then(async () => {
|
|
31
|
+
if (dead || !current() || controller.signal.aborted) return false;
|
|
32
|
+
const r = await host.fetch(path, { method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" }, body: text, signal: controller.signal });
|
|
33
|
+
// Settle the body before returning the physical reservation, even if the caller already timed out.
|
|
34
|
+
try { await r.body?.cancel(); } catch { /* no data is consumed */ }
|
|
35
|
+
const now = time();
|
|
36
|
+
return !dead && current() && !controller.signal.aborted && Number.isFinite(began) && began >= 0 && now !== null && now >= began && now - began < 10000 && r.ok && !r.redirected;
|
|
37
|
+
}).catch(() => false).finally(() => { host.clearTimeout(timer); controller.abort(); own.delete(op); all.delete(op); });
|
|
38
|
+
timer = host.setTimeout(cancel, 10000);
|
|
39
|
+
return Promise.race([work, aborted]);
|
|
40
|
+
},
|
|
41
|
+
dispose: stop,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Frozen, server-rendered session identity only. A header-token/public/View load has no identity. */
|
|
46
|
+
export function chatEffectIdentity(host) {
|
|
47
|
+
try {
|
|
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;
|
|
50
|
+
return Object.freeze({ epoch: x.ownerEpoch, scope: x.ownerScope });
|
|
51
|
+
} catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
export function chatEffectState(state, name) {
|
|
54
|
+
const inst = state.instances?.find(i => i.name === name);
|
|
55
|
+
if (!inst) return null;
|
|
56
|
+
if (["stopped", "paused"].includes(inst.status)) return "stopped";
|
|
57
|
+
if (Object.hasOwn(state.awaiting ?? {}, name)) return "needs_you";
|
|
58
|
+
return ["working", "stuck"].includes(state.exec?.[name]) ? "working" : state.exec?.[name] === "idle" ? "idle" : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `observe` supplies closed host gates, never event/message content. No timers poll server state. */
|
|
62
|
+
export function createChatEffectRuntime({ window: host, name, element, current, observe, onChange = () => {}, factories = {} }) {
|
|
63
|
+
let dead = false, error = null, loader = null, registry = null, writer = null, active = null, queued = false, generation = 0, metadata = null, ctx = null, lastContext = null;
|
|
64
|
+
const paused = new Map(), owners = new Map();
|
|
65
|
+
const placementIdentity = p => JSON.stringify([p.by, p.manifest.layer, p.params]);
|
|
66
|
+
const identity = chatEffectIdentity(host), auth = host.AgendAuth;
|
|
67
|
+
const alive = () => !dead && current() === true && element.isConnected === true && !auth.sessionEnded();
|
|
68
|
+
const tell = () => { if (!dead && current()) onChange({ error, rows: registry?.snapshot() ?? [], effects: metadata?.effects ?? [], loading: metadata === null && !error }); };
|
|
69
|
+
const fail = reason => { if (dead) return; error = reason; generation++; metadata = null; active?.controller.abort(); registry?.dispose?.(); registry = null; loader?.dispose?.(); loader = null; writer?.dispose(); tell(); };
|
|
70
|
+
function dispose() {
|
|
71
|
+
if (dead) return; dead = true; generation++; active?.controller.abort(); registry?.dispose?.(); loader?.dispose?.(); writer?.dispose(); offAuth?.();
|
|
72
|
+
for (const value of owners.values()) value.collector.dispose(); owners.clear();
|
|
73
|
+
}
|
|
74
|
+
let offAuth = null;
|
|
75
|
+
if (!identity || typeof auth?.onSessionEnd !== "function" || typeof auth?.sessionEnded !== "function" || typeof name !== "string" || name.length < 1 || name.length > 128 || !current()) return { ok: false, dispose };
|
|
76
|
+
offAuth = auth.onSessionEnd(dispose); // May synchronously replay an already-ended episode.
|
|
77
|
+
if (dead || !alive()) { dispose(); return { ok: false, dispose }; }
|
|
78
|
+
const clock = () => host.performance.now();
|
|
79
|
+
try {
|
|
80
|
+
const make = (key, fn) => factories[key] ?? fn;
|
|
81
|
+
writer = createEffectWriter(host, alive);
|
|
82
|
+
loader = make("loader", createEffectRuntimeLoader)({ window: host, ownerEpoch: identity.epoch, current: alive });
|
|
83
|
+
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,
|
|
85
|
+
load: async (d, signal) => {
|
|
86
|
+
const mine = generation, value = await loader.resource({ id: d.id, instance: name }, signal);
|
|
87
|
+
if (!alive() || mine !== generation || signal.aborted) return null;
|
|
88
|
+
if (!value) { fail("resource_unavailable"); return null; }
|
|
89
|
+
return value;
|
|
90
|
+
},
|
|
91
|
+
safety: make("safety", createEffectSafety)({ storage: host.localStorage, crypto: host.crypto }),
|
|
92
|
+
stockSnapshot: ({ frameRoot }) => make("stock", stockEffectTrust)(host, { frameRoot, ownerCss: observe().ownerCss }),
|
|
93
|
+
onChange() { tell(); emit(); },
|
|
94
|
+
onMount(owner) {
|
|
95
|
+
if (!alive() || !owner.current()) return;
|
|
96
|
+
const collector = make("collector", createEffectProfileCollector)({ key: owner.key, id: owner.id, device: owner.device, clock });
|
|
97
|
+
if (collector.ok) owners.set(owner, { collector, began: clock(), attempted: null });
|
|
98
|
+
},
|
|
99
|
+
onRetire(owner) { const row = owners.get(owner); row?.collector.dispose(); owners.delete(owner); },
|
|
100
|
+
onProfile(raw, owner) {
|
|
101
|
+
const row = owners.get(owner); if (!row || !alive() || !owner.current()) return;
|
|
102
|
+
row.collector.add(raw);
|
|
103
|
+
const now = clock();
|
|
104
|
+
if (now - row.began < 10000 && raw.verdict !== "paused" || row.attempted !== null && now - row.attempted < 10000) return;
|
|
105
|
+
const report = row.collector.report(); if (!report || !owner.current()) return;
|
|
106
|
+
row.attempted = now;
|
|
107
|
+
void writer.post(`/ui/effects/profile/${owner.id}?instance=${encodeURIComponent(name)}&ownerEpoch=${identity.epoch}`, report.profile);
|
|
108
|
+
}, clock,
|
|
109
|
+
});
|
|
110
|
+
if (!registry.ok) throw Error("runtime_unavailable");
|
|
111
|
+
} catch { fail("runtime_unavailable"); }
|
|
112
|
+
function descriptors() {
|
|
113
|
+
return (metadata?.effects ?? []).filter(p => p.manifest.regions.includes("chat")).map((p, order) => ({ key: `chat:${p.id}`, id: p.id, by: p.by, region: "chat", layer: p.manifest.layer,
|
|
114
|
+
order, paused: paused.get(p.id) === placementIdentity(p), visible: true, instance: name, params: p.params, element }));
|
|
115
|
+
}
|
|
116
|
+
function emit() {
|
|
117
|
+
if (!alive() || error || !registry?.ok) return;
|
|
118
|
+
const value = observe();
|
|
119
|
+
if (["idle", "working", "needs_you", "stopped"].includes(value.state)) registry.publish("state", { state: value.state }, name);
|
|
120
|
+
if (["light", "dark"].includes(value.mode)) registry.publish("theme", { mode: value.mode });
|
|
121
|
+
}
|
|
122
|
+
function sync() {
|
|
123
|
+
if (!alive()) { dispose(); return; } if (error) return;
|
|
124
|
+
const value = observe(); ctx = value.context;
|
|
125
|
+
const stamp = JSON.stringify(ctx);
|
|
126
|
+
if (stamp !== lastContext) { lastContext = stamp; registry.update(descriptors(), ctx); }
|
|
127
|
+
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();
|
|
129
|
+
}
|
|
130
|
+
async function read() {
|
|
131
|
+
if (!alive() || error || active) return;
|
|
132
|
+
const op = { controller: new host.AbortController(), generation }; active = op;
|
|
133
|
+
try {
|
|
134
|
+
const answer = await loader.placements(name, op.controller.signal);
|
|
135
|
+
if (!alive() || active !== op) return;
|
|
136
|
+
if (op.generation !== generation) return;
|
|
137
|
+
if (!answer) { fail("resource_unavailable"); return; }
|
|
138
|
+
metadata = answer;
|
|
139
|
+
for (const [id, stamp] of paused) if (!answer.effects.some(p => p.id === id && placementIdentity(p) === stamp)) paused.delete(id);
|
|
140
|
+
lastContext = null; sync(); tell();
|
|
141
|
+
} finally {
|
|
142
|
+
if (active === op) active = null;
|
|
143
|
+
if (queued && alive() && !error) { queued = false; sync(); }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { ok: !error && !dead, sync,
|
|
147
|
+
changed() { if (!alive() || error) return; generation++; metadata = null; registry.update([], observe().context); lastContext = null; if (active) queued = true; else sync(); tell(); },
|
|
148
|
+
reply(event) { if (alive() && !error && event.instance === name) registry.publish("reply", {}, name); },
|
|
149
|
+
pause(id) { if (!alive() || !metadata?.effects.some(p => p.id === id)) return false; paused.set(id, placementIdentity(metadata.effects.find(p => p.id === id))); lastContext = null; sync(); return true; },
|
|
150
|
+
resume(id) { if (!alive() || error || !metadata?.effects.some(p => p.id === id)) return false; paused.delete(id); lastContext = null; sync(); return registry.resume(`chat:${id}`); },
|
|
151
|
+
async remove(id) { if (!alive() || error || !metadata?.effects.some(p => p.id === id)) return false; const mine = generation; const ok = await writer.post("/ui/effects/remove", { instance: name, id }); if (!alive() || mine !== generation) return false; if (ok) this.changed(); return ok; },
|
|
152
|
+
snapshot: () => ({ error, rows: registry?.snapshot() ?? [], effects: metadata?.effects ?? [], loading: metadata === null && !error }), dispose,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** #1629: host-owned static cursor controller; no product startup, metadata reader, Worker or frame. */
|
|
2
|
+
import { effectHostStyle } from "./effect-host-style.js";
|
|
3
|
+
import { checkStaticEffectCursor } from "./effect-runtime-loader.js";
|
|
4
|
+
const owners = new WeakMap(), ATTRIBUTE = "data-agend-effect-cursor";
|
|
5
|
+
let serial = 0;
|
|
6
|
+
const finite = n => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
7
|
+
function selection(value) {
|
|
8
|
+
try {
|
|
9
|
+
if (!value || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return null;
|
|
10
|
+
const fields = Object.getOwnPropertyDescriptors(value);
|
|
11
|
+
if (Reflect.ownKeys(fields).length !== 3 || !["id", "generation", "hotspot"].every(k => fields[k] && Object.hasOwn(fields[k], "value"))) return null;
|
|
12
|
+
const { id, generation, hotspot } = value;
|
|
13
|
+
if (typeof id !== "string" || !/^[a-f0-9]{32}$/.test(id) || !Number.isSafeInteger(generation) || generation < 0 || !Array.isArray(hotspot) || Object.getPrototypeOf(hotspot) !== Array.prototype) return null;
|
|
14
|
+
const entries = Object.getOwnPropertyDescriptors(hotspot);
|
|
15
|
+
if (Reflect.ownKeys(entries).length !== 3 || entries.length?.value !== 2 || ![0, 1].every(k => entries[k] && Object.hasOwn(entries[k], "value") && Number.isInteger(entries[k].value) && entries[k].value >= 0 && entries[k].value <= 63)) return null;
|
|
16
|
+
return Object.freeze({ id, generation, hotspot: Object.freeze([entries[0].value, entries[1].value]) });
|
|
17
|
+
} catch { return null; }
|
|
18
|
+
}
|
|
19
|
+
function cursorMatches(value, url, hotspot) {
|
|
20
|
+
const m = typeof value === "string" && /^url\((["']?)(blob:[^"'()\s]+)\1\)\s+(\d+)\s+(\d+),\s*auto$/.exec(value);
|
|
21
|
+
return !!m && m[2] === url && Number(m[3]) === hotspot[0] && Number(m[4]) === hotspot[1];
|
|
22
|
+
}
|
|
23
|
+
/** readImage must be the existing shared loader's settled cursor operation. enabled is a host gate, never effect data. */
|
|
24
|
+
export function createEffectCursor({ window: host, current, readImage, onChange = () => {} } = {}) {
|
|
25
|
+
const unavailable = { ok: false, update() {}, snapshot: () => ({ status: "unavailable", id: null }), dispose() {} };
|
|
26
|
+
if (!host?.document || owners.has(host) || ![current, readImage, onChange, host.getComputedStyle, host.Blob, host.URL?.createObjectURL, host.URL?.revokeObjectURL, host.AbortController, host.setTimeout, host.clearTimeout, host.performance?.now].every(x => typeof x === "function")) return unavailable;
|
|
27
|
+
const token = {}; owners.set(host, token);
|
|
28
|
+
const doc = host.document; let dead = false, previous = null, wanted = null, pending = null, active = null, blocked = null, generation = 0, checking = false, notifying = false, status = "off";
|
|
29
|
+
const safe = fn => { try { return fn(); } catch { return null; } };
|
|
30
|
+
const release = () => { if (dead && !pending && owners.get(host) === token) owners.delete(host); };
|
|
31
|
+
function alive() {
|
|
32
|
+
if (dead || checking) return false;
|
|
33
|
+
checking = true; const value = safe(() => current() === true && host.document === doc); checking = false;
|
|
34
|
+
return !dead && value === true;
|
|
35
|
+
}
|
|
36
|
+
function now() {
|
|
37
|
+
const value = safe(() => host.performance.now());
|
|
38
|
+
if (!finite(value) || previous !== null && value < previous) { dispose(); return null; }
|
|
39
|
+
previous = value; return value;
|
|
40
|
+
}
|
|
41
|
+
function target() {
|
|
42
|
+
const all = doc.querySelectorAll("#app > .shell"), el = all.length === 1 ? all[0] : null;
|
|
43
|
+
return el?.ownerDocument === doc && el.isConnected === true ? el : null;
|
|
44
|
+
}
|
|
45
|
+
function remove(row) {
|
|
46
|
+
if (!row) return;
|
|
47
|
+
// Removing our exact attribute disarms even a stylesheet whose CSSOM has become unreadable.
|
|
48
|
+
safe(() => { if (row.element.getAttribute(ATTRIBUTE) === row.token) row.element.removeAttribute(ATTRIBUTE); });
|
|
49
|
+
safe(() => row.rule?.style.removeProperty("cursor"));
|
|
50
|
+
safe(() => { const at = [...row.sheet.cssRules].indexOf(row.rule); if (at >= 0) row.sheet.deleteRule(at); });
|
|
51
|
+
if (row.url) { safe(() => host.URL.revokeObjectURL(row.url)); row.url = null; }
|
|
52
|
+
}
|
|
53
|
+
function clear() { const old = active; active = null; remove(old); }
|
|
54
|
+
function tell() { if (dead || notifying) return; notifying = true; try { safe(() => onChange(snapshot())); } finally { notifying = false; } }
|
|
55
|
+
function snapshot() { return Object.freeze({ status: dead ? "unavailable" : status, id: wanted?.selection.id ?? null }); }
|
|
56
|
+
function dispose() {
|
|
57
|
+
if (dead) return; dead = true; generation++; wanted = null; clear();
|
|
58
|
+
if (pending) { safe(() => pending.controller.abort()); remove(pending); }
|
|
59
|
+
safe(() => host.removeEventListener?.("pagehide", dispose)); release();
|
|
60
|
+
}
|
|
61
|
+
if (!alive() || now() === null || !effectHostStyle(host)) { dispose(); return unavailable; }
|
|
62
|
+
safe(() => host.addEventListener?.("pagehide", dispose));
|
|
63
|
+
function valid(op) {
|
|
64
|
+
if (!alive() || pending !== op || op.generation !== generation || op.controller.signal.aborted || wanted !== op.wanted) return false;
|
|
65
|
+
const t = now();
|
|
66
|
+
return t !== null && t >= op.began && t - op.began < 10000 && target() === op.element && effectHostStyle(host) === op.sheet;
|
|
67
|
+
}
|
|
68
|
+
function fail(op) {
|
|
69
|
+
if (!dead && pending === op && generation === op.generation && wanted === op.wanted) { blocked = wanted; status = "unavailable"; tell(); }
|
|
70
|
+
}
|
|
71
|
+
async function load(op) {
|
|
72
|
+
try {
|
|
73
|
+
if (!valid(op)) return;
|
|
74
|
+
const bytes = await readImage(op.wanted.selection, op.controller.signal);
|
|
75
|
+
if (!valid(op)) return;
|
|
76
|
+
const checked = checkStaticEffectCursor(bytes); if (!checked) { fail(op); return; }
|
|
77
|
+
const view = new DataView(checked.buffer, checked.byteOffset, checked.byteLength), width = view.getUint32(16), height = view.getUint32(20);
|
|
78
|
+
if (op.wanted.selection.hotspot.some((v, n) => v >= (n === 0 ? width : height))) { fail(op); return; }
|
|
79
|
+
op.url = host.URL.createObjectURL(new host.Blob([checked], { type: "image/png" }));
|
|
80
|
+
const url = new URL(op.url); if (url.protocol !== "blob:" || url.origin !== host.location.origin || !/^blob:[^"'()\s\\]+$/.test(op.url)) { fail(op); return; }
|
|
81
|
+
const image = doc.createElement("img"); image.alt = ""; image.src = op.url; op.image = image;
|
|
82
|
+
if (typeof image.decode !== "function") { fail(op); return; }
|
|
83
|
+
await image.decode();
|
|
84
|
+
if (!valid(op)) return;
|
|
85
|
+
if (image.naturalWidth !== width || image.naturalHeight !== height) { fail(op); return; }
|
|
86
|
+
if (op.element.hasAttribute(ATTRIBUTE)) { fail(op); return; }
|
|
87
|
+
op.token = `agend-effect-cursor-${++serial}`;
|
|
88
|
+
const index = op.sheet.insertRule(`#app > .shell[${ATTRIBUTE}="${op.token}"]{}`, op.sheet.cssRules.length); op.rule = op.sheet.cssRules[index];
|
|
89
|
+
const hotspot = op.wanted.selection.hotspot, value = `url("${op.url}") ${hotspot[0]} ${hotspot[1]}, auto`;
|
|
90
|
+
op.rule.style.setProperty("cursor", value); op.element.setAttribute(ATTRIBUTE, op.token);
|
|
91
|
+
if (!valid(op) || !cursorMatches(op.rule.style.getPropertyValue("cursor"), op.url, hotspot) || !cursorMatches(host.getComputedStyle(op.element).cursor, op.url, hotspot)) { fail(op); return; }
|
|
92
|
+
active = op; status = "running"; tell();
|
|
93
|
+
} catch { fail(op); }
|
|
94
|
+
finally {
|
|
95
|
+
safe(() => host.clearTimeout(op.timer));
|
|
96
|
+
if (active !== op) remove(op);
|
|
97
|
+
if (pending === op) { pending = null; if (!dead && wanted === op.wanted && !active && blocked !== wanted) { blocked = wanted; status = "unavailable"; tell(); } }
|
|
98
|
+
release(); if (!dead) start();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function start() {
|
|
102
|
+
if (!alive() || pending || active || !wanted || blocked === wanted) return;
|
|
103
|
+
const began = now(), element = safe(target), sheet = effectHostStyle(host);
|
|
104
|
+
if (began === null || !element || !sheet || element.hasAttribute(ATTRIBUTE)) { blocked = wanted; status = "unavailable"; tell(); return; }
|
|
105
|
+
const op = { wanted, generation, began, element, sheet, controller: new host.AbortController(), url: null, rule: null, token: null, timer: null }; pending = op; status = "loading";
|
|
106
|
+
op.timer = host.setTimeout(() => { if (pending !== op) return; op.controller.abort(); remove(op); fail(op); }, 10000);
|
|
107
|
+
tell(); void load(op);
|
|
108
|
+
}
|
|
109
|
+
function update(value, enabled) {
|
|
110
|
+
if (!alive() || now() === null) { dispose(); return; }
|
|
111
|
+
const next = enabled === true ? selection(value) : null;
|
|
112
|
+
const same = !!next && wanted && next.id === wanted.selection.id && next.generation === wanted.selection.generation && next.hotspot.every((n, i) => n === wanted.selection.hotspot[i]);
|
|
113
|
+
if (!same) {
|
|
114
|
+
generation++; wanted = next ? { selection: next } : null; blocked = null; clear();
|
|
115
|
+
if (pending) { safe(() => pending.controller.abort()); remove(pending); }
|
|
116
|
+
status = next ? "loading" : "off";
|
|
117
|
+
}
|
|
118
|
+
if (active) {
|
|
119
|
+
const row = active;
|
|
120
|
+
if (safe(target) !== row.element || effectHostStyle(host) !== row.sheet || row.element.getAttribute(ATTRIBUTE) !== row.token || !cursorMatches(safe(() => host.getComputedStyle(row.element).cursor), row.url, row.wanted.selection.hotspot)) { clear(); blocked = wanted; status = "unavailable"; }
|
|
121
|
+
}
|
|
122
|
+
start(); tell();
|
|
123
|
+
}
|
|
124
|
+
return { ok: true, update, snapshot, dispose };
|
|
125
|
+
}
|