@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,140 @@
|
|
|
1
|
+
// #1408 step 3: the setup wizard — the same four steps as `agend quickstart`, one dialog. It pre-fills from the running
|
|
2
|
+
// configuration and, when it finishes, writes the files (a write an admin may have to confirm, #1423) and hands over
|
|
3
|
+
// to the ordinary Apply: the app's runner (settings-apply.js) posts the apply and watches its job, with the same
|
|
4
|
+
// progress, deadline and restart recovery as every other change.
|
|
5
|
+
import { html, useEffect, useState } from "/assets/app-html.js";
|
|
6
|
+
import { t } from "/assets/app-i18n.js";
|
|
7
|
+
import { useLease } from "/assets/app-ctx.js";
|
|
8
|
+
import { Dialog } from "/assets/ui-dialog.js";
|
|
9
|
+
import { api } from "./settings-confirm.js";
|
|
10
|
+
import { startOperation } from "./settings-apply.js";
|
|
11
|
+
import { BACKENDS, DEFAULT_SCHEMA, impactOf, toYaml } from "./settings-model.js";
|
|
12
|
+
import { Select } from "./settings-dialogs.js";
|
|
13
|
+
|
|
14
|
+
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
15
|
+
const STEPS = 4;
|
|
16
|
+
const post = (path, body) => api(path, { method: "POST", body: JSON.stringify(body) }).catch(() => ({ ok: false, status: 0, body: { error: tn("failed") } }));
|
|
17
|
+
|
|
18
|
+
export function SetupWizard({ ctx, onClose }) {
|
|
19
|
+
const lease = useLease("wizard");
|
|
20
|
+
const [w, setW] = useState(null);
|
|
21
|
+
const [busy, setBusy] = useState("");
|
|
22
|
+
const [err, setErr] = useState("");
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
(async () => {
|
|
25
|
+
const env = await api("/api/settings/quickstart/environment").catch(() => null);
|
|
26
|
+
if (!lease.current()) return;
|
|
27
|
+
const e = (env && env.body) || { backends: [], channels: [], has_fleet: false };
|
|
28
|
+
setW({ step: 1, env: e, backend: (e.backends || [])[0] || "claude-code", working_directory: "", instance_name: "agent-1",
|
|
29
|
+
platform: "telegram", token: "", token_env: "AGEND_BOT_TOKEN", group_id: "", guild_id: "", general_channel_id: "", admin_user_id: "",
|
|
30
|
+
identity: null, guilds: [], plan: null, offset: 0 });
|
|
31
|
+
})();
|
|
32
|
+
}, [lease]);
|
|
33
|
+
if (!w) return html`<${Dialog} title=${tn("wizardTitle")} onClose=${onClose}><p class="note">…</p></${Dialog}>`;
|
|
34
|
+
const set = (k) => (v) => setW((x) => ({ ...x, [k]: v }));
|
|
35
|
+
const input = () => ({
|
|
36
|
+
platform: w.platform, token_env: w.token_env, backend: w.backend, working_directory: w.working_directory, instance_name: w.instance_name,
|
|
37
|
+
group_id: w.group_id || undefined, guild_id: w.guild_id || undefined, general_channel_id: w.general_channel_id || undefined,
|
|
38
|
+
admin_user_id: w.admin_user_id || undefined,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const verify = async () => {
|
|
42
|
+
setBusy(tn("wizardVerifying")); setErr("");
|
|
43
|
+
const res = await post("/api/settings/quickstart/probe", { action: "verify", platform: w.platform, token: w.token });
|
|
44
|
+
if (!lease.current()) return;
|
|
45
|
+
const identity = (res.body && res.body.identity) || { valid: false };
|
|
46
|
+
let guilds = [];
|
|
47
|
+
if (identity.valid && w.platform === "discord") {
|
|
48
|
+
const g = await post("/api/settings/quickstart/probe", { action: "guilds", token: w.token });
|
|
49
|
+
if (!lease.current()) return;
|
|
50
|
+
guilds = (g.body && g.body.guilds) || [];
|
|
51
|
+
}
|
|
52
|
+
setBusy("");
|
|
53
|
+
setW((x) => ({ ...x, identity, guilds }));
|
|
54
|
+
};
|
|
55
|
+
const detect = async () => {
|
|
56
|
+
setBusy(tn("wizardWaitingStart")); setErr("");
|
|
57
|
+
const res = await post("/api/settings/quickstart/probe", { action: "await-telegram-start", token: w.token, offset: w.offset || 0 });
|
|
58
|
+
if (!lease.current()) return;
|
|
59
|
+
setBusy("");
|
|
60
|
+
if (!res.ok) { setErr((res.body && res.body.error) || tn("applyFailed")); return; }
|
|
61
|
+
if (res.body.found) setW((x) => ({ ...x, offset: res.body.offset, group_id: String(res.body.found.groupId), admin_user_id: String(res.body.found.userId) }));
|
|
62
|
+
else { setW((x) => ({ ...x, offset: res.body.offset })); setErr(tn("wizardNoStartYet")); }
|
|
63
|
+
};
|
|
64
|
+
const next = async () => {
|
|
65
|
+
setErr("");
|
|
66
|
+
if (w.step === 1) { if (!w.working_directory.startsWith("/")) { setErr(tn("wizardNeedDir")); return; } setW({ ...w, step: 2 }); return; }
|
|
67
|
+
if (w.step === 2) { setW({ ...w, step: 3 }); return; }
|
|
68
|
+
if (w.step === 3) {
|
|
69
|
+
if (!w.identity || !w.identity.valid) { setErr(tn("wizardNeedVerify")); return; }
|
|
70
|
+
setBusy(tn("wizardPlanLoading"));
|
|
71
|
+
const res = await post("/api/settings/quickstart/plan", input());
|
|
72
|
+
if (!lease.current()) return;
|
|
73
|
+
setBusy("");
|
|
74
|
+
if (!res.ok) { setErr((res.body && res.body.error) || tn("applyFailed")); return; }
|
|
75
|
+
setW((x) => ({ ...x, plan: res.body, step: 4 }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await finish();
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Write the files, then the ordinary Apply — one operation, handed to the app before the commit is even sent
|
|
82
|
+
* (#1453 review): the commit (which may wait for an admin's confirmation), then the apply and its job. While it runs
|
|
83
|
+
* no other Apply can start, leaving the page warns. The token leaves the form now; the operation holds it in the
|
|
84
|
+
* commit's body until that write completes, then drops it.
|
|
85
|
+
*/
|
|
86
|
+
const finish = () => {
|
|
87
|
+
const body = { ...input(), token: w.token };
|
|
88
|
+
// It writes the connection and the first agent: it costs what a connection change does (the server's schema).
|
|
89
|
+
const handed = startOperation([{ label: tn("wizardTitle"), impact: impactOf((ctx && ctx.schema) || DEFAULT_SCHEMA, "fleet.channels"),
|
|
90
|
+
request: { method: "POST", url: "/api/settings/quickstart/commit", body, sensitive: true } }]);
|
|
91
|
+
if (!handed) { setErr(tn("applyBusyLocal")); return; }
|
|
92
|
+
setW((x) => ({ ...x, token: "" }));
|
|
93
|
+
onClose();
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let body;
|
|
97
|
+
if (w.step === 1) {
|
|
98
|
+
body = html`<div class="field"><label for="wz-be">${tn("wizardBackend")}</label>
|
|
99
|
+
<${Select} id="wz-be" value=${w.backend} onChange=${set("backend")} options=${w.env.backends && w.env.backends.length ? w.env.backends : BACKENDS} />
|
|
100
|
+
<p class="note">${w.env.backends && w.env.backends.length ? tn("wizardBackendFound", w.env.backends.join(", ")) : tn("wizardBackendNone")}</p></div>
|
|
101
|
+
<div class="field"><label for="wz-wd">${tn("workingDir")}</label><input id="wz-wd" type="text" placeholder="/home/you/projects/app" value=${w.working_directory} onInput=${(e) => set("working_directory")(e.target.value.trim())} /></div>
|
|
102
|
+
<div class="field"><label for="wz-name">${tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${w.instance_name} onInput=${(e) => set("instance_name")(e.target.value.trim())} /></div>`;
|
|
103
|
+
} else if (w.step === 2) {
|
|
104
|
+
body = html`<div class="seg-inline" role="group" aria-label=${tn("wizardPlatform")}>
|
|
105
|
+
${["telegram", "discord"].map((p) => html`<button key=${p} type="button" class=${`btn${w.platform === p ? " btn-primary" : ""}`} aria-pressed=${w.platform === p ? "true" : "false"}
|
|
106
|
+
onClick=${() => setW({ ...w, platform: p, identity: null })}>${p === "telegram" ? "Telegram" : "Discord"}</button>`)}</div>
|
|
107
|
+
<p class="note">${w.platform === "telegram" ? tn("wizardTelegramHint") : tn("wizardDiscordHint")}</p>`;
|
|
108
|
+
} else if (w.step === 3) {
|
|
109
|
+
body = html`<div class="field"><label for="wz-token">${tn("wizardToken")}</label>
|
|
110
|
+
<input id="wz-token" type="password" autocomplete="new-password" placeholder="123456:ABC-DEF…" value=${w.token} onInput=${(e) => setW({ ...w, token: e.target.value.trim(), identity: null })} />
|
|
111
|
+
<p class="note">${w.platform === "telegram" ? tn("wizardTokenTelegram") : tn("wizardTokenDiscord")}</p></div>
|
|
112
|
+
<div class="field"><label for="wz-env">${tn("tokenEnv")}</label><input id="wz-env" type="text" value=${w.token_env} onInput=${(e) => set("token_env")(e.target.value.trim())} /></div>
|
|
113
|
+
<div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${!!busy} onClick=${verify}>${tn("wizardVerify")}</button>
|
|
114
|
+
${w.identity ? html`<span class=${`feedback${w.identity.valid ? "" : " error"}`} role="status">${w.identity.valid ? (w.identity.username || tn("verified")) : (w.identity.reason || tn("verifyFailed"))}</span>` : null}</div>
|
|
115
|
+
${w.platform === "discord" ? html`
|
|
116
|
+
<div class="field"><label for="wz-guild">${tn("guildIdField")}</label><${Select} id="wz-guild" value=${w.guild_id} onChange=${set("guild_id")}
|
|
117
|
+
options=${["", ...w.guilds.map((g) => ({ value: g.id, label: `${g.name} (${g.id})` }))]} /></div>
|
|
118
|
+
<div class="field"><label for="wz-gen">${tn("wizardGeneralChannel")}</label><input id="wz-gen" type="text" value=${w.general_channel_id} onInput=${(e) => set("general_channel_id")(e.target.value.trim())} /></div>
|
|
119
|
+
<div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" placeholder=${tn("discordUserPlaceholder")} value=${w.admin_user_id} onInput=${(e) => set("admin_user_id")(e.target.value.trim())} /></div>`
|
|
120
|
+
: html`
|
|
121
|
+
<div class="field"><label for="wz-group">${tn("groupIdField")}</label><input id="wz-group" type="text" placeholder="-1001234567890" value=${w.group_id} onInput=${(e) => set("group_id")(e.target.value.trim())} />
|
|
122
|
+
<p class="note">${tn("wizardDetectHint")}</p></div>
|
|
123
|
+
<div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${!!busy} onClick=${detect}>${tn("wizardDetect")}</button></div>
|
|
124
|
+
<div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" placeholder=${tn("telegramUserPlaceholder")} value=${w.admin_user_id} onInput=${(e) => set("admin_user_id")(e.target.value.trim())} /></div>`}`;
|
|
125
|
+
} else {
|
|
126
|
+
body = !w.plan ? html`<p class="note">${tn("wizardPlanLoading")}</p>` : html`
|
|
127
|
+
<p><strong>${tn("wizardWillWrite")}</strong></p>
|
|
128
|
+
<pre class="s-yaml">${toYaml({ channels: [w.plan.channel], instances: { [w.plan.instance.name]: { working_directory: w.plan.instance.working_directory, backend: w.plan.instance.backend } } })}</pre>
|
|
129
|
+
<p class="note">${tn("wizardEnvKeys", (w.plan.env_keys || []).join(", "))}</p>
|
|
130
|
+
${(w.plan.warnings || []).map((x, i) => html`<p key=${i} class="feedback warning">${x}</p>`)}`;
|
|
131
|
+
}
|
|
132
|
+
return html`<${Dialog} title=${tn("wizardTitle")} onClose=${onClose} wide=${true} busy=${busy === tn("wizardCommitting")}
|
|
133
|
+
actions=${html`<button type="button" class="btn" disabled=${w.step === 1 || !!busy} onClick=${() => setW({ ...w, step: w.step - 1 })}>${tn("wizardBack")}</button>
|
|
134
|
+
<button type="button" class="btn btn-primary" disabled=${!!busy} onClick=${next}>${w.step === STEPS ? tn("wizardFinish") : tn("wizardNext")}</button>`}>
|
|
135
|
+
<p class="note">${tn("wizardStep", w.step, STEPS)}${w.env.has_fleet ? ` · ${tn("wizardRerun")}` : ""}</p>
|
|
136
|
+
<div class="form">${body}</div>
|
|
137
|
+
${busy ? html`<p class="note" role="status">${busy}</p>` : null}
|
|
138
|
+
${err ? html`<p class="feedback error" role="alert">${err}</p>` : null}
|
|
139
|
+
</${Dialog}>`;
|
|
140
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Shared by the panels. Loaded from /assets/agend-auth.js.
|
|
2
|
+
//
|
|
3
|
+
// Writes are authorized by the session cookie *and* a per-session CSRF value, which this
|
|
4
|
+
// script fetches once and adds to every same-origin write. It wraps `fetch` so the panels'
|
|
5
|
+
// own request code did not have to change; the shared shell (design Phase 2) replaces this.
|
|
6
|
+
// It also says so — once, and without losing the page — when the session has ended.
|
|
7
|
+
(() => {
|
|
8
|
+
"use strict";
|
|
9
|
+
if (window.AgendAuth) return;
|
|
10
|
+
|
|
11
|
+
// An old link carries `?token=` (e.g. /view?token=…, which is open to read and so is served, not
|
|
12
|
+
// answered with the sign-in page). It is not a credential any more, but it is still the fleet's
|
|
13
|
+
// token: take it out of the address bar and this history entry at once.
|
|
14
|
+
try {
|
|
15
|
+
const here = new URL(location.href);
|
|
16
|
+
if (here.searchParams.has("token")) {
|
|
17
|
+
here.searchParams.delete("token");
|
|
18
|
+
history.replaceState(null, "", here.pathname + here.search + here.hash);
|
|
19
|
+
}
|
|
20
|
+
} catch { /* nothing to clean */ }
|
|
21
|
+
|
|
22
|
+
// Before U2, /view kept the fleet's web token in localStorage ("agend_web_token") and sent it with every request.
|
|
23
|
+
// It is not read any more, but the token still opens every write through X-Agend-Token: remove that one key — and
|
|
24
|
+
// nothing else of the page's preferences — on any panel this browser opens. Storage can be blocked (privacy mode,
|
|
25
|
+
// a sandboxed frame); that must not stop the rest of this script.
|
|
26
|
+
try { window.localStorage.removeItem("agend_web_token"); } catch { /* no storage, nothing stored */ }
|
|
27
|
+
|
|
28
|
+
const nativeFetch = window.fetch.bind(window);
|
|
29
|
+
let csrfPending = null;
|
|
30
|
+
|
|
31
|
+
function csrf() {
|
|
32
|
+
if (!csrfPending) {
|
|
33
|
+
csrfPending = nativeFetch("/auth/session", { credentials: "same-origin", cache: "no-store" })
|
|
34
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
35
|
+
.then((j) => (j && typeof j.csrf === "string" ? j.csrf : null))
|
|
36
|
+
.catch(() => null)
|
|
37
|
+
.then((value) => { if (!value) csrfPending = null; return value; });
|
|
38
|
+
}
|
|
39
|
+
return csrfPending;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let bannerShown = false;
|
|
43
|
+
function sessionEndedBanner() {
|
|
44
|
+
if (bannerShown || !document.body) return;
|
|
45
|
+
bannerShown = true;
|
|
46
|
+
const bar = document.createElement("div");
|
|
47
|
+
bar.setAttribute("role", "alert");
|
|
48
|
+
bar.style.cssText = "position:fixed;top:0;left:0;right:0;z-index:2147483647;padding:10px 16px;background:#f85149;color:#fff;font:14px system-ui,sans-serif;text-align:center";
|
|
49
|
+
const zh = /^zh/i.test(navigator.language || "");
|
|
50
|
+
bar.append(document.createTextNode(zh ? "登入已結束。" : "Your session has ended. "));
|
|
51
|
+
const link = document.createElement("a");
|
|
52
|
+
link.href = "/signin?next=" + encodeURIComponent(location.pathname + location.search);
|
|
53
|
+
link.textContent = zh ? "重新登入" : "Sign in again";
|
|
54
|
+
link.style.cssText = "color:#fff;font-weight:600;text-decoration:underline";
|
|
55
|
+
bar.append(link);
|
|
56
|
+
document.body.prepend(bar);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
window.fetch = async function (input, init) {
|
|
60
|
+
const isRequest = typeof Request !== "undefined" && input instanceof Request;
|
|
61
|
+
const method = String((init && init.method) || (isRequest ? input.method : "GET")).toUpperCase();
|
|
62
|
+
let target;
|
|
63
|
+
try { target = new URL(isRequest ? input.url : String(input), location.href); } catch { return nativeFetch(input, init); }
|
|
64
|
+
const sameOrigin = target.origin === location.origin;
|
|
65
|
+
if (sameOrigin && method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
|
|
66
|
+
const value = await csrf();
|
|
67
|
+
if (value) {
|
|
68
|
+
const headers = new Headers((init && init.headers) || (isRequest ? input.headers : undefined));
|
|
69
|
+
headers.set("X-Agend-CSRF", value);
|
|
70
|
+
init = Object.assign({}, init, { headers });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const response = await nativeFetch(input, init);
|
|
74
|
+
if (sameOrigin && response.status === 401 && !target.pathname.startsWith("/auth/")) sessionEndedBanner();
|
|
75
|
+
return response;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
window.AgendAuth = {
|
|
79
|
+
csrf,
|
|
80
|
+
async signOut() {
|
|
81
|
+
const value = await csrf();
|
|
82
|
+
const r = await nativeFetch("/auth/logout", { method: "POST", credentials: "same-origin", headers: value ? { "X-Agend-CSRF": value } : {} });
|
|
83
|
+
// A sign-out the server could not make durable is not quietly reported as done: say so before leaving.
|
|
84
|
+
if (r.status === 500) {
|
|
85
|
+
let message = "Signed out for now, but the change could not be saved — a fleet restart may sign this browser back in.";
|
|
86
|
+
try { const body = await r.json(); if (body && typeof body.error === "string") message = body.error; } catch { /* keep the default */ }
|
|
87
|
+
// Said on the sign-in page this goes to (#1408 step 5: no browser alert): the page shows it once.
|
|
88
|
+
try { sessionStorage.setItem("agend_signout_note", message); } catch { /* private mode: nothing to carry it */ }
|
|
89
|
+
}
|
|
90
|
+
location.href = "/signin";
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
})();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// #1408 §4: what a panel may own while it is mounted, and nothing it can leak.
|
|
2
|
+
//
|
|
3
|
+
// Every navigation — a new panel, the same panel on another route (View A → B), a language switch, Retry — gets a
|
|
4
|
+
// new lease. Work started under a lease asks lease.current() after EVERY await and before any DOM write, title,
|
|
5
|
+
// focus or action it publishes; once the lease is superseded or disposed the answer is false and the continuation
|
|
6
|
+
// does nothing. An AbortController cannot do this alone: a fetch that already has its body, or an import, still
|
|
7
|
+
// resumes. Timers, listeners, fetches and stream subscriptions taken through the lease are released by dispose().
|
|
8
|
+
import { useEffect, useMemo, useRef } from "./app-html.js";
|
|
9
|
+
|
|
10
|
+
let leases = 0;
|
|
11
|
+
const live = new Set(); // leases not yet disposed (the leak tests read leaseCount())
|
|
12
|
+
|
|
13
|
+
export function createLease(timers = globalThis) {
|
|
14
|
+
const id = ++leases;
|
|
15
|
+
let alive = true;
|
|
16
|
+
const timeouts = new Set(), intervals = new Set(), offs = new Set(), aborts = new Set();
|
|
17
|
+
const lease = {
|
|
18
|
+
id,
|
|
19
|
+
current: () => alive,
|
|
20
|
+
/** setTimeout that never fires after dispose, and is cleared by it. */
|
|
21
|
+
timeout(fn, ms) {
|
|
22
|
+
if (!alive) return null;
|
|
23
|
+
const h = timers.setTimeout(() => { timeouts.delete(h); if (alive) fn(); }, ms);
|
|
24
|
+
timeouts.add(h);
|
|
25
|
+
return h;
|
|
26
|
+
},
|
|
27
|
+
interval(fn, ms) {
|
|
28
|
+
if (!alive) return null;
|
|
29
|
+
const h = timers.setInterval(() => { if (alive) fn(); }, ms);
|
|
30
|
+
intervals.add(h);
|
|
31
|
+
return h;
|
|
32
|
+
},
|
|
33
|
+
clear(h) { if (timeouts.delete(h)) timers.clearTimeout(h); if (intervals.delete(h)) timers.clearInterval(h); },
|
|
34
|
+
/** addEventListener, removed on dispose. */
|
|
35
|
+
on(target, type, fn, opts) {
|
|
36
|
+
if (!alive || !target) return () => {};
|
|
37
|
+
target.addEventListener(type, fn, opts);
|
|
38
|
+
const off = () => { if (offs.delete(off)) target.removeEventListener(type, fn, opts); };
|
|
39
|
+
offs.add(off);
|
|
40
|
+
return off;
|
|
41
|
+
},
|
|
42
|
+
/** Keep an unsubscribe function (a stream subscription, a store listener) until dispose. */
|
|
43
|
+
hold(off) { if (!alive) { off(); return off; } offs.add(off); return off; },
|
|
44
|
+
/** fetch with this lease's abort signal. The caller still checks current() after each await. */
|
|
45
|
+
fetch(url, opts = {}) {
|
|
46
|
+
const ac = new AbortController();
|
|
47
|
+
aborts.add(ac);
|
|
48
|
+
return timers.fetch(url, { ...opts, signal: ac.signal }).finally(() => aborts.delete(ac));
|
|
49
|
+
},
|
|
50
|
+
dispose() {
|
|
51
|
+
if (!alive) return;
|
|
52
|
+
alive = false;
|
|
53
|
+
live.delete(lease);
|
|
54
|
+
for (const h of timeouts) timers.clearTimeout(h);
|
|
55
|
+
for (const h of intervals) timers.clearInterval(h);
|
|
56
|
+
for (const off of [...offs]) off();
|
|
57
|
+
for (const ac of aborts) ac.abort();
|
|
58
|
+
timeouts.clear(); intervals.clear(); offs.clear(); aborts.clear();
|
|
59
|
+
},
|
|
60
|
+
/** For the leak tests: what this lease still holds. */
|
|
61
|
+
holding: () => timeouts.size + intervals.size + offs.size + aborts.size,
|
|
62
|
+
};
|
|
63
|
+
live.add(lease);
|
|
64
|
+
return lease;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Leases created and not yet disposed, across the page (the 50-mount leak tests assert this returns to its base). */
|
|
68
|
+
export function leaseCount() { return live.size; }
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A panel's lease for one navigation: a new one whenever `key` changes (route, language, retry), the old one
|
|
72
|
+
* disposed before the new route's effects run, and the last one disposed on unmount.
|
|
73
|
+
*/
|
|
74
|
+
export function useLease(key) {
|
|
75
|
+
const prev = useRef(null);
|
|
76
|
+
// The old lease ends the moment the new navigation renders, not one effect later: nothing of A lands after B began.
|
|
77
|
+
const lease = useMemo(() => { if (prev.current) prev.current.dispose(); return (prev.current = createLease()); }, [key]);
|
|
78
|
+
useEffect(() => () => lease.dispose(), [lease]);
|
|
79
|
+
return lease;
|
|
80
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// #1408: the app's view layer — the vendored Preact (10.29.8) and htm (3.1.1), byte-checked against
|
|
2
|
+
// vendor/vendor.json. Templates are tagged literals (html`<div class="x">…</div>`): no JSX, no build step.
|
|
3
|
+
// No `style` prop anywhere (the CSP has no 'unsafe-inline' for styles): classes and tokens only.
|
|
4
|
+
import { h, render, Fragment, createContext, createRef, cloneElement, toChildArray } from "./preact.module.js";
|
|
5
|
+
import htm from "./htm.module.js";
|
|
6
|
+
|
|
7
|
+
export { h, render, Fragment, createContext, createRef, cloneElement, toChildArray };
|
|
8
|
+
export { useState, useEffect, useLayoutEffect, useRef, useMemo, useCallback, useContext, useReducer, useId } from "./preact-hooks.module.js";
|
|
9
|
+
export const html = htm.bind(h);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// #1408 §4: one dictionary for the app, namespaced (app.*, chat.*, fleet.*, …). A panel registers its own namespace
|
|
2
|
+
// when it loads; t("chat.send") resolves it in the chosen language and falls back to English, then to the key.
|
|
3
|
+
// The language is this browser's choice (agend_lang, the key every panel has used), else the device's.
|
|
4
|
+
|
|
5
|
+
const tables = { en: {}, "zh-TW": {} };
|
|
6
|
+
const listeners = new Set();
|
|
7
|
+
|
|
8
|
+
function stored() {
|
|
9
|
+
try { const v = localStorage.getItem("agend_lang"); return v === "en" || v === "zh-TW" ? v : null; } catch { return null; }
|
|
10
|
+
}
|
|
11
|
+
let current = stored() || (typeof navigator !== "undefined" && /^zh/i.test(navigator.language || "") ? "zh-TW" : "en");
|
|
12
|
+
|
|
13
|
+
/** Add a namespace: `register("chat", { en: {...}, "zh-TW": {...} })`. */
|
|
14
|
+
export function register(ns, byLang) {
|
|
15
|
+
for (const lang of Object.keys(tables)) {
|
|
16
|
+
for (const [k, v] of Object.entries((byLang && byLang[lang]) || {})) tables[lang][`${ns}.${k}`] = v;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function lang() { return current; }
|
|
21
|
+
|
|
22
|
+
/** Choose a language for this browser; every subscriber re-renders (a new navigation lease, §4). */
|
|
23
|
+
export function setLang(next) {
|
|
24
|
+
if (next !== "en" && next !== "zh-TW") return;
|
|
25
|
+
current = next;
|
|
26
|
+
try { localStorage.setItem("agend_lang", next); } catch { /* this page only */ }
|
|
27
|
+
if (typeof document !== "undefined" && document.documentElement) document.documentElement.lang = next === "zh-TW" ? "zh-Hant" : "en";
|
|
28
|
+
for (const fn of listeners) fn(next);
|
|
29
|
+
}
|
|
30
|
+
export function onLang(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
|
31
|
+
|
|
32
|
+
/** "{0} is working…" with its values filled in. Text only: callers render it as text, never as markup. */
|
|
33
|
+
export function t(key, ...values) {
|
|
34
|
+
let s = tables[current][key] ?? tables.en[key] ?? key;
|
|
35
|
+
values.forEach((v, i) => { s = s.split(`{${i}}`).join(String(v)); });
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
register("app", {
|
|
40
|
+
en: {
|
|
41
|
+
brand: "AgEnD", fleet: "Fleet", view: "View", settings: "Settings", instances: "Instances", newInstance: "New instance",
|
|
42
|
+
collapse: "Hide the sidebar", expand: "Show the sidebar", openMenu: "Open the sidebar", closeMenu: "Close the sidebar",
|
|
43
|
+
theme: "Theme", themeSystem: "System", themeLight: "Light", themeDark: "Dark", language: "Language",
|
|
44
|
+
previews: "Allow HTML previews on this device", tour: "Tour", tourTitle: "Show the web chat tour again",
|
|
45
|
+
needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat",
|
|
46
|
+
statusRunning: "running", statusStopped: "stopped", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
|
|
47
|
+
connPolling: "Live updates paused — refreshing every 5 s", connDown: "Disconnected — trying again…", connConnecting: "Connecting…",
|
|
48
|
+
hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
|
|
49
|
+
skip: "Skip to the conversation", loadFailed: "This could not be loaded.", retry: "Try again", loading: "Loading…",
|
|
50
|
+
noInstances: "No instances yet", noInstancesHint: "Create one to start chatting.",
|
|
51
|
+
session: "Session", signIn: "Sign in", signedIn: "Signed in", signedOut: "Not signed in", thisDevice: "This device",
|
|
52
|
+
lastActive: "active {0}", justNow: "just now", minAgo: "{0} min ago", hrAgo: "{0} h ago", signOut: "Sign out",
|
|
53
|
+
signOutAll: "Sign out everywhere", devices: "Signed-in devices", expires: "Ends by {0} at the latest, or after {1} unused",
|
|
54
|
+
failed: "That did not work — try again.", close: "Close", more: "More actions", menu: "Menu",
|
|
55
|
+
pendingTitle: "Changes waiting for confirmation", pending_pending: "Waiting for confirmation", pending_applying: "Confirmed — applying…",
|
|
56
|
+
pending_applied: "Confirmed and applied", pending_rejected: "Not applied", pending_expired: "Expired — nothing was changed",
|
|
57
|
+
pending_stale: "Not applied — settings changed meanwhile", pending_failed: "Confirmed, but applying it failed",
|
|
58
|
+
pendingChat: "A fleet admin confirms this in chat (General topic).", pendingHost: "Confirm it on the host: {0}", pendingLeft: "{0} left", withdraw: "Withdraw",
|
|
59
|
+
opWriting: "Applying settings… {0}/{1}", opWaiting: "Settings: waiting for confirmation in chat", opApplying: "Applying settings…",
|
|
60
|
+
opDone: "Settings applied", opRestart: "Settings saved — AgEnD needs a restart to use them", opPartial: "Some settings were not applied",
|
|
61
|
+
opFailed: "Settings were not applied", openSettings: "Open Settings",
|
|
62
|
+
pendingSent: "Sent for confirmation — a fleet admin confirms it in chat.",
|
|
63
|
+
needsNav: "Needs you", needsTab: "Needs you", needsCount: "{0} waiting",
|
|
64
|
+
confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
|
|
65
|
+
// The server's own wording (src/locale.ts needs.reason.*), so the web says what Discord says.
|
|
66
|
+
needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed",
|
|
67
|
+
},
|
|
68
|
+
"zh-TW": {
|
|
69
|
+
brand: "AgEnD", fleet: "Fleet", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance",
|
|
70
|
+
collapse: "隱藏側邊欄", expand: "顯示側邊欄", openMenu: "打開側邊欄", closeMenu: "關閉側邊欄",
|
|
71
|
+
theme: "主題", themeSystem: "跟隨系統", themeLight: "淺色", themeDark: "深色", language: "語言",
|
|
72
|
+
previews: "允許這台裝置預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
|
|
73
|
+
needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天",
|
|
74
|
+
statusRunning: "執行中", statusStopped: "已停止", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
|
|
75
|
+
connPolling: "即時更新暫停中——每 5 秒重新整理", connDown: "已斷線——正在重試…", connConnecting: "連線中…",
|
|
76
|
+
hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
|
|
77
|
+
skip: "跳到對話", loadFailed: "無法載入。", retry: "再試一次", loading: "載入中…",
|
|
78
|
+
noInstances: "還沒有 instance", noInstancesHint: "建立一個就能開始聊天。",
|
|
79
|
+
session: "登入狀態", signIn: "登入", signedIn: "已登入", signedOut: "尚未登入", thisDevice: "這台裝置",
|
|
80
|
+
lastActive: "{0}活動", justNow: "剛剛", minAgo: "{0} 分鐘前", hrAgo: "{0} 小時前", signOut: "登出",
|
|
81
|
+
signOutAll: "全部登出", devices: "已登入的裝置", expires: "最晚 {0} 結束;閒置 {1} 也會結束",
|
|
82
|
+
failed: "沒有成功,請再試一次。", close: "關閉", more: "更多動作", menu: "選單",
|
|
83
|
+
pendingTitle: "等待確認的變更", pending_pending: "等待確認", pending_applying: "已確認——套用中…",
|
|
84
|
+
pending_applied: "已確認並套用", pending_rejected: "未套用", pending_expired: "已逾時——沒有任何變更",
|
|
85
|
+
pending_stale: "未套用——設定在這期間已變動", pending_failed: "已確認,但套用失敗",
|
|
86
|
+
pendingChat: "由 fleet 管理員在聊天(General topic)中確認。", pendingHost: "請在主機上確認:{0}", pendingLeft: "剩 {0}", withdraw: "撤回",
|
|
87
|
+
opWriting: "正在套用設定… {0}/{1}", opWaiting: "設定:等待在聊天中確認", opApplying: "正在套用設定…",
|
|
88
|
+
opDone: "設定已套用", opRestart: "設定已儲存——需要重新啟動 AgEnD 才會生效", opPartial: "部分設定沒有套用",
|
|
89
|
+
opFailed: "設定沒有套用", openSettings: "開啟設定",
|
|
90
|
+
pendingSent: "已送出確認——由 fleet 管理員在聊天中確認。",
|
|
91
|
+
needsNav: "等你處理", needsTab: "待處理", needsCount: "{0} 件待處理",
|
|
92
|
+
confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
|
|
93
|
+
needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// #1408 §3: navigation without reloads. Links stay real <a href>s; a plain left click on one that names a route of the
|
|
2
|
+
// app is taken over (pushState), and Back/Forward follow (popstate). A modified or middle click, a download, another
|
|
3
|
+
// origin, or a path that is not a page of the app is left to the browser.
|
|
4
|
+
// Every navigation bumps `seq`: panels key their lease (app-ctx.js) on the route, so work from the route left behind
|
|
5
|
+
// can no longer land.
|
|
6
|
+
// A panel with work that leaving would lose (Settings' staged changes, §5) sets a leave guard: every navigation of
|
|
7
|
+
// the app asks it first, Back/Forward included (the address bar is put back when it says no).
|
|
8
|
+
import { parseRoute, legacyHashTarget } from "./app-route.js";
|
|
9
|
+
import { createStore } from "./app-store.js";
|
|
10
|
+
|
|
11
|
+
export const navStore = createStore({ route: null, seq: 0 });
|
|
12
|
+
|
|
13
|
+
let win = null;
|
|
14
|
+
let guard = null;
|
|
15
|
+
let here = null; // the path the app shows now (popstate has already changed the address)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `fn(route, path)` → false keeps the app where it is. A guard that has to ask (the app's own dialog, which answers
|
|
19
|
+
* later) returns false at once and, on a yes, goes on with `navigate(path, { force: true })`. One guard at a time;
|
|
20
|
+
* returns the remover, which only removes this one.
|
|
21
|
+
*/
|
|
22
|
+
export function setLeaveGuard(fn) {
|
|
23
|
+
guard = fn;
|
|
24
|
+
return () => { if (guard === fn) guard = null; };
|
|
25
|
+
}
|
|
26
|
+
const mayLeave = (route, path) => !guard || guard(route, path) !== false;
|
|
27
|
+
|
|
28
|
+
/** Start following the address bar. An old /ui#instance=<name> link becomes /ui/chat/<name> before the first render. */
|
|
29
|
+
export function startRouter(w = window) {
|
|
30
|
+
win = w;
|
|
31
|
+
const legacy = legacyHashTarget(w.location.pathname, w.location.hash);
|
|
32
|
+
if (legacy) w.history.replaceState(null, "", legacy);
|
|
33
|
+
here = w.location.pathname + w.location.search;
|
|
34
|
+
navStore.set({ route: parseRoute(w.location.pathname), seq: 1 });
|
|
35
|
+
w.addEventListener("popstate", () => {
|
|
36
|
+
const route = parseRoute(w.location.pathname);
|
|
37
|
+
const path = w.location.pathname + w.location.search;
|
|
38
|
+
if (!mayLeave(route, path)) { w.history.pushState(null, "", here); return; }
|
|
39
|
+
here = w.location.pathname + w.location.search;
|
|
40
|
+
navStore.set(s => ({ route, seq: s.seq + 1 }));
|
|
41
|
+
});
|
|
42
|
+
w.document.addEventListener("click", onLinkClick);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Go to `path`. A path that is not a route of the app is a full load. `force` skips the leave guard (it said yes). */
|
|
46
|
+
export function navigate(path, opts = {}) {
|
|
47
|
+
const url = new URL(path, win.location.href);
|
|
48
|
+
const route = parseRoute(url.pathname);
|
|
49
|
+
if (!route || url.origin !== win.location.origin) { win.location.assign(url.href); return; }
|
|
50
|
+
if (!opts.force && !mayLeave(route, url.pathname + url.search)) return;
|
|
51
|
+
if (url.pathname + url.search !== win.location.pathname + win.location.search) {
|
|
52
|
+
win.history[opts.replace ? "replaceState" : "pushState"](null, "", url.pathname + url.search);
|
|
53
|
+
}
|
|
54
|
+
here = url.pathname + url.search;
|
|
55
|
+
navStore.set(s => ({ route, seq: s.seq + 1 }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The same route again (Retry, a language switch): a new lease, no history entry. */
|
|
59
|
+
export function renavigate() { navStore.set(s => ({ route: s.route, seq: s.seq + 1 })); }
|
|
60
|
+
|
|
61
|
+
export function onLinkClick(e) {
|
|
62
|
+
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
63
|
+
const a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
|
|
64
|
+
if (!a || (a.target && a.target !== "_self") || a.hasAttribute("download")) return;
|
|
65
|
+
// A link to a place on this page (the skip link, #main) is the browser's: it moves focus and scrolls, and is not
|
|
66
|
+
// a navigation of the app (#1425 review).
|
|
67
|
+
if ((a.getAttribute("href") || "").startsWith("#")) return;
|
|
68
|
+
const url = new URL(a.getAttribute("href"), win.location.href);
|
|
69
|
+
if (url.hash && url.pathname === win.location.pathname && url.search === win.location.search) return;
|
|
70
|
+
if (url.origin !== win.location.origin || !parseRoute(url.pathname)) return;
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
navigate(url.pathname + url.search);
|
|
73
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// #1386 §6.3 (#1408 step 4): browser notifications for "Needs you", per device and opt-in, desktop browsers only.
|
|
2
|
+
//
|
|
3
|
+
// The list itself is the app store's `needs` (SSE `needs` / the /ui/poll field — no read of its own, #1374). This
|
|
4
|
+
// module only watches it, while a signed-in page is open in any panel:
|
|
5
|
+
// - a notification only for an item id this page has not seen, and only while the page is hidden (a visible page
|
|
6
|
+
// shows the list; what it showed is seen);
|
|
7
|
+
// - a terminal wait (awaiting_input) only once it is ≥ 5 s old, so a dialog AgEnD answers by itself never pings
|
|
8
|
+
// anyone; the list is never delayed;
|
|
9
|
+
// - tag = item id, so the same item never stacks; a click focuses the tab and opens that instance's chat.
|
|
10
|
+
// The toggle asks Notification.requestPermission() on that click only. Not offered on a phone (the constructor is
|
|
11
|
+
// missing or throws there; Discord's notification is the one to rely on) or on a plain-HTTP LAN address.
|
|
12
|
+
import { appStore, createStore } from "./app-store.js";
|
|
13
|
+
import { t } from "./app-i18n.js";
|
|
14
|
+
|
|
15
|
+
const STORE_KEY = "agend_needs_notify";
|
|
16
|
+
const TERMINAL_WAIT_MS = 5000;
|
|
17
|
+
const now = () => Date.now(); // item.since is the server's epoch ms: an age compares wall clocks
|
|
18
|
+
|
|
19
|
+
/** Why notifications cannot be offered here, or "ok". */
|
|
20
|
+
export function notifySupport(env = globalThis) {
|
|
21
|
+
if (typeof env.Notification !== "function") return "unsupported";
|
|
22
|
+
const ua = (env.navigator && env.navigator.userAgent) || "";
|
|
23
|
+
if (/Android|iPhone|iPad|iPod|Mobile/i.test(ua)) return "mobile";
|
|
24
|
+
if (env.isSecureContext === false) return "insecure";
|
|
25
|
+
return "ok";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stored() { try { return localStorage.getItem(STORE_KEY) === "1"; } catch { return false; } }
|
|
29
|
+
function store(on) { try { if (on) localStorage.setItem(STORE_KEY, "1"); else localStorage.removeItem(STORE_KEY); } catch { /* this page only */ } }
|
|
30
|
+
|
|
31
|
+
/** On: chosen on this device and still permitted by the browser. */
|
|
32
|
+
export function notifyOn(env = globalThis) {
|
|
33
|
+
return notifySupport(env) === "ok" && stored() && env.Notification.permission === "granted";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The choice as the page knows it, for every toggle mounted now or later (#1463 review: a toggle that mounted while a
|
|
38
|
+
* permission request was open must still learn its answer). `state`: "on" | "off" | "denied"; `asking` while the
|
|
39
|
+
* browser's permission prompt is open (the toggle is off meanwhile, and a second click asks nothing).
|
|
40
|
+
*/
|
|
41
|
+
export const notifyStore = createStore({ state: (() => { try { return notifyOn() ? "on" : "off"; } catch { return "off"; } })(), asking: false });
|
|
42
|
+
export function refreshNotify(env = globalThis) {
|
|
43
|
+
notifyStore.set((s) => ({ ...s, state: notifyOn(env) ? "on" : s.state === "denied" ? "denied" : "off" }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The toggle: asks the browser on this click only. Resolves to the state after it ("on", "denied", "off"). */
|
|
47
|
+
export async function setNotify(on, env = globalThis) {
|
|
48
|
+
if (notifyStore.get().asking) return notifyStore.get().state;
|
|
49
|
+
if (!on) { store(false); notifyStore.set({ state: "off", asking: false }); return "off"; }
|
|
50
|
+
if (notifySupport(env) !== "ok") return "off";
|
|
51
|
+
let permission = env.Notification.permission;
|
|
52
|
+
if (permission !== "granted") {
|
|
53
|
+
notifyStore.set((s) => ({ ...s, asking: true }));
|
|
54
|
+
try { permission = await env.Notification.requestPermission(); } catch { permission = "denied"; }
|
|
55
|
+
}
|
|
56
|
+
const state = permission === "granted" ? "on" : "denied";
|
|
57
|
+
store(state === "on");
|
|
58
|
+
notifyStore.set({ state, asking: false });
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const reasonText = (item) => t(`app.needs_reason_${item.reason}`);
|
|
63
|
+
|
|
64
|
+
let stop = null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Follow the store for the life of the page (full mode only). `open(instance)` is how a click opens a chat.
|
|
68
|
+
* Returns the stop function (for tests); a second start replaces the first.
|
|
69
|
+
*/
|
|
70
|
+
export function startNeedsNotifier({ open, env = globalThis } = {}) {
|
|
71
|
+
if (stop) stop();
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const timers = new Map(); // id → a terminal wait's 5-s check
|
|
74
|
+
const doc = env.document;
|
|
75
|
+
const hidden = () => !!(doc && doc.hidden);
|
|
76
|
+
const notify = (item) => {
|
|
77
|
+
seen.add(item.id);
|
|
78
|
+
if (!notifyOn(env)) return;
|
|
79
|
+
try {
|
|
80
|
+
const n = new env.Notification(item.instance, { body: item.detail ? `${reasonText(item)}: ${item.detail}` : reasonText(item), tag: item.id });
|
|
81
|
+
n.onclick = () => { try { env.focus && env.focus(); } catch { /* ignore */ } if (open) open(item.instance); n.close(); };
|
|
82
|
+
} catch { /* a browser that refuses at the last moment: the list still shows it */ }
|
|
83
|
+
};
|
|
84
|
+
const consider = (items) => {
|
|
85
|
+
const listed = new Set(items.map((i) => i.id));
|
|
86
|
+
for (const [id, h] of timers) if (!listed.has(id)) { clearTimeout(h); timers.delete(id); }
|
|
87
|
+
for (const item of items) {
|
|
88
|
+
if (seen.has(item.id) || timers.has(item.id)) continue;
|
|
89
|
+
if (!hidden()) { seen.add(item.id); continue; }
|
|
90
|
+
const wait = item.type === "awaiting_input" ? TERMINAL_WAIT_MS - (now() - item.since) : 0;
|
|
91
|
+
if (wait > 0) {
|
|
92
|
+
timers.set(item.id, setTimeout(() => {
|
|
93
|
+
timers.delete(item.id);
|
|
94
|
+
const still = (appStore.get().needs || []).find((i) => i.id === item.id);
|
|
95
|
+
if (!still || seen.has(item.id)) return;
|
|
96
|
+
if (hidden()) notify(still); else seen.add(item.id);
|
|
97
|
+
}, wait));
|
|
98
|
+
} else notify(item);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
// Coming back to the page: what is listed now has been seen.
|
|
102
|
+
const onVisible = () => { if (!hidden()) for (const i of appStore.get().needs || []) seen.add(i.id); };
|
|
103
|
+
if (doc && doc.addEventListener) doc.addEventListener("visibilitychange", onVisible);
|
|
104
|
+
consider(appStore.get().needs || []);
|
|
105
|
+
const off = appStore.subscribe((s) => consider(Array.isArray(s.needs) ? s.needs : []));
|
|
106
|
+
stop = () => {
|
|
107
|
+
off();
|
|
108
|
+
if (doc && doc.removeEventListener) doc.removeEventListener("visibilitychange", onVisible);
|
|
109
|
+
for (const h of timers.values()) clearTimeout(h);
|
|
110
|
+
timers.clear();
|
|
111
|
+
stop = null;
|
|
112
|
+
};
|
|
113
|
+
return stop;
|
|
114
|
+
}
|