@songsid/agend 2.1.12 → 2.2.0-alpha.2
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 +4 -2
- package/README.zh-TW.md +4 -2
- package/dist/agent-cli.js +6 -1
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +68 -12
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-token-header.d.ts +13 -0
- package/dist/agent-token-header.js +16 -0
- package/dist/agent-token-header.js.map +1 -0
- package/dist/auth/one-time-code.d.ts +19 -0
- package/dist/auth/one-time-code.js +49 -0
- package/dist/auth/one-time-code.js.map +1 -0
- package/dist/auth-api.d.ts +52 -0
- package/dist/auth-api.js +346 -0
- package/dist/auth-api.js.map +1 -0
- package/dist/backend/antigravity.js +4 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +4 -0
- package/dist/backend/claude-code.js +97 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +59 -30
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +1 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro-agent.d.ts +66 -0
- package/dist/backend/kiro-agent.js +302 -0
- package/dist/backend/kiro-agent.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +80 -0
- package/dist/backend/kiro-identity.js +537 -0
- package/dist/backend/kiro-identity.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
- package/dist/backend/kiro-v2-store-worker.js +87 -0
- package/dist/backend/kiro-v2-store-worker.js.map +1 -0
- package/dist/backend/kiro-v2-store.d.ts +54 -0
- package/dist/backend/kiro-v2-store.js +131 -0
- package/dist/backend/kiro-v2-store.js.map +1 -0
- package/dist/backend/kiro-v3-identity.d.ts +5 -0
- package/dist/backend/kiro-v3-identity.js +22 -0
- package/dist/backend/kiro-v3-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +90 -7
- package/dist/backend/kiro.js +500 -124
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +1 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +51 -3
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-analysis.d.ts +92 -0
- package/dist/cache-analysis.js +147 -0
- package/dist/cache-analysis.js.map +1 -0
- package/dist/cache-ledger.d.ts +108 -0
- package/dist/cache-ledger.js +230 -0
- package/dist/cache-ledger.js.map +1 -0
- package/dist/cache-prices.d.ts +32 -0
- package/dist/cache-prices.js +73 -0
- package/dist/cache-prices.js.map +1 -0
- package/dist/cache-scan.d.ts +39 -0
- package/dist/cache-scan.js +115 -0
- package/dist/cache-scan.js.map +1 -0
- package/dist/cache-service.d.ts +115 -0
- package/dist/cache-service.js +417 -0
- package/dist/cache-service.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +1 -0
- package/dist/channel/adapters/discord.js +29 -10
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -0
- package/dist/channel/adapters/telegram.js +13 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +9 -1
- package/dist/classic-channel-manager.d.ts +18 -10
- package/dist/classic-channel-manager.js +45 -18
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli-entry.d.ts +19 -0
- package/dist/cli-entry.js +36 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli.js +471 -146
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +5 -3
- package/dist/command-table.js +29 -27
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +46 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/connection-secrets.d.ts +6 -0
- package/dist/connection-secrets.js +4 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/daemon.d.ts +95 -4
- package/dist/daemon.js +748 -111
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.d.ts +6 -1
- package/dist/darwin-memory.js +17 -6
- package/dist/darwin-memory.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +85 -0
- package/dist/delivery-consumed-watch.js +244 -0
- package/dist/delivery-consumed-watch.js.map +1 -0
- package/dist/delivery-outbox.d.ts +61 -0
- package/dist/delivery-outbox.js +120 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +14 -5
- package/dist/delivery-reconciliation.js +27 -4
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/delivery-reconciliation.test.js +35 -1
- package/dist/delivery-reconciliation.test.js.map +1 -1
- package/dist/event-log.d.ts +4 -1
- package/dist/event-log.js +6 -3
- package/dist/event-log.js.map +1 -1
- package/dist/event-loop-watch.d.ts +28 -0
- package/dist/event-loop-watch.js +75 -3
- package/dist/event-loop-watch.js.map +1 -1
- package/dist/fleet-context.d.ts +22 -2
- package/dist/fleet-control-audit.d.ts +5 -0
- package/dist/fleet-control-audit.js +7 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-level-config.d.ts +2 -2
- package/dist/fleet-level-config.js +28 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +339 -12
- package/dist/fleet-manager.js +2623 -461
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.js +2 -3
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
- package/dist/host-memory.d.ts +4 -0
- package/dist/host-memory.js.map +1 -1
- package/dist/install-lock.d.ts +49 -0
- package/dist/install-lock.js +200 -0
- package/dist/install-lock.js.map +1 -0
- package/dist/instance-config-impact.js +8 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -5
- package/dist/instance-lifecycle.js +162 -53
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +164 -16
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +2 -0
- package/dist/login-controller.js +17 -5
- package/dist/login-controller.js.map +1 -1
- package/dist/memory-pressure.d.ts +7 -8
- package/dist/memory-pressure.js +76 -15
- package/dist/memory-pressure.js.map +1 -1
- package/dist/monotonic-budget.d.ts +2 -0
- package/dist/monotonic-budget.js +28 -0
- package/dist/monotonic-budget.js.map +1 -0
- package/dist/needs-you-hub.d.ts +114 -0
- package/dist/needs-you-hub.js +370 -0
- package/dist/needs-you-hub.js.map +1 -0
- package/dist/needs-you-live.d.ts +116 -0
- package/dist/needs-you-live.js +228 -0
- package/dist/needs-you-live.js.map +1 -0
- package/dist/needs-you.d.ts +132 -0
- package/dist/needs-you.js +164 -0
- package/dist/needs-you.js.map +1 -0
- package/dist/node-version-guard.d.ts +30 -0
- package/dist/node-version-guard.js +50 -0
- package/dist/node-version-guard.js.map +1 -0
- package/dist/operator-control-paths.d.ts +4 -0
- package/dist/operator-control-paths.js +24 -0
- package/dist/operator-control-paths.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +9 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +5 -5
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.d.ts +26 -0
- package/dist/package-preimage.js +96 -0
- package/dist/package-preimage.js.map +1 -0
- package/dist/profile-control.js +4 -20
- package/dist/profile-control.js.map +1 -1
- package/dist/public-web-gateway.d.ts +18 -0
- package/dist/public-web-gateway.js +141 -0
- package/dist/public-web-gateway.js.map +1 -0
- package/dist/public-web-link.d.ts +53 -0
- package/dist/public-web-link.js +175 -0
- package/dist/public-web-link.js.map +1 -0
- package/dist/quickstart-api.d.ts +8 -1
- package/dist/quickstart-api.js +86 -48
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +16 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/restart-guard.d.ts +52 -0
- package/dist/restart-guard.js +109 -0
- package/dist/restart-guard.js.map +1 -0
- package/dist/scheduler/db.d.ts +7 -1
- package/dist/scheduler/db.js +44 -2
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/retry-label.d.ts +11 -0
- package/dist/scheduler/retry-label.js +24 -0
- package/dist/scheduler/retry-label.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -2
- package/dist/scheduler/scheduler.js +220 -8
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +22 -0
- package/dist/scheduler/types.js.map +1 -1
- package/dist/secret-store.d.ts +5 -0
- package/dist/secret-store.js +20 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/service-activation.d.ts +152 -0
- package/dist/service-activation.js +407 -0
- package/dist/service-activation.js.map +1 -0
- package/dist/service-installer.d.ts +63 -1
- package/dist/service-installer.js +141 -16
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +65 -0
- package/dist/service-plan.js +108 -0
- package/dist/service-plan.js.map +1 -0
- package/dist/service-restart-selection.d.ts +6 -5
- package/dist/service-restart-selection.js +6 -5
- package/dist/service-restart-selection.js.map +1 -1
- package/dist/settings-api.d.ts +10 -2
- package/dist/settings-api.js +216 -72
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +20 -0
- package/dist/settings-baseline.js +104 -0
- package/dist/settings-baseline.js.map +1 -0
- package/dist/settings-change.d.ts +21 -0
- package/dist/settings-change.js +161 -0
- package/dist/settings-change.js.map +1 -0
- package/dist/settings-confirmation.d.ts +105 -0
- package/dist/settings-confirmation.js +232 -0
- package/dist/settings-confirmation.js.map +1 -0
- package/dist/settings-control.d.ts +41 -0
- package/dist/settings-control.js +294 -0
- package/dist/settings-control.js.map +1 -0
- package/dist/settings-effect.d.ts +24 -0
- package/dist/settings-effect.js +169 -0
- package/dist/settings-effect.js.map +1 -0
- package/dist/settings-http-confirmation.d.ts +29 -0
- package/dist/settings-http-confirmation.js +123 -0
- package/dist/settings-http-confirmation.js.map +1 -0
- package/dist/settings-instance-patch.d.ts +4 -0
- package/dist/settings-instance-patch.js +26 -0
- package/dist/settings-instance-patch.js.map +1 -0
- package/dist/settings-request-capability.d.ts +16 -0
- package/dist/settings-request-capability.js +32 -0
- package/dist/settings-request-capability.js.map +1 -0
- package/dist/settings-transaction.d.ts +65 -0
- package/dist/settings-transaction.js +223 -0
- package/dist/settings-transaction.js.map +1 -0
- package/dist/setup-auth.d.ts +10 -18
- package/dist/setup-auth.js +14 -38
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +75 -18
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-guide.d.ts +13 -0
- package/dist/setup-guide.js +25 -0
- package/dist/setup-guide.js.map +1 -0
- package/dist/setup-host.d.ts +16 -0
- package/dist/setup-host.js +199 -35
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +5 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/slash-authz.d.ts +17 -3
- package/dist/slash-authz.js +5 -0
- package/dist/slash-authz.js.map +1 -1
- package/dist/spawn-gate.js +3 -3
- package/dist/spawn-gate.js.map +1 -1
- package/dist/sqlite-open-errors.d.ts +12 -0
- package/dist/sqlite-open-errors.js +13 -0
- package/dist/sqlite-open-errors.js.map +1 -0
- package/dist/statusline-watcher.d.ts +5 -0
- package/dist/statusline-watcher.js +2 -0
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/steer-capability.d.ts +6 -0
- package/dist/steer-capability.js +8 -0
- package/dist/steer-capability.js.map +1 -1
- package/dist/sync-work-attribution.d.ts +14 -0
- package/dist/sync-work-attribution.js +43 -8
- package/dist/sync-work-attribution.js.map +1 -1
- package/dist/tips.js +10 -5
- package/dist/tips.js.map +1 -1
- package/dist/tmux-control.d.ts +57 -5
- package/dist/tmux-control.js +379 -61
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +20 -6
- package/dist/tmux-manager.js +90 -47
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +59 -0
- package/dist/tmux-read.js +213 -0
- package/dist/tmux-read.js.map +1 -0
- package/dist/topic-commands.d.ts +21 -10
- package/dist/topic-commands.js +205 -63
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.js +2 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +2 -0
- package/dist/tunnel/cloudflared-install.js +1 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +14 -0
- package/dist/tunnel/purpose-lane.js +43 -0
- package/dist/tunnel/purpose-lane.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +2 -0
- package/dist/turn-reply-guard.js +10 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/ui/app.html +18 -0
- package/dist/ui/chat-commands.js +43 -0
- package/dist/ui/chat-render.js +478 -0
- package/dist/ui/chat-store.js +318 -0
- package/dist/ui/chat-strings.js +89 -0
- package/dist/ui/chat-thread.js +246 -0
- package/dist/ui/chat-tour.js +116 -0
- package/dist/ui/fleet-cache.js +168 -0
- package/dist/ui/fleet-org.js +142 -0
- package/dist/ui/panel-chat.js +504 -0
- package/dist/ui/panel-fleet.js +427 -0
- package/dist/ui/panel-needs.js +202 -0
- package/dist/ui/panel-settings.js +754 -0
- package/dist/ui/preview-panel.js +202 -0
- package/dist/ui/preview.js +270 -0
- package/dist/ui/settings-apply.js +239 -0
- package/dist/ui/settings-confirm.js +186 -0
- package/dist/ui/settings-dialogs.js +546 -0
- package/dist/ui/settings-model.js +277 -0
- package/dist/ui/settings-strings.js +654 -0
- package/dist/ui/settings-wizard.js +140 -0
- package/dist/ui/shared/agend-auth.js +93 -0
- package/dist/ui/shared/app-ctx.js +80 -0
- package/dist/ui/shared/app-html.js +9 -0
- package/dist/ui/shared/app-i18n.js +95 -0
- package/dist/ui/shared/app-nav.js +73 -0
- package/dist/ui/shared/app-needs.js +114 -0
- package/dist/ui/shared/app-route.js +73 -0
- package/dist/ui/shared/app-session.js +66 -0
- package/dist/ui/shared/app-shell.js +346 -0
- package/dist/ui/shared/app-store.js +51 -0
- package/dist/ui/shared/app-stream.js +201 -0
- package/dist/ui/shared/app.css +820 -0
- package/dist/ui/shared/app.js +88 -0
- package/dist/ui/shared/fonts/OFL.txt +92 -0
- package/dist/ui/shared/fonts/README.md +21 -0
- package/dist/ui/shared/fonts/inter.woff2 +0 -0
- package/dist/ui/shared/htm.module.js +1 -0
- package/dist/ui/shared/panel-view.js +655 -0
- package/dist/ui/shared/preact-hooks.module.js +2 -0
- package/dist/ui/shared/preact.module.js +2 -0
- package/dist/ui/shared/signin.js +142 -0
- package/dist/ui/shared/theme.js +29 -0
- package/dist/ui/shared/tokens.css +153 -0
- package/dist/ui/shared/ui-confirm.js +43 -0
- package/dist/ui/shared/ui-dialog.js +45 -0
- package/dist/ui/shared/ui-icons.js +63 -0
- package/dist/ui/shared/ui-menu.js +39 -0
- package/dist/ui/shared/ui-states.js +20 -0
- package/dist/ui/shared/ui-toast.js +18 -0
- package/dist/ui/shared/vendor/htm.LICENSE +202 -0
- package/dist/ui/shared/vendor/preact.LICENSE +21 -0
- package/dist/ui/shared/vendor/vendor.json +39 -0
- package/dist/ui/shared/view-strings.js +309 -0
- package/dist/ui/signin.html +52 -0
- package/dist/update-check.d.ts +6 -2
- package/dist/update-check.js +12 -3
- package/dist/update-check.js.map +1 -1
- package/dist/update-dispatch.d.ts +28 -0
- package/dist/update-dispatch.js +73 -0
- package/dist/update-dispatch.js.map +1 -0
- package/dist/update-install.d.ts +129 -0
- package/dist/update-install.js +262 -0
- package/dist/update-install.js.map +1 -0
- package/dist/upgrade-notices.d.ts +17 -0
- package/dist/upgrade-notices.js +93 -0
- package/dist/upgrade-notices.js.map +1 -0
- package/dist/usage/statusline-usage.d.ts +2 -0
- package/dist/usage/statusline-usage.js +14 -10
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +6 -1
- package/dist/view-api.js +62 -51
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +85 -14
- package/dist/web-api.js +557 -77
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +71 -27
- package/dist/web-auth.js +169 -69
- package/dist/web-auth.js.map +1 -1
- package/dist/web-body.d.ts +3 -0
- package/dist/web-body.js +55 -0
- package/dist/web-body.js.map +1 -0
- package/dist/web-channel-echo.d.ts +9 -0
- package/dist/web-channel-echo.js +55 -0
- package/dist/web-channel-echo.js.map +1 -0
- package/dist/web-chat-history.d.ts +125 -0
- package/dist/web-chat-history.js +184 -0
- package/dist/web-chat-history.js.map +1 -0
- package/dist/web-commands.d.ts +77 -0
- package/dist/web-commands.js +81 -0
- package/dist/web-commands.js.map +1 -0
- package/dist/web-continuation.d.ts +7 -0
- package/dist/web-continuation.js +22 -0
- package/dist/web-continuation.js.map +1 -0
- package/dist/web-host-guard.d.ts +34 -0
- package/dist/web-host-guard.js +57 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-login.d.ts +72 -0
- package/dist/web-login.js +132 -0
- package/dist/web-login.js.map +1 -0
- package/dist/web-org.d.ts +55 -0
- package/dist/web-org.js +67 -0
- package/dist/web-org.js.map +1 -0
- package/dist/web-preview.d.ts +77 -0
- package/dist/web-preview.js +240 -0
- package/dist/web-preview.js.map +1 -0
- package/dist/web-request-context.d.ts +10 -0
- package/dist/web-request-context.js +13 -0
- package/dist/web-request-context.js.map +1 -0
- package/dist/web-session.d.ts +167 -0
- package/dist/web-session.js +389 -0
- package/dist/web-session.js.map +1 -0
- package/dist/web-shell-routes.d.ts +59 -0
- package/dist/web-shell-routes.js +96 -0
- package/dist/web-shell-routes.js.map +1 -0
- package/dist/web-upload.d.ts +183 -0
- package/dist/web-upload.js +468 -0
- package/dist/web-upload.js.map +1 -0
- package/dist/website-tips-renderer.d.ts +2 -2
- package/dist/website-tips-renderer.js +3 -3
- package/dist/website-tips-renderer.js.map +1 -1
- package/launcher/agend +110 -0
- package/launcher/agend-agent +110 -0
- package/launcher/agend-agent.cjs +4 -0
- package/launcher/agend.cjs +4 -0
- package/launcher/install-admission.cjs +105 -0
- package/launcher/launch.cjs +64 -0
- package/launcher/postinstall.cjs +114 -0
- package/launcher/run-in-process.cjs +13 -0
- package/launcher/runtime-platform.cjs +86 -0
- package/launcher/runtime-select.cjs +217 -0
- package/package.json +18 -7
- package/scripts/preinstall-guard.cjs +55 -0
- package/templates/launchd.plist.ejs +2 -2
- package/templates/systemd.service.ejs +1 -1
- package/dist/ui/dashboard.html +0 -774
- package/dist/ui/settings.html +0 -2165
- package/dist/ui/view.html +0 -1139
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// #1481: an agent's HTML preview (#1306) in a panel beside the conversation — a split with a draggable divider on a
|
|
2
|
+
// wide screen, a full-screen sheet on a narrow one. Nothing about the preview itself changes here: the frame is the one
|
|
3
|
+
// preview.js makes (mountPreview: the preview origin, sandbox allow-scripts only), started by AgendPreview.start like a
|
|
4
|
+
// card's, only told to fill the panel. It runs on a click, as a card's does:
|
|
5
|
+
// - "Open in panel" on an idle card opens the panel with its Preview button — the panel runs nothing by itself;
|
|
6
|
+
// - on a card whose preview is running, the same button moves that run to the panel (stopped in the card, started in
|
|
7
|
+
// the panel: a moved frame reloads anyway);
|
|
8
|
+
// - a newer version of the block in a later reply is offered, never swapped in, and offered with Preview to click.
|
|
9
|
+
// Closing the panel or leaving the chat stops its frame; one preview per page still holds (start() stops any other).
|
|
10
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
11
|
+
import { t } from "/assets/app-i18n.js";
|
|
12
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
13
|
+
import "./chat-render.js";
|
|
14
|
+
import "./preview.js";
|
|
15
|
+
|
|
16
|
+
const R = () => globalThis.AgendChatRender;
|
|
17
|
+
const P = () => globalThis.AgendPreview;
|
|
18
|
+
|
|
19
|
+
// ── The panel's state, one per page (the chat shows one instance at a time) ──
|
|
20
|
+
|
|
21
|
+
/** open: { key, instance, msgKey, n, code, sender, ts } | null; run: start it on mount (a moved run). */
|
|
22
|
+
let state = { open: null, run: 0 };
|
|
23
|
+
const subs = new Set();
|
|
24
|
+
function set(next) { state = next; for (const fn of [...subs]) { try { fn(state); } catch { /* one listener's error stops no other */ } } }
|
|
25
|
+
export const panelStore = { get: () => state, subscribe(fn) { subs.add(fn); return () => { subs.delete(fn); }; } };
|
|
26
|
+
|
|
27
|
+
/** The frame's key in preview.js: its own, so the card that opened it keeps its own state. */
|
|
28
|
+
export const frameKey = (key) => `panel:${key}`;
|
|
29
|
+
/** Show `spec` in the panel; `run`: start it there (a click that moves a running preview). The block shown before stops. */
|
|
30
|
+
export function openPanel(spec, { run = false } = {}) {
|
|
31
|
+
if (state.open && P()) P().stop(frameKey(state.open.key), "stopped", "");
|
|
32
|
+
set({ open: { ...spec }, run: run ? state.run + 1 : 0 });
|
|
33
|
+
}
|
|
34
|
+
/** Close the panel: its frame stops first. */
|
|
35
|
+
export function closePanel() {
|
|
36
|
+
if (state.open && P()) P().stop(frameKey(state.open.key), "stopped", "");
|
|
37
|
+
if (state.open) set({ open: null, run: 0 });
|
|
38
|
+
}
|
|
39
|
+
/** The card key the panel shows, or null. */
|
|
40
|
+
export const shownKey = () => (state.open ? state.open.key : null);
|
|
41
|
+
|
|
42
|
+
/** The text of a block's <title>, or "" — what says two blocks are versions of one thing. */
|
|
43
|
+
export function htmlTitle(code) {
|
|
44
|
+
const m = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(String(code || ""));
|
|
45
|
+
return m ? m[1].replace(/\s+/g, " ").trim() : "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A newer version of the shown block: the last complete ```html block in an agent reply after the one it came from.
|
|
50
|
+
* When the shown block has a <title>, only a block with the same title counts (another page is not a new version);
|
|
51
|
+
* without one, the newest block does. `msgs` in order; `keyOf` makes a message's key.
|
|
52
|
+
*/
|
|
53
|
+
export function newerVersion(open, msgs, keyOf) {
|
|
54
|
+
if (!open || !Array.isArray(msgs)) return null;
|
|
55
|
+
const at = msgs.findIndex(x => keyOf(x) === open.msgKey);
|
|
56
|
+
if (at < 0) return null;
|
|
57
|
+
const title = htmlTitle(open.code);
|
|
58
|
+
for (let i = msgs.length - 1; i > at; i--) {
|
|
59
|
+
const x = msgs[i];
|
|
60
|
+
if (x.role !== "agent") continue;
|
|
61
|
+
const fences = R().htmlFences(x.text);
|
|
62
|
+
for (let n = fences.length - 1; n >= 0; n--) {
|
|
63
|
+
const f = fences[n];
|
|
64
|
+
if (!f.terminated || (title && htmlTitle(f.code) !== title)) continue;
|
|
65
|
+
const msgKey = keyOf(x);
|
|
66
|
+
return { key: `${msgKey}:f${n}`, instance: open.instance, msgKey, n, code: f.code, sender: x.sender, ts: x.ts };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── The panel's width: this device's choice (a convenience — the panel works without storage) ──
|
|
73
|
+
|
|
74
|
+
const WIDTH_KEY = "agend_preview_panel_w";
|
|
75
|
+
export const MIN_PANEL = 320, MIN_THREAD = 360, STEP = 32;
|
|
76
|
+
function storedWidth() { try { const v = Number(localStorage.getItem(WIDTH_KEY)); return Number.isFinite(v) && v > 0 ? v : null; } catch { return null; } }
|
|
77
|
+
function storeWidth(w) { try { localStorage.setItem(WIDTH_KEY, String(Math.round(w))); } catch { /* this page only */ } }
|
|
78
|
+
/** The width a panel may have in a split `total` px wide: at least MIN_PANEL, leaving the thread MIN_THREAD. */
|
|
79
|
+
export function clampWidth(w, total) {
|
|
80
|
+
const max = Math.max(MIN_PANEL, total - MIN_THREAD - 8);
|
|
81
|
+
return Math.round(Math.min(max, Math.max(MIN_PANEL, w)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const timeOf = (ts) => (ts ? new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "");
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The panel, inside the chat's split (`split`: that element — the divider sets its width there, through CSSOM).
|
|
88
|
+
* props: name (the instance), msgs() → its messages, subscribe(fn) → unsubscribe (the chat store's), keyOf(x),
|
|
89
|
+
* reveal(msgKey) (the thread shows that message), download(code).
|
|
90
|
+
*/
|
|
91
|
+
export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, download }) {
|
|
92
|
+
const [st, setSt] = useState(state);
|
|
93
|
+
const [view, setView] = useState("preview");
|
|
94
|
+
const [run, setRun] = useState({ state: "idle", reason: "" });
|
|
95
|
+
const [newer, setNewer] = useState(null);
|
|
96
|
+
const holder = useRef(null), divider = useRef(null), panel = useRef(null);
|
|
97
|
+
const open = st.open && st.open.instance === name ? st.open : null;
|
|
98
|
+
|
|
99
|
+
useEffect(() => panelStore.subscribe(setSt), []);
|
|
100
|
+
// This device's choice (here or in another tab) decides whether Preview is offered: re-read it when it changes.
|
|
101
|
+
const [, choice] = useState(0);
|
|
102
|
+
useEffect(() => (P() ? P().onChange(() => choice(n => n + 1)) : undefined), []);
|
|
103
|
+
// Leaving the chat (another instance, another panel) closes the panel: its frame stops with the thread's.
|
|
104
|
+
useEffect(() => () => closePanel(), []);
|
|
105
|
+
|
|
106
|
+
const ui = { state: (s, reason) => setRun({ state: s === "starting" || s === "running" ? s : "idle", reason: reason || "" }) };
|
|
107
|
+
const start = () => {
|
|
108
|
+
if (!open || !holder.current) return;
|
|
109
|
+
setView("preview");
|
|
110
|
+
P().start(frameKey(open.key), holder.current, open.code, ui, { fill: true });
|
|
111
|
+
};
|
|
112
|
+
const stop = () => { if (open) P().stop(frameKey(open.key), "stopped", ""); };
|
|
113
|
+
// A new block in the panel: what ran before is gone (openPanel stopped it); a moved run starts here.
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
setRun({ state: "idle", reason: "" });
|
|
116
|
+
setView("preview");
|
|
117
|
+
if (open && st.run) start();
|
|
118
|
+
// Focus goes to the panel's close control that is on screen (Back on a phone, × beside the thread).
|
|
119
|
+
if (panel.current) { const f = [...panel.current.querySelectorAll(".pv-panel-close")].find(b => b.offsetParent !== null); if (f) f.focus(); }
|
|
120
|
+
}, [open && open.key, st.run]);
|
|
121
|
+
// A later reply with a newer version of this block: offered, not swapped in.
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!open) { setNewer(null); return undefined; }
|
|
124
|
+
const check = () => setNewer(newerVersion(open, msgs() || [], keyOf));
|
|
125
|
+
check();
|
|
126
|
+
return subscribe((instance, kind) => { if (instance === name && kind === "msgs") check(); });
|
|
127
|
+
}, [open && open.key, name]);
|
|
128
|
+
|
|
129
|
+
// The divider: drag, or the arrow keys, Home and End; the width is this device's.
|
|
130
|
+
const widthNow = useRef(MIN_PANEL);
|
|
131
|
+
const applyWidth = (w, save) => {
|
|
132
|
+
const sp = split.current; if (!sp) return;
|
|
133
|
+
const total = sp.clientWidth || 0;
|
|
134
|
+
const v = clampWidth(w, total);
|
|
135
|
+
widthNow.current = v;
|
|
136
|
+
sp.style.setProperty("--pv-w", `${v}px`); // CSSOM: the page's CSP allows no style attribute
|
|
137
|
+
if (divider.current) { divider.current.setAttribute("aria-valuenow", String(v)); divider.current.setAttribute("aria-valuemax", String(clampWidth(Infinity, total))); }
|
|
138
|
+
if (save) storeWidth(v);
|
|
139
|
+
};
|
|
140
|
+
useEffect(() => { if (open) applyWidth(storedWidth() ?? (split.current && split.current.clientWidth || 0) / 2, false); }, [!!open]);
|
|
141
|
+
const width = () => widthNow.current;
|
|
142
|
+
const setWidth = (w) => applyWidth(w, true);
|
|
143
|
+
const onKey = (e) => {
|
|
144
|
+
const k = e.key;
|
|
145
|
+
if (k === "ArrowLeft") setWidth(width() + STEP);
|
|
146
|
+
else if (k === "ArrowRight") setWidth(width() - STEP);
|
|
147
|
+
else if (k === "Home") setWidth(Infinity);
|
|
148
|
+
else if (k === "End") setWidth(0);
|
|
149
|
+
else return;
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
};
|
|
152
|
+
const onDown = (e) => {
|
|
153
|
+
const sp = split.current; if (!sp || e.button !== 0) return;
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
const el = e.currentTarget;
|
|
156
|
+
try { el.setPointerCapture(e.pointerId); } catch { /* the move still works while over the divider */ }
|
|
157
|
+
sp.classList.add("resizing"); // frames take no pointer events meanwhile, or the drag would stop over one
|
|
158
|
+
const right = sp.getBoundingClientRect().right;
|
|
159
|
+
const move = (ev) => applyWidth(right - ev.clientX - 4, false);
|
|
160
|
+
const up = () => { sp.classList.remove("resizing"); storeWidth(widthNow.current); el.removeEventListener("pointermove", move); el.removeEventListener("pointerup", up); el.removeEventListener("pointercancel", up); };
|
|
161
|
+
el.addEventListener("pointermove", move);
|
|
162
|
+
el.addEventListener("pointerup", up);
|
|
163
|
+
el.addEventListener("pointercancel", up);
|
|
164
|
+
};
|
|
165
|
+
// Esc in the panel closes it (and is not the chat's "stop the reply").
|
|
166
|
+
const onPanelKey = (e) => { if (e.key === "Escape" && !e.isComposing) { e.preventDefault(); closePanel(); } };
|
|
167
|
+
|
|
168
|
+
if (!open) return null;
|
|
169
|
+
const going = run.state === "starting" || run.state === "running";
|
|
170
|
+
const a = P().availability();
|
|
171
|
+
const note = run.reason || (going ? (run.state === "starting" ? t("chat.pvStarting") : "") : a.ok ? t("chat.pvPanelIdle") : a.reason);
|
|
172
|
+
const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
|
|
173
|
+
return html`<div class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
|
|
174
|
+
aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>
|
|
175
|
+
<aside class="pv-panel" ref=${panel} aria-label=${t("chat.pvPanel")} onKeyDown=${onPanelKey}>
|
|
176
|
+
<div class="pv-panel-head">
|
|
177
|
+
<button type="button" class="icon-btn only-narrow pv-panel-close" title=${t("chat.pvBack")} aria-label=${t("chat.pvBack")} onClick=${closePanel}><${Icon} name="back" size=${18} /></button>
|
|
178
|
+
<div class="pv-panel-title"><strong>${t("chat.pvPanel")}</strong>
|
|
179
|
+
<button type="button" class="pv-source" title=${source} onClick=${() => { if (window.matchMedia && window.matchMedia("(max-width: 899px)").matches) closePanel(); reveal(open.msgKey); }}>${source}</button></div>
|
|
180
|
+
<button type="button" class="icon-btn only-wide pv-panel-close" title=${t("chat.pvClose")} aria-label=${t("chat.pvClose")} onClick=${closePanel}><${Icon} name="close" size=${18} /></button>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="pv-panel-bar">
|
|
183
|
+
<div class="seg-inline" role="group" aria-label=${t("chat.pvPanel")}>
|
|
184
|
+
<button type="button" class=${`btn btn-sm${view === "preview" ? " on" : ""}`} aria-pressed=${view === "preview" ? "true" : "false"} onClick=${() => setView("preview")}><${Icon} name="eye" size=${14} />${t("chat.pvPage")}</button>
|
|
185
|
+
<button type="button" class=${`btn btn-sm${view === "code" ? " on" : ""}`} aria-pressed=${view === "code" ? "true" : "false"} onClick=${() => setView("code")}><${Icon} name="code" size=${14} />${t("chat.pvCode")}</button>
|
|
186
|
+
</div>
|
|
187
|
+
<span class="grow"></span>
|
|
188
|
+
${going ? html`<button type="button" class="btn btn-sm pv-panel-reload" title=${t("chat.pvReload")} onClick=${() => { stop(); start(); }}><${Icon} name="restart" size=${14} /><span class="lbl">${t("chat.pvReload")}</span></button>
|
|
189
|
+
<button type="button" class="btn btn-sm pv-panel-stop" title=${t("chat.pvStop")} onClick=${stop}><${Icon} name="stop" size=${14} /><span class="lbl">${t("chat.pvStop")}</span></button>`
|
|
190
|
+
: html`<button type="button" class="btn btn-sm btn-primary pv-panel-run" title=${t("chat.pvPreview")} disabled=${!a.ok} onClick=${start}><${Icon} name="play" size=${14} /><span class="lbl">${t("chat.pvPreview")}</span></button>`}
|
|
191
|
+
<button type="button" class="btn btn-sm pv-panel-dl" title=${t("chat.pvDownload")} onClick=${() => download(open.code)}><${Icon} name="download" size=${14} /><span class="lbl">${t("chat.pvDownload")}</span></button>
|
|
192
|
+
</div>
|
|
193
|
+
${newer && newer.key !== open.key ? html`<div class="pv-newer" role="status"><span>${t("chat.pvNewer", timeOf(newer.ts))}</span>
|
|
194
|
+
<button type="button" class="btn btn-sm pv-newer-show" onClick=${() => openPanel(newer)}>${t("chat.pvNewerShow")}</button></div>` : null}
|
|
195
|
+
${going ? html`<div class="pv-banner">${P().BANNER}</div>` : null}
|
|
196
|
+
${note && view === "preview" ? html`<div class="pv-note">${note}</div>` : null}
|
|
197
|
+
<div class=${`pv-panel-body${view === "code" ? " show-code" : ""}`}>
|
|
198
|
+
<div class="pv-holder" ref=${holder}></div>
|
|
199
|
+
${view === "code" ? html`<pre class="pv-code"><code>${open.code}</code></pre>` : null}
|
|
200
|
+
</div>
|
|
201
|
+
</aside>`;
|
|
202
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* #1306: running an agent's HTML in the web chat — the parent side. Design: docs/design/1306-inline-html-preview.md.
|
|
3
|
+
*
|
|
4
|
+
* The HTML runs in a frame from a SEPARATE listener (the preview origin), sandboxed to an opaque origin, so it cannot
|
|
5
|
+
* act as the signed-in person: it has no dashboard origin, no cookie it can use, no CSRF value. It reaches the frame
|
|
6
|
+
* only by postMessage, after the frame proved it is this fleet's listener (the boot id). What it is NOT promised: that
|
|
7
|
+
* it cannot send data out — so previews are off on every device until that device opts in, and every preview carries
|
|
8
|
+
* the banner saying so.
|
|
9
|
+
*
|
|
10
|
+
* This file knows nothing about the chat, the composer, the fleet's API or navigation: the message listener below can
|
|
11
|
+
* reach the frames it made and nothing else (a test asserts it). Imported by the chat panel (#1408: the import sets globalThis.AgendPreview); also
|
|
12
|
+
* exports itself for the tests.
|
|
13
|
+
*/
|
|
14
|
+
(function (root, factory) {
|
|
15
|
+
"use strict";
|
|
16
|
+
var api = factory(root);
|
|
17
|
+
if (typeof module === "object" && module && module.exports) module.exports = api;
|
|
18
|
+
// The global as well: the chat imports this file for its side effect (#1408), in the browser and under the tests.
|
|
19
|
+
if (root) root.AgendPreview = api;
|
|
20
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function (root) {
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
|
+
var LIMITS = { maxBytes: 1024 * 1024, minHeight: 40, maxHeight: 4000, readyMs: 3000, watchdogMs: 10000, resizePerSec: 10, minDelta: 2, growthFreeze: 5, growthWindowMs: 2000 };
|
|
24
|
+
var OPT_IN_KEY = "agend_html_preview"; // localStorage: "on" — this device allows previews
|
|
25
|
+
var NEVER_KEY = "agend_html_preview_never"; // sessionStorage: "1" — never preview on this device (this session)
|
|
26
|
+
var BANNER = "Previews run the agent's HTML in an isolated frame. It cannot use your login, but it may be able to send data out. Only preview content you trust. A preview can slow or freeze this tab.";
|
|
27
|
+
|
|
28
|
+
var cfg = { dashboardOrigin: "", previewOrigin: "", boot: "", reason: "" };
|
|
29
|
+
var changed = []; // the page's "this device's choice changed" callbacks (checkbox, cards)
|
|
30
|
+
var live = new Map(); // cardKey → { key, iframe, ch, holder, state, … } (at most one: one running preview per page)
|
|
31
|
+
var listening = false;
|
|
32
|
+
|
|
33
|
+
function now() { return root.performance && typeof root.performance.now === "function" ? root.performance.now() : Date.now(); }
|
|
34
|
+
function store(kind) { try { return root[kind] || null; } catch (e) { return null; } }
|
|
35
|
+
function utf8Bytes(s) { try { return new root.TextEncoder().encode(s).length; } catch (e) { return String(s).length * 3; } }
|
|
36
|
+
function randomCh() {
|
|
37
|
+
var b = new Uint8Array(16);
|
|
38
|
+
root.crypto.getRandomValues(b);
|
|
39
|
+
var s = "";
|
|
40
|
+
for (var i = 0; i < b.length; i++) s += (b[i] < 16 ? "0" : "") + b[i].toString(16);
|
|
41
|
+
return s;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** What the server said about this page load (the <body data-*> attributes /ui was served with). */
|
|
45
|
+
function init(data) {
|
|
46
|
+
cfg = {
|
|
47
|
+
dashboardOrigin: String((data && data.dashboardOrigin) || ""),
|
|
48
|
+
previewOrigin: String((data && data.previewOrigin) || ""),
|
|
49
|
+
boot: String((data && data.previewBoot) || ""),
|
|
50
|
+
reason: String((data && data.previewReason) || ""),
|
|
51
|
+
};
|
|
52
|
+
if (!listening && typeof root.addEventListener === "function") {
|
|
53
|
+
root.addEventListener("message", onMessage);
|
|
54
|
+
// The opt-in is per device: another tab of this dashboard turning it off (or clearing storage) stops the
|
|
55
|
+
// previews running here at once. Turning it on elsewhere starts nothing here — only a click on Preview does.
|
|
56
|
+
root.addEventListener("storage", onStorage);
|
|
57
|
+
listening = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function onStorage(event) {
|
|
61
|
+
if (event && event.key !== OPT_IN_KEY && event.key !== null) return; // key null: storage was cleared
|
|
62
|
+
if (!optedIn()) stopAll("off");
|
|
63
|
+
notify();
|
|
64
|
+
}
|
|
65
|
+
/** Call `fn` whenever this device's choice may have changed (here or in another tab). Returns the unsubscribe: a
|
|
66
|
+
* chat that leaves takes its callback with it (#1408 §4), so 50 visits leave no callback behind. */
|
|
67
|
+
function onChange(fn) {
|
|
68
|
+
if (typeof fn !== "function") return function () {};
|
|
69
|
+
changed.push(fn);
|
|
70
|
+
return function () { var i = changed.indexOf(fn); if (i >= 0) changed.splice(i, 1); };
|
|
71
|
+
}
|
|
72
|
+
function listeners() { return changed.length; }
|
|
73
|
+
function notify() { for (var i = 0; i < changed.length; i++) { try { changed[i](); } catch (e) { /* one listener's error stops no other */ } } }
|
|
74
|
+
|
|
75
|
+
// ── This device's choice ────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function optedIn() { var s = store("localStorage"); try { return !!s && s.getItem(OPT_IN_KEY) === "on"; } catch (e) { return false; } }
|
|
78
|
+
/** Allow (or stop allowing) previews on this device. Turning it off stops every running preview at once. */
|
|
79
|
+
function setOptIn(on) {
|
|
80
|
+
var s = store("localStorage");
|
|
81
|
+
try { if (s) { if (on) s.setItem(OPT_IN_KEY, "on"); else s.removeItem(OPT_IN_KEY); } } catch (e) { /* this page only */ }
|
|
82
|
+
if (!on) stopAll("off");
|
|
83
|
+
notify();
|
|
84
|
+
}
|
|
85
|
+
function never() { var s = store("sessionStorage"); try { return !!s && s.getItem(NEVER_KEY) === "1"; } catch (e) { return false; } }
|
|
86
|
+
function setNever(on) {
|
|
87
|
+
var s = store("sessionStorage");
|
|
88
|
+
try { if (s) { if (on) s.setItem(NEVER_KEY, "1"); else s.removeItem(NEVER_KEY); } } catch (e) { /* this page only */ }
|
|
89
|
+
if (on) stopAll("off");
|
|
90
|
+
notify();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether this page may run a preview at all, and why not: the server must have chosen a preview origin for this
|
|
95
|
+
* load, the browser must really be at the origin the server believed (a proxy that rewrites Host would otherwise get
|
|
96
|
+
* a frame from the wrong place), and the device must have opted in and not switched previews off.
|
|
97
|
+
*/
|
|
98
|
+
function availability() {
|
|
99
|
+
if (!cfg.previewOrigin || !cfg.boot) return { ok: false, why: "server", reason: cfg.reason || "Previews are not available here." };
|
|
100
|
+
var at = root.location && root.location.origin;
|
|
101
|
+
if (!cfg.dashboardOrigin || at !== cfg.dashboardOrigin) {
|
|
102
|
+
return { ok: false, why: "origin", reason: "Previews are off: this page is at " + at + ", but the fleet believes it is at " + (cfg.dashboardOrigin || "an unknown address") + " (a proxy that rewrites Host). Pass the external Host through, or set web.preview_origin." };
|
|
103
|
+
}
|
|
104
|
+
if (never()) return { ok: false, why: "never", reason: "Previews are switched off on this device for this session." };
|
|
105
|
+
if (!optedIn()) return { ok: false, why: "optin", reason: "Previews are off on this device. Allow them from the card's menu or the sidebar." };
|
|
106
|
+
return { ok: true, why: "", reason: "" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── The frame ───────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The ONE place the dashboard makes an iframe or writes `sandbox` (a test fails if anything else does):
|
|
113
|
+
* opaque origin (allow-scripts only — never allow-same-origin, forms, popups, top navigation, modals, downloads),
|
|
114
|
+
* no permissions, no referrer, from <preview origin>/frame — the only URL the dashboard's frame-src allows.
|
|
115
|
+
*/
|
|
116
|
+
function mountPreview(doc) {
|
|
117
|
+
var f = doc.createElement("iframe");
|
|
118
|
+
f.setAttribute("sandbox", "allow-scripts");
|
|
119
|
+
f.setAttribute("allow", "");
|
|
120
|
+
f.setAttribute("referrerpolicy", "no-referrer");
|
|
121
|
+
f.setAttribute("loading", "eager");
|
|
122
|
+
f.setAttribute("title", "Untrusted HTML preview");
|
|
123
|
+
f.className = "preview-frame";
|
|
124
|
+
f.src = cfg.previewOrigin + "/frame";
|
|
125
|
+
return f;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run `html` in `holder` (a node of the card) for `key`. `ui` is told what happened: ui.state(name, reason) with
|
|
130
|
+
* "starting" | "running" | "stopped" | "unavailable". One preview at a time: any other running one stops first.
|
|
131
|
+
* opts.fill (#1481, the side panel): the frame fills its holder — the same frame from mountPreview, only sized by the
|
|
132
|
+
* panel instead of by the frame's height messages, which it then ignores.
|
|
133
|
+
*/
|
|
134
|
+
function start(key, holder, html, ui, opts) {
|
|
135
|
+
var a = availability();
|
|
136
|
+
if (!a.ok) { ui.state("unavailable", a.reason); return false; }
|
|
137
|
+
if (typeof html !== "string" || utf8Bytes(html) > LIMITS.maxBytes) { ui.state("unavailable", "This HTML is over 1 MiB; it is not previewed."); return false; }
|
|
138
|
+
stopAll("another");
|
|
139
|
+
var doc = holder.ownerDocument;
|
|
140
|
+
var iframe = mountPreview(doc);
|
|
141
|
+
var card = {
|
|
142
|
+
key: key, iframe: iframe, ch: randomCh(), holder: holder, html: html, ui: ui, rendered: false,
|
|
143
|
+
height: LIMITS.minHeight, lastApplied: -Infinity, appliedInSecond: [], frozen: false, growth: [], pendingFrame: false,
|
|
144
|
+
readyTimer: null, watchdogTimer: null, fill: !!(opts && opts.fill),
|
|
145
|
+
};
|
|
146
|
+
if (card.fill) iframe.classList.add("fill");
|
|
147
|
+
live.set(key, card);
|
|
148
|
+
card.readyTimer = root.setTimeout(function () {
|
|
149
|
+
if (live.get(key) === card && !card.rendered) stop(key, "unavailable", "Preview unavailable: the preview did not answer within 3 s. If an earlier preview froze, it may still be running — open the dashboard in a new tab. Over SSH, forward the preview port too.");
|
|
150
|
+
}, LIMITS.readyMs);
|
|
151
|
+
holder.appendChild(iframe);
|
|
152
|
+
if (!card.fill) setHeight(card, LIMITS.minHeight);
|
|
153
|
+
ui.state("starting", "");
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function setHeight(card, px) { card.height = px; card.iframe.style.height = px + "px"; } // CSSOM — never a style attribute
|
|
158
|
+
|
|
159
|
+
function armWatchdog(card) {
|
|
160
|
+
if (card.watchdogTimer) root.clearTimeout(card.watchdogTimer);
|
|
161
|
+
card.watchdogTimer = root.setTimeout(function () {
|
|
162
|
+
if (live.get(card.key) !== card) return;
|
|
163
|
+
// Best effort: a frame that froze this tab freezes this timer too. Only while the page is visible.
|
|
164
|
+
var hidden = root.document && root.document.visibilityState === "hidden";
|
|
165
|
+
if (hidden) { armWatchdog(card); return; }
|
|
166
|
+
stop(card.key, "stopped", "The preview stopped answering, so it was closed.");
|
|
167
|
+
}, LIMITS.watchdogMs);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Stop the preview for `key`: remove its frame and its timers. A preview is never moved or reloaded silently. */
|
|
171
|
+
function stop(key, state, reason) {
|
|
172
|
+
var card = live.get(key);
|
|
173
|
+
if (!card) return false;
|
|
174
|
+
live.delete(key);
|
|
175
|
+
if (card.readyTimer) root.clearTimeout(card.readyTimer);
|
|
176
|
+
if (card.watchdogTimer) root.clearTimeout(card.watchdogTimer);
|
|
177
|
+
if (card.iframe.parentNode) card.iframe.parentNode.removeChild(card.iframe);
|
|
178
|
+
card.ui.state(state || "stopped", reason || "");
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
function stopAll(why) {
|
|
182
|
+
var keys = [];
|
|
183
|
+
live.forEach(function (_c, k) { keys.push(k); });
|
|
184
|
+
for (var i = 0; i < keys.length; i++) stop(keys[i], "stopped", why === "off" ? "Previews were switched off on this device." : why === "another" ? "Another preview started." : "");
|
|
185
|
+
return keys;
|
|
186
|
+
}
|
|
187
|
+
/** Stop every preview inside `node` (before the chat replaces, moves or removes it); returns the stopped keys. */
|
|
188
|
+
function stopIn(node) {
|
|
189
|
+
var keys = [];
|
|
190
|
+
live.forEach(function (c, k) { if (node && (node === c.holder || (typeof node.contains === "function" && node.contains(c.holder)))) keys.push(k); });
|
|
191
|
+
for (var i = 0; i < keys.length; i++) stop(keys[i], "stopped", "");
|
|
192
|
+
return keys;
|
|
193
|
+
}
|
|
194
|
+
function running(key) { return live.has(key); }
|
|
195
|
+
function liveCount() { return live.size; }
|
|
196
|
+
function liveFrame(key) { var c = live.get(key); return c ? c.iframe : null; }
|
|
197
|
+
|
|
198
|
+
// ── Messages from frames (design §4.2) ──────────────────
|
|
199
|
+
|
|
200
|
+
var KEYS = { ready: "boot,ch,type,v", resize: "ch,height,type,v", heartbeat: "ch,type,v" };
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The only window message listener. A message counts only from one of OUR frames (event.source is that frame's
|
|
204
|
+
* window, and its origin is the opaque "null"), with one of three exact shapes; each has one effect, on that frame
|
|
205
|
+
* only: ready → send the HTML once (boot checked, opt-in checked again), resize → its height, heartbeat → its
|
|
206
|
+
* watchdog. It has no reference to the chat, the composer, the fleet's API, navigation or storage.
|
|
207
|
+
*/
|
|
208
|
+
function onMessage(event) {
|
|
209
|
+
var card = null;
|
|
210
|
+
live.forEach(function (c) { if (event.source && event.source === c.iframe.contentWindow) card = c; });
|
|
211
|
+
if (!card || event.origin !== "null") return;
|
|
212
|
+
var d = event.data;
|
|
213
|
+
if (!d || typeof d !== "object" || Array.isArray(d) || d.v !== 1 || typeof d.type !== "string" || !Object.prototype.hasOwnProperty.call(KEYS, d.type)) return;
|
|
214
|
+
if (Object.keys(d).sort().join(",") !== KEYS[d.type]) return;
|
|
215
|
+
if (d.type === "ready") {
|
|
216
|
+
if (d.ch !== null || typeof d.boot !== "string" || card.rendered) return;
|
|
217
|
+
if (d.boot !== cfg.boot) { stop(card.key, "unavailable", "Preview unavailable: something else answered on the preview port."); return; }
|
|
218
|
+
if (!availability().ok) { stop(card.key, "unavailable", availability().reason); return; }
|
|
219
|
+
card.rendered = true;
|
|
220
|
+
root.clearTimeout(card.readyTimer); card.readyTimer = null;
|
|
221
|
+
card.iframe.contentWindow.postMessage({ v: 1, type: "render", ch: card.ch, html: card.html }, "*");
|
|
222
|
+
armWatchdog(card);
|
|
223
|
+
card.ui.state("running", "");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (d.ch !== card.ch || !card.rendered) return;
|
|
227
|
+
if (d.type === "heartbeat") {
|
|
228
|
+
// A heartbeat never outlives the permission: if this device stopped allowing previews (and the storage event
|
|
229
|
+
// was missed), the next heartbeat ends it.
|
|
230
|
+
if (!availability().ok) { stop(card.key, "stopped", "Previews were switched off on this device."); return; }
|
|
231
|
+
armWatchdog(card);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (typeof d.height !== "number" || !isFinite(d.height) || Math.floor(d.height) !== d.height) return;
|
|
235
|
+
requestHeight(card, d.height);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Height rules (design §4.2): clamp to [40, 4000]; ≤ 1 per animation frame and ≤ 10 per second; Δ < 2 px ignored;
|
|
239
|
+
* after 5 consecutive increases within 2 s the height freezes and the frame scrolls. */
|
|
240
|
+
function requestHeight(card, h) {
|
|
241
|
+
if (card.fill) return; // the panel sizes it (#1481)
|
|
242
|
+
var want = Math.max(LIMITS.minHeight, Math.min(LIMITS.maxHeight, h));
|
|
243
|
+
if (Math.abs(want - card.height) < LIMITS.minDelta) return;
|
|
244
|
+
var t = now();
|
|
245
|
+
if (want > card.height) {
|
|
246
|
+
card.growth = card.growth.filter(function (x) { return t - x < LIMITS.growthWindowMs; });
|
|
247
|
+
card.growth.push(t);
|
|
248
|
+
if (card.growth.length > LIMITS.growthFreeze) card.frozen = true;
|
|
249
|
+
} else card.growth = [];
|
|
250
|
+
if (card.frozen) return;
|
|
251
|
+
card.appliedInSecond = card.appliedInSecond.filter(function (x) { return t - x < 1000; });
|
|
252
|
+
if (card.appliedInSecond.length >= LIMITS.resizePerSec) return;
|
|
253
|
+
card.wanted = want;
|
|
254
|
+
if (card.pendingFrame) return;
|
|
255
|
+
card.pendingFrame = true;
|
|
256
|
+
var raf = typeof root.requestAnimationFrame === "function" ? root.requestAnimationFrame : function (f) { return root.setTimeout(f, 16); };
|
|
257
|
+
raf(function () {
|
|
258
|
+
card.pendingFrame = false;
|
|
259
|
+
if (live.get(card.key) !== card) return;
|
|
260
|
+
card.appliedInSecond.push(now());
|
|
261
|
+
setHeight(card, card.wanted);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
init: init, availability: availability, optedIn: optedIn, setOptIn: setOptIn, never: never, setNever: setNever, onChange: onChange, listeners: listeners, onStorage: onStorage,
|
|
267
|
+
start: start, stop: stop, stopAll: stopAll, stopIn: stopIn, running: running, liveCount: liveCount, liveFrame: liveFrame,
|
|
268
|
+
mountPreview: mountPreview, onMessage: onMessage, BANNER: BANNER, LIMITS: LIMITS,
|
|
269
|
+
};
|
|
270
|
+
});
|