@songsid/agend 2.2.0-beta.9 → 2.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -24
- package/README.zh-TW.md +40 -24
- package/dist/agent-cli.js +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +28 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +20 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +3 -1
- package/dist/auth-api.js +32 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +33 -0
- package/dist/backend/antigravity.js +129 -21
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +9 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +3 -0
- package/dist/backend/grok.js +9 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -1
- package/dist/backend/kiro.js +7 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +24 -0
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +14 -0
- package/dist/channel/adapters/discord.js +66 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +26 -1
- package/dist/channel/adapters/telegram.js +98 -7
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +1 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +31 -0
- package/dist/cli.js +124 -24
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +5 -2
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.d.ts +86 -0
- package/dist/compact-watch.js +288 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/config-validator.js +55 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.js +394 -29
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -1
- package/dist/delivery-outbox.js +6 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.js +7 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +12 -0
- package/dist/fleet-manager.d.ts +247 -9
- package/dist/fleet-manager.js +1227 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/install-lock.d.ts +18 -1
- package/dist/install-lock.js +30 -3
- package/dist/install-lock.js.map +1 -1
- package/dist/instance-avatar.d.ts +5 -4
- package/dist/instance-avatar.js +15 -8
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +2 -2
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/launchd-handoff.d.ts +43 -0
- package/dist/launchd-handoff.js +56 -0
- package/dist/launchd-handoff.js.map +1 -0
- package/dist/locale.js +202 -30
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +10 -2
- package/dist/log-service.js.map +1 -1
- package/dist/login-shell.d.ts +21 -0
- package/dist/login-shell.js +98 -0
- package/dist/login-shell.js.map +1 -0
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +32 -0
- package/dist/outbound-handlers.js +150 -45
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +76 -0
- package/dist/outbound-schemas.js +65 -4
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +29 -0
- package/dist/provider-probe.js +198 -2
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +25 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +8 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +12 -0
- package/dist/quickstart.js +36 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +86 -5
- package/dist/reply-buttons-controller.js +271 -7
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +0 -2
- package/dist/restart-progress.js +0 -2
- package/dist/restart-progress.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/sd-notify.d.ts +22 -1
- package/dist/sd-notify.js +63 -10
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +42 -26
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +24 -0
- package/dist/service-installer.js +49 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +11 -0
- package/dist/service-plan.js +23 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tool-permissions.js +53 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +72 -2
- package/dist/topic-commands.js +156 -38
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/transcript-jsonl.d.ts +7 -2
- package/dist/transcript-jsonl.js +13 -7
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-sources.js +6 -1
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +59 -14
- package/dist/ui/chat-store.js +78 -8
- package/dist/ui/chat-strings.js +43 -16
- package/dist/ui/chat-thread.js +175 -31
- package/dist/ui/chat-tour.js +18 -15
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +101 -27
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +18 -5
- package/dist/ui/panel-log.js +10 -10
- package/dist/ui/panel-needs.js +13 -10
- package/dist/ui/panel-settings.js +66 -12
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-dialogs.js +11 -6
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-model.js +3 -1
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +269 -10
- package/dist/ui/settings-token.js +7 -0
- package/dist/ui/settings-wizard.js +5 -3
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +3 -1
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +15 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +266 -11
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +115 -0
- package/dist/ui/shared/effect-policy.js +137 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +35 -15
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/instance-switch.js +5 -2
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +96 -12
- package/dist/ui/shared/signin.js +17 -9
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/view-strings.js +40 -2
- package/dist/ui/signin.html +9 -3
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-launch.d.ts +48 -1
- package/dist/update-launch.js +159 -5
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/providers.d.ts +28 -8
- package/dist/usage/providers.js +96 -41
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +214 -17
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +86 -2
- package/dist/web-api.js +1160 -92
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +39 -1
- package/dist/web-chat-history.js +69 -9
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +39 -3
- package/dist/web-commands.js +22 -5
- package/dist/web-commands.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +29 -0
- package/dist/web-effect-upload.js +190 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +180 -0
- package/dist/web-owner-css-store.js +384 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +66 -0
- package/dist/web-prefs.js +243 -35
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-session.d.ts +14 -0
- package/dist/web-session.js +10 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +119 -4
- package/dist/web-upload.js +252 -39
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/launcher/install-admission.cjs +61 -6
- package/package.json +3 -3
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">). */
|
|
@@ -31,6 +33,12 @@ const COPY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill=
|
|
|
31
33
|
/** Escapes & " ' < >: safe for text and inside a quoted attribute. */
|
|
32
34
|
export function escAttr(s) { return String(s == null ? "" : s).replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"); }
|
|
33
35
|
export const msgKey = (x) => `${x.boot}-${x.id}`;
|
|
36
|
+
/**
|
|
37
|
+
* Board 00ef2430: a thread opens with only its newest messages drawn — enough for a screen — so switching chats paints
|
|
38
|
+
* at once; the older ones are drawn after that, a chunk at a time between frames, the reader's place kept.
|
|
39
|
+
*/
|
|
40
|
+
export const THREAD_TAIL = 24;
|
|
41
|
+
export const THREAD_CHUNK = 40;
|
|
34
42
|
|
|
35
43
|
/**
|
|
36
44
|
* Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
|
|
@@ -47,6 +55,10 @@ export function createThread(list, scroller, opts) {
|
|
|
47
55
|
// moves down or stays), and those must not count as the reader leaving the bottom. Reaching the bottom pins again.
|
|
48
56
|
let pinned = true;
|
|
49
57
|
let lastTop = 0;
|
|
58
|
+
// The oldest message drawn (by key): everything after it is drawn, so new messages never push the window. Null: none
|
|
59
|
+
// drawn yet — the next render draws the newest THREAD_TAIL and the rest follows (grow).
|
|
60
|
+
let firstKey = null;
|
|
61
|
+
let growTimer = null, growFrame = null, disposed = false;
|
|
50
62
|
const cardRefresh = new WeakMap(); // card node → re-read availability (weak: a card that left takes its source with it)
|
|
51
63
|
const tr = opts.t, trf = opts.tf;
|
|
52
64
|
const stopIn = (node) => (P() ? P().stopIn(node) : []);
|
|
@@ -58,7 +70,7 @@ export function createThread(list, scroller, opts) {
|
|
|
58
70
|
if (el.textContent !== v) el.textContent = v;
|
|
59
71
|
}
|
|
60
72
|
// #1770: "today" ends at the fleet's midnight; the labels are rewritten then (and on every render), while there are any.
|
|
61
|
-
let offDay = null;
|
|
73
|
+
let offDay = null, trialTimer = null;
|
|
62
74
|
function armDay() {
|
|
63
75
|
if (nodes.size && !offDay) offDay = onDayChange(() => { for (const v of nodes.values()) timeLabel(v.node); });
|
|
64
76
|
else if (!nodes.size && offDay) { offDay(); offDay = null; }
|
|
@@ -70,13 +82,39 @@ export function createThread(list, scroller, opts) {
|
|
|
70
82
|
// #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
|
|
71
83
|
// is kept by: it is written into the node (timeLabels) — crossing midnight relabels in place and reloads no preview.
|
|
72
84
|
const time = x.ts ? `<span class="time" data-ts="${escAttr(x.ts)}" title="${escAttr(chatTimeFull(x.ts))}"></span>` : `<span class="time"></span>`;
|
|
85
|
+
const trial = effectTrialMessage(x);
|
|
86
|
+
if (trial) return trialHtml(trial, time, x);
|
|
73
87
|
if (x.role === "status") return statusHtml(x, time);
|
|
74
88
|
const u = opts.isUser(x);
|
|
75
89
|
// renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
|
|
76
90
|
const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
|
|
77
|
-
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>`;
|
|
91
|
+
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>`;
|
|
78
92
|
const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
|
|
79
|
-
|
|
93
|
+
// A person's click on a reply button shows as the button's label, never the line the agent was given (its value).
|
|
94
|
+
const clicked = x.role === "user" && x.click && typeof x.click.label === "string";
|
|
95
|
+
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">${R().renderMarkdown(x.text, { htmlCards: x.role === "agent", emojis: opts.emojis, mentions: 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>`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A button click as a chip: 🔘 and the label (escaped: the label is the agent's text). With `of` (live, while its
|
|
99
|
+
* reply is in this thread) it is a button that shows that reply; restored from disk, plain.
|
|
100
|
+
*/
|
|
101
|
+
function clickHtml(click) {
|
|
102
|
+
const inner = `<span class="click-mark" aria-hidden="true">🔘</span><span class="sr-only">${escAttr(tr("chat.clicked"))} </span>${escAttr(click.label)}`;
|
|
103
|
+
const of = typeof click.of === "string" && /^[0-9a-f]{32}$/.test(click.of) ? click.of : null;
|
|
104
|
+
return `<div class="click-chip" data-trust="thread-control">${of
|
|
105
|
+
? `<button type="button" class="click-link" data-act="showButtons" data-arg="${escAttr(of)}" title="${escAttr(tr("chat.clickShow"))}">${inner}</button>`
|
|
106
|
+
: `<span class="click-text">${inner}</span>`}</div>`;
|
|
107
|
+
}
|
|
108
|
+
function trialNow() { try { const t = opts.serverNow ? opts.serverNow() : Date.now(); return Number.isFinite(t) ? t : Infinity; } catch { return Infinity; } }
|
|
109
|
+
function trialHtml(offer, time, message) {
|
|
110
|
+
const expired = trialNow() >= offer.expiresAt, available = !PUBLIC_LINK() && !expired && opts.canTryEffect?.(message) === true;
|
|
111
|
+
const button = available ? `<button type="button" class="btn btn-sm effect-trial-start" data-act="effectTrial" data-arg="${escAttr(msgKey(message))}">${escAttr(tr("chat.effectTrialStart"))}</button>` : "";
|
|
112
|
+
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(expired ? "chat.effectTrialExpired" : "chat.effectTrialLocal"))}</div>${button}</div></div></div>`;
|
|
113
|
+
}
|
|
114
|
+
function armTrials() {
|
|
115
|
+
if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
|
|
116
|
+
const now = trialNow(), next = msgs.map(x => effectTrialMessage(x)).filter(x => x && x.expiresAt > now).reduce((n, x) => Math.min(n, x.expiresAt), Infinity);
|
|
117
|
+
if (Number.isFinite(next)) trialTimer = setTimeout(() => { trialTimer = null; render(msgs); }, Math.max(1, Math.min(2147483647, next - now)));
|
|
80
118
|
}
|
|
81
119
|
/**
|
|
82
120
|
* #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
|
|
@@ -88,10 +126,12 @@ export function createThread(list, scroller, opts) {
|
|
|
88
126
|
const lines = R().lineCount(text);
|
|
89
127
|
const manyLines = lines > STATUS_FOLD_LINES;
|
|
90
128
|
const more = manyLines || text.length > STATUS_FOLD_CHARS
|
|
91
|
-
? `<button type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
|
|
129
|
+
? `<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>`
|
|
92
130
|
: "";
|
|
93
|
-
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>`;
|
|
94
|
-
|
|
131
|
+
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>`;
|
|
132
|
+
// #1990: the other instance's name opens its chat (an in-app link the router takes: no target).
|
|
133
|
+
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("");
|
|
134
|
+
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>`;
|
|
95
135
|
}
|
|
96
136
|
/**
|
|
97
137
|
* #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
|
|
@@ -99,6 +139,7 @@ export function createThread(list, scroller, opts) {
|
|
|
99
139
|
*/
|
|
100
140
|
function replyButtonsHtml(b) {
|
|
101
141
|
if (!b || typeof b.id !== "string" || !Array.isArray(b.labels) || !b.labels.length) return "";
|
|
142
|
+
if (b.poll && typeof b.poll === "object") return pollHtml(b);
|
|
102
143
|
const busy = !!(opts.replyButtonBusy && opts.replyButtonBusy(b.id));
|
|
103
144
|
const off = b.state !== "open" || busy;
|
|
104
145
|
const items = b.labels.map((label, i) => {
|
|
@@ -106,14 +147,53 @@ export function createThread(list, scroller, opts) {
|
|
|
106
147
|
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>`;
|
|
107
148
|
}).join("");
|
|
108
149
|
const note = b.state === "chosen" ? trf("chat.rbChosen", b.by || "") : b.state === "expired" ? tr("chat.rbExpired") : "";
|
|
109
|
-
return `<div class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
|
|
150
|
+
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>`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* #1823: a poll. Anyone here may click while it is open; the page marks its own vote (opts.pollMine), which no one else
|
|
154
|
+
* sees. Counts only when the server sends them (revealed: live, or closed); names only at close with show_voters. The
|
|
155
|
+
* time left is not in the html (a node is kept by its html): pollLeft writes it, every second.
|
|
156
|
+
*/
|
|
157
|
+
function pollHtml(b) {
|
|
158
|
+
const p = b.poll, closed = b.state === "closed";
|
|
159
|
+
const busy = !!(opts.replyButtonBusy && opts.replyButtonBusy(b.id));
|
|
160
|
+
const mine = (opts.pollMine && opts.pollMine(b.id)) || [];
|
|
161
|
+
const counts = Array.isArray(p.counts) && p.counts.length === b.labels.length ? p.counts : null;
|
|
162
|
+
const items = b.labels.map((label, i) => {
|
|
163
|
+
const me = !closed && mine.includes(i);
|
|
164
|
+
const tally = counts ? ` · ${counts[i]}${closed && p.answered ? ` (${Math.round((counts[i] / p.answered) * 100)}%)` : ""}` : "";
|
|
165
|
+
return `<button type="button" class="btn btn-sm rb-btn${me ? " chosen" : ""}" data-act="replyButton" data-arg="${escAttr(`${b.id}:${i}`)}" aria-pressed="${me ? "true" : "false"}"${closed || busy ? " disabled" : ""}>${escAttr(`${me ? "✓ " : ""}${label}${tally}`)}</button>`;
|
|
166
|
+
}).join("");
|
|
167
|
+
const status = closed
|
|
168
|
+
? escAttr(trf("chat.pollClosed", p.answered))
|
|
169
|
+
: `${escAttr(trf("chat.pollAnswered", p.answered))} · ⏳ <span class="poll-left" data-deadline="${escAttr(p.deadline)}" title="${escAttr(tr("chat.pollLeft"))}"></span>`;
|
|
170
|
+
const voters = closed && Array.isArray(p.voters)
|
|
171
|
+
? `<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>`
|
|
172
|
+
: "";
|
|
173
|
+
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}`;
|
|
174
|
+
}
|
|
175
|
+
/** #1823: the time left on this node's open polls, now. */
|
|
176
|
+
function pollLeft(node) {
|
|
177
|
+
for (const el of node.querySelectorAll(".poll-left[data-deadline]")) {
|
|
178
|
+
const v = R().pollRemaining(Number(el.dataset.deadline) - (opts.serverNow ? opts.serverNow() : Date.now()));
|
|
179
|
+
if (el.textContent !== v) el.textContent = v;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// #1823: while an open poll is on screen, its time left is rewritten every second.
|
|
183
|
+
let pollTick = null;
|
|
184
|
+
function armPolls() {
|
|
185
|
+
const open = nodes.size > 0 && !!list.querySelector(".poll-left[data-deadline]");
|
|
186
|
+
if (open && !pollTick) pollTick = setInterval(() => pollLeft(list), 1000);
|
|
187
|
+
else if (!open && pollTick) { clearInterval(pollTick); pollTick = null; }
|
|
110
188
|
}
|
|
189
|
+
|
|
111
190
|
function msgNode(html, x, stoppedKeys) {
|
|
112
191
|
const tpl = list.ownerDocument.createElement("template");
|
|
113
192
|
tpl.innerHTML = html;
|
|
114
193
|
const node = tpl.content.firstElementChild;
|
|
115
194
|
decorateCode(node);
|
|
116
195
|
timeLabel(node);
|
|
196
|
+
pollLeft(node);
|
|
117
197
|
if (x) decorateHtmlCards(node, x, stoppedKeys || []);
|
|
118
198
|
if (x) decorateTextCards(node, x);
|
|
119
199
|
return node;
|
|
@@ -124,7 +204,7 @@ export function createThread(list, scroller, opts) {
|
|
|
124
204
|
const doc = list.ownerDocument;
|
|
125
205
|
for (const pre of root.querySelectorAll(".md pre")) {
|
|
126
206
|
const box = doc.createElement("div"); box.className = "codeblock";
|
|
127
|
-
const head = doc.createElement("div"); head.className = "cb-head";
|
|
207
|
+
const head = doc.createElement("div"); head.className = "cb-head"; head.setAttribute("data-trust", "thread-control");
|
|
128
208
|
const lang = doc.createElement("span"); lang.className = "cb-lang"; lang.textContent = pre.dataset.lang || "text";
|
|
129
209
|
const wrap = doc.createElement("button"); wrap.type = "button"; wrap.className = "chip-btn"; wrap.dataset.act = "toggleWrap"; wrap.textContent = tr("chat.wrap");
|
|
130
210
|
const copy = doc.createElement("button"); copy.type = "button"; copy.className = "chip-btn"; copy.dataset.act = "copyCode"; copy.textContent = tr("chat.copy");
|
|
@@ -133,7 +213,7 @@ export function createThread(list, scroller, opts) {
|
|
|
133
213
|
const lines = R().lineCount(pre.textContent);
|
|
134
214
|
if (lines > R().CODE_FOLD_LINES) {
|
|
135
215
|
box.classList.add("folded");
|
|
136
|
-
const more = doc.createElement("button"); more.type = "button"; more.className = "cb-more"; more.dataset.act = "toggleFold";
|
|
216
|
+
const more = doc.createElement("button"); more.type = "button"; more.className = "cb-more"; more.setAttribute("data-trust", "thread-control"); more.dataset.act = "toggleFold";
|
|
137
217
|
more.dataset.lines = String(lines); more.textContent = trf("chat.showAll", lines);
|
|
138
218
|
box.append(more);
|
|
139
219
|
}
|
|
@@ -151,7 +231,7 @@ export function createThread(list, scroller, opts) {
|
|
|
151
231
|
if (!fence) continue;
|
|
152
232
|
ph.textContent = "";
|
|
153
233
|
if (ph.classList.contains("truncated") || !fence.terminated) {
|
|
154
|
-
const note = doc.createElement("div"); note.className = "pv-note"; note.textContent = tr("chat.pvTruncated"); ph.append(note);
|
|
234
|
+
const note = doc.createElement("div"); note.className = "pv-note"; note.setAttribute("data-trust", "thread-control"); note.textContent = tr("chat.pvTruncated"); ph.append(note);
|
|
155
235
|
continue;
|
|
156
236
|
}
|
|
157
237
|
buildCard(ph, x, `${msgKey(x)}:f${n}`, { n, code: fence.code }, stoppedKeys);
|
|
@@ -181,12 +261,12 @@ export function createThread(list, scroller, opts) {
|
|
|
181
261
|
const doc = list.ownerDocument;
|
|
182
262
|
const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
|
|
183
263
|
ph.textContent = "";
|
|
184
|
-
const head = el("div", "tc-head");
|
|
264
|
+
const head = el("div", "tc-head"); head.setAttribute("data-trust", "thread-control");
|
|
185
265
|
const label = el("span", "tc-label", att.name);
|
|
186
266
|
const show = el("button", "btn btn-sm tc-show", tr("chat.tcShow")); show.type = "button";
|
|
187
267
|
const dl = el("a", "btn btn-sm tc-dl", tr("chat.tcDownload")); dl.href = `/ui/file/${att.id}`; dl.setAttribute("download", att.name);
|
|
188
268
|
head.append(label, show, dl);
|
|
189
|
-
const note = el("div", "pv-note");
|
|
269
|
+
const note = el("div", "pv-note"); note.setAttribute("data-trust", "thread-control");
|
|
190
270
|
const body = el("div", "tc-body"); body.hidden = true;
|
|
191
271
|
ph.append(head, note, body);
|
|
192
272
|
let text = null, reading = false, all = false;
|
|
@@ -199,7 +279,8 @@ export function createThread(list, scroller, opts) {
|
|
|
199
279
|
// Formatting is built only within TEXT_PREVIEW_NODES: the markup is measured as a string first, never as DOM.
|
|
200
280
|
const plain = () => {
|
|
201
281
|
const pre = el("pre", "tc-pre"), code = el("code", null, src);
|
|
202
|
-
|
|
282
|
+
const note = el("div", "pv-note", tr("chat.tcPlain")); note.setAttribute("data-trust", "thread-control");
|
|
283
|
+
pre.append(code); body.append(note, pre);
|
|
203
284
|
};
|
|
204
285
|
if (type === "md") {
|
|
205
286
|
const html = R().renderMarkdown(src);
|
|
@@ -219,7 +300,7 @@ export function createThread(list, scroller, opts) {
|
|
|
219
300
|
}
|
|
220
301
|
}
|
|
221
302
|
if (cut) {
|
|
222
|
-
const more = el("button", "btn btn-sm tc-more", trf("chat.tcShowAll", lines.length)); more.type = "button";
|
|
303
|
+
const more = el("button", "btn btn-sm tc-more", trf("chat.tcShowAll", lines.length)); more.setAttribute("data-trust", "thread-control"); more.type = "button";
|
|
223
304
|
more.onclick = () => { all = true; draw(); };
|
|
224
305
|
body.append(more);
|
|
225
306
|
}
|
|
@@ -280,7 +361,7 @@ export function createThread(list, scroller, opts) {
|
|
|
280
361
|
const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
|
|
281
362
|
const att = source.att || null;
|
|
282
363
|
let code = att ? null : source.code;
|
|
283
|
-
const head = el("div", "pv-head");
|
|
364
|
+
const head = el("div", "pv-head"); head.setAttribute("data-trust", "thread-control");
|
|
284
365
|
const label = el("span", "pv-label", att ? `${tr("chat.pvHtml")} · ${att.name}` : tr("chat.pvHtml"));
|
|
285
366
|
const run = el("button", "btn btn-sm pv-run", tr("chat.pvPreview")); run.type = "button";
|
|
286
367
|
const stopB = el("button", "btn btn-sm pv-stop", tr("chat.pvStop")); stopB.type = "button"; stopB.hidden = true;
|
|
@@ -290,15 +371,15 @@ export function createThread(list, scroller, opts) {
|
|
|
290
371
|
const panel = opts.panel || null;
|
|
291
372
|
const toPanel = panel ? el("button", "btn btn-sm btn-ghost pv-open", tr("chat.pvOpenPanel")) : null;
|
|
292
373
|
if (toPanel) toPanel.type = "button";
|
|
293
|
-
const menu = el("details", "pv-menu");
|
|
374
|
+
const menu = el("details", "pv-menu"); menu.setAttribute("data-trust", "preview-menu");
|
|
294
375
|
const sum = el("summary", null, "⋯"); sum.title = tr("chat.pvMenu"); sum.setAttribute("aria-label", tr("chat.pvMenu"));
|
|
295
376
|
const optB = el("button", "chip-btn pv-opt"); optB.type = "button";
|
|
296
377
|
const neverB = el("button", "chip-btn pv-never"); neverB.type = "button";
|
|
297
378
|
menu.append(sum, optB, neverB);
|
|
298
379
|
head.append(label, run, stopB, ...(toPanel ? [toPanel] : []), ...(dl ? [dl] : []), menu);
|
|
299
|
-
const note = el("div", "pv-note");
|
|
380
|
+
const note = el("div", "pv-note"); note.setAttribute("data-trust", "thread-control");
|
|
300
381
|
// #1568: the banner, with its "I understand" (hidden until the next sign-in; the frame's border stays).
|
|
301
|
-
const banner = el("div", "pv-banner"); banner.hidden = true;
|
|
382
|
+
const banner = el("div", "pv-banner"); banner.setAttribute("data-trust", "thread-control"); banner.hidden = true;
|
|
302
383
|
const ack = el("button", "btn btn-sm btn-ghost pv-ack", P().bannerAckLabel()); ack.type = "button"; ack.title = P().bannerAckTitle();
|
|
303
384
|
banner.append(el("span", "pv-banner-text", P().banner()), ack);
|
|
304
385
|
const holder = el("div", "pv-holder");
|
|
@@ -388,44 +469,97 @@ export function createThread(list, scroller, opts) {
|
|
|
388
469
|
const ro = typeof ResizeObserver === "function" ? new ResizeObserver(resized) : null;
|
|
389
470
|
if (ro) { ro.observe(list); ro.observe(scroller); }
|
|
390
471
|
|
|
472
|
+
/** Where drawing starts in `msgs`: at the oldest drawn message, or (none yet) the newest THREAD_TAIL. */
|
|
473
|
+
function startIndex() {
|
|
474
|
+
const i = firstKey === null ? -1 : msgs.findIndex((x) => msgKey(x) === firstKey);
|
|
475
|
+
// Whatever is drawn stays drawn: the list may have reordered (merged across fleet restarts) or lost its oldest to
|
|
476
|
+
// the cap — so the window starts at the earlier of the oldest drawn key and the first message still drawn.
|
|
477
|
+
const still = nodes.size ? msgs.findIndex((x) => nodes.has(msgKey(x))) : -1;
|
|
478
|
+
const at = [i, still].filter((n) => n >= 0);
|
|
479
|
+
return at.length ? Math.min(...at) : Math.max(0, msgs.length - THREAD_TAIL);
|
|
480
|
+
}
|
|
481
|
+
/** Draw the next chunk of older messages, keeping the reader where they were; then the next, until all are drawn. */
|
|
482
|
+
function grow() {
|
|
483
|
+
growTimer = growFrame = null;
|
|
484
|
+
if (disposed) return; // the chat left: nothing more is drawn
|
|
485
|
+
const at = startIndex();
|
|
486
|
+
if (at <= 0) return;
|
|
487
|
+
firstKey = msgKey(msgs[Math.max(0, at - THREAD_CHUNK)]);
|
|
488
|
+
const fromBottom = scroller.scrollHeight - scroller.scrollTop;
|
|
489
|
+
draw({ older: true }); // drawn above the reader: never "new below" (render alone counts those)
|
|
490
|
+
if (pinned) toBottom();
|
|
491
|
+
else { scroller.scrollTop = scroller.scrollHeight - fromBottom; lastTop = scroller.scrollTop; }
|
|
492
|
+
scheduleGrow();
|
|
493
|
+
}
|
|
494
|
+
/** After the next frame has painted (the click's own frame stays light), draw a chunk more. */
|
|
495
|
+
function scheduleGrow() {
|
|
496
|
+
if (growTimer !== null || growFrame !== null || startIndex() <= 0) return;
|
|
497
|
+
const later = () => { growFrame = null; growTimer = setTimeout(grow, 0); };
|
|
498
|
+
if (typeof requestAnimationFrame === "function") growFrame = requestAnimationFrame(later); else later();
|
|
499
|
+
}
|
|
500
|
+
/** Everything drawn now (a remembered scroll position, a message to show that is not drawn yet). */
|
|
501
|
+
function growAll() {
|
|
502
|
+
if (startIndex() <= 0) return;
|
|
503
|
+
if (growFrame !== null && typeof cancelAnimationFrame === "function") cancelAnimationFrame(growFrame);
|
|
504
|
+
if (growTimer !== null) clearTimeout(growTimer);
|
|
505
|
+
growTimer = growFrame = null;
|
|
506
|
+
firstKey = msgs.length ? msgKey(msgs[0]) : null;
|
|
507
|
+
draw({ older: true });
|
|
508
|
+
}
|
|
509
|
+
|
|
391
510
|
/** Draw `next` (this instance's messages). `restore`: the scroll position to return to (null = the bottom). */
|
|
392
511
|
function render(next, renderOpts = {}) {
|
|
393
512
|
const stick = pinned || nearBottom();
|
|
394
513
|
msgs = next || [];
|
|
514
|
+
armTrials();
|
|
395
515
|
if (!msgs.length) {
|
|
396
516
|
for (const v of nodes.values()) stopIn(v.node);
|
|
397
517
|
nodes.clear();
|
|
518
|
+
firstKey = null;
|
|
398
519
|
armDay();
|
|
399
520
|
list.textContent = "";
|
|
400
521
|
opts.onEmpty(true);
|
|
401
522
|
return;
|
|
402
523
|
}
|
|
403
524
|
opts.onEmpty(false);
|
|
404
|
-
|
|
525
|
+
// A position to return to that is not the bottom needs the messages above it: all of them, at once (as before).
|
|
526
|
+
if ("restore" in renderOpts && renderOpts.restore != null) firstKey = msgKey(msgs[0]);
|
|
527
|
+
const added = draw({});
|
|
528
|
+
if (firstKey === null || startIndex() > 0) { firstKey = msgKey(msgs[startIndex()]); scheduleGrow(); }
|
|
529
|
+
if ("restore" in renderOpts) {
|
|
530
|
+
unseen = 0;
|
|
531
|
+
if (renderOpts.restore == null) toBottom();
|
|
532
|
+
else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); lastTop = scroller.scrollTop; }
|
|
533
|
+
} else if (stick) toBottom();
|
|
534
|
+
else unseen += added;
|
|
535
|
+
reportJump();
|
|
536
|
+
}
|
|
537
|
+
/** Put the messages from startIndex() on in the list (nodes reused when unchanged). Returns how many are new. */
|
|
538
|
+
function draw(drawOpts) {
|
|
539
|
+
const shown = msgs.slice(startIndex());
|
|
540
|
+
const keep = new Set(shown.map(msgKey));
|
|
405
541
|
for (const [k, v] of nodes) if (!keep.has(k)) { stopIn(v.node); v.node.remove(); nodes.delete(k); }
|
|
406
542
|
let prev = null, added = 0;
|
|
407
|
-
for (const x of
|
|
408
|
-
const key = msgKey(x)
|
|
543
|
+
for (const x of shown) {
|
|
544
|
+
const key = msgKey(x);
|
|
409
545
|
let have = nodes.get(key);
|
|
546
|
+
// Filling in older messages: a node drawn from this very message object is current — not drawn again.
|
|
547
|
+
if (drawOpts.older && have && have.x === x) { prev = have.node; continue; }
|
|
548
|
+
const html = msgHtml(x);
|
|
410
549
|
if (!have || have.html !== html) {
|
|
411
550
|
const stopped = have ? stopIn(have.node) : [];
|
|
412
551
|
const node = msgNode(html, x, stopped);
|
|
413
552
|
if (have) have.node.replaceWith(node); else added++;
|
|
414
|
-
have = { html, node }; nodes.set(key, have);
|
|
415
|
-
} else timeLabel(have.node);
|
|
553
|
+
have = { html, node, x, trialButton: node.querySelector(".effect-trial-offer > button.effect-trial-start") }; nodes.set(key, have);
|
|
554
|
+
} else { have.x = x; timeLabel(have.node); }
|
|
416
555
|
const want = prev ? prev.nextSibling : list.firstChild;
|
|
417
556
|
// A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
|
|
418
557
|
if (have.node !== want) { if (have.node.parentNode === list) stopIn(have.node); list.insertBefore(have.node, want); }
|
|
419
558
|
prev = have.node;
|
|
420
559
|
}
|
|
421
560
|
armDay();
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (renderOpts.restore == null) toBottom();
|
|
425
|
-
else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); lastTop = scroller.scrollTop; }
|
|
426
|
-
} else if (stick) toBottom();
|
|
427
|
-
else unseen += added;
|
|
428
|
-
reportJump();
|
|
561
|
+
armPolls();
|
|
562
|
+
return added;
|
|
429
563
|
}
|
|
430
564
|
|
|
431
565
|
function onScroll() {
|
|
@@ -440,6 +574,7 @@ export function createThread(list, scroller, opts) {
|
|
|
440
574
|
function jumpLatest() { toBottom(); unseen = 0; reportJump(); }
|
|
441
575
|
/** Show one message (the panel's "from …" link, #1481): scrolled into view and briefly marked. */
|
|
442
576
|
function reveal(key) {
|
|
577
|
+
if (!nodes.has(key) && msgs.some((x) => msgKey(x) === key)) growAll(); // an older message not drawn yet
|
|
443
578
|
const have = nodes.get(key); if (!have) return false;
|
|
444
579
|
if (typeof have.node.scrollIntoView === "function") have.node.scrollIntoView({ block: "center" });
|
|
445
580
|
have.node.classList.add("flash");
|
|
@@ -489,15 +624,24 @@ export function createThread(list, scroller, opts) {
|
|
|
489
624
|
else if (act === "replyButton") {
|
|
490
625
|
const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
|
|
491
626
|
if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
|
|
492
|
-
}
|
|
627
|
+
} else if (act === "effectTrial") {
|
|
628
|
+
// Only the current host-produced keyed card and a real local click can request admission. No token is read from DOM.
|
|
629
|
+
const key = el.dataset.arg, row = msgs.find(x => msgKey(x) === key), owner = nodes.get(key), node = owner?.node, offer = effectTrialMessage(row);
|
|
630
|
+
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"
|
|
631
|
+
|| !offer || trialNow() >= offer.expiresAt || opts.canTryEffect?.(row) !== true) return;
|
|
632
|
+
opts.startEffectTrial?.(row);
|
|
633
|
+
} else if (act === "showButtons") { const x = msgs.find(y => y.buttons && y.buttons.id === el.dataset.arg); if (x) reveal(msgKey(x)); }
|
|
493
634
|
}
|
|
494
635
|
|
|
495
636
|
/** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
|
|
496
637
|
function dispose() {
|
|
638
|
+
disposed = true;
|
|
639
|
+
if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
|
|
497
640
|
if (ro) ro.disconnect();
|
|
498
641
|
if (P()) P().stopAll("leave");
|
|
499
642
|
nodes.clear();
|
|
500
643
|
armDay();
|
|
644
|
+
armPolls();
|
|
501
645
|
}
|
|
502
646
|
|
|
503
647
|
return { render, onScroll, onClick, jumpLatest, reveal, refreshCards, dispose, nodes, get unseen() { return unseen; } };
|
package/dist/ui/chat-tour.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// #1366: the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// #1366: the chat tour, in the app shell (#1408). Since #1854 (rank 6) it never starts by itself: a first visit opens
|
|
2
|
+
// on a quiet page, and Settings → This device → Chat tour starts it on an agent's chat. It walks only the steps whose
|
|
3
|
+
// target is on the page. Got it, Skip or Esc ends it and records it (agend_tour_done, kept as before: no new key). A
|
|
4
|
+
// card that outlines one thing at a time: no overlay, nothing blocked, and no style attribute — the outline is a class
|
|
5
|
+
// (#1300).
|
|
5
6
|
//
|
|
6
7
|
// Esc order: the phone drawer covers the card, so it closes first (the shell); then the tour; only then may Esc reach
|
|
7
8
|
// the chat and stop a reply. The Esc that closed the tour is spent: its key repeats (a held key) must not go on to stop
|
|
@@ -24,6 +25,8 @@ const STEPS = [
|
|
|
24
25
|
: document.querySelector(".badge-await") || byId("instanceList")) },
|
|
25
26
|
];
|
|
26
27
|
let at = -1, spot = null, returnTo = null, escHeld = false, installed = false;
|
|
28
|
+
/** The steps of the running tour: those whose target was on the page when it started (#1854: target-aware). */
|
|
29
|
+
let steps = STEPS;
|
|
27
30
|
|
|
28
31
|
export function tourDone() { try { return localStorage.getItem(TOUR_KEY) === "1"; } catch { return true; } }
|
|
29
32
|
export function tourStep() { return at; }
|
|
@@ -32,13 +35,15 @@ export function startTour() {
|
|
|
32
35
|
// Replayed from the drawer: the drawer closes first (focus goes back to ☰), so the card is seen.
|
|
33
36
|
closeDrawer();
|
|
34
37
|
if (at < 0) returnTo = document.activeElement;
|
|
38
|
+
steps = STEPS.filter((s) => s.spot());
|
|
39
|
+
if (!steps.length) return;
|
|
35
40
|
show(0);
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
/** The current step's outline follows a redraw (a new badge, Send swapped for Stop reply) — same card, same focus. */
|
|
39
44
|
export function refreshTourSpot() {
|
|
40
45
|
if (at < 0) return;
|
|
41
|
-
const next =
|
|
46
|
+
const next = steps[at].spot() || null;
|
|
42
47
|
if (next === spot && (!next || next.classList.contains("tour-spot"))) return;
|
|
43
48
|
if (spot) spot.classList.remove("tour-spot");
|
|
44
49
|
spot = next;
|
|
@@ -46,32 +51,32 @@ export function refreshTourSpot() {
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
function show(i) {
|
|
49
|
-
if (i < 0 || i >=
|
|
54
|
+
if (i < 0 || i >= steps.length) return;
|
|
50
55
|
at = i;
|
|
51
56
|
if (spot) spot.classList.remove("tour-spot");
|
|
52
|
-
spot =
|
|
57
|
+
spot = steps[i].spot() || null;
|
|
53
58
|
if (spot) spot.classList.add("tour-spot");
|
|
54
59
|
let card = byId("tour");
|
|
55
60
|
if (!card) {
|
|
56
|
-
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");
|
|
57
62
|
card.setAttribute("role", "dialog"); card.setAttribute("aria-labelledby", "tourTitle");
|
|
58
63
|
card.addEventListener("click", onCardClick);
|
|
59
64
|
document.body.appendChild(card);
|
|
60
65
|
}
|
|
61
66
|
card.textContent = "";
|
|
62
67
|
const title = document.createElement("h2"); title.id = "tourTitle"; title.textContent = t("chat.tourTitle");
|
|
63
|
-
const text = document.createElement("p"); text.className = "tour-text"; text.setAttribute("aria-live", "polite"); text.textContent = t(
|
|
68
|
+
const text = document.createElement("p"); text.className = "tour-text"; text.setAttribute("aria-live", "polite"); text.textContent = t(steps[i].text);
|
|
64
69
|
const foot = document.createElement("div"); foot.className = "tour-foot";
|
|
65
|
-
const count = document.createElement("span"); count.className = "tour-count"; count.textContent = t("chat.tourStep", i + 1,
|
|
70
|
+
const count = document.createElement("span"); count.className = "tour-count"; count.textContent = t("chat.tourStep", i + 1, steps.length);
|
|
66
71
|
const button = (label, act, arg, cls) => {
|
|
67
72
|
const b = document.createElement("button"); b.type = "button"; b.className = `btn btn-sm ${cls}`; b.dataset.tour = act;
|
|
68
73
|
if (arg != null) b.dataset.arg = String(arg);
|
|
69
74
|
b.textContent = t(label); return b;
|
|
70
75
|
};
|
|
71
76
|
foot.append(count);
|
|
72
|
-
if (i <
|
|
77
|
+
if (i < steps.length - 1) foot.append(button("chat.tourSkip", "end", null, "btn-ghost"));
|
|
73
78
|
if (i > 0) foot.append(button("chat.tourBack", "move", -1, "btn-ghost"));
|
|
74
|
-
const next = i <
|
|
79
|
+
const next = i < steps.length - 1 ? button("chat.tourNext", "move", 1, "btn-primary") : button("chat.tourDone", "end", null, "btn-primary");
|
|
75
80
|
foot.append(next);
|
|
76
81
|
card.append(title, text, foot);
|
|
77
82
|
next.focus();
|
|
@@ -105,12 +110,10 @@ export function onTourKey(e) {
|
|
|
105
110
|
if (at >= 0) { e.preventDefault(); e.stopPropagation(); endTour(); escHeld = true; }
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
/** Once per page: the key handler
|
|
113
|
+
/** Once per page: the key handler and the language follow-up. Nothing is shown: the tour is started by a person (#1854). */
|
|
109
114
|
export function installTour() {
|
|
110
115
|
if (installed) return;
|
|
111
116
|
installed = true;
|
|
112
117
|
document.addEventListener("keydown", onTourKey, true);
|
|
113
118
|
onLang(() => { if (at >= 0) show(at); });
|
|
114
|
-
// Shown first on the chat (it points at the chat's own controls), never over View or another panel.
|
|
115
|
-
if (!tourDone() && /^\/ui(\/chat\/|$)/.test(location.pathname)) setTimeout(startTour, 0);
|
|
116
119
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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;
|
|
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; safe(() => op.resolve?.(false));
|
|
30
|
+
safe(() => host.clearTimeout(op.timer)); safe(() => op.controller.abort());
|
|
31
|
+
if (op.reply && !op.stopQueued) { op.stopQueued = true; cleanup.push(op); }
|
|
32
|
+
// Physical paint is removed before callbacks, cleanup I/O or another explicit click.
|
|
33
|
+
safe(() => op.dispose?.()); op.dispose = null; tell(); drain(); return true;
|
|
34
|
+
}
|
|
35
|
+
function arm(op) {
|
|
36
|
+
safe(() => host.clearTimeout(op.timer));
|
|
37
|
+
if (!eligible(op)) { end(op, "trial_ended"); return; }
|
|
38
|
+
const t = clock(); if (t === null || t >= op.until) { end(op, "trial_expired"); return; }
|
|
39
|
+
op.timer = host.setTimeout(() => { if (!op.dead) arm(op); }, Math.max(1, Math.ceil(op.until - t)));
|
|
40
|
+
}
|
|
41
|
+
async function read(response) {
|
|
42
|
+
let reader = null;
|
|
43
|
+
try {
|
|
44
|
+
if (!response || response.redirected || response.status !== 200 || !/^application\/json(?:\s*;|$)/i.test(response.headers.get("content-type") || "")) return null;
|
|
45
|
+
const length = response.headers.get("content-length"); if (length !== null && (!/^\d+$/.test(length) || Number(length) > LIMIT)) return null;
|
|
46
|
+
reader = response.body?.getReader(); if (!reader) return null;
|
|
47
|
+
const chunks = []; let size = 0;
|
|
48
|
+
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); }
|
|
49
|
+
const bytes = new Uint8Array(size); let offset = 0; for (const c of chunks) { bytes.set(c, offset); offset += c.byteLength; }
|
|
50
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
51
|
+
} catch { return null; }
|
|
52
|
+
finally { try { if (reader) await reader.cancel(); else await response?.body?.cancel(); } catch { /* retained until cancellation settles */ } }
|
|
53
|
+
}
|
|
54
|
+
function drain() {
|
|
55
|
+
if (physical || !cleanup.length) return;
|
|
56
|
+
const op = cleanup.shift(), token = {}; physical = token; const controller = new host.AbortController();
|
|
57
|
+
const timer = host.setTimeout(() => controller.abort(), 10000);
|
|
58
|
+
// Stop remains permitted after CSS/gate/session changes. It cannot mount or mutate appearance.
|
|
59
|
+
Promise.resolve().then(async () => {
|
|
60
|
+
const response = await host.fetch(`/ui/effect-trials/${op.reply.trial}/stop?ownerEpoch=${identity.epoch}`, {
|
|
61
|
+
method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" }, body: "{}", signal: controller.signal,
|
|
62
|
+
});
|
|
63
|
+
try { await response.body?.cancel(); } catch { /* no response fields are consumed */ }
|
|
64
|
+
}).catch(() => {}).finally(() => { safe(() => host.clearTimeout(timer)); if (physical === token) physical = null; tell(); drain(); });
|
|
65
|
+
}
|
|
66
|
+
function dispose() {
|
|
67
|
+
if (dead) return; dead = true; end(active); for (const off of releases.splice(0)) safe(off); tell();
|
|
68
|
+
}
|
|
69
|
+
const api = { ok: false, start: async () => false, stop: () => end(active), sync() { const op = active; if (op && !eligible(op)) end(op, "trial_ended"); },
|
|
70
|
+
snapshot: () => Object.freeze({ busy: !!physical || cleanup.length > 0, phase: active ? active.reply ? "running" : "starting" : "idle" }), dispose };
|
|
71
|
+
if (!identity || owners.has(host) || typeof current !== "function" || typeof allowed !== "function" || typeof onStart !== "function"
|
|
72
|
+
|| typeof host.AgendAuth?.trialSession !== "function" || typeof host.AgendAuth?.onSessionEnd !== "function") return Object.freeze(api);
|
|
73
|
+
owners.add(host);
|
|
74
|
+
try {
|
|
75
|
+
const off = host.AgendAuth.onSessionEnd(dispose);
|
|
76
|
+
if (dead) { safe(off); return Object.freeze(api); }
|
|
77
|
+
releases.push(off);
|
|
78
|
+
for (const [target, type, fn] of [[host, "blur", () => end(active)], [host, "pagehide", dispose], [host.document, "visibilitychange", () => { if (host.document.visibilityState !== "visible") end(active); }]]) {
|
|
79
|
+
target.addEventListener(type, fn); releases.push(() => target.removeEventListener(type, fn));
|
|
80
|
+
}
|
|
81
|
+
} catch { dispose(); return Object.freeze(api); }
|
|
82
|
+
if (dead) return Object.freeze(api);
|
|
83
|
+
api.ok = true;
|
|
84
|
+
api.start = async message => {
|
|
85
|
+
if (dead || active || physical || cleanup.length) return false;
|
|
86
|
+
const offer = safe(() => effectTrialMessage(message)), instance = safe(() => message.instance), began = clock();
|
|
87
|
+
if (!offer || began === null || began + 10000 > Number.MAX_SAFE_INTEGER || safe(() => host.Date.now() < offer.expiresAt) !== true) return false;
|
|
88
|
+
const op = { instance, offer, began, until: began + 10000, controller: new host.AbortController(), timer: null, reply: null, dead: false, stopQueued: false, dispose: null };
|
|
89
|
+
active = op; const token = {}; physical = token;
|
|
90
|
+
const cancelled = new Promise(resolve => { op.resolve = resolve; });
|
|
91
|
+
const work = Promise.resolve().then(async () => {
|
|
92
|
+
if (!eligible(op)) return false;
|
|
93
|
+
arm(op); const session = await host.AgendAuth.trialSession();
|
|
94
|
+
if (!eligible(op) || !session || session.localAdmin !== true || session.epoch !== identity.epoch) return false;
|
|
95
|
+
const response = await host.fetch(`/ui/effect-trials/start?ownerEpoch=${identity.epoch}`, {
|
|
96
|
+
method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" },
|
|
97
|
+
body: JSON.stringify({ offer: offer.offer, instance }), signal: op.controller.signal,
|
|
98
|
+
});
|
|
99
|
+
const reply = checkEffectTrialStart(await read(response), offer.offer, offer.id);
|
|
100
|
+
if (!reply) return false;
|
|
101
|
+
op.reply = reply;
|
|
102
|
+
if (op.dead && !op.stopQueued) { op.stopQueued = true; cleanup.push(op); }
|
|
103
|
+
if (reply.offer.title !== offer.title || reply.offer.expiresAt !== offer.expiresAt) return false;
|
|
104
|
+
if (!eligible(op)) { if (!op.stopQueued) { op.stopQueued = true; cleanup.push(op); } return false; }
|
|
105
|
+
op.until = began + reply.remainingMs;
|
|
106
|
+
if (op.until > Number.MAX_SAFE_INTEGER || !eligible(op)) return false;
|
|
107
|
+
arm(op);
|
|
108
|
+
const handle = Object.freeze({ reply, startedAt: began, instance, current: () => eligible(op), stop: () => end(op), });
|
|
109
|
+
const disposer = onStart(handle);
|
|
110
|
+
if (typeof disposer !== "function") return false;
|
|
111
|
+
if (!eligible(op)) { safe(disposer); return false; }
|
|
112
|
+
op.dispose = disposer; tell(); return eligible(op);
|
|
113
|
+
}).catch(() => false).then(ok => { if (!ok) end(op); return ok; }).finally(() => { if (physical === token) physical = null; tell(); drain(); });
|
|
114
|
+
// The logical caller may stop immediately, while the physical request/cancel still owns the lane.
|
|
115
|
+
tell();
|
|
116
|
+
return Promise.race([work, cancelled]);
|
|
117
|
+
};
|
|
118
|
+
return Object.freeze(api);
|
|
119
|
+
}
|