@songsid/agend 2.3.0-beta.1 → 2.3.0-beta.11
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 +43 -7
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +4 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +14 -6
- package/dist/agent-file-read.js +207 -19
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-whats-new.d.ts +11 -0
- package/dist/agent-whats-new.js +29 -15
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +45 -13
- 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/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/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.d.ts +7 -0
- package/dist/channel/adapters/discord.js +35 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +22 -10
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +6 -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 +130 -31
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +103 -4
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +7 -0
- package/dist/daemon.js +63 -16
- 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 +10 -0
- package/dist/fleet-manager.d.ts +417 -11
- package/dist/fleet-manager.js +1387 -109
- package/dist/fleet-manager.js.map +1 -1
- 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 +257 -23
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +478 -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-config-impact.js +3 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +12 -1
- package/dist/instructions.js +13 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +100 -8
- package/dist/locale.js.map +1 -1
- package/dist/ls-attach-resolve.d.ts +95 -0
- package/dist/ls-attach-resolve.js +174 -0
- package/dist/ls-attach-resolve.js.map +1 -0
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/outbound-handlers.d.ts +10 -0
- package/dist/outbound-handlers.js +122 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +164 -4
- package/dist/outbound-schemas.js +99 -3
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +8 -2
- 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/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 +12 -0
- package/dist/scheduler/scheduler.js +83 -10
- 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 +19 -0
- package/dist/settings-api.js +137 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +104 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +17 -1
- package/dist/settings-effect.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 +53 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -1
- package/dist/topic-commands.js +18 -4
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +48 -1
- package/dist/ui/app-effects.js +77 -31
- package/dist/ui/chat-ambient.js +307 -0
- package/dist/ui/chat-effect-posters.js +8 -8
- package/dist/ui/chat-effects.js +101 -13
- package/dist/ui/chat-render.js +21 -3
- package/dist/ui/chat-strings.js +59 -6
- package/dist/ui/chat-thread.js +159 -21
- package/dist/ui/effect-trial-controller.js +13 -8
- package/dist/ui/effect-trial-panel.js +19 -16
- 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/panel-chat.js +154 -49
- package/dist/ui/panel-details.js +2 -1
- package/dist/ui/panel-fleet.js +43 -17
- package/dist/ui/panel-settings.js +26 -8
- package/dist/ui/preview.js +3 -2
- package/dist/ui/settings-appearance.js +42 -18
- package/dist/ui/settings-catalog.js +92 -0
- package/dist/ui/settings-dialogs.js +40 -12
- package/dist/ui/settings-effects.js +4 -3
- 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 +23 -9
- package/dist/ui/settings-owner-effects.js +9 -9
- package/dist/ui/settings-strings.js +120 -42
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/agend-auth.js +1 -1
- package/dist/ui/shared/app-i18n.js +4 -2
- package/dist/ui/shared/app-prefs.js +5 -4
- package/dist/ui/shared/app-route.js +19 -1
- package/dist/ui/shared/app-session.js +2 -0
- package/dist/ui/shared/app-shell.js +92 -27
- package/dist/ui/shared/app-store.js +26 -1
- package/dist/ui/shared/app-stream.js +3 -1
- package/dist/ui/shared/app-themes.js +24 -6
- package/dist/ui/shared/app.css +365 -17
- package/dist/ui/shared/app.js +21 -14
- package/dist/ui/shared/card-strings.js +4 -0
- package/dist/ui/shared/effect-app-runtime.js +68 -19
- package/dist/ui/shared/effect-chat-runtime.js +16 -3
- package/dist/ui/shared/effect-frame.js +26 -7
- package/dist/ui/shared/effect-geometry.js +24 -9
- package/dist/ui/shared/effect-policy.js +21 -7
- package/dist/ui/shared/effect-profiler.js +31 -9
- package/dist/ui/shared/effect-registry.js +87 -48
- package/dist/ui/shared/effect-stock-snapshot.js +9 -0
- package/dist/ui/shared/effect-stock-trust.js +62 -10
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-source.js +1 -1
- package/dist/ui/shared/effect-trial-stage.js +54 -31
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/fx-motion.js +67 -0
- package/dist/ui/shared/identity-card.js +75 -4
- package/dist/ui/shared/identity-color.js +3 -3
- package/dist/ui/shared/instance-nav.js +194 -28
- package/dist/ui/shared/panel-view.js +24 -4
- package/dist/ui/shared/tokens.css +9 -3
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +22 -2
- package/dist/ui/shared/view-strings.js +68 -2
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/ui/signin.html +1 -1
- 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 +39 -0
- package/dist/view-api.js +65 -4
- 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 +69 -5
- package/dist/web-api.js +362 -45
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-policy.d.ts +4 -6
- package/dist/web-appearance-policy.js +5 -12
- package/dist/web-appearance-policy.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +3 -0
- package/dist/web-appearance-tools.js +22 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +27 -2
- package/dist/web-appearance-view.js +129 -14
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +17 -3
- package/dist/web-appearance.js +82 -7
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +5 -10
- package/dist/web-asset-parts.js +3 -3
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +3 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +14 -0
- package/dist/web-card.js +40 -6
- package/dist/web-card.js.map +1 -1
- 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 +50 -0
- package/dist/web-chat-history.js +87 -5
- 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.js +46 -1
- package/dist/web-effect-bundle.js.map +1 -1
- 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-profiles.d.ts +4 -4
- package/dist/web-effect-service.d.ts +11 -4
- package/dist/web-effect-service.js +28 -5
- package/dist/web-effect-service.js.map +1 -1
- package/dist/web-effect-tools.d.ts +1 -0
- package/dist/web-effect-tools.js +8 -5
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-trials.d.ts +4 -4
- package/dist/web-effect-trials.js +6 -6
- package/dist/web-effect-trials.js.map +1 -1
- package/dist/web-effect-upload.d.ts +2 -4
- package/dist/web-effect-upload.js +3 -8
- package/dist/web-effect-upload.js.map +1 -1
- package/dist/web-effect-worker.d.ts +1 -1
- package/dist/web-effect-worker.js +2 -2
- package/dist/web-effect-worker.js.map +1 -1
- package/dist/web-effect.d.ts +14 -0
- package/dist/web-effect.js +21 -0
- package/dist/web-effect.js.map +1 -1
- 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 +1 -1
- package/dist/web-identity-color.js +2 -2
- package/dist/web-identity-color.js.map +1 -1
- package/dist/web-identity-palette.d.ts +1 -1
- package/dist/web-identity-palette.js +2 -2
- package/dist/web-identity-palette.js.map +1 -1
- package/dist/web-image-meta.d.ts +13 -0
- package/dist/web-image-meta.js +62 -0
- package/dist/web-image-meta.js.map +1 -1
- 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 +1 -1
- package/dist/web-mod-agent.js +12 -8
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +14 -1
- package/dist/web-mod-bundle.js +68 -3
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-export.d.ts +9 -0
- package/dist/web-mod-export.js +21 -0
- package/dist/web-mod-export.js.map +1 -1
- package/dist/web-mod-store.d.ts +7 -1
- package/dist/web-mod-store.js +21 -7
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +14 -0
- package/dist/web-prefs.js +24 -4
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +6 -2
- package/dist/web-preview.js +4 -2
- 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 +49 -2
- package/dist/web-settings-patch.js +145 -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 +270 -0
- package/dist/web-settings-tools.js.map +1 -0
- 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 +39 -0
- package/dist/web-theme-store.js +122 -11
- package/dist/web-theme-store.js.map +1 -1
- package/dist/web-theme.d.ts +2 -0
- package/dist/web-theme.js +10 -1
- package/dist/web-theme.js.map +1 -1
- 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/workflow-templates/default.md +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +8 -0
package/dist/types.d.ts
CHANGED
|
@@ -345,6 +345,12 @@ export interface WebConfig {
|
|
|
345
345
|
echo_to_channel?: boolean;
|
|
346
346
|
/** Show the AI subscription usage panel on /view and serve /api/ai-usage (default true). */
|
|
347
347
|
usage_panel?: boolean;
|
|
348
|
+
/**
|
|
349
|
+
* #2122: serve the usage report (Fleet → Usage, GET /ui/usage-report) over the temporary public link too (default
|
|
350
|
+
* false). It is up to 400 days of per-agent, per-model, per-profile activity and amounts — more than the live quota
|
|
351
|
+
* rail or the cache report the link already shows. Guests never get it.
|
|
352
|
+
*/
|
|
353
|
+
usage_report_on_public_link?: boolean;
|
|
348
354
|
/**
|
|
349
355
|
* #1937: the largest file the web chat takes, in MB (whole number, 1–500; default 50). Never more than
|
|
350
356
|
* upload_max_total_mb; over the public link a file is at most 95 MB whatever this says (the quick tunnel's limit).
|
|
@@ -378,7 +384,7 @@ export interface WebConfig {
|
|
|
378
384
|
/**
|
|
379
385
|
* #1620: how long a web session lasts. Only the LOCAL surface (the dashboard reached directly — loopback, a LAN, a
|
|
380
386
|
* tailnet) is configurable; a public-link session's limits stay fixed. Hours, 1 ≤ idle_hours ≤ absolute_hours ≤ 720;
|
|
381
|
-
* defaults
|
|
387
|
+
* defaults 48 (absolute, from sign-in) and 48 (idle, since the last use; #1670). Shortening applies to existing sessions.
|
|
382
388
|
*/
|
|
383
389
|
session?: {
|
|
384
390
|
local?: {
|
|
@@ -417,6 +423,47 @@ export interface WebConfig {
|
|
|
417
423
|
* (and warned about); a change applies to new uploads only.
|
|
418
424
|
*/
|
|
419
425
|
appearance?: import("./web-appearance-limits.js").AppearanceLimitsConfig;
|
|
426
|
+
/**
|
|
427
|
+
* #1920: guest web sign-in — a non-admin signed in as their own Discord or Telegram account, seeing only the instances
|
|
428
|
+
* they already use in chat (derived on every request). Default off.
|
|
429
|
+
*/
|
|
430
|
+
guests?: WebGuestsConfig;
|
|
431
|
+
/**
|
|
432
|
+
* #1939: when a signed-in web session's messages carry authority (docs/design/1939-verified-web-sessions.md §3).
|
|
433
|
+
* S1 records it in receipts only; no agent acts on it yet.
|
|
434
|
+
*/
|
|
435
|
+
verified_authority?: WebVerifiedAuthorityConfig;
|
|
436
|
+
}
|
|
437
|
+
/** #1939: fleet.yaml `web.verified_authority` (docs/design/1939-verified-web-sessions.md §0, §3, §10). */
|
|
438
|
+
export interface WebVerifiedAuthorityConfig {
|
|
439
|
+
/**
|
|
440
|
+
* The server switch (§10): web messages carry authority only while it is on (default true, §0.1). Turning it off makes
|
|
441
|
+
* every web authority read as none at once; turning it back on never brings back authority given before.
|
|
442
|
+
*/
|
|
443
|
+
enabled?: boolean;
|
|
444
|
+
/** "Only this computer can authorize": only loopback requests qualify (default false: LAN and tailnet too). */
|
|
445
|
+
loopback_only?: boolean;
|
|
446
|
+
/** A session's authorization lasts this many hours after it was given, 1–48 (default 48). */
|
|
447
|
+
max_session_hours?: number;
|
|
448
|
+
}
|
|
449
|
+
/** #1920: fleet.yaml `web.guests`. */
|
|
450
|
+
export interface WebGuestsConfig {
|
|
451
|
+
/** Guest sign-in at all (default false). Turning it off ends every guest session. */
|
|
452
|
+
enabled?: boolean;
|
|
453
|
+
/** Guests may chat (default true); false: read-only. */
|
|
454
|
+
chat?: boolean;
|
|
455
|
+
/**
|
|
456
|
+
* An `open` connection's guests reach nothing until an admin names instances here, per connection id (the user's
|
|
457
|
+
* decision: least privilege, since an open group has no list of people).
|
|
458
|
+
*/
|
|
459
|
+
open_instances?: Record<string, string[]>;
|
|
460
|
+
/** An admin's narrowing, per `<connection id>:<user id>`: intersected with the derived scope, never widening it. */
|
|
461
|
+
narrow?: Record<string, string[]>;
|
|
462
|
+
/** A guest session's limits: absolute 1–24 h (default 12), idle 15–720 min (default 120). */
|
|
463
|
+
session?: {
|
|
464
|
+
absolute_hours?: number;
|
|
465
|
+
idle_minutes?: number;
|
|
466
|
+
};
|
|
420
467
|
}
|
|
421
468
|
/** `login:` section — remote /login behaviour. */
|
|
422
469
|
export interface LoginConfig {
|
package/dist/ui/app-effects.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/** #1629: one session-only app effect owner. Importing this module starts nothing. */
|
|
2
2
|
import { appStore } from "/assets/app-store.js";
|
|
3
|
+
import { motionBlocked, onMotionChange } from "/assets/fx-motion.js";
|
|
3
4
|
import { navStore } from "/assets/app-nav.js";
|
|
4
|
-
import { onPrefsChanged
|
|
5
|
-
import { resolvedMode, themesStore } from "/assets/app-themes.js";
|
|
5
|
+
import { onPrefsChanged } from "/assets/app-prefs.js";
|
|
6
|
+
import { resolvedMode, themesStore, themesShown } from "/assets/app-themes.js";
|
|
6
7
|
import { ownerCssLifecycle, safeMode } from "/assets/owner-css.js";
|
|
7
8
|
import { constrainedDevice } from "/assets/effect-policy.js";
|
|
8
9
|
import { createAppEffectRuntime } from "/assets/effect-app-runtime.js";
|
|
10
|
+
import { effectBootstrapReady } from "/assets/effect-frame.js";
|
|
9
11
|
import { createEffectCursor } from "/assets/effect-cursor.js";
|
|
10
12
|
import { createOwnerEffectRegions } from "/assets/effect-owner-regions.js";
|
|
11
|
-
import { chatEffectIdentity } from "/assets/effect-chat-runtime.js";
|
|
13
|
+
import { chatEffectIdentity, effectPlaybackIdentity } from "/assets/effect-chat-runtime.js";
|
|
12
14
|
import { createEffectTrialController } from "./effect-trial-controller.js";
|
|
13
15
|
import { effectTrialMessage } from "./effect-trial-offer.js";
|
|
14
16
|
import { claimPanel, panelVersion, panelStore, openTrialPanel, closePanel } from "./preview-panel.js";
|
|
@@ -19,8 +21,9 @@ export function getAppEffects(host = window) { const value = owners.get(host); r
|
|
|
19
21
|
export function startAppEffects({ store, window: host = window, factories = {} } = {}) {
|
|
20
22
|
const existing = owners.get(host);
|
|
21
23
|
if (existing) { if (!existing.dead) existing.bindStore(store); return getAppEffects(host); }
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const playbackIdentity = effectPlaybackIdentity(host), ownerIdentity = chatEffectIdentity(host);
|
|
25
|
+
if (!playbackIdentity || typeof host.AgendAuth?.onSessionEnd !== "function" || typeof host.AgendAuth?.sessionEnded !== "function") return null;
|
|
26
|
+
const doc = host.document, releases = [], ended = new Set(), updates = new Set(), trialUpdates = new Set(); let replyOff = null, replyStore = null, regions = null, cursor = null, cursorStamp = null, trialControl = null, trialIntent = null, trialHint = ownerIdentity ? null : false, trialSerial = 0, trialStamp = null, chatBinding = null;
|
|
24
27
|
const safe = fn => { try { return fn(); } catch { return null; } };
|
|
25
28
|
const hold = off => { if (entry.dead) safe(() => off?.()); else releases.push(off); };
|
|
26
29
|
const entry = { dead: false, api: null,
|
|
@@ -38,11 +41,23 @@ export function startAppEffects({ store, window: host = window, factories = {} }
|
|
|
38
41
|
if (entry.dead || replyStore !== value) safe(() => off?.()); else replyOff = off;
|
|
39
42
|
}
|
|
40
43
|
},
|
|
41
|
-
|
|
44
|
+
trialStatus(message) {
|
|
42
45
|
const offer = safe(() => effectTrialMessage(message)), nav = navStore.get();
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
const now = safe(() => host.Date.now());
|
|
47
|
+
if (!offer || !Number.isFinite(now) || now >= offer.expiresAt) return "expired";
|
|
48
|
+
if (trialHint === null && !entry.dead) return "checking";
|
|
49
|
+
if (entry.dead || !trialHint || trialControl?.ok !== true) return "localAdmin";
|
|
50
|
+
if (!chatBinding || safe(() => chatBinding.current()) !== true || message.instance !== nav.route?.instance || nav.route?.panel !== "chat") return "chatChanged";
|
|
51
|
+
const state = trialControl.snapshot();
|
|
52
|
+
if (state.phase !== "idle" || state.busy) return "busy";
|
|
53
|
+
return trialPlaybackStatus();
|
|
45
54
|
},
|
|
55
|
+
trialFailure(message) {
|
|
56
|
+
const offer = safe(() => effectTrialMessage(message)), state = trialControl?.snapshot();
|
|
57
|
+
return offer && state?.phase === "idle" && state.offer === offer.offer && !["stopped", "replaced"].includes(state.reason) ? state.reason : null;
|
|
58
|
+
},
|
|
59
|
+
canTry(message) { return entry.trialStatus(message) === "ready"; },
|
|
60
|
+
stopTrial() { trialControl?.stop(); },
|
|
46
61
|
startTrial(message) {
|
|
47
62
|
if (!entry.canTry(message)) return Promise.resolve(false);
|
|
48
63
|
const nav = navStore.get(); trialIntent = { name: message.instance, seq: nav.seq, claim: claimPanel(), handle: null, chat: chatBinding };
|
|
@@ -67,13 +82,33 @@ export function startAppEffects({ store, window: host = window, factories = {} }
|
|
|
67
82
|
},
|
|
68
83
|
};
|
|
69
84
|
owners.set(host, entry); // Reentrant setup never constructs another owner.
|
|
70
|
-
let media, coarse
|
|
85
|
+
let media, coarse;
|
|
86
|
+
function ownerCssForPlayback() {
|
|
87
|
+
if (doc.body.dataset.tier !== "guest") return ownerCssLifecycle.get().state;
|
|
88
|
+
// Guests never install owner CSS. Prove its absence rather than borrowing the uninstalled
|
|
89
|
+
// lifecycle's initial unknown, and refuse unexpected styles instead of trusting a missing id.
|
|
90
|
+
try {
|
|
91
|
+
if (doc.body.dataset.ownerCss || doc.querySelectorAll("#owner-css").length) return "unknown";
|
|
92
|
+
// A CSP-refused or disabled STYLE has no paint. Recheck on every proof so enabling
|
|
93
|
+
// it never borrows an earlier absence verdict (WebKit capture inserts a refused no-op).
|
|
94
|
+
for (const style of doc.querySelectorAll("style")) if (style.sheet && style.sheet.disabled !== true) return "unknown";
|
|
95
|
+
const allowed = new Set(["/assets/tokens.css", "/assets/app.css"]);
|
|
96
|
+
for (const link of doc.querySelectorAll('link[rel~="stylesheet"]')) {
|
|
97
|
+
const url = new URL(link.getAttribute("href"), host.location.origin);
|
|
98
|
+
if (url.origin !== host.location.origin || !allowed.has(url.pathname) || url.search || url.hash) return "unknown";
|
|
99
|
+
}
|
|
100
|
+
return "off";
|
|
101
|
+
} catch { return "unknown"; }
|
|
102
|
+
}
|
|
103
|
+
function guestChatVersion(value) {
|
|
104
|
+
if (doc.body.dataset.tier !== "guest") return null;
|
|
105
|
+
const name = navStore.get().route?.instance, v = value.instances?.find(i => i.name === name)?.effect_chat_v;
|
|
106
|
+
return typeof v === "string" && /^[a-f0-9]{32}$/.test(v) ? v : null;
|
|
107
|
+
}
|
|
71
108
|
function context() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
show: prefInForce("fx.show")?.value !== false, previewReady: ready,
|
|
76
|
-
optIn: prefInForce("preview.optIn")?.value === true && preview?.optedIn() === true && preview?.never() !== true,
|
|
109
|
+
return { safe: safeMode(), publicLink: doc.body.dataset.publicLink === "1", reducedMotion: motionBlocked(host), saveData: host.navigator?.connection?.saveData === true,
|
|
110
|
+
hidden: doc.visibilityState !== "visible", enabled: entry.api?.playbackAvailable !== false && typeof host.OffscreenCanvas === "function" && typeof host.Worker === "function" && typeof host.MutationObserver === "function" && typeof host.ResizeObserver === "function",
|
|
111
|
+
show: themesShown(), transportReady: effectBootstrapReady(host, host.location?.origin, doc.body.dataset.effectBoot),
|
|
77
112
|
constrained: constrainedDevice({ memory: host.navigator?.deviceMemory, cores: host.navigator?.hardwareConcurrency, coarse: coarse?.matches === true, width: host.innerWidth }),
|
|
78
113
|
modal: !!doc.querySelector("dialog[open],.menu-list,.pop,[role=alertdialog],.oc-keep,.shell.sb-open"), openInstance: null };
|
|
79
114
|
}
|
|
@@ -82,10 +117,23 @@ export function startAppEffects({ store, window: host = window, factories = {} }
|
|
|
82
117
|
return !entry.dead && host.document === doc && !!intent && !!intent.chat && chatBinding === intent.chat && intent.chat.current() === true && intent.name === name && nav.seq === intent.seq && nav.route?.panel === "chat" && nav.route.instance === name
|
|
83
118
|
&& (panelVersion() === intent.claim || intent.handle !== null && panelStore.get().open?.handle === intent.handle);
|
|
84
119
|
}
|
|
120
|
+
// One priority order feeds both presentation and redraw identity: false→different-false must redraw.
|
|
121
|
+
function trialPlaybackStatus() {
|
|
122
|
+
const c = context();
|
|
123
|
+
if (c.safe) return "safe";
|
|
124
|
+
if (!c.show) return "effectsOff";
|
|
125
|
+
if (c.reducedMotion) return "reducedMotion";
|
|
126
|
+
if (!c.enabled) return "unsupported";
|
|
127
|
+
if (!c.transportReady) return "transport";
|
|
128
|
+
if (c.publicLink) return "localAdmin";
|
|
129
|
+
if (c.hidden || doc.hasFocus?.() !== true) return "inactive";
|
|
130
|
+
if (c.modal) return "dialog";
|
|
131
|
+
return "ready";
|
|
132
|
+
}
|
|
85
133
|
function trialAllowed() {
|
|
86
134
|
const c = context();
|
|
87
|
-
return !entry.dead && doc.visibilityState === "visible" && doc.hasFocus?.() === true
|
|
88
|
-
&& c.enabled && c.show && c.
|
|
135
|
+
return !entry.dead && doc.visibilityState === "visible" && doc.hasFocus?.() === true
|
|
136
|
+
&& c.enabled && c.show && c.transportReady && !c.safe && !c.publicLink && !c.reducedMotion && !c.hidden && !c.modal;
|
|
89
137
|
}
|
|
90
138
|
function mountTrial(handle) {
|
|
91
139
|
const intent = trialIntent; if (!intent || !trialCurrent(handle.instance) || !handle.current() || !Number.isSafeInteger(++trialSerial)) throw Error("trial_unavailable");
|
|
@@ -97,7 +145,7 @@ export function startAppEffects({ store, window: host = window, factories = {} }
|
|
|
97
145
|
if (panelStore.get().open?.handle === handle) closePanel();
|
|
98
146
|
};
|
|
99
147
|
try {
|
|
100
|
-
openTrialPanel({ key: binding.key, instance: handle.instance, binding, handle,
|
|
148
|
+
openTrialPanel({ key: binding.key, instance: handle.instance, binding, handle,
|
|
101
149
|
onStage(value) {
|
|
102
150
|
stage = value;
|
|
103
151
|
if (!handle.current()) { cleanup(); handle.stop(); return; }
|
|
@@ -111,7 +159,7 @@ export function startAppEffects({ store, window: host = window, factories = {} }
|
|
|
111
159
|
}
|
|
112
160
|
function notifyTrial() {
|
|
113
161
|
if (entry.dead) return;
|
|
114
|
-
const nav = navStore.get(), next = JSON.stringify([trialHint, safe(() => chatBinding?.current()) === true,
|
|
162
|
+
const nav = navStore.get(), next = JSON.stringify([trialHint, safe(() => chatBinding?.current()) === true, trialPlaybackStatus(), nav.route?.panel, nav.route?.instance, trialControl?.snapshot()]);
|
|
115
163
|
if (next === trialStamp) return; trialStamp = next;
|
|
116
164
|
for (const fn of [...trialUpdates]) safe(fn);
|
|
117
165
|
}
|
|
@@ -120,47 +168,45 @@ export function startAppEffects({ store, window: host = window, factories = {} }
|
|
|
120
168
|
function syncCursor() {
|
|
121
169
|
if (entry.dead || !cursor) return;
|
|
122
170
|
const c = context();
|
|
123
|
-
cursor.update(entry.api?.cursorSelection?.() ?? null, c.enabled && c.show && c.
|
|
171
|
+
cursor.update(entry.api?.cursorSelection?.() ?? null, c.enabled && c.show && c.transportReady && !c.safe && !c.publicLink && !c.reducedMotion && !c.hidden && !c.modal && !cursorDragging());
|
|
124
172
|
}
|
|
125
173
|
try {
|
|
126
174
|
if (appStore.get().connection === "ended") { entry.dispose(); return null; }
|
|
127
|
-
preview = host.AgendPreview;
|
|
128
175
|
media = host.matchMedia?.("(prefers-reduced-motion: reduce)") ?? { matches: true };
|
|
129
176
|
coarse = host.matchMedia?.("(pointer: coarse)") ?? { matches: false };
|
|
130
177
|
entry.api = (factories.runtime ?? createAppEffectRuntime)({ window: host, current: () => !entry.dead && host.document === doc,
|
|
131
|
-
observe: () => ({ context: context(), ownerCss:
|
|
178
|
+
observe: () => ({ context: context(), ownerCss: ownerCssForPlayback(), mode: resolvedMode() }), onDispose: () => entry.dispose(),
|
|
132
179
|
resolveOwnerTarget: (slot, manifest, proof) => !entry.dead ? regions?.resolve?.(slot, manifest.box, proof) ?? null : null,
|
|
133
180
|
onChange: () => { syncCursor(); notify(); } });
|
|
134
181
|
if (entry.dead || !entry.api.ok) { entry.dispose(); return null; }
|
|
135
|
-
trialControl = (factories.trialController ?? createEffectTrialController)({ window: host, current: trialCurrent, allowed: trialAllowed, onStart: mountTrial, onChange: notify });
|
|
182
|
+
if (ownerIdentity) trialControl = (factories.trialController ?? createEffectTrialController)({ window: host, current: trialCurrent, allowed: trialAllowed, onStart: mountTrial, onChange: notify });
|
|
136
183
|
hold(() => trialControl?.dispose());
|
|
137
|
-
if (typeof host.AgendAuth.trialSession === "function") Promise.resolve().then(() => host.AgendAuth.trialSession()).then(value => {
|
|
184
|
+
if (ownerIdentity && typeof host.AgendAuth.trialSession === "function") Promise.resolve().then(() => host.AgendAuth.trialSession()).then(value => {
|
|
138
185
|
if (entry.dead) return; trialHint = value?.localAdmin === true && value.epoch === chatEffectIdentity(host)?.epoch; notify();
|
|
139
|
-
}).catch(() => {});
|
|
186
|
+
}).catch(() => { if (!entry.dead) { trialHint = false; notify(); } });
|
|
140
187
|
hold(panelStore.subscribe(() => { trialControl?.sync(); notifyTrial(); }));
|
|
141
|
-
const ownRegions = (factories.regions ?? createOwnerEffectRegions)({ window: host, current: () => !entry.dead && host.document === doc });
|
|
188
|
+
const ownRegions = ownerIdentity && (factories.regions ?? createOwnerEffectRegions)({ window: host, current: () => !entry.dead && host.document === doc });
|
|
142
189
|
regions = ownRegions?.ok ? ownRegions : null; hold(() => ownRegions?.dispose?.());
|
|
143
190
|
if (entry.dead) return null;
|
|
144
|
-
const ownCursor = safe(() => (factories.cursor ?? createEffectCursor)({ window: host, current: () => !entry.dead && host.document === doc,
|
|
191
|
+
const ownCursor = ownerIdentity && safe(() => (factories.cursor ?? createEffectCursor)({ window: host, current: () => !entry.dead && host.document === doc,
|
|
145
192
|
readImage: (selection, signal) => entry.api?.cursorImageSettled?.(selection, signal) ?? Promise.resolve(null),
|
|
146
193
|
onChange(value) { const next = JSON.stringify(value); if (next !== cursorStamp) { cursorStamp = next; notify(); } },
|
|
147
194
|
}));
|
|
148
195
|
cursor = ownCursor?.ok ? ownCursor : null; hold(() => ownCursor?.dispose?.());
|
|
149
196
|
if (entry.dead) return null;
|
|
150
197
|
const sync = () => { if (!entry.dead) { trialControl?.sync(); entry.api.sync(); syncCursor(); notifyTrial(); } };
|
|
151
|
-
let revision = appStore.get().themesRev, prefsRevision = appStore.get().prefsRev, navSeq = navStore.get().seq;
|
|
198
|
+
let revision = appStore.get().themesRev, prefsRevision = appStore.get().prefsRev, guestRevision = guestChatVersion(appStore.get()), navSeq = navStore.get().seq;
|
|
152
199
|
hold(navStore.subscribe(value => { if (entry.dead || value.seq === navSeq) return; navSeq = value.seq; trialControl?.stop(); chatBinding = null; entry.api.detachChat(); syncCursor(); }));
|
|
153
200
|
hold(appStore.subscribe(value => {
|
|
154
201
|
if (entry.dead) return;
|
|
155
202
|
if (value.connection === "ended") { entry.dispose(); return; }
|
|
156
|
-
const chatChanged = value.themesRev !== revision, ownerChanged = value.prefsRev !== prefsRevision;
|
|
157
|
-
revision = value.themesRev; prefsRevision = value.prefsRev;
|
|
203
|
+
const guestNext = guestChatVersion(value), chatChanged = value.themesRev !== revision || guestNext !== guestRevision, ownerChanged = value.prefsRev !== prefsRevision;
|
|
204
|
+
revision = value.themesRev; prefsRevision = value.prefsRev; guestRevision = guestNext;
|
|
158
205
|
if (chatChanged) entry.api.changedChat();
|
|
159
206
|
if (ownerChanged) entry.api.changedOwner();
|
|
160
207
|
if (!chatChanged && !ownerChanged) sync();
|
|
161
208
|
}));
|
|
162
|
-
hold(ownerCssLifecycle.subscribe(sync)); hold(themesStore.subscribe(sync)); hold(onPrefsChanged(sync));
|
|
163
|
-
if (typeof preview?.onChange === "function") hold(preview.onChange(sync));
|
|
209
|
+
hold(ownerCssLifecycle.subscribe(sync)); hold(themesStore.subscribe(sync)); hold(onPrefsChanged(sync)); hold(onMotionChange(sync));
|
|
164
210
|
if (typeof host.AgendTheme?.onChange === "function") hold(host.AgendTheme.onChange(sync));
|
|
165
211
|
for (const [target, type] of [[media, "change"], [coarse, "change"], [doc, "visibilitychange"], [host, "resize"], [host.navigator?.connection, "change"]]) {
|
|
166
212
|
if (target?.addEventListener) { target.addEventListener(type, sync); hold(() => target.removeEventListener(type, sync)); }
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// #2117: an agent's ambient effect on its own chat — falling petals, rising lanterns, twinkles, stars on a click. The
|
|
2
|
+
// agent sends data only (web-ambient.ts checks it; this checks the bounds again); the page draws it with plain nodes
|
|
3
|
+
// animated by the Web Animations API: transform and opacity only, positions through the CSSOM (no style attribute,
|
|
4
|
+
// #1300), every node aria-hidden and pointer-events: none. The looping layer sits behind the thread (.amb-layer, under
|
|
5
|
+
// the scroller and the dock); a burst shows over the messages for a moment (.amb-burst) and never takes the click.
|
|
6
|
+
//
|
|
7
|
+
// Its own gates (not the Canvas Worker runtime's, #2115): the viewer's "Show agents' themes, effects and widgets", safe
|
|
8
|
+
// mode, Web Animations support, and reduced motion only when this device chose to follow the system's setting
|
|
9
|
+
// (fx-motion.js; the user's direction: effects play by default). A hidden tab pauses. Nothing else gates it: it runs
|
|
10
|
+
// no agent code and fetches nothing but its own glyph pictures (fleet decision: no gate without a threat it stops).
|
|
11
|
+
// Caps: 24 looping particles, bursts of at most 16, at most 3 bursts alive — 72 nodes and animations per chat.
|
|
12
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
13
|
+
import { t } from "/assets/app-i18n.js";
|
|
14
|
+
import { useStore } from "/assets/app-store.js";
|
|
15
|
+
import { onPrefsChanged } from "/assets/app-prefs.js";
|
|
16
|
+
import { safeMode } from "/assets/owner-css.js";
|
|
17
|
+
import { setThemesShown, themeImageUrl, themesShown, themesStore } from "/assets/app-themes.js";
|
|
18
|
+
import { markMotionNoteShown, motionBlocked, motionNoteDue, onMotionChange, setFollowsReducedMotion } from "/assets/fx-motion.js";
|
|
19
|
+
import "./chat-strings.js";
|
|
20
|
+
|
|
21
|
+
export const AMBIENT_CAPS = { loop: 24, burst: 16, bursts: 3 };
|
|
22
|
+
const LOOP_PRESETS = ["fall", "rise", "twinkle"];
|
|
23
|
+
const RULES = {
|
|
24
|
+
fall: { count: [1, 24], duration: [4, 30] }, rise: { count: [1, 24], duration: [4, 30] },
|
|
25
|
+
twinkle: { count: [1, 24], duration: [1, 10] }, "burst-on-click": { count: [4, 16], duration: [0.3, 2] },
|
|
26
|
+
};
|
|
27
|
+
const COLOR = /^#[0-9a-f]{6}$/;
|
|
28
|
+
const ASSET = /^[0-9a-f]{32}$/;
|
|
29
|
+
const own = (o, k) => !!o && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
|
|
30
|
+
const num = (v, lo, hi) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi;
|
|
31
|
+
const pair = (v, lo, hi) => Array.isArray(v) && v.length === 2 && num(v[0], lo, hi) && num(v[1], lo, hi) && v[0] <= v[1];
|
|
32
|
+
|
|
33
|
+
/** A spec as the host keeps it, checked again; null when anything is off (it is then not drawn at all). */
|
|
34
|
+
export function sanitizeAmbient(spec) {
|
|
35
|
+
if (!spec || typeof spec !== "object" || !own(RULES, spec.preset)) return null;
|
|
36
|
+
const r = RULES[spec.preset];
|
|
37
|
+
if (!Array.isArray(spec.glyphs) || spec.glyphs.length < 1 || spec.glyphs.length > 4) return null;
|
|
38
|
+
const glyphs = [];
|
|
39
|
+
for (const g of spec.glyphs) {
|
|
40
|
+
if (typeof g === "string" && g.length >= 1 && g.length <= 32) glyphs.push(g);
|
|
41
|
+
else if (g && typeof g === "object" && typeof g.asset === "string" && ASSET.test(g.asset)) glyphs.push({ asset: g.asset });
|
|
42
|
+
else return null;
|
|
43
|
+
}
|
|
44
|
+
const colors = Array.isArray(spec.colors) ? spec.colors.filter((c) => typeof c === "string" && COLOR.test(c)).slice(0, 5) : [];
|
|
45
|
+
if (!Number.isInteger(spec.count) || !num(spec.count, r.count[0], r.count[1])) return null;
|
|
46
|
+
if (!pair(spec.duration, r.duration[0], r.duration[1]) || !pair(spec.size, 10, 40) || !pair(spec.spread, 20, 120)) return null;
|
|
47
|
+
if (!num(spec.sway, 0, 80) || !num(spec.rotate, 0, 720)) return null;
|
|
48
|
+
if (!Array.isArray(spec.opacity) || spec.opacity.length !== 3 || !spec.opacity.every((o) => num(o, 0, 1))) return null;
|
|
49
|
+
return { preset: spec.preset, glyphs, colors, count: spec.count, duration: spec.duration, size: spec.size, sway: spec.sway, spread: spec.spread, rotate: spec.rotate, opacity: spec.opacity };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `name`'s ambients from GET /ui/themes, checked: { loop, click } (either null). Null when it has none. */
|
|
53
|
+
export function ambientFor(name) {
|
|
54
|
+
const a = themesStore.get().ambient;
|
|
55
|
+
if (!a || typeof a !== "object" || !own(a, name)) return null;
|
|
56
|
+
const mine = a[name];
|
|
57
|
+
const loop = own(mine, "loop") ? sanitizeAmbient(mine.loop && mine.loop.spec) : null;
|
|
58
|
+
const click = own(mine, "click") ? sanitizeAmbient(mine.click && mine.click.spec) : null;
|
|
59
|
+
if (loop && !LOOP_PRESETS.includes(loop.preset)) return null;
|
|
60
|
+
if (click && click.preset !== "burst-on-click") return null;
|
|
61
|
+
return loop || click ? { loop, click } : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Why this page does not draw ambients now, or null. */
|
|
65
|
+
export function ambientBlock(win = globalThis) {
|
|
66
|
+
if (safeMode()) return "safe";
|
|
67
|
+
if (!themesShown()) return "hidden";
|
|
68
|
+
if (motionBlocked(win)) return "reducedMotion";
|
|
69
|
+
const El = win.Element;
|
|
70
|
+
if (!El || !El.prototype || typeof El.prototype.animate !== "function") return "unsupported";
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The gate as state: moves with the switches, the themes and the device's reduced-motion answer. */
|
|
75
|
+
function useAmbientBlock() {
|
|
76
|
+
useStore(themesStore);
|
|
77
|
+
const [, bump] = useState(0);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const again = () => bump((n) => n + 1);
|
|
80
|
+
const offPrefs = onPrefsChanged((changed) => { if (changed.has("fx.show")) again(); });
|
|
81
|
+
const offMotion = onMotionChange(again);
|
|
82
|
+
return () => { if (typeof offPrefs === "function") offPrefs(); offMotion(); };
|
|
83
|
+
}, []);
|
|
84
|
+
return ambientBlock();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rand = (lo, hi) => lo + Math.random() * (hi - lo);
|
|
88
|
+
const pick = (list) => list[Math.floor(Math.random() * list.length)];
|
|
89
|
+
|
|
90
|
+
/** One particle node: a text glyph (its colour, else the accent) or a glyph picture. */
|
|
91
|
+
function particle(doc, name, spec, cls) {
|
|
92
|
+
const g = pick(spec.glyphs), size = Math.round(rand(spec.size[0], spec.size[1]));
|
|
93
|
+
let el;
|
|
94
|
+
if (typeof g === "string") {
|
|
95
|
+
el = doc.createElement("span");
|
|
96
|
+
el.textContent = g;
|
|
97
|
+
el.style.setProperty("font-size", `${size}px`);
|
|
98
|
+
if (spec.colors.length) el.style.setProperty("color", pick(spec.colors));
|
|
99
|
+
} else {
|
|
100
|
+
el = doc.createElement("img");
|
|
101
|
+
el.src = themeImageUrl(name, g.asset);
|
|
102
|
+
el.alt = "";
|
|
103
|
+
el.decoding = "async";
|
|
104
|
+
el.draggable = false;
|
|
105
|
+
el.style.setProperty("width", `${size}px`);
|
|
106
|
+
el.style.setProperty("height", `${size}px`);
|
|
107
|
+
}
|
|
108
|
+
el.className = cls;
|
|
109
|
+
el.setAttribute("aria-hidden", "true");
|
|
110
|
+
return { el, size };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Draw `ambient` ({ loop, click }) for `name` into `layer` (behind) and `front` (bursts), listening on `surface` for
|
|
115
|
+
* clicks. Returns { stop() }: every node and animation it made goes, every listener it added is removed.
|
|
116
|
+
*/
|
|
117
|
+
export function startAmbient({ name, ambient, layer, front, surface, doc = globalThis.document }) {
|
|
118
|
+
const anims = new Set();
|
|
119
|
+
const loopNodes = [];
|
|
120
|
+
let bursts = 0, stopped = false;
|
|
121
|
+
const track = (a) => { anims.add(a); if (doc.visibilityState === "hidden") a.pause(); return a; };
|
|
122
|
+
|
|
123
|
+
const drawLoop = () => {
|
|
124
|
+
for (const n of loopNodes.splice(0)) n.remove();
|
|
125
|
+
for (const a of [...anims]) if (a._amb === "loop") { a.cancel(); anims.delete(a); }
|
|
126
|
+
const spec = ambient.loop;
|
|
127
|
+
if (!spec) return;
|
|
128
|
+
const h = layer.clientHeight || 600, w = layer.clientWidth || 360;
|
|
129
|
+
const [o0, o1, o2] = spec.opacity;
|
|
130
|
+
for (let i = 0; i < Math.min(spec.count, AMBIENT_CAPS.loop); i++) {
|
|
131
|
+
const { el, size } = particle(doc, name, spec, "amb-p");
|
|
132
|
+
const dur = rand(spec.duration[0], spec.duration[1]) * 1000;
|
|
133
|
+
const turn = spec.rotate * (Math.random() < 0.5 ? -1 : 1);
|
|
134
|
+
let frames;
|
|
135
|
+
if (spec.preset === "twinkle") {
|
|
136
|
+
el.style.setProperty("left", `${rand(0, Math.max(0, w - size))}px`);
|
|
137
|
+
el.style.setProperty("top", `${rand(0, Math.max(0, h - size))}px`);
|
|
138
|
+
frames = [
|
|
139
|
+
{ transform: "scale(0.6) rotate(0deg)", opacity: o0 },
|
|
140
|
+
{ transform: `scale(1) rotate(${turn / 2}deg)`, opacity: o1, offset: 0.5 },
|
|
141
|
+
{ transform: `scale(0.6) rotate(${turn}deg)`, opacity: o2 },
|
|
142
|
+
];
|
|
143
|
+
} else {
|
|
144
|
+
el.style.setProperty("left", `${rand(0, Math.max(0, w - size))}px`);
|
|
145
|
+
el.style.setProperty("top", "0px");
|
|
146
|
+
const dx = rand(-spec.sway, spec.sway), down = spec.preset === "fall";
|
|
147
|
+
const from = down ? -size - 4 : h + 4, to = down ? h + 4 : -size - 4;
|
|
148
|
+
frames = [
|
|
149
|
+
{ transform: `translate(0px, ${from}px) rotate(0deg)`, opacity: o0 },
|
|
150
|
+
{ transform: `translate(${dx * 0.6}px, ${from + (to - from) * 0.15}px) rotate(${turn * 0.15}deg)`, opacity: o1, offset: 0.15 },
|
|
151
|
+
{ transform: `translate(${-dx * 0.4}px, ${from + (to - from) * 0.6}px) rotate(${turn * 0.6}deg)`, opacity: (o1 + o2) / 2, offset: 0.6 },
|
|
152
|
+
{ transform: `translate(${dx}px, ${to}px) rotate(${turn}deg)`, opacity: o2 },
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
layer.appendChild(el);
|
|
156
|
+
loopNodes.push(el);
|
|
157
|
+
// A negative delay starts each one part-way through, so the layer is already full when it appears.
|
|
158
|
+
const a = el.animate(frames, { duration: dur, delay: -Math.random() * dur, iterations: Infinity, easing: "linear" });
|
|
159
|
+
a._amb = "loop";
|
|
160
|
+
track(a);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const burst = (x, y) => {
|
|
165
|
+
const spec = ambient.click;
|
|
166
|
+
if (!spec || bursts >= AMBIENT_CAPS.bursts || stopped) return;
|
|
167
|
+
bursts++;
|
|
168
|
+
const [, o1, o2] = spec.opacity;
|
|
169
|
+
let left = Math.min(spec.count, AMBIENT_CAPS.burst);
|
|
170
|
+
const done = (el, a) => { el.remove(); anims.delete(a); if (--left === 0) bursts--; };
|
|
171
|
+
for (let i = 0, n = left; i < n; i++) {
|
|
172
|
+
const { el, size } = particle(doc, name, spec, "amb-p");
|
|
173
|
+
el.style.setProperty("left", `${x - size / 2}px`);
|
|
174
|
+
el.style.setProperty("top", `${y - size / 2}px`);
|
|
175
|
+
const angle = (i / n) * Math.PI * 2 + rand(-0.3, 0.3), dist = rand(spec.spread[0], spec.spread[1]);
|
|
176
|
+
const turn = rand(-spec.rotate, spec.rotate) / 2;
|
|
177
|
+
front.appendChild(el);
|
|
178
|
+
const a = el.animate([
|
|
179
|
+
{ transform: "translate(0px, 0px) scale(0.4) rotate(0deg)", opacity: o1 },
|
|
180
|
+
{ transform: `translate(${Math.cos(angle) * dist}px, ${Math.sin(angle) * dist}px) scale(1) rotate(${turn}deg)`, opacity: Math.min(o2, o1) },
|
|
181
|
+
], { duration: rand(spec.duration[0], spec.duration[1]) * 1000, easing: "cubic-bezier(.2,.7,.3,1)", fill: "forwards" });
|
|
182
|
+
a._amb = "burst";
|
|
183
|
+
track(a);
|
|
184
|
+
let gone = false;
|
|
185
|
+
const finish = () => { if (!gone) { gone = true; done(el, a); } };
|
|
186
|
+
a.onfinish = finish; a.oncancel = finish;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Passive and never cancelling: the click still lands on what is under it. Only in the thread (not the composer),
|
|
191
|
+
// and not on a menu or dialog open over it: stars are for the conversation.
|
|
192
|
+
const onDown = (ev) => {
|
|
193
|
+
if (!ambient.click || ev.button > 0) return;
|
|
194
|
+
const target = ev.target;
|
|
195
|
+
if (!target || typeof target.closest !== "function" || !target.closest(".scroller") || target.closest("[role=menu], [role=dialog]")) return;
|
|
196
|
+
const box = front.getBoundingClientRect();
|
|
197
|
+
burst(ev.clientX - box.left, ev.clientY - box.top);
|
|
198
|
+
};
|
|
199
|
+
const onVisible = () => { for (const a of anims) { if (doc.visibilityState === "hidden") a.pause(); else a.play(); } };
|
|
200
|
+
// Size moves (a rotated phone, the preview panel opening): the loop is drawn again for the new box, a moment later.
|
|
201
|
+
let resizeTimer = null, lastW = layer.clientWidth, lastH = layer.clientHeight;
|
|
202
|
+
const ro = typeof globalThis.ResizeObserver === "function" ? new globalThis.ResizeObserver(() => {
|
|
203
|
+
if (layer.clientWidth === lastW && layer.clientHeight === lastH) return;
|
|
204
|
+
lastW = layer.clientWidth; lastH = layer.clientHeight;
|
|
205
|
+
clearTimeout(resizeTimer);
|
|
206
|
+
resizeTimer = setTimeout(() => { if (!stopped) drawLoop(); }, 200);
|
|
207
|
+
}) : null;
|
|
208
|
+
|
|
209
|
+
drawLoop();
|
|
210
|
+
if (ambient.click) surface.addEventListener("pointerdown", onDown, { passive: true });
|
|
211
|
+
doc.addEventListener("visibilitychange", onVisible);
|
|
212
|
+
if (ro && ambient.loop) ro.observe(layer);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
stop() {
|
|
216
|
+
stopped = true;
|
|
217
|
+
clearTimeout(resizeTimer);
|
|
218
|
+
if (ro) ro.disconnect();
|
|
219
|
+
surface.removeEventListener("pointerdown", onDown, { passive: true });
|
|
220
|
+
doc.removeEventListener("visibilitychange", onVisible);
|
|
221
|
+
for (const a of anims) a.cancel();
|
|
222
|
+
anims.clear();
|
|
223
|
+
for (const n of loopNodes.splice(0)) n.remove();
|
|
224
|
+
while (front.firstChild) front.firstChild.remove();
|
|
225
|
+
},
|
|
226
|
+
/** For tests and the smoke: what is alive now. */
|
|
227
|
+
stats() { return { nodes: layer.children.length + front.children.length, animations: anims.size, bursts }; },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The chat's ambient: renders its two layers inside .chat-main (`surface`) and runs them. `part` is "layer" (behind,
|
|
233
|
+
* right after the theme picture) or "front" (after the dock); the "layer" instance owns the run and finds its sibling.
|
|
234
|
+
*/
|
|
235
|
+
export function ChatAmbient({ name, surface, part }) {
|
|
236
|
+
const block = useAmbientBlock();
|
|
237
|
+
const ambient = block ? null : ambientFor(name);
|
|
238
|
+
const key = ambient ? JSON.stringify(ambient) : "";
|
|
239
|
+
const ref = useRef(null);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (part !== "layer" || !ambient) return undefined;
|
|
242
|
+
const layer = ref.current, host = surface.current;
|
|
243
|
+
const front = host && host.querySelector(".amb-burst");
|
|
244
|
+
if (!layer || !host || !front) return undefined;
|
|
245
|
+
host.classList.add("has-amb");
|
|
246
|
+
const run = startAmbient({ name, ambient, layer, front, surface: host });
|
|
247
|
+
if (typeof globalThis !== "undefined") globalThis.__agendAmbient = run; // the smoke reads stats(); nothing else
|
|
248
|
+
return () => { run.stop(); host.classList.remove("has-amb"); if (globalThis.__agendAmbient === run) delete globalThis.__agendAmbient; };
|
|
249
|
+
}, [name, key, part]);
|
|
250
|
+
if (!ambient) return null;
|
|
251
|
+
return part === "layer" ? html`<div ref=${ref} class="amb-layer" aria-hidden="true"></div>` : html`<div class="amb-burst" aria-hidden="true"></div>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The one-time note (the user's direction): the system asks for reduced motion and this chat's ambient or Canvas Worker plays anyway —
|
|
256
|
+
* say so once on this device, and turn it off right here (this device's switch: the same on the public link, where
|
|
257
|
+
* Settings is not). Shown only while an ambient or Canvas Worker is playing.
|
|
258
|
+
*/
|
|
259
|
+
export function AmbientMotionNote({ name, canvasPlaying = false }) {
|
|
260
|
+
const block = useAmbientBlock();
|
|
261
|
+
const shown = canvasPlaying || !block && !!ambientFor(name);
|
|
262
|
+
const noteDue = motionNoteDue();
|
|
263
|
+
const [open, setOpen] = useState(false);
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (!shown || open || !noteDue) return;
|
|
266
|
+
markMotionNoteShown();
|
|
267
|
+
setOpen(true);
|
|
268
|
+
}, [shown, noteDue]);
|
|
269
|
+
if (!open || !shown) return null;
|
|
270
|
+
const off = () => { setOpen(false); setFollowsReducedMotion(true); };
|
|
271
|
+
return html`<div class="amb-note" data-trust="thread-control" role="status">
|
|
272
|
+
<span>${t("chat.ambientMotionNote")}</span>
|
|
273
|
+
<button type="button" class="amb-note-off" onClick=${off}>${t("chat.ambientMotionNoteLink")}</button>
|
|
274
|
+
<button type="button" class="amb-note-x" aria-label=${t("chat.ambientMotionNoteClose")} onClick=${() => setOpen(false)}>×</button>
|
|
275
|
+
</div>`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The fix for a block, done right here: both switches are this viewer's own (the show switch is a local choice on the
|
|
280
|
+
* public link; the reduced-motion one is per device), so it works on the public link too; safe mode is the URL. Both
|
|
281
|
+
* stay in Settings → Web appearance as well.
|
|
282
|
+
*/
|
|
283
|
+
const FIX = {
|
|
284
|
+
hidden: () => setThemesShown(true),
|
|
285
|
+
reducedMotion: () => setFollowsReducedMotion(false),
|
|
286
|
+
safe: () => { const u = new URL(globalThis.location.href); u.searchParams.delete("safe"); globalThis.location.assign(u.toString()); },
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The chat ⋯ entries for `name`'s ambient: why it is not showing here (a visible reason; choosing it fixes it when the
|
|
291
|
+
* page can), and — for an admin, never over the public link — Clear for each kind.
|
|
292
|
+
*/
|
|
293
|
+
export function ambientMenuItems(name, { publicLink, clear }) {
|
|
294
|
+
const a = themesStore.get().ambient;
|
|
295
|
+
const mine = a && own(a, name) ? a[name] : null;
|
|
296
|
+
if (!mine || typeof mine !== "object" || !(own(mine, "loop") || own(mine, "click"))) return [];
|
|
297
|
+
const items = [];
|
|
298
|
+
const block = ambientBlock();
|
|
299
|
+
if (block) {
|
|
300
|
+
items.push({
|
|
301
|
+
key: "ambient-blocked", label: t(`chat.ambientBlocked_${block}`), icon: "info",
|
|
302
|
+
onSelect: () => { if (own(FIX, block)) FIX[block](); },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (!publicLink) for (const k of ["loop", "click"]) if (own(mine, k)) items.push({ key: `ambient-clear-${k}`, label: t(k === "loop" ? "chat.ambientClearLoop" : "chat.ambientClearClick"), icon: "trash", onSelect: () => clear(name, k) });
|
|
306
|
+
return items;
|
|
307
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** #1629: static Chat fallback. It requires no preview origin, device consent, Worker or personal identity. */
|
|
2
2
|
import { useLayoutEffect } from "/assets/app-html.js";
|
|
3
3
|
import { appStore } from "/assets/app-store.js";
|
|
4
|
-
import { onPrefsChanged
|
|
4
|
+
import { onPrefsChanged } from "/assets/app-prefs.js";
|
|
5
5
|
import { themesShown, themesStore } from "/assets/app-themes.js";
|
|
6
6
|
import { ownerCssLifecycle } from "/assets/owner-css.js";
|
|
7
|
-
import {
|
|
7
|
+
import { effectPlaybackIdentity } from "/assets/effect-chat-runtime.js";
|
|
8
|
+
import { effectBootstrapReady } from "/assets/effect-frame.js";
|
|
9
|
+
import { motionBlocked, onMotionChange } from "/assets/fx-motion.js";
|
|
8
10
|
import { createChatPosters } from "/assets/effect-posters.js";
|
|
9
11
|
|
|
10
12
|
export function ChatEffectPosters({ name, lease, element }) {
|
|
@@ -15,12 +17,11 @@ export function ChatEffectPosters({ name, lease, element }) {
|
|
|
15
17
|
const cleanup = () => { if (!live) return; live = false; try { api?.dispose?.(); } catch { /* optional */ } for (const off of releases) { try { off?.(); } catch { /* release all */ } } };
|
|
16
18
|
lease.hold(cleanup);
|
|
17
19
|
try {
|
|
18
|
-
const
|
|
20
|
+
const motion = host.matchMedia?.("(prefers-reduced-motion: reduce)") ?? { matches: true };
|
|
19
21
|
const observe = () => {
|
|
20
|
-
let previewReady = false; try { previewReady = preview?.availability().ok === true; } catch { /* fallback */ }
|
|
21
22
|
// This is a surface fallback, not a replacement for an explicit Pause, queued effect or failed live read.
|
|
22
|
-
const playback =
|
|
23
|
-
|
|
23
|
+
const playback = effectPlaybackIdentity(host) && typeof host.Worker === "function" && typeof host.OffscreenCanvas === "function" &&
|
|
24
|
+
effectBootstrapReady(host, host.location?.origin, host.document.body.dataset.effectBoot) && !motionBlocked(host);
|
|
24
25
|
return { shown: themesShown(), fallback: !playback, ownerCss: ownerCssLifecycle.get().state };
|
|
25
26
|
};
|
|
26
27
|
api = createChatPosters({ window: host, name, element: element.current, current, observe });
|
|
@@ -32,8 +33,7 @@ export function ChatEffectPosters({ name, lease, element }) {
|
|
|
32
33
|
if (s.connection === "ended") { api.dispose(); return; }
|
|
33
34
|
if (s.themesRev !== revision) { revision = s.themesRev; api.changed(); } else sync();
|
|
34
35
|
}));
|
|
35
|
-
releases.push(ownerCssLifecycle.subscribe(sync)); releases.push(themesStore.subscribe(sync)); releases.push(onPrefsChanged(sync));
|
|
36
|
-
if (typeof preview?.onChange === "function") releases.push(preview.onChange(sync));
|
|
36
|
+
releases.push(ownerCssLifecycle.subscribe(sync)); releases.push(themesStore.subscribe(sync)); releases.push(onPrefsChanged(sync)); releases.push(onMotionChange(sync));
|
|
37
37
|
for (const target of [motion, host.navigator?.connection]) if (target?.addEventListener) { target.addEventListener("change", sync); releases.push(() => target.removeEventListener("change", sync)); }
|
|
38
38
|
sync(); return cleanup;
|
|
39
39
|
} catch { cleanup(); return cleanup; }
|