@songsid/agend 2.2.1 → 2.2.6
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 +8 -0
- package/dist/daemon.js +298 -22
- 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 +118 -10
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.js +35 -2
- package/dist/login-controller.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/tunnel/cloudflared.js +37 -2
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/types.d.ts +3 -1
- package/dist/tunnel/types.js.map +1 -1
- 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-terminal-http.js +15 -2
- package/dist/web-terminal-http.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
package/dist/ui/chat-thread.js
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
// Unrelated updates (a new message, another message's tick) stop nothing (#1306 §6.2, #1408 §4).
|
|
9
9
|
import "./chat-render.js";
|
|
10
10
|
import "./preview.js";
|
|
11
|
+
import { effectTrialMessage } from "./effect-trial-offer.js";
|
|
11
12
|
import { loadHtmlAttachment } from "./html-attachment.js";
|
|
12
13
|
import { chatTime, chatTimeFull, onDayChange } from "./chat-time.js";
|
|
14
|
+
import { textLinkSegments } from "/assets/app-route.js";
|
|
13
15
|
|
|
14
16
|
const R = () => globalThis.AgendChatRender;
|
|
15
17
|
/** #1589: the public link shows attachments as downloads only (its page says so: <body data-public-link="1">). */
|
|
@@ -37,6 +39,41 @@ export const msgKey = (x) => `${x.boot}-${x.id}`;
|
|
|
37
39
|
*/
|
|
38
40
|
export const THREAD_TAIL = 24;
|
|
39
41
|
export const THREAD_CHUNK = 40;
|
|
42
|
+
/** #2158: a reader's scroll (smooth, momentum) is theirs until it has been this quiet since their last input or scroll. */
|
|
43
|
+
export const READER_QUIET_MS = 250;
|
|
44
|
+
const SCROLL_KEYS = new Set(["PageUp", "PageDown", "ArrowUp", "ArrowDown", "Home", "End", " "]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* #2053: a message's rendered Markdown, kept across renders and chat switches. A draw compares each message's html with
|
|
48
|
+
* its node's, so every render of a thread rendered every drawn message's Markdown again (~320 calls per 200-message
|
|
49
|
+
* switch). renderMarkdown's output depends only on the text, whether its HTML cards are on (an agent's message), and —
|
|
50
|
+
* for a text that has them — whether the page knows each server-emoji id and the name it has for each mention: the key
|
|
51
|
+
* holds exactly those, so a cached html is always what renderMarkdown would return now. Bounded: least recently used out.
|
|
52
|
+
*/
|
|
53
|
+
export const MD_CACHE_MAX = 4000;
|
|
54
|
+
const mdCache = new Map();
|
|
55
|
+
const EMOJI_IDS = /<a?:[A-Za-z0-9_]{2,32}:(\d{15,25})>/g;
|
|
56
|
+
const MENTION_IDS = /<(@!?|@&|#)(\d{15,25})>/g;
|
|
57
|
+
const MENTION_KINDS = { "@": "u", "@!": "u", "@&": "r", "#": "c" };
|
|
58
|
+
export function renderedMarkdown(text, cards, emojis, mentions) {
|
|
59
|
+
const src = typeof text === "string" ? text : String(text == null ? "" : text);
|
|
60
|
+
let sig = cards ? "c" : "-";
|
|
61
|
+
if (src.includes("<")) {
|
|
62
|
+
// Read as renderMarkdown reads it: it drops NUL first (its placeholder mark), so "<@\0id>" is a mention there too.
|
|
63
|
+
const seen = src.includes("\u0000") ? src.replace(/\u0000/g, "") : src;
|
|
64
|
+
for (const m of seen.matchAll(EMOJI_IDS)) sig += `|e${m[1]}${emojis && emojis(m[1]) ? 1 : 0}`;
|
|
65
|
+
for (const m of seen.matchAll(MENTION_IDS)) { const name = mentions ? mentions(MENTION_KINDS[m[1]], m[2]) : null; sig += `|m${m[1]}${m[2]}=${typeof name === "string" ? name.length + ":" + name : "-"}`; }
|
|
66
|
+
}
|
|
67
|
+
const key = `${sig}\u0000${src}`;
|
|
68
|
+
let html = mdCache.get(key);
|
|
69
|
+
if (html === undefined) html = R().renderMarkdown(src, { htmlCards: cards, emojis, mentions });
|
|
70
|
+
else mdCache.delete(key);
|
|
71
|
+
mdCache.set(key, html);
|
|
72
|
+
while (mdCache.size > MD_CACHE_MAX) mdCache.delete(mdCache.keys().next().value);
|
|
73
|
+
return html;
|
|
74
|
+
}
|
|
75
|
+
/** Tests only: forget every rendered Markdown. */
|
|
76
|
+
export function clearRenderedMarkdown() { mdCache.clear(); }
|
|
40
77
|
|
|
41
78
|
/**
|
|
42
79
|
* Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
|
|
@@ -45,6 +82,17 @@ export const THREAD_CHUNK = 40;
|
|
|
45
82
|
*/
|
|
46
83
|
export function createThread(list, scroller, opts) {
|
|
47
84
|
const nodes = new Map(); // key → { html, node }
|
|
85
|
+
// A server emoji's picture that does not load reads `:name:` (its alt) instead of an empty box (error does not bubble:
|
|
86
|
+
// caught on the way down).
|
|
87
|
+
list.addEventListener("error", (e) => {
|
|
88
|
+
const img = e.target;
|
|
89
|
+
if (!img || img.tagName !== "IMG" || !img.classList || !img.classList.contains("emoji")) return;
|
|
90
|
+
const span = list.ownerDocument.createElement("span");
|
|
91
|
+
span.className = "emoji-text";
|
|
92
|
+
span.textContent = img.getAttribute("alt") || "";
|
|
93
|
+
span.title = img.getAttribute("title") || span.textContent;
|
|
94
|
+
img.replaceWith(span);
|
|
95
|
+
}, true);
|
|
48
96
|
let msgs = [];
|
|
49
97
|
let unseen = 0;
|
|
50
98
|
// The reader is at the newest message. While pinned, content that grows after it was drawn — an image loading, a
|
|
@@ -52,12 +100,15 @@ export function createThread(list, scroller, opts) {
|
|
|
52
100
|
// Only a move UP unpins: the browser's own scroll anchoring also fires scroll events when content grows (the view
|
|
53
101
|
// moves down or stays), and those must not count as the reader leaving the bottom. Reaching the bottom pins again.
|
|
54
102
|
let pinned = true;
|
|
55
|
-
|
|
103
|
+
// Where the view was (and how tall it was) at the last scroll the page saw or made: a move up from there unpins.
|
|
104
|
+
let lastTop = 0, lastView = 0;
|
|
105
|
+
const mark = () => { lastTop = scroller.scrollTop; lastView = scroller.clientHeight; };
|
|
56
106
|
// The oldest message drawn (by key): everything after it is drawn, so new messages never push the window. Null: none
|
|
57
107
|
// drawn yet — the next render draws the newest THREAD_TAIL and the rest follows (grow).
|
|
58
108
|
let firstKey = null;
|
|
59
|
-
let growTimer = null, growFrame = null, disposed = false;
|
|
109
|
+
let growTimer = null, growFrame = null, disposed = false, parked = false;
|
|
60
110
|
const cardRefresh = new WeakMap(); // card node → re-read availability (weak: a card that left takes its source with it)
|
|
111
|
+
const cardDrop = new WeakMap(); // card node → drop the HTML read it is waiting on (the chat left: park)
|
|
61
112
|
const tr = opts.t, trf = opts.tf;
|
|
62
113
|
const stopIn = (node) => (P() ? P().stopIn(node) : []);
|
|
63
114
|
/** #1770: write a message node's time label for now; unchanged text is not touched. */
|
|
@@ -68,7 +119,7 @@ export function createThread(list, scroller, opts) {
|
|
|
68
119
|
if (el.textContent !== v) el.textContent = v;
|
|
69
120
|
}
|
|
70
121
|
// #1770: "today" ends at the fleet's midnight; the labels are rewritten then (and on every render), while there are any.
|
|
71
|
-
let offDay = null;
|
|
122
|
+
let offDay = null, trialTimer = null;
|
|
72
123
|
function armDay() {
|
|
73
124
|
if (nodes.size && !offDay) offDay = onDayChange(() => { for (const v of nodes.values()) timeLabel(v.node); });
|
|
74
125
|
else if (!nodes.size && offDay) { offDay(); offDay = null; }
|
|
@@ -80,15 +131,17 @@ export function createThread(list, scroller, opts) {
|
|
|
80
131
|
// #1770: the label ("17:38", "Yesterday 17:38", "10/11 17:38") depends on today, so it is not part of the html a node
|
|
81
132
|
// is kept by: it is written into the node (timeLabels) — crossing midnight relabels in place and reloads no preview.
|
|
82
133
|
const time = x.ts ? `<span class="time" data-ts="${escAttr(x.ts)}" title="${escAttr(chatTimeFull(x.ts))}"></span>` : `<span class="time"></span>`;
|
|
134
|
+
const trial = effectTrialMessage(x);
|
|
135
|
+
if (trial) return trialHtml(trial, time, x);
|
|
83
136
|
if (x.role === "status") return statusHtml(x, time);
|
|
84
137
|
const u = opts.isUser(x);
|
|
85
138
|
// renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
|
|
86
139
|
const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
|
|
87
|
-
const tools = u ? "" : `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
140
|
+
const tools = u ? "" : `<div data-trust="thread-control" class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
88
141
|
const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
|
|
89
142
|
// A person's click on a reply button shows as the button's label, never the line the agent was given (its value).
|
|
90
143
|
const clicked = x.role === "user" && x.click && typeof x.click.label === "string";
|
|
91
|
-
return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div><div class="body">${clicked ? clickHtml(x.click) : `<div class="md">${
|
|
144
|
+
return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div><div class="body">${clicked ? clickHtml(x.click) : `<div class="md">${renderedMarkdown(x.text, x.role === "agent", opts.emojis, opts.mentions)}</div>`}${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
|
|
92
145
|
}
|
|
93
146
|
/**
|
|
94
147
|
* A button click as a chip: 🔘 and the label (escaped: the label is the agent's text). With `of` (live, while its
|
|
@@ -97,10 +150,29 @@ export function createThread(list, scroller, opts) {
|
|
|
97
150
|
function clickHtml(click) {
|
|
98
151
|
const inner = `<span class="click-mark" aria-hidden="true">🔘</span><span class="sr-only">${escAttr(tr("chat.clicked"))} </span>${escAttr(click.label)}`;
|
|
99
152
|
const of = typeof click.of === "string" && /^[0-9a-f]{32}$/.test(click.of) ? click.of : null;
|
|
100
|
-
return `<div class="click-chip">${of
|
|
153
|
+
return `<div class="click-chip" data-trust="thread-control">${of
|
|
101
154
|
? `<button type="button" class="click-link" data-act="showButtons" data-arg="${escAttr(of)}" title="${escAttr(tr("chat.clickShow"))}">${inner}</button>`
|
|
102
155
|
: `<span class="click-text">${inner}</span>`}</div>`;
|
|
103
156
|
}
|
|
157
|
+
function trialNow() { try { const t = opts.serverNow ? opts.serverNow() : Date.now(); return Number.isFinite(t) ? t : Infinity; } catch { return Infinity; } }
|
|
158
|
+
const trialReasons = new Set(["checking", "localAdmin", "chatChanged", "busy", "safe", "effectsOff", "reducedMotion", "saveData", "unsupported", "transport", "inactive", "dialog", "expired"]);
|
|
159
|
+
function trialStatus(message) {
|
|
160
|
+
const reason = opts.effectTrialStatus?.(message);
|
|
161
|
+
return reason === "ready" || trialReasons.has(reason) ? reason : opts.canTryEffect?.(message) === true ? "ready" : "localAdmin";
|
|
162
|
+
}
|
|
163
|
+
function trialHtml(offer, time, message) {
|
|
164
|
+
const reason = trialNow() >= offer.expiresAt ? "expired" : PUBLIC_LINK() ? "localAdmin" : trialStatus(message);
|
|
165
|
+
const rawFailure = opts.effectTrialFailure?.(message), failure = typeof rawFailure === "string" ? rawFailure.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 80) : null;
|
|
166
|
+
const available = reason === "ready", fix = !PUBLIC_LINK() && !available && reason !== "expired" && reason !== "checking" && typeof opts.fixEffectTrial === "function";
|
|
167
|
+
const label = available ? "chat.effectTrialStart" : reason === "busy" ? "chat.effectTrialStopOther" : reason === "localAdmin" ? "chat.effectTrialSignIn" : reason === "reducedMotion" ? "chat.effectTrialPlayMotion" : "chat.effectTrialSettings";
|
|
168
|
+
const button = available || fix ? `<button type="button" class="btn btn-sm effect-trial-start" data-act="effectTrial" data-arg="${escAttr(msgKey(message))}">${escAttr(tr(label))}</button>` : "";
|
|
169
|
+
return `<div class="msg status-line"><div class="meta">${time}</div><div class="body"><div class="effect-trial-offer" data-trust="thread-control"><strong>${escAttr(tr("chat.effectTrial"))}: ${escAttr(offer.title)}</strong><div>${escAttr(tr(available ? "chat.effectTrialLocal" : "chat.effectTrialReason_" + reason))}</div>${failure ? `<div class="effect-trial-failure" role="status">${escAttr(tr("chat.effectTrialStopped"))}: ${escAttr(failure)}</div>` : ""}${button}</div></div></div>`;
|
|
170
|
+
}
|
|
171
|
+
function armTrials() {
|
|
172
|
+
if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
|
|
173
|
+
const now = trialNow(), next = msgs.map(x => effectTrialMessage(x)).filter(x => x && x.expiresAt > now).reduce((n, x) => Math.min(n, x.expiresAt), Infinity);
|
|
174
|
+
if (Number.isFinite(next)) trialTimer = setTimeout(() => { trialTimer = null; render(msgs); }, Math.max(1, Math.min(2147483647, next - now)));
|
|
175
|
+
}
|
|
104
176
|
/**
|
|
105
177
|
* #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
|
|
106
178
|
* a preview card, a button or a person's bubble. Checked before isUser, which reads the sender: a fleet name need not
|
|
@@ -111,10 +183,12 @@ export function createThread(list, scroller, opts) {
|
|
|
111
183
|
const lines = R().lineCount(text);
|
|
112
184
|
const manyLines = lines > STATUS_FOLD_LINES;
|
|
113
185
|
const more = manyLines || text.length > STATUS_FOLD_CHARS
|
|
114
|
-
? `<button type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
|
|
186
|
+
? `<button data-trust="thread-control" type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
|
|
115
187
|
: "";
|
|
116
|
-
const tools = `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
117
|
-
|
|
188
|
+
const tools = `<div data-trust="thread-control" class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
189
|
+
// #1990: the other instance's name opens its chat (an in-app link the router takes: no target).
|
|
190
|
+
const body = textLinkSegments(text, x.links).map(sg => sg.href ? `<a data-trust="link" class="st-link" href="${escAttr(sg.href)}">${escAttr(sg.text)}</a>` : escAttr(sg.text)).join("");
|
|
191
|
+
return `<div class="msg status-line${more ? " folded" : ""}"><div class="meta">${time}</div><div class="body"><div class="st-text">${body}</div>${more}</div>${tools}</div>`;
|
|
118
192
|
}
|
|
119
193
|
/**
|
|
120
194
|
* #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
|
|
@@ -130,7 +204,7 @@ export function createThread(list, scroller, opts) {
|
|
|
130
204
|
return `<button type="button" class="btn btn-sm rb-btn${chosen ? " chosen" : ""}" data-act="replyButton" data-arg="${escAttr(`${b.id}:${i}`)}"${off ? " disabled" : ""}>${escAttr(chosen ? `✓ ${label}` : label)}</button>`;
|
|
131
205
|
}).join("");
|
|
132
206
|
const note = b.state === "chosen" ? trf("chat.rbChosen", b.by || "") : b.state === "expired" ? tr("chat.rbExpired") : "";
|
|
133
|
-
return `<div class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
|
|
207
|
+
return `<div data-trust="reply" class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
|
|
134
208
|
}
|
|
135
209
|
/**
|
|
136
210
|
* #1823: a poll. Anyone here may click while it is open; the page marks its own vote (opts.pollMine), which no one else
|
|
@@ -151,9 +225,9 @@ export function createThread(list, scroller, opts) {
|
|
|
151
225
|
? escAttr(trf("chat.pollClosed", p.answered))
|
|
152
226
|
: `${escAttr(trf("chat.pollAnswered", p.answered))} · ⏳ <span class="poll-left" data-deadline="${escAttr(p.deadline)}" title="${escAttr(tr("chat.pollLeft"))}"></span>`;
|
|
153
227
|
const voters = closed && Array.isArray(p.voters)
|
|
154
|
-
? `<ul class="poll-voters" aria-label="${escAttr(tr("chat.pollVoters"))}">${b.labels.map((label, i) => `<li>${escAttr(`${label}: ${(p.voters[i] || []).join(", ") || "—"}`)}</li>`).join("")}</ul>`
|
|
228
|
+
? `<ul data-trust="reply" class="poll-voters" aria-label="${escAttr(tr("chat.pollVoters"))}">${b.labels.map((label, i) => `<li>${escAttr(`${label}: ${(p.voters[i] || []).join(", ") || "—"}`)}</li>`).join("")}</ul>`
|
|
155
229
|
: "";
|
|
156
|
-
return `<div class="rb-row poll" role="group" aria-label="${escAttr(tr("chat.pollGroup"))}">${items}<span class="rb-note">${status}</span></div>${voters}`;
|
|
230
|
+
return `<div data-trust="reply" class="rb-row poll" role="group" aria-label="${escAttr(tr("chat.pollGroup"))}">${items}<span class="rb-note">${status}</span></div>${voters}`;
|
|
157
231
|
}
|
|
158
232
|
/** #1823: the time left on this node's open polls, now. */
|
|
159
233
|
function pollLeft(node) {
|
|
@@ -187,7 +261,7 @@ export function createThread(list, scroller, opts) {
|
|
|
187
261
|
const doc = list.ownerDocument;
|
|
188
262
|
for (const pre of root.querySelectorAll(".md pre")) {
|
|
189
263
|
const box = doc.createElement("div"); box.className = "codeblock";
|
|
190
|
-
const head = doc.createElement("div"); head.className = "cb-head";
|
|
264
|
+
const head = doc.createElement("div"); head.className = "cb-head"; head.setAttribute("data-trust", "thread-control");
|
|
191
265
|
const lang = doc.createElement("span"); lang.className = "cb-lang"; lang.textContent = pre.dataset.lang || "text";
|
|
192
266
|
const wrap = doc.createElement("button"); wrap.type = "button"; wrap.className = "chip-btn"; wrap.dataset.act = "toggleWrap"; wrap.textContent = tr("chat.wrap");
|
|
193
267
|
const copy = doc.createElement("button"); copy.type = "button"; copy.className = "chip-btn"; copy.dataset.act = "copyCode"; copy.textContent = tr("chat.copy");
|
|
@@ -196,7 +270,7 @@ export function createThread(list, scroller, opts) {
|
|
|
196
270
|
const lines = R().lineCount(pre.textContent);
|
|
197
271
|
if (lines > R().CODE_FOLD_LINES) {
|
|
198
272
|
box.classList.add("folded");
|
|
199
|
-
const more = doc.createElement("button"); more.type = "button"; more.className = "cb-more"; more.dataset.act = "toggleFold";
|
|
273
|
+
const more = doc.createElement("button"); more.type = "button"; more.className = "cb-more"; more.setAttribute("data-trust", "thread-control"); more.dataset.act = "toggleFold";
|
|
200
274
|
more.dataset.lines = String(lines); more.textContent = trf("chat.showAll", lines);
|
|
201
275
|
box.append(more);
|
|
202
276
|
}
|
|
@@ -214,7 +288,7 @@ export function createThread(list, scroller, opts) {
|
|
|
214
288
|
if (!fence) continue;
|
|
215
289
|
ph.textContent = "";
|
|
216
290
|
if (ph.classList.contains("truncated") || !fence.terminated) {
|
|
217
|
-
const note = doc.createElement("div"); note.className = "pv-note"; note.textContent = tr("chat.pvTruncated"); ph.append(note);
|
|
291
|
+
const note = doc.createElement("div"); note.className = "pv-note"; note.setAttribute("data-trust", "thread-control"); note.textContent = tr("chat.pvTruncated"); ph.append(note);
|
|
218
292
|
continue;
|
|
219
293
|
}
|
|
220
294
|
buildCard(ph, x, `${msgKey(x)}:f${n}`, { n, code: fence.code }, stoppedKeys);
|
|
@@ -244,15 +318,15 @@ export function createThread(list, scroller, opts) {
|
|
|
244
318
|
const doc = list.ownerDocument;
|
|
245
319
|
const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
|
|
246
320
|
ph.textContent = "";
|
|
247
|
-
const head = el("div", "tc-head");
|
|
321
|
+
const head = el("div", "tc-head"); head.setAttribute("data-trust", "thread-control");
|
|
248
322
|
const label = el("span", "tc-label", att.name);
|
|
249
323
|
const show = el("button", "btn btn-sm tc-show", tr("chat.tcShow")); show.type = "button";
|
|
250
324
|
const dl = el("a", "btn btn-sm tc-dl", tr("chat.tcDownload")); dl.href = `/ui/file/${att.id}`; dl.setAttribute("download", att.name);
|
|
251
325
|
head.append(label, show, dl);
|
|
252
|
-
const note = el("div", "pv-note");
|
|
326
|
+
const note = el("div", "pv-note"); note.setAttribute("data-trust", "thread-control");
|
|
253
327
|
const body = el("div", "tc-body"); body.hidden = true;
|
|
254
328
|
ph.append(head, note, body);
|
|
255
|
-
let text = null, reading =
|
|
329
|
+
let text = null, reading = 0, reads = 0, all = false;
|
|
256
330
|
const draw = () => {
|
|
257
331
|
body.textContent = "";
|
|
258
332
|
const lines = text.split(/\r?\n/);
|
|
@@ -262,7 +336,8 @@ export function createThread(list, scroller, opts) {
|
|
|
262
336
|
// Formatting is built only within TEXT_PREVIEW_NODES: the markup is measured as a string first, never as DOM.
|
|
263
337
|
const plain = () => {
|
|
264
338
|
const pre = el("pre", "tc-pre"), code = el("code", null, src);
|
|
265
|
-
|
|
339
|
+
const note = el("div", "pv-note", tr("chat.tcPlain")); note.setAttribute("data-trust", "thread-control");
|
|
340
|
+
pre.append(code); body.append(note, pre);
|
|
266
341
|
};
|
|
267
342
|
if (type === "md") {
|
|
268
343
|
const html = R().renderMarkdown(src);
|
|
@@ -282,17 +357,20 @@ export function createThread(list, scroller, opts) {
|
|
|
282
357
|
}
|
|
283
358
|
}
|
|
284
359
|
if (cut) {
|
|
285
|
-
const more = el("button", "btn btn-sm tc-more", trf("chat.tcShowAll", lines.length)); more.type = "button";
|
|
360
|
+
const more = el("button", "btn btn-sm tc-more", trf("chat.tcShowAll", lines.length)); more.setAttribute("data-trust", "thread-control"); more.type = "button";
|
|
286
361
|
more.onclick = () => { all = true; draw(); };
|
|
287
362
|
body.append(more);
|
|
288
363
|
}
|
|
289
364
|
};
|
|
365
|
+
// #2053: as an HTML card's read (cardDrop), one still waiting when the chat left is dropped, the card not left Loading.
|
|
366
|
+
cardDrop.set(ph, () => { if (reading) { reading = 0; note.textContent = ""; } });
|
|
290
367
|
show.onclick = async () => {
|
|
291
368
|
if (reading) return;
|
|
292
369
|
if (text !== null) { body.hidden = !body.hidden; show.textContent = tr(body.hidden ? "chat.tcShow" : "chat.tcHide"); return; }
|
|
293
|
-
reading =
|
|
370
|
+
const mine = reading = ++reads; note.textContent = tr("chat.pvLoading");
|
|
294
371
|
const r = await loadHtmlAttachment({ id: att.id, name: att.name, size: att.size });
|
|
295
|
-
reading
|
|
372
|
+
if (reading !== mine) return; // dropped (its chat left meanwhile): a new click reads again
|
|
373
|
+
reading = 0;
|
|
296
374
|
if (!ph.isConnected) return; // the card left the page while it was read: nothing to draw
|
|
297
375
|
if (!r.ok) { note.textContent = r.reason === "over" ? tr("chat.tcOver") : attReason(r.reason); return; }
|
|
298
376
|
note.textContent = "";
|
|
@@ -343,7 +421,7 @@ export function createThread(list, scroller, opts) {
|
|
|
343
421
|
const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
|
|
344
422
|
const att = source.att || null;
|
|
345
423
|
let code = att ? null : source.code;
|
|
346
|
-
const head = el("div", "pv-head");
|
|
424
|
+
const head = el("div", "pv-head"); head.setAttribute("data-trust", "thread-control");
|
|
347
425
|
const label = el("span", "pv-label", att ? `${tr("chat.pvHtml")} · ${att.name}` : tr("chat.pvHtml"));
|
|
348
426
|
const run = el("button", "btn btn-sm pv-run", tr("chat.pvPreview")); run.type = "button";
|
|
349
427
|
const stopB = el("button", "btn btn-sm pv-stop", tr("chat.pvStop")); stopB.type = "button"; stopB.hidden = true;
|
|
@@ -353,15 +431,15 @@ export function createThread(list, scroller, opts) {
|
|
|
353
431
|
const panel = opts.panel || null;
|
|
354
432
|
const toPanel = panel ? el("button", "btn btn-sm btn-ghost pv-open", tr("chat.pvOpenPanel")) : null;
|
|
355
433
|
if (toPanel) toPanel.type = "button";
|
|
356
|
-
const menu = el("details", "pv-menu");
|
|
434
|
+
const menu = el("details", "pv-menu"); menu.setAttribute("data-trust", "preview-menu");
|
|
357
435
|
const sum = el("summary", null, "⋯"); sum.title = tr("chat.pvMenu"); sum.setAttribute("aria-label", tr("chat.pvMenu"));
|
|
358
436
|
const optB = el("button", "chip-btn pv-opt"); optB.type = "button";
|
|
359
437
|
const neverB = el("button", "chip-btn pv-never"); neverB.type = "button";
|
|
360
438
|
menu.append(sum, optB, neverB);
|
|
361
439
|
head.append(label, run, stopB, ...(toPanel ? [toPanel] : []), ...(dl ? [dl] : []), menu);
|
|
362
|
-
const note = el("div", "pv-note");
|
|
440
|
+
const note = el("div", "pv-note"); note.setAttribute("data-trust", "thread-control");
|
|
363
441
|
// #1568: the banner, with its "I understand" (hidden until the next sign-in; the frame's border stays).
|
|
364
|
-
const banner = el("div", "pv-banner"); banner.hidden = true;
|
|
442
|
+
const banner = el("div", "pv-banner"); banner.setAttribute("data-trust", "thread-control"); banner.hidden = true;
|
|
365
443
|
const ack = el("button", "btn btn-sm btn-ghost pv-ack", P().bannerAckLabel()); ack.type = "button"; ack.title = P().bannerAckTitle();
|
|
366
444
|
banner.append(el("span", "pv-banner-text", P().banner()), ack);
|
|
367
445
|
const holder = el("div", "pv-holder");
|
|
@@ -430,6 +508,10 @@ export function createThread(list, scroller, opts) {
|
|
|
430
508
|
optB.onclick = () => { menu.open = false; opts.setPreviewOptIn(!P().optedIn()); };
|
|
431
509
|
neverB.onclick = () => { menu.open = false; P().setNever(!P().never()); refreshCards(); };
|
|
432
510
|
cardRefresh.set(ph, refresh);
|
|
511
|
+
// #2053: a kept card is shown again later. A read still waiting when its chat left is dropped then: its answer —
|
|
512
|
+
// whenever it comes — starts nothing (no preview, no panel without a new click), and the card is not left Loading
|
|
513
|
+
// (shown again, it is refreshed: refreshCards).
|
|
514
|
+
cardDrop.set(ph, () => { reading = 0; });
|
|
433
515
|
refresh(stoppedKeys.includes(key) ? tr("chat.pvChanged") : null);
|
|
434
516
|
}
|
|
435
517
|
/** Re-read this device's preview choice on every card in this thread. */
|
|
@@ -444,12 +526,45 @@ export function createThread(list, scroller, opts) {
|
|
|
444
526
|
// newest bubble clipped behind it, and the next growth past 48 px put the pill over it.
|
|
445
527
|
const atBottom = () => R().isNearBottom(scroller.scrollTop, scroller.clientHeight, scroller.scrollHeight, 1);
|
|
446
528
|
function reportJump() { opts.onJump({ show: !nearBottom(), unseen }); }
|
|
447
|
-
function toBottom() { scroller.scrollTop = scroller.scrollHeight; pinned = true;
|
|
529
|
+
function toBottom() { snap(); scroller.scrollTop = scroller.scrollHeight; pinned = true; mark(); }
|
|
530
|
+
/**
|
|
531
|
+
* #2184: a thread's height is rarely a whole number of pixels, and a pinned view's offset is: so each time older messages
|
|
532
|
+
* were drawn above, the whole view moved by the change in that fraction (0.6–0.7 px — the faint jitter of a chat
|
|
533
|
+
* opened cold). A sub-pixel margin above the list keeps the thread's height whole, so the bottom never moves.
|
|
534
|
+
*/
|
|
535
|
+
// #2188: one read (the list's box excludes its own margin, so nothing is reset first) and a write only on a change —
|
|
536
|
+
// a reset-then-measure laid a 200-message thread out twice on every pinned draw (2.3 s at 4× CPU on a kept chat).
|
|
537
|
+
let snapped = null;
|
|
538
|
+
function snap() {
|
|
539
|
+
const h = list.getBoundingClientRect().height, f = h - Math.floor(h);
|
|
540
|
+
const want = f > 0.001 && f < 0.999 ? (1 - f) + "px" : "0px";
|
|
541
|
+
if (want !== snapped) { snapped = want; list.style.setProperty("--thread-snap", want); }
|
|
542
|
+
}
|
|
448
543
|
// Sizes change without a scroll event (layout after the render, the dock under the thread): while pinned, the view
|
|
449
544
|
// stays flush with the bottom; a reader who scrolled up is never moved.
|
|
450
545
|
const resized = () => { if (pinned && !atBottom()) toBottom(); reportJump(); };
|
|
451
|
-
|
|
546
|
+
let ro = typeof ResizeObserver === "function" ? new ResizeObserver(resized) : null;
|
|
452
547
|
if (ro) { ro.observe(list); ro.observe(scroller); }
|
|
548
|
+
// #2158: the reader's own hand on the thread — a wheel, a touch, the scrollbar (a pointer held), a scrolling key
|
|
549
|
+
// outside a text field — and the scroll that follows it until it has been quiet READER_QUIET_MS.
|
|
550
|
+
let inputAt = -Infinity, held = false;
|
|
551
|
+
const touched = () => { inputAt = performance.now(); };
|
|
552
|
+
const hold = (on) => () => { held = on; touched(); };
|
|
553
|
+
const onKey = (ev) => {
|
|
554
|
+
const el = ev.target;
|
|
555
|
+
if (SCROLL_KEYS.has(ev.key) && !(el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName || "")))) touched();
|
|
556
|
+
};
|
|
557
|
+
const onScroller = [["wheel", touched], ["touchstart", hold(true)], ["touchend", hold(false)], ["touchcancel", hold(false)], ["pointerdown", hold(true)]];
|
|
558
|
+
const onDocument = [["pointerup", hold(false)], ["pointercancel", hold(false)], ["keydown", onKey]];
|
|
559
|
+
/** Listen for the reader's input on `sc` (and its document: a pointer let go anywhere, a key), or stop. */
|
|
560
|
+
function watchReader(sc, on) {
|
|
561
|
+
const doc = sc.ownerDocument, how = on ? "addEventListener" : "removeEventListener";
|
|
562
|
+
for (const [type, fn] of onScroller) sc[how](type, fn, { passive: true });
|
|
563
|
+
if (doc) for (const [type, fn] of onDocument) doc[how](type, fn, true);
|
|
564
|
+
if (!on) held = false;
|
|
565
|
+
}
|
|
566
|
+
const readerActive = () => held || performance.now() - inputAt < READER_QUIET_MS;
|
|
567
|
+
watchReader(scroller, true);
|
|
453
568
|
|
|
454
569
|
/** Where drawing starts in `msgs`: at the oldest drawn message, or (none yet) the newest THREAD_TAIL. */
|
|
455
570
|
function startIndex() {
|
|
@@ -463,14 +578,14 @@ export function createThread(list, scroller, opts) {
|
|
|
463
578
|
/** Draw the next chunk of older messages, keeping the reader where they were; then the next, until all are drawn. */
|
|
464
579
|
function grow() {
|
|
465
580
|
growTimer = growFrame = null;
|
|
466
|
-
if (disposed) return;
|
|
581
|
+
if (disposed || parked) return; // the chat left: nothing more is drawn (a kept one fills in when shown again)
|
|
467
582
|
const at = startIndex();
|
|
468
583
|
if (at <= 0) return;
|
|
469
584
|
firstKey = msgKey(msgs[Math.max(0, at - THREAD_CHUNK)]);
|
|
470
585
|
const fromBottom = scroller.scrollHeight - scroller.scrollTop;
|
|
471
586
|
draw({ older: true }); // drawn above the reader: never "new below" (render alone counts those)
|
|
472
587
|
if (pinned) toBottom();
|
|
473
|
-
else { scroller.scrollTop = scroller.scrollHeight - fromBottom;
|
|
588
|
+
else { scroller.scrollTop = scroller.scrollHeight - fromBottom; mark(); }
|
|
474
589
|
scheduleGrow();
|
|
475
590
|
}
|
|
476
591
|
/** After the next frame has painted (the click's own frame stays light), draw a chunk more. */
|
|
@@ -493,6 +608,7 @@ export function createThread(list, scroller, opts) {
|
|
|
493
608
|
function render(next, renderOpts = {}) {
|
|
494
609
|
const stick = pinned || nearBottom();
|
|
495
610
|
msgs = next || [];
|
|
611
|
+
armTrials();
|
|
496
612
|
if (!msgs.length) {
|
|
497
613
|
for (const v of nodes.values()) stopIn(v.node);
|
|
498
614
|
nodes.clear();
|
|
@@ -510,7 +626,7 @@ export function createThread(list, scroller, opts) {
|
|
|
510
626
|
if ("restore" in renderOpts) {
|
|
511
627
|
unseen = 0;
|
|
512
628
|
if (renderOpts.restore == null) toBottom();
|
|
513
|
-
else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom();
|
|
629
|
+
else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); mark(); }
|
|
514
630
|
} else if (stick) toBottom();
|
|
515
631
|
else unseen += added;
|
|
516
632
|
reportJump();
|
|
@@ -531,7 +647,7 @@ export function createThread(list, scroller, opts) {
|
|
|
531
647
|
const stopped = have ? stopIn(have.node) : [];
|
|
532
648
|
const node = msgNode(html, x, stopped);
|
|
533
649
|
if (have) have.node.replaceWith(node); else added++;
|
|
534
|
-
have = { html, node, x }; nodes.set(key, have);
|
|
650
|
+
have = { html, node, x, trialButton: node.querySelector(".effect-trial-offer > button.effect-trial-start") }; nodes.set(key, have);
|
|
535
651
|
} else { have.x = x; timeLabel(have.node); }
|
|
536
652
|
const want = prev ? prev.nextSibling : list.firstChild;
|
|
537
653
|
// A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
|
|
@@ -544,10 +660,23 @@ export function createThread(list, scroller, opts) {
|
|
|
544
660
|
}
|
|
545
661
|
|
|
546
662
|
function onScroll() {
|
|
663
|
+
// #2158: the thread got shorter under a pinned view (text settling on a cold load, a card) and the browser clamped
|
|
664
|
+
// scrollTop to its new end — for the view as tall as it was then; the view got shorter too before WebKit sent that
|
|
665
|
+
// clamp's scroll event (it comes before the ResizeObserver). A view resting exactly at the bottom of the view as it
|
|
666
|
+
// was at the last mark, with no hand of the reader's on the thread, was put there by the browser: still pinned, back
|
|
667
|
+
// at the bottom. Geometry alone can't tell (a reader's move up by the dock's growth lands there too): any input of
|
|
668
|
+
// theirs, and the scroll after it, is theirs and unpins as before. The page's own scrolls (reveal) have no input but
|
|
669
|
+
// don't land there.
|
|
670
|
+
const reader = readerActive();
|
|
671
|
+
if (reader) touched(); // the reader's scroll goes on (smooth, momentum): still theirs while it does
|
|
672
|
+
else if (pinned && !atBottom() && Math.abs(scroller.scrollHeight - lastView - scroller.scrollTop) <= 1) toBottom();
|
|
547
673
|
const bottom = nearBottom(), top = scroller.scrollTop;
|
|
548
|
-
|
|
674
|
+
// #2087: pinned only at the exact bottom — a reader still wheeling through the last 48 px was pinned there, so
|
|
675
|
+
// the next resize (a picture, a card, the dock) pulled the view the rest of the way down under them. Near the bottom
|
|
676
|
+
// still counts for a new message (render follows it) and for the pill; nothing else moves a view the reader moves.
|
|
677
|
+
if (atBottom()) pinned = true;
|
|
549
678
|
else if (top < lastTop) pinned = false;
|
|
550
|
-
|
|
679
|
+
mark();
|
|
551
680
|
if (bottom) unseen = 0;
|
|
552
681
|
reportJump();
|
|
553
682
|
return bottom ? null : scroller.scrollTop;
|
|
@@ -605,18 +734,56 @@ export function createThread(list, scroller, opts) {
|
|
|
605
734
|
else if (act === "replyButton") {
|
|
606
735
|
const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
|
|
607
736
|
if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
|
|
737
|
+
} else if (act === "effectTrial") {
|
|
738
|
+
// Only the current host-produced keyed card and a real local click can request admission. No token is read from DOM.
|
|
739
|
+
const key = el.dataset.arg, row = msgs.find(x => msgKey(x) === key), owner = nodes.get(key), node = owner?.node, offer = effectTrialMessage(row);
|
|
740
|
+
if (PUBLIC_LINK() || e.defaultPrevented || e.isTrusted !== true || owner?.trialButton !== el || el.className !== "btn btn-sm effect-trial-start" || !node?.contains(el) || el.closest(".effect-trial-offer")?.getAttribute("data-trust") !== "thread-control"
|
|
741
|
+
|| !offer || trialNow() >= offer.expiresAt) return;
|
|
742
|
+
if (trialStatus(row) === "ready" && opts.canTryEffect?.(row) === true) opts.startEffectTrial?.(row);
|
|
743
|
+
else opts.fixEffectTrial?.(row, trialStatus(row));
|
|
608
744
|
} else if (act === "showButtons") { const x = msgs.find(y => y.buttons && y.buttons.id === el.dataset.arg); if (x) reveal(msgKey(x)); }
|
|
609
745
|
}
|
|
610
746
|
|
|
611
747
|
/** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
|
|
612
|
-
|
|
748
|
+
/**
|
|
749
|
+
* #2053: kept for a later visit — the chat left as it always did (every preview stops: "leave"), but its drawn nodes
|
|
750
|
+
* stay. Nothing of it runs meanwhile: no timer (day labels, polls, effect trials, filling in), no size observer; the
|
|
751
|
+
* caller drops its subscriptions. Shown again: attach, then render — only what changed is drawn.
|
|
752
|
+
*/
|
|
753
|
+
function park() {
|
|
754
|
+
parked = true;
|
|
755
|
+
watchReader(scroller, false);
|
|
756
|
+
if (P()) P().stopAll("leave");
|
|
757
|
+
for (const node of list.querySelectorAll(".html-card[data-att], .text-card[data-att]")) { const f = cardDrop.get(node); if (f) f(); }
|
|
758
|
+
if (ro) { ro.disconnect(); ro = null; }
|
|
759
|
+
if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
|
|
760
|
+
if (growFrame !== null && typeof cancelAnimationFrame === "function") cancelAnimationFrame(growFrame);
|
|
761
|
+
if (growTimer !== null) clearTimeout(growTimer);
|
|
762
|
+
growTimer = growFrame = null;
|
|
763
|
+
if (offDay) { offDay(); offDay = null; }
|
|
764
|
+
if (pollTick) { clearInterval(pollTick); pollTick = null; }
|
|
765
|
+
}
|
|
766
|
+
/** In view in `sc` (the view's scroller): sizes watched there from now on; the caller restores the position. */
|
|
767
|
+
function attach(sc) {
|
|
768
|
+
if (!parked) watchReader(scroller, false); // moved from the scroller it was made with
|
|
769
|
+
parked = false;
|
|
770
|
+
scroller = sc;
|
|
771
|
+
watchReader(sc, true);
|
|
772
|
+
if (ro) ro.disconnect();
|
|
773
|
+
ro = typeof ResizeObserver === "function" ? new ResizeObserver(resized) : null;
|
|
774
|
+
if (ro) { ro.observe(list); ro.observe(sc); }
|
|
775
|
+
}
|
|
776
|
+
/** `own`: only this thread's previews stop (a thread let go while another chat is in view); else every one, as leaving. */
|
|
777
|
+
function dispose(own = false) {
|
|
613
778
|
disposed = true;
|
|
779
|
+
if (!parked) watchReader(scroller, false);
|
|
780
|
+
if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
|
|
614
781
|
if (ro) ro.disconnect();
|
|
615
|
-
if (P()) P().stopAll("leave");
|
|
782
|
+
if (P()) { if (own) P().stopIn(list); else P().stopAll("leave"); }
|
|
616
783
|
nodes.clear();
|
|
617
784
|
armDay();
|
|
618
785
|
armPolls();
|
|
619
786
|
}
|
|
620
787
|
|
|
621
|
-
return { render, onScroll, onClick, jumpLatest, reveal, refreshCards, dispose, nodes, get unseen() { return unseen; } };
|
|
788
|
+
return { render, onScroll, onClick, jumpLatest, reveal, refreshCards, park, attach, dispose, nodes, get unseen() { return unseen; } };
|
|
622
789
|
}
|
package/dist/ui/chat-tour.js
CHANGED
|
@@ -58,7 +58,7 @@ function show(i) {
|
|
|
58
58
|
if (spot) spot.classList.add("tour-spot");
|
|
59
59
|
let card = byId("tour");
|
|
60
60
|
if (!card) {
|
|
61
|
-
card = document.createElement("section"); card.id = "tour"; card.className = "tour";
|
|
61
|
+
card = document.createElement("section"); card.id = "tour"; card.className = "tour"; card.setAttribute("data-trust", "tour");
|
|
62
62
|
card.setAttribute("role", "dialog"); card.setAttribute("aria-labelledby", "tourTitle");
|
|
63
63
|
card.addEventListener("click", onCardClick);
|
|
64
64
|
document.body.appendChild(card);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** #1629: explicit-click control lane. Responses never grant authority; Start reauthorizes on the server. */
|
|
2
|
+
import { effectTrialMessage } from "./effect-trial-offer.js";
|
|
3
|
+
import { checkEffectTrialStart } from "/assets/effect-runtime-loader.js";
|
|
4
|
+
import { chatEffectIdentity } from "/assets/effect-chat-runtime.js";
|
|
5
|
+
const owners = new WeakSet(), safe = fn => { try { return fn(); } catch { return null; } };
|
|
6
|
+
const LIMIT = 128 * 1024;
|
|
7
|
+
export function createEffectTrialController({ window: host, current, allowed, onStart, onChange = () => {} }) {
|
|
8
|
+
let dead = false, active = null, physical = null, previous = null, broken = false, clockBusy = false, endedReason = null, endedOffer = null;
|
|
9
|
+
const cleanup = [], releases = [], identity = safe(() => chatEffectIdentity(host));
|
|
10
|
+
const tell = () => { if (!dead) safe(onChange); };
|
|
11
|
+
function clock() {
|
|
12
|
+
if (broken || clockBusy) { broken = true; return null; }
|
|
13
|
+
clockBusy = true; const t = safe(() => host.performance.now()); clockBusy = false;
|
|
14
|
+
if (broken || typeof t !== "number" || !Number.isFinite(t) || t < 0 || t > Number.MAX_SAFE_INTEGER || previous !== null && t < previous) { broken = true; return null; }
|
|
15
|
+
return previous = t;
|
|
16
|
+
}
|
|
17
|
+
function eligible(op) {
|
|
18
|
+
if (dead || broken || !op || active !== op || op.dead || op.checking || host.AgendAuth?.sessionEnded?.() !== false) return false;
|
|
19
|
+
op.checking = true;
|
|
20
|
+
try {
|
|
21
|
+
const t = clock();
|
|
22
|
+
if (t === null || t < op.began || t >= op.until || safe(() => current(op.instance) === true && allowed() === true) !== true) return false;
|
|
23
|
+
const after = clock();
|
|
24
|
+
return !dead && !op.dead && active === op && after !== null && after >= op.began && after < op.until;
|
|
25
|
+
} finally { op.checking = false; }
|
|
26
|
+
}
|
|
27
|
+
function end(op, reason = "stopped") {
|
|
28
|
+
if (!op || op.dead) return false;
|
|
29
|
+
op.dead = true; if (active === op) active = null;
|
|
30
|
+
endedReason = typeof reason === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(reason) ? reason : "trial_failed";
|
|
31
|
+
endedOffer = op.offer.offer;
|
|
32
|
+
safe(() => op.resolve?.(false));
|
|
33
|
+
safe(() => host.clearTimeout(op.timer)); safe(() => op.controller.abort());
|
|
34
|
+
if (op.reply && !op.stopQueued) { op.stopQueued = true; cleanup.push(op); }
|
|
35
|
+
// Physical paint is removed before callbacks, cleanup I/O or another explicit click.
|
|
36
|
+
safe(() => op.dispose?.()); op.dispose = null;
|
|
37
|
+
if (!["stopped", "replaced"].includes(endedReason)) safe(() => host.console?.warn("[AgEnD effects] Try stopped:", endedReason));
|
|
38
|
+
tell(); drain(); return true;
|
|
39
|
+
}
|
|
40
|
+
function arm(op) {
|
|
41
|
+
safe(() => host.clearTimeout(op.timer));
|
|
42
|
+
if (!eligible(op)) { end(op, "trial_ended"); return; }
|
|
43
|
+
const t = clock(); if (t === null || t >= op.until) { end(op, "trial_expired"); return; }
|
|
44
|
+
op.timer = host.setTimeout(() => { if (!op.dead) arm(op); }, Math.max(1, Math.ceil(op.until - t)));
|
|
45
|
+
}
|
|
46
|
+
async function read(response) {
|
|
47
|
+
let reader = null;
|
|
48
|
+
try {
|
|
49
|
+
if (!response || response.redirected || response.status !== 200 || !/^application\/json(?:\s*;|$)/i.test(response.headers.get("content-type") || "")) return null;
|
|
50
|
+
const length = response.headers.get("content-length"); if (length !== null && (!/^\d+$/.test(length) || Number(length) > LIMIT)) return null;
|
|
51
|
+
reader = response.body?.getReader(); if (!reader) return null;
|
|
52
|
+
const chunks = []; let size = 0;
|
|
53
|
+
for (;;) { const r = await reader.read(); if (r.done) break; if (!(r.value instanceof Uint8Array) || (size += r.value.byteLength) > LIMIT) return null; chunks.push(r.value); }
|
|
54
|
+
const bytes = new Uint8Array(size); let offset = 0; for (const c of chunks) { bytes.set(c, offset); offset += c.byteLength; }
|
|
55
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
56
|
+
} catch { return null; }
|
|
57
|
+
finally { try { if (reader) await reader.cancel(); else await response?.body?.cancel(); } catch { /* retained until cancellation settles */ } }
|
|
58
|
+
}
|
|
59
|
+
function drain() {
|
|
60
|
+
if (physical || !cleanup.length) return;
|
|
61
|
+
const op = cleanup.shift(), token = {}; physical = token; const controller = new host.AbortController();
|
|
62
|
+
const timer = host.setTimeout(() => controller.abort(), 10000);
|
|
63
|
+
// Stop remains permitted after CSS/gate/session changes. It cannot mount or mutate appearance.
|
|
64
|
+
Promise.resolve().then(async () => {
|
|
65
|
+
const response = await host.fetch(`/ui/effect-trials/${op.reply.trial}/stop?ownerEpoch=${identity.epoch}`, {
|
|
66
|
+
method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" }, body: "{}", signal: controller.signal,
|
|
67
|
+
});
|
|
68
|
+
try { await response.body?.cancel(); } catch { /* no response fields are consumed */ }
|
|
69
|
+
}).catch(() => {}).finally(() => { safe(() => host.clearTimeout(timer)); if (physical === token) physical = null; tell(); drain(); });
|
|
70
|
+
}
|
|
71
|
+
function dispose() {
|
|
72
|
+
if (dead) return; dead = true; end(active); for (const off of releases.splice(0)) safe(off); tell();
|
|
73
|
+
}
|
|
74
|
+
const api = { ok: false, start: async () => false, stop: () => end(active), sync() { const op = active; if (op && !eligible(op)) end(op, "trial_ended"); },
|
|
75
|
+
snapshot: () => Object.freeze({ busy: !!physical || cleanup.length > 0, phase: active ? active.reply ? "running" : "starting" : "idle", reason: active ? null : endedReason, offer: active ? active.offer.offer : endedOffer }), dispose };
|
|
76
|
+
if (!identity || owners.has(host) || typeof current !== "function" || typeof allowed !== "function" || typeof onStart !== "function"
|
|
77
|
+
|| typeof host.AgendAuth?.trialSession !== "function" || typeof host.AgendAuth?.onSessionEnd !== "function") return Object.freeze(api);
|
|
78
|
+
owners.add(host);
|
|
79
|
+
try {
|
|
80
|
+
const off = host.AgendAuth.onSessionEnd(dispose);
|
|
81
|
+
if (dead) { safe(off); return Object.freeze(api); }
|
|
82
|
+
releases.push(off);
|
|
83
|
+
for (const [target, type, fn] of [[host, "blur", () => end(active, "trial_window_blur")], [host, "pagehide", dispose], [host.document, "visibilitychange", () => { if (host.document.visibilityState !== "visible") end(active, "hidden"); }]]) {
|
|
84
|
+
target.addEventListener(type, fn); releases.push(() => target.removeEventListener(type, fn));
|
|
85
|
+
}
|
|
86
|
+
} catch { dispose(); return Object.freeze(api); }
|
|
87
|
+
if (dead) return Object.freeze(api);
|
|
88
|
+
api.ok = true;
|
|
89
|
+
api.start = async message => {
|
|
90
|
+
if (dead || active || physical || cleanup.length) return false;
|
|
91
|
+
const offer = safe(() => effectTrialMessage(message)), instance = safe(() => message.instance), began = clock();
|
|
92
|
+
if (!offer || began === null || began + 10000 > Number.MAX_SAFE_INTEGER || safe(() => host.Date.now() < offer.expiresAt) !== true) return false;
|
|
93
|
+
const op = { instance, offer, began, until: began + 10000, controller: new host.AbortController(), timer: null, reply: null, dead: false, stopQueued: false, dispose: null };
|
|
94
|
+
endedReason = null; endedOffer = null; active = op; const token = {}; physical = token;
|
|
95
|
+
const cancelled = new Promise(resolve => { op.resolve = resolve; });
|
|
96
|
+
const work = Promise.resolve().then(async () => {
|
|
97
|
+
if (!eligible(op)) return false;
|
|
98
|
+
arm(op); const session = await host.AgendAuth.trialSession();
|
|
99
|
+
if (!eligible(op) || !session || session.localAdmin !== true || session.epoch !== identity.epoch) return false;
|
|
100
|
+
const response = await host.fetch(`/ui/effect-trials/start?ownerEpoch=${identity.epoch}`, {
|
|
101
|
+
method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify({ offer: offer.offer, instance }), signal: op.controller.signal,
|
|
103
|
+
});
|
|
104
|
+
const reply = checkEffectTrialStart(await read(response), offer.offer, offer.id);
|
|
105
|
+
if (!reply) return false;
|
|
106
|
+
op.reply = reply;
|
|
107
|
+
if (op.dead && !op.stopQueued) { op.stopQueued = true; cleanup.push(op); }
|
|
108
|
+
if (reply.offer.title !== offer.title || reply.offer.expiresAt !== offer.expiresAt) return false;
|
|
109
|
+
if (!eligible(op)) { if (!op.stopQueued) { op.stopQueued = true; cleanup.push(op); } return false; }
|
|
110
|
+
op.until = began + reply.remainingMs;
|
|
111
|
+
if (op.until > Number.MAX_SAFE_INTEGER || !eligible(op)) return false;
|
|
112
|
+
arm(op);
|
|
113
|
+
const handle = Object.freeze({ reply, startedAt: began, instance, current: () => eligible(op), stop: (reason = "stopped") => end(op, reason), });
|
|
114
|
+
const disposer = onStart(handle);
|
|
115
|
+
if (typeof disposer !== "function") return false;
|
|
116
|
+
if (!eligible(op)) { safe(disposer); return false; }
|
|
117
|
+
op.dispose = disposer; tell(); return eligible(op);
|
|
118
|
+
}).catch(() => false).then(ok => { if (!ok) end(op, "trial_start_failed"); return ok; }).finally(() => { if (physical === token) physical = null; tell(); drain(); });
|
|
119
|
+
// The logical caller may stop immediately, while the physical request/cancel still owns the lane.
|
|
120
|
+
tell();
|
|
121
|
+
return Promise.race([work, cancelled]);
|
|
122
|
+
};
|
|
123
|
+
return Object.freeze(api);
|
|
124
|
+
}
|