@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
package/dist/ui/settings.html
DELETED
|
@@ -1,2165 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>AgEnD Settings</title>
|
|
7
|
-
<style>
|
|
8
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
-
:root {
|
|
10
|
-
--bg:#ffffff; --card:#f9fafb; --border:#e5e7eb; --fg:#111827; --muted:#6b7280;
|
|
11
|
-
--accent:#2563eb; --accent-hover:#1d4ed8; --green:#059669; --red:#dc2626; --yellow:#b45309;
|
|
12
|
-
--radius:8px; --radius-sm:6px;
|
|
13
|
-
}
|
|
14
|
-
body { background: var(--bg); color: var(--fg); font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 14px; line-height: 1.5; padding-bottom: 88px; }
|
|
15
|
-
header { display: flex; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg); z-index: 20; }
|
|
16
|
-
header h1 { font-size: 18px; font-weight: 700; }
|
|
17
|
-
.grow { flex: 1; }
|
|
18
|
-
main { max-width: 860px; margin: 0 auto; padding: 24px; }
|
|
19
|
-
section { margin-bottom: 28px; }
|
|
20
|
-
.sec-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
|
21
|
-
.sec-head h2 { font-size: 16px; font-weight: 600; }
|
|
22
|
-
.sec-head .grow { flex: 1; }
|
|
23
|
-
.card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
24
|
-
.card.empty { padding: 16px; color: var(--muted); }
|
|
25
|
-
.item { padding: 9px 16px; border-bottom: 1px solid var(--border); }
|
|
26
|
-
.item:last-child { border-bottom: none; }
|
|
27
|
-
.item-row { display: flex; align-items: center; gap: 10px; min-height: 24px; }
|
|
28
|
-
.group-head { display: flex; align-items: center; gap: 8px; padding: 7px 16px; cursor: pointer; font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .3px; background: #f3f4f6; border-bottom: 1px solid var(--border); user-select: none; }
|
|
29
|
-
.group-head:hover { color: var(--fg); }
|
|
30
|
-
.group-head .caret { width: 12px; flex: 0 0 12px; font-size: 10px; }
|
|
31
|
-
.group-head .gcount { color: var(--muted); font-weight: 400; }
|
|
32
|
-
.item .del-btn { visibility: hidden; }
|
|
33
|
-
.item:hover .del-btn { visibility: visible; }
|
|
34
|
-
.dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 9px; background: #9ca3af; }
|
|
35
|
-
.dot.running, .dot.idle { background: var(--green); } .dot.working { background:var(--accent); } .dot.paused { background:#f59e0b; } .dot.crashed, .dot.stuck { background: var(--red); } .dot.stopped { background: #9ca3af; }
|
|
36
|
-
.name { font-weight: 600; }
|
|
37
|
-
.tag { font-size: 12px; color: var(--muted); background: #eef2ff; color: #3730a3; padding: 1px 7px; border-radius: 10px; }
|
|
38
|
-
.tag.persona { background: #fdf2f8; color: #9d174d; }
|
|
39
|
-
.tag.tg { background: #e0f2fe; color: #075985; }
|
|
40
|
-
.tag.se-default { background: transparent; color: var(--muted); border: 1px dashed var(--border); }
|
|
41
|
-
.sub { font-size: 13px; color: var(--muted); margin-top: 3px; margin-left: 19px; }
|
|
42
|
-
.spacer { flex: 1; }
|
|
43
|
-
/* "Connections & Bots" rows carry up to ten short facts. Left to shrink, every one of them wraps inside
|
|
44
|
-
its own box (a chip becomes two lines, "存取模式:" and "設定" break mid-word) and the tail is clipped by
|
|
45
|
-
the card. So: no fact wraps inside itself; when the row is too long it wraps between facts, and the
|
|
46
|
-
status/settings cluster stays together at the end. Long values (env names, user ids) may break
|
|
47
|
-
anywhere rather than push the row wider than its card. */
|
|
48
|
-
.conn-row { flex-wrap: wrap; row-gap: 6px; }
|
|
49
|
-
.conn-row > * { flex: 0 0 auto; max-width: 100%; }
|
|
50
|
-
.conn-row .name, .conn-row .tag, .conn-row button, .conn-row .row-end > * { white-space: nowrap; }
|
|
51
|
-
.conn-row .tag { overflow: hidden; text-overflow: ellipsis; }
|
|
52
|
-
.conn-row .sub { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; }
|
|
53
|
-
.conn-row .row-end { display: inline-flex; align-items: center; gap: 10px; margin-left: auto; }
|
|
54
|
-
button { font-family: inherit; font-size: 13px; border-radius: var(--radius-sm); padding: 6px 12px; border: 1px solid var(--border); background: #fff; color: var(--fg); cursor: pointer; }
|
|
55
|
-
button:hover { background: #f3f4f6; }
|
|
56
|
-
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
|
|
57
|
-
button.primary:hover { background: var(--accent-hover); }
|
|
58
|
-
button.ghost { border-color: transparent; background: transparent; color: var(--muted); }
|
|
59
|
-
button.ghost:hover { background: #f3f4f6; color: var(--fg); }
|
|
60
|
-
button.danger { color: var(--red); }
|
|
61
|
-
button.danger:hover { background: #fef2f2; }
|
|
62
|
-
button.sm { padding: 4px 9px; font-size: 12px; }
|
|
63
|
-
label { display: block; font-size: 12px; font-weight: 500; color: var(--muted); margin: 10px 0 4px; }
|
|
64
|
-
input[type=text], input[type=password], input[type=number], select { width: 100%; font-family: inherit; font-size: 13px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; color: var(--fg); }
|
|
65
|
-
input:focus, select:focus { outline: 2px solid #bfdbfe; border-color: var(--accent); }
|
|
66
|
-
.edit { padding: 14px 16px; background: #fff; border-top: 1px solid var(--border); }
|
|
67
|
-
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
68
|
-
.actions { display: flex; gap: 8px; margin-top: 12px; align-items: center; flex-wrap: wrap; }
|
|
69
|
-
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
|
|
70
|
-
.chip { display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--border); border-radius: 14px; padding: 3px 6px 3px 10px; font-size: 12px; }
|
|
71
|
-
.chip .x { cursor: pointer; color: var(--muted); font-weight: 700; } .chip .x:hover { color: var(--red); }
|
|
72
|
-
.chip-add { display: flex; gap: 6px; margin-top: 6px; max-width: 320px; }
|
|
73
|
-
.field { padding: 12px 16px; border-bottom: 1px solid var(--border); }
|
|
74
|
-
.field:last-child { border-bottom: none; }
|
|
75
|
-
.msg { font-size: 12px; margin-left: 4px; }
|
|
76
|
-
.msg .e { color: var(--red); } .msg .w { color: #b45309; } .msg .s { color: var(--green); }
|
|
77
|
-
.hint { font-size: 12px; color: var(--muted); margin-top: 6px; }
|
|
78
|
-
.impact { display:inline-block; font-size:11px; color:var(--muted); margin-left:6px; font-weight:400; }
|
|
79
|
-
.feedback { min-height:17px; font-size:12px; margin-top:3px; }
|
|
80
|
-
.feedback.error { color:var(--red); } .feedback.warning { color:var(--yellow); }
|
|
81
|
-
input.invalid, select.invalid { border-color:var(--red); background:#fef2f2; }
|
|
82
|
-
button:disabled { opacity:.5; cursor:not-allowed; }
|
|
83
|
-
.settings-section { border-bottom:1px solid var(--border); background:#fff; }
|
|
84
|
-
.settings-section:last-child { border-bottom:none; }
|
|
85
|
-
.settings-section > summary { cursor:pointer; list-style:none; padding:12px 16px; font-weight:650; background:#f9fafb; }
|
|
86
|
-
.settings-section > summary::-webkit-details-marker { display:none; }
|
|
87
|
-
.settings-section > summary::before { content:"▶"; display:inline-block; width:18px; color:var(--muted); font-size:10px; }
|
|
88
|
-
.settings-section[open] > summary::before { content:"▼"; }
|
|
89
|
-
.section-body { padding:0 0 2px; }
|
|
90
|
-
.inherit-row { display:flex; gap:8px; align-items:center; margin-top:6px; color:var(--muted); font-size:12px; }
|
|
91
|
-
.adv-body { display: none; padding: 14px 16px; }
|
|
92
|
-
.adv-body.open { display: block; }
|
|
93
|
-
/* One expandable drawer instead of a hidden third level: nothing is removed,
|
|
94
|
-
everything is one click away. */
|
|
95
|
-
.drawer { border:1px solid var(--border); border-radius:var(--radius-sm); margin-top:12px; background:var(--card); }
|
|
96
|
-
.drawer > summary { cursor:pointer; padding:10px 12px; font-weight:600; font-size:13px; list-style:none; }
|
|
97
|
-
.drawer > summary::-webkit-details-marker { display:none; }
|
|
98
|
-
.drawer > summary::before { content:"▶ "; color:var(--muted); }
|
|
99
|
-
.drawer[open] > summary::before { content:"▼ "; }
|
|
100
|
-
.drawer > .drawer-body { padding:0 12px 12px; }
|
|
101
|
-
.modal-backdrop { position:fixed; inset:0; background:rgba(17,24,39,.45); z-index:40; display:none; }
|
|
102
|
-
.modal-backdrop.open { display:block; }
|
|
103
|
-
.modal { position:fixed; z-index:41; inset:0; display:none; align-items:center; justify-content:center; padding:16px; }
|
|
104
|
-
.modal.open { display:flex; }
|
|
105
|
-
.modal-panel { background:var(--bg); border-radius:var(--radius); width:min(720px, 100%); max-height:88vh; display:flex; flex-direction:column; box-shadow:0 18px 48px rgba(0,0,0,.22); }
|
|
106
|
-
.modal-head { display:flex; align-items:baseline; gap:10px; padding:16px 20px 8px; border-bottom:1px solid var(--border); }
|
|
107
|
-
.modal-head h3 { font-size:16px; }
|
|
108
|
-
.modal-body { padding:16px 20px; overflow:auto; }
|
|
109
|
-
.modal-foot { display:flex; align-items:center; gap:10px; padding:12px 20px; border-top:1px solid var(--border); flex-wrap:wrap; }
|
|
110
|
-
.modal-foot .grow { flex:1; }
|
|
111
|
-
.more-menu { position:relative; }
|
|
112
|
-
.more-menu summary { list-style:none; cursor:pointer; padding:4px 9px; color:var(--muted); }
|
|
113
|
-
.more-menu summary::-webkit-details-marker { display:none; }
|
|
114
|
-
.more-menu .menu { position:absolute; right:0; top:30px; z-index:10; background:#fff; border:1px solid var(--border); border-radius:var(--radius-sm); box-shadow:0 8px 24px rgba(0,0,0,.12); padding:5px; min-width:130px; }
|
|
115
|
-
.more-menu .menu button { width:100%; text-align:left; border:0; }
|
|
116
|
-
.pending-bar { position:fixed; left:0; right:0; bottom:0; display:none; align-items:center; gap:12px; padding:12px max(16px, calc((100vw - 860px) / 2 + 24px)); background:#111827; color:#fff; z-index:35; box-shadow:0 -4px 18px rgba(0,0,0,.18); }
|
|
117
|
-
.pending-bar.open { display:flex; }
|
|
118
|
-
.apply-panel { position:fixed; left:0; right:0; bottom:0; display:none; flex-direction:column; gap:8px; padding:14px max(16px, calc((100vw - 860px) / 2 + 24px)); background:#111827; color:#fff; z-index:36; box-shadow:0 -4px 18px rgba(0,0,0,.18); max-height:60vh; overflow:auto; }
|
|
119
|
-
.apply-panel.open { display:flex; }
|
|
120
|
-
.apply-panel .apply-head { display:flex; align-items:baseline; gap:10px; }
|
|
121
|
-
.apply-panel .sub { color:#9ca3af; }
|
|
122
|
-
.apply-row { display:flex; align-items:center; gap:10px; font-size:13px; }
|
|
123
|
-
.apply-row .name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
124
|
-
.apply-row .err { color:#fca5a5; }
|
|
125
|
-
.pending-bar .grow { flex:1; }
|
|
126
|
-
.pending-bar button.primary { background:#fff; color:#111827; border-color:#fff; }
|
|
127
|
-
.level-label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.4px; margin-bottom:8px; }
|
|
128
|
-
.toolbar { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
|
|
129
|
-
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
|
|
130
|
-
.seg button { border: none; border-radius: 0; }
|
|
131
|
-
.seg button.on { background: var(--accent); color: #fff; }
|
|
132
|
-
textarea { width: 100%; min-height: 380px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px; color: var(--fg); background: #fff; resize: vertical; }
|
|
133
|
-
pre.preview { background: #f9fafb; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; overflow: auto; max-height: 480px; white-space: pre; min-height: 380px; }
|
|
134
|
-
.banner { display: none; padding: 10px 24px; font-size: 13px; background: #ecfdf5; color: var(--green); border-bottom: 1px solid var(--border); }
|
|
135
|
-
.help-btn { width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--border); background: #f3f4f6; cursor: pointer; font-size: 16px; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
|
|
136
|
-
.help-btn:hover { background: var(--accent); border-color: var(--accent); }
|
|
137
|
-
.help-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.3); display: none; z-index: 40; }
|
|
138
|
-
.help-backdrop.open { display: block; }
|
|
139
|
-
.help-panel { position: fixed; top: 0; right: 0; height: 100%; width: 380px; max-width: 90vw; background: var(--bg); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,.12); transform: translateX(100%); transition: transform .2s ease; z-index: 50; overflow-y: auto; padding: 20px 22px; }
|
|
140
|
-
.help-panel.open { transform: translateX(0); }
|
|
141
|
-
.help-panel h3 { font-size: 16px; margin-bottom: 8px; }
|
|
142
|
-
.help-panel h4 { font-size: 14px; margin: 16px 0 6px; color: var(--accent); }
|
|
143
|
-
.help-panel ul { margin: 0 0 8px 18px; }
|
|
144
|
-
.help-panel li { font-size: 13px; margin: 3px 0; }
|
|
145
|
-
.help-panel .close { position: absolute; top: 14px; right: 16px; cursor: pointer; color: var(--muted); font-size: 22px; line-height: 1; background: none; border: none; }
|
|
146
|
-
.help-panel .tip { margin-top: 16px; padding: 10px 12px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 13px; }
|
|
147
|
-
@media (max-width: 640px) {
|
|
148
|
-
header { padding:10px 12px; flex-wrap:wrap; } header h1 { font-size:16px; }
|
|
149
|
-
main { padding:12px 10px; } section { margin-bottom:18px; }
|
|
150
|
-
.grid2 { grid-template-columns:1fr; gap:0; }
|
|
151
|
-
.item { padding:9px 10px; } .item-row { flex-wrap:wrap; }
|
|
152
|
-
.item-row .spacer { flex-basis:100%; height:0; }
|
|
153
|
-
.sub { margin-left:0; } .field { padding:10px 12px; }
|
|
154
|
-
#agentSearch { width:130px !important; margin-right:0 !important; }
|
|
155
|
-
button { min-height:36px; } button.sm { min-height:32px; }
|
|
156
|
-
.pending-bar { padding:10px 12px; flex-wrap:wrap; }
|
|
157
|
-
.pending-bar .grow { display:none; }
|
|
158
|
-
.pending-bar button { flex:1; }
|
|
159
|
-
}
|
|
160
|
-
/* #1005 status-emoji editor */
|
|
161
|
-
.se-preview { display: flex; flex-wrap: wrap; gap: 6px 14px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; margin-bottom: 6px; }
|
|
162
|
-
.se-item { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--muted); }
|
|
163
|
-
.se-glyph { font-size: 20px; line-height: 1; min-width: 22px; text-align: center; }
|
|
164
|
-
.se-glyph img { width: 22px; height: 22px; object-fit: contain; vertical-align: middle; }
|
|
165
|
-
.se-row { display: grid; grid-template-columns: 120px 1fr auto auto; gap: 6px; align-items: center; margin: 4px 0; }
|
|
166
|
-
.se-row label { margin: 0; }
|
|
167
|
-
.se-picker { grid-column: 1 / -1; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px; background: #fff; }
|
|
168
|
-
.se-grid { display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0 8px; }
|
|
169
|
-
.se-grid button { padding: 3px 6px; font-size: 18px; line-height: 1.2; }
|
|
170
|
-
.se-grid button img { width: 22px; height: 22px; object-fit: contain; vertical-align: middle; }
|
|
171
|
-
.se-grid button.off { opacity: .45; }
|
|
172
|
-
</style>
|
|
173
|
-
</head>
|
|
174
|
-
<body>
|
|
175
|
-
<header>
|
|
176
|
-
<h1 data-i18n="title">⚙️ AgEnD Settings</h1>
|
|
177
|
-
<span id="verBadge" data-i18n-title="whatsNew" title="What's New" style="font-size:12px;color:var(--muted);cursor:pointer;padding:2px 8px;border:1px solid var(--border);border-radius:12px">v…</span>
|
|
178
|
-
<div class="grow"></div>
|
|
179
|
-
<button class="help-btn" id="langBtn" data-i18n-title="languageButton" title="Language">🌐</button>
|
|
180
|
-
<button class="help-btn" id="helpBtn" data-i18n-title="helpButton" title="Help">ℹ️</button>
|
|
181
|
-
<button class="sm" id="wizardBtn" data-i18n="wizardButton">🧭 Setup wizard</button>
|
|
182
|
-
<input type="text" id="agentSearch" data-i18n-ph="searchPh" placeholder="🔍 Search agents, bots, channels…" style="width:220px;margin-right:8px">
|
|
183
|
-
</header>
|
|
184
|
-
<div class="help-backdrop" id="helpBackdrop"></div>
|
|
185
|
-
<aside class="help-panel" id="helpPanel">
|
|
186
|
-
<button class="close" id="helpClose" data-i18n-title="close" title="Close">×</button>
|
|
187
|
-
<div id="helpContent"></div>
|
|
188
|
-
</aside>
|
|
189
|
-
<div class="banner" id="banner"></div>
|
|
190
|
-
<div class="modal-backdrop" id="modalBackdrop"></div>
|
|
191
|
-
<div class="modal" id="modal" role="dialog" aria-modal="true">
|
|
192
|
-
<div class="modal-panel">
|
|
193
|
-
<div class="modal-head"><h3 id="modalTitle"></h3><span class="sub" id="modalSubtitle"></span></div>
|
|
194
|
-
<div class="modal-body" id="modalBody"></div>
|
|
195
|
-
<div class="modal-foot">
|
|
196
|
-
<span class="sub" id="modalImpact"></span>
|
|
197
|
-
<span class="grow"></span>
|
|
198
|
-
<span id="modalActions"></span>
|
|
199
|
-
<button id="modalCancel" data-i18n="cancel">Cancel</button>
|
|
200
|
-
<button class="primary" id="modalDone" data-i18n="modalDone">Stage change</button>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
<main>
|
|
205
|
-
<section>
|
|
206
|
-
<div class="sec-head"><h2 data-i18n="myAgents">🤖 My Agents</h2><div class="grow"></div><button class="sm" id="newAgentBtn" data-i18n="new">+ New</button></div>
|
|
207
|
-
<div id="newAgent"></div>
|
|
208
|
-
<div class="card" id="agents"></div>
|
|
209
|
-
</section>
|
|
210
|
-
|
|
211
|
-
<section id="classicSection">
|
|
212
|
-
<div class="sec-head"><h2 data-i18n="classicChannels">🎮 ClassicBot Channels</h2></div>
|
|
213
|
-
<div class="card" id="classicList"></div>
|
|
214
|
-
</section>
|
|
215
|
-
|
|
216
|
-
<section>
|
|
217
|
-
<div class="sec-head"><h2>🔌 Connections & Bots</h2><div class="grow"></div><button class="sm" id="newBotBtn" data-i18n="add">+ Add</button></div>
|
|
218
|
-
<div id="newBot"></div>
|
|
219
|
-
<div class="card" id="bots"></div>
|
|
220
|
-
<div class="card" id="providerSecrets" style="margin-top:12px"></div>
|
|
221
|
-
</section>
|
|
222
|
-
|
|
223
|
-
<section>
|
|
224
|
-
<div class="sec-head"><h2 data-i18n="general">⚙️ General Settings</h2></div>
|
|
225
|
-
<div class="card" id="general"></div>
|
|
226
|
-
</section>
|
|
227
|
-
|
|
228
|
-
<section>
|
|
229
|
-
<div class="level-label">Developer · Level 3</div>
|
|
230
|
-
<div class="sec-head"><h2>📋 Developer YAML</h2><div class="grow"></div><button class="sm" id="advToggle" data-i18n="expand">▶ Expand</button></div>
|
|
231
|
-
<div class="card"><div class="adv-body" id="advBody"></div></div>
|
|
232
|
-
</section>
|
|
233
|
-
</main>
|
|
234
|
-
<div class="apply-panel" id="applyPanel">
|
|
235
|
-
<div class="apply-head"><strong id="applyTitle">Applying changes…</strong><span id="applyNote" class="sub"></span></div>
|
|
236
|
-
<div id="applyRows"></div>
|
|
237
|
-
</div>
|
|
238
|
-
<div class="pending-bar" id="pendingBar">
|
|
239
|
-
<strong id="pendingCount">0 changes not applied</strong>
|
|
240
|
-
<span id="pendingImpact">⚡ Immediately · 🔄 Restart this Agent · 🔄🔄 Restart AgEnD</span>
|
|
241
|
-
<span class="grow"></span>
|
|
242
|
-
<button id="discardChanges">Discard</button>
|
|
243
|
-
<button class="primary" id="applyChanges">Apply changes</button>
|
|
244
|
-
</div>
|
|
245
|
-
<script>
|
|
246
|
-
(() => {
|
|
247
|
-
"use strict";
|
|
248
|
-
// No token in the URL: the gate redeemed it for an HttpOnly session cookie,
|
|
249
|
-
// which the browser attaches to these same-origin requests on its own.
|
|
250
|
-
const $ = (id) => document.getElementById(id);
|
|
251
|
-
const el = (tag, attrs = {}, ...kids) => { const e = document.createElement(tag); for (const k in attrs) { if (k === "class") e.className = attrs[k]; else if (k === "html") e.innerHTML = attrs[k]; else if (k.startsWith("on")) e.addEventListener(k.slice(2), attrs[k]); else e.setAttribute(k, attrs[k]); } for (const c of kids) if (c != null) e.append(c); return e; };
|
|
252
|
-
const esc = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&","<":"<",">":">" }[c]));
|
|
253
|
-
const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "antigravity", "grok", "muse"];
|
|
254
|
-
|
|
255
|
-
// ── i18n ──
|
|
256
|
-
const I18N = {
|
|
257
|
-
en: {
|
|
258
|
-
title: "⚙️ AgEnD Settings", reloadFleet: "↻ Reload Fleet", myAgents: "🤖 My Agents", bots: "🔌 Bots", classicChannels: "🎮 ClassicBot Channels", noClassic: "No ClassicBot channels — start one with /start in a chat.",
|
|
259
|
-
general: "⚙️ General Settings", whatsNew: "🆕 What's New", advanced: "📋 Advanced",
|
|
260
|
-
expand: "▶ Expand", collapse: "▼ Collapse", new: "+ New", add: "+ Add", searchPh: "🔍 Search…",
|
|
261
|
-
edit: "Edit", close: "Close", save: "Save", cancel: "Cancel", create: "Create", start: "Start", stop: "Stop",
|
|
262
|
-
backend: "Backend", model: "Model", workingDir: "Working directory", channelBinding: "Channel binding",
|
|
263
|
-
description: "Description", systemPrompt: "System prompt", tags: "Tags", generalTopicField: "General topic (fleet dispatcher)",
|
|
264
|
-
runtimeResources: "Runtime & Resources", autoPause: "Auto-pause after", minutes: "minutes", inheritDefault: "Inherit from defaults",
|
|
265
|
-
statusEmojis: "Status emojis", statusEmojisInstance: "This agent's status emojis (override the connection's)", statusEmojisChannel: "Status emojis for this connection's bots",
|
|
266
|
-
se_received: "Received", se_queued: "Queued", se_processing: "Processing", se_delivered: "Delivered", se_failed: "Failed", se_progress_prefix: "Progress prefix", se_photo: "Photo saved", se_attachment: "File saved",
|
|
267
|
-
se_src_instance: "agent", se_src_platform: "connection", se_src_builtin: "default", se_pick: "Pick…", se_useDefault: "Use the default", se_fallback: "the default is used instead",
|
|
268
|
-
se_unicode: "Emoji", se_telegramOnly: "Telegram allows only these reactions", se_serverEmojis: "Server emojis", se_noServerEmojis: "No custom emojis on this server.", se_primaryServer: "primary", se_externalEmojiHint: "Another server's emoji needs the bot's Use External Emojis permission in the channel it reacts in.", se_unavailable: "unavailable",
|
|
269
|
-
se_hint: "Empty keeps the default. The preview is resolved by AgEnD exactly as the bot will react.",
|
|
270
|
-
detection: "Detection", startup: "Startup", backendSection: "Backend", advancedSection: "Advanced & Access", hangEnabled: "Hang detector enabled", hangTimeout: "Stuck timeout", agentMode: "Agent mode", toolSet: "Tool set", toolProgress: "Tool progress", crossInstanceVisibility: "Bot-to-bot messages in topics", crossInstanceVisibilityHint: "full: the whole message · summary: one line · hidden: nothing. Messages are always delivered; the Mirror Topic still shows them.", replyCompletionGuard: "Require a delivered reply", replyGuardSupported: "Available for Claude Code and Kiro legacy/TUI in MCP mode.", replyGuardUnsupported: "Stored but inactive: {0} in {1} mode does not support reply-drop recovery.", logLevel: "Log level", concurrency: "Startup concurrency", staggerDelay: "Stagger delay", lightweight: "Lightweight mode", modelFailover: "Fallback model", displayName: "Display name", pause: "Pause", wake: "Wake", accessMode: "Access mode", affected: "Affected instances",
|
|
271
|
-
language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)", localeAuto: "Auto (follow system)",
|
|
272
|
-
classicAdmins: "ClassicBot admins", allowedGuilds: "Allowed guilds", type: "Type", adapterId: "Adapter id",
|
|
273
|
-
tokenEnv: "Bot token env var", groupId: "Group / guild id", nameField: "Name",
|
|
274
|
-
envHint: "Put the actual token in ~/.agend/.env under this env var, then Reload Fleet.",
|
|
275
|
-
noAgents: "No agents yet — click + New.", noBots: "No bots configured.", online: "✅ Online",
|
|
276
|
-
currentVersion: "Current version",
|
|
277
|
-
helpTitle: "📖 Help", tipReload: "💡 Click [↻ Reload Fleet] after changes to apply them.",
|
|
278
|
-
wnIntro: "Recent highlights:", releaseNotes: "📄 View full Release Notes →", languageButton: "Language", helpButton: "Help", showAdditional: "Show additional controls", hideAdvanced: "Hide advanced", showAdvanced: "Show advanced",
|
|
279
|
-
saved: "Saved", failed: "Failed", actionFailed: "{0} failed", noAgentMatch: "No agents match \"{0}\".", deleteAgent: "Delete agent \"{0}\"? This will be applied only after you confirm the pending changes.", nameRequired: "name required", tokenEnvRequired: "token env required", removeBot: "Remove bot \"{0}\"? (the token in .env is left untouched)",
|
|
280
|
-
applying: "Applying changes…", applyDone: "✓ Changes applied", applyPartial: "⚠️ Some changes failed", applyClose: "Close",
|
|
281
|
-
settingsButton: "Settings", modalDone: "Stage change", advancedSection: "Advanced",
|
|
282
|
-
wizardButton: "🧭 Setup wizard", wizardTitle: "Set up AgEnD", wizardStep: "Step {0} of {1}",
|
|
283
|
-
wizardRerun: "A fleet is already configured — finishing replaces the connection that uses the same token variable.",
|
|
284
|
-
wizardBack: "Back", wizardNext: "Next", wizardFinish: "Create and start",
|
|
285
|
-
wizardBackend: "Backend", wizardBackendFound: "Found on this host: {0}", wizardBackendNone: "No backend CLI found on this host — install one first.",
|
|
286
|
-
wizardAgentName: "First agent name", wizardNeedDir: "Enter an absolute working directory.",
|
|
287
|
-
wizardTelegramHint: "Create a bot with @BotFather, then add it to a group.",
|
|
288
|
-
wizardDiscordHint: "Create an application in the Discord developer portal and invite the bot to your server.",
|
|
289
|
-
wizardToken: "Bot token", wizardTokenTelegram: "From @BotFather. It is written to ~/.agend/.env and never shown again.",
|
|
290
|
-
wizardTokenDiscord: "From the Bot tab of your application. It is written to ~/.agend/.env and never shown again.",
|
|
291
|
-
wizardVerify: "Verify", wizardVerifying: "Asking the provider…",
|
|
292
|
-
wizardDetect: "Detect from a message", wizardDetectHint: "Or post any message in the group and press Detect.",
|
|
293
|
-
wizardWaitingStart: "Waiting for a message in the group…", wizardNoStartYet: "No group message yet — post one and press Detect again.",
|
|
294
|
-
wizardGeneralChannel: "General channel id", wizardAdminUser: "Your user id (becomes the first allowed user)",
|
|
295
|
-
wizardNeedVerify: "Verify the token before continuing.",
|
|
296
|
-
wizardPlanLoading: "Building the plan…", wizardWillWrite: "This will be written:", wizardEnvKeys: "And into ~/.agend/.env: {0}",
|
|
297
|
-
wizardEnvPerms: "⚠️ ~/.agend/.env is readable by other users on this host.",
|
|
298
|
-
guildIdField: "Guild id", groupIdField: "Group id", notSet: "(not set)", deleteBot: "Delete this bot", noAllowedUsers: "no allowed users",
|
|
299
|
-
searchAll: "🔍 Search agents, bots, channels…", accessMovedHint: "Who may use the bot is set on the connection itself — open Settings on \"{0}\" under Connections & Bots.",
|
|
300
|
-
applyRestartNeeded: "Saved — restart AgEnD to apply", applyNoChange: "no change needed",
|
|
301
|
-
applyRestartHint: "This change needs AgEnD itself restarted. You can restart it here, or run /restart in your chat channel.",
|
|
302
|
-
applyBusy: "AgEnD is already reloading its configuration. Your changes are saved — try Apply again in a moment.",
|
|
303
|
-
restartFleetButton: "Restart AgEnD", restartFleetConfirm: "Restart AgEnD now?\n\nEvery agent stops and starts again, work in progress is interrupted, and the panel is unreachable for up to a minute. A notice is posted to your chat channel.",
|
|
304
|
-
restartFleetStarted: "Restarting AgEnD — this page keeps checking.",
|
|
305
|
-
restartFleetBusy: "Restarting…", restartFleetTitle: "Restarting AgEnD…",
|
|
306
|
-
restartFleetWaiting: "AgEnD is restarting. This page keeps checking and will say when it is back — there is nothing to press.",
|
|
307
|
-
working: "Working…", restartFleetRefused: "Restart refused: {0}",
|
|
308
|
-
restartFleetRateLimited: "Restart refused: {0} (try again in about {1} minutes)",
|
|
309
|
-
signatureMismatch: "fleet.yaml and the running configuration disagree on: {0}. Restarting will not clear this — check daemon.log first.",
|
|
310
|
-
applyStillWorking: "Still restarting ({0}s) — AgEnD is still working; this page keeps checking.",
|
|
311
|
-
applyLostJob: "Lost track of this apply (the fleet may have restarted). Reload the page to see the current state.",
|
|
312
|
-
changesPending: "{0} change(s) not applied", impactNow: "⚡ Immediately", impactAgent: "🔄 Restart this Agent", impactFleet: "🔄🔄 Restart AgEnD", queued: "Queued: {0}", applyFailed: "Apply failed", changesApplied: "✓ Changes applied. AgEnD is reconciling affected Agents.", reviewChanges: "Review changes", backendUnavailable: "{0} (removed — choose another backend)", readyApply: "Ready to apply", copied: "Copied", expectedMapping: "expected a mapping", configLoadFailed: "Failed to load config — open the Settings link again.",
|
|
313
|
-
confirmOpenAccess: "Open access allows anyone in this channel to operate the bot. Continue?", confirmLockedEmpty: "Locked access has no allowed users. This may leave nobody able to administer the bot. Continue?", accessLockedEmpty: "Locked mode has no allowed users; add at least one administrator to avoid lockout.", accessChangeCancelled: "Access mode change cancelled",
|
|
314
|
-
mustNonNegative: "Must be 0 or greater", mustPositive: "Must be greater than 0", nonnegativeInteger: "Must be a non-negative integer", failoverEmpty: "Empty list disables failover", concurrencyRange: "Must be an integer from 1 to 20", concurrencyWarning: "May exceed CPU capacity on smaller hosts", staggerRange: "Must be between 0 and 30000", staggerWarning: "Very low values can cause startup spikes",
|
|
315
|
-
},
|
|
316
|
-
"zh-TW": {
|
|
317
|
-
statusEmojis: "狀態 emoji", statusEmojisInstance: "此 Agent 的狀態 emoji(覆蓋連線設定)", statusEmojisChannel: "此連線 bot 的狀態 emoji",
|
|
318
|
-
se_received: "已收到", se_queued: "排隊中", se_processing: "處理中", se_delivered: "已送達", se_failed: "失敗", se_progress_prefix: "進度前綴", se_photo: "已存圖片", se_attachment: "已存檔案",
|
|
319
|
-
se_src_instance: "Agent", se_src_platform: "連線", se_src_builtin: "預設", se_pick: "選擇…", se_useDefault: "使用預設", se_fallback: "改用預設值",
|
|
320
|
-
se_unicode: "Emoji", se_telegramOnly: "Telegram 只允許這些反應", se_serverEmojis: "伺服器 emoji", se_noServerEmojis: "此伺服器沒有自訂 emoji。", se_primaryServer: "主伺服器", se_externalEmojiHint: "使用其他伺服器的 emoji,bot 在要加反應的頻道需有「使用外部表情符號」權限。", se_unavailable: "無法使用",
|
|
321
|
-
se_hint: "留空即使用預設值。預覽由 AgEnD 依 bot 實際 react 的方式解析。",
|
|
322
|
-
title: "⚙️ AgEnD 設定", reloadFleet: "↻ 重新載入", myAgents: "🤖 我的 Agent", bots: "🔌 機器人", classicChannels: "🎮 ClassicBot 頻道", noClassic: "沒有 ClassicBot 頻道 — 在聊天中用 /start 建立。",
|
|
323
|
-
general: "⚙️ 一般設定", whatsNew: "🆕 更新重點", advanced: "📋 進階",
|
|
324
|
-
expand: "▶ 展開", collapse: "▼ 收合", new: "+ 新增", add: "+ 加入", searchPh: "🔍 搜尋…",
|
|
325
|
-
edit: "編輯", close: "關閉", save: "儲存", cancel: "取消", create: "建立", start: "啟動", stop: "停止",
|
|
326
|
-
backend: "後端", model: "模型", workingDir: "工作目錄", channelBinding: "頻道綁定",
|
|
327
|
-
description: "描述", systemPrompt: "System prompt", tags: "標籤", generalTopicField: "General topic(fleet 分派器)",
|
|
328
|
-
runtimeResources: "執行環境與資源", autoPause: "閒置後自動暫停", minutes: "分鐘", inheritDefault: "沿用全域預設值",
|
|
329
|
-
detection: "偵測", startup: "啟動", backendSection: "後端", advancedSection: "進階與存取", hangEnabled: "啟用卡住偵測", hangTimeout: "卡住逾時", agentMode: "Agent 模式", toolSet: "工具集", toolProgress: "工具進度", crossInstanceVisibility: "Agent 之間的訊息在主題中", crossInstanceVisibilityHint: "full:完整訊息 · summary:一行摘要 · hidden:不顯示。訊息一律照常送達,Mirror Topic 也照常顯示。", replyCompletionGuard: "要求送出回覆", replyGuardSupported: "Claude Code 與 Kiro legacy/TUI 的 MCP 模式可用。", replyGuardUnsupported: "設定會保留,但不會生效:{0} 的 {1} 模式不支援回覆遺失補救。", logLevel: "Log 等級", concurrency: "啟動併發數", staggerDelay: "錯開延遲", lightweight: "輕量模式", modelFailover: "備援模型", displayName: "顯示名稱", pause: "暫停", wake: "喚醒", accessMode: "存取模式", affected: "受影響 instances",
|
|
330
|
-
language: "語言(bot 回覆)", defaultBackend: "預設後端", defaultModel: "預設模型", allowedUsers: "允許使用者(主 bot)", localeAuto: "自動(跟隨系統)",
|
|
331
|
-
classicAdmins: "ClassicBot 管理員", allowedGuilds: "允許的伺服器", type: "類型", adapterId: "Adapter id",
|
|
332
|
-
tokenEnv: "Bot token 環境變數", groupId: "群組 / guild id", nameField: "名稱",
|
|
333
|
-
envHint: "把實際 token 放到 ~/.agend/.env 的這個環境變數下,然後點重新載入。",
|
|
334
|
-
noAgents: "還沒有 agent — 點 + 新增。", noBots: "尚未設定機器人。", online: "✅ 上線",
|
|
335
|
-
currentVersion: "目前版本",
|
|
336
|
-
helpTitle: "📖 使用說明", tipReload: "💡 修改後點 [↻ 重新載入] 讓設定生效。",
|
|
337
|
-
wnIntro: "近期更新重點:", releaseNotes: "📄 查看完整 Release Notes →", languageButton: "語言", helpButton: "使用說明", showAdditional: "顯示更多控制項", hideAdvanced: "隱藏進階設定", showAdvanced: "顯示進階設定",
|
|
338
|
-
saved: "已儲存", failed: "失敗", actionFailed: "{0}失敗", noAgentMatch: "沒有符合「{0}」的 Agent。", deleteAgent: "確定刪除 Agent「{0}」嗎?只有在確認套用待處理變更後才會生效。", nameRequired: "必須填寫名稱", tokenEnvRequired: "必須填寫 token 環境變數", removeBot: "確定移除機器人「{0}」嗎?(.env 中的 token 會保留)",
|
|
339
|
-
applying: "正在套用變更…", applyDone: "✓ 變更已套用", applyPartial: "⚠️ 有部分變更失敗", applyClose: "關閉",
|
|
340
|
-
settingsButton: "設定", modalDone: "加入待套用", advancedSection: "進階",
|
|
341
|
-
wizardButton: "🧭 設定精靈", wizardTitle: "設定 AgEnD", wizardStep: "第 {0} 步,共 {1} 步",
|
|
342
|
-
wizardRerun: "已經有設定好的 fleet —— 完成後會取代使用相同 token 變數的那個連線。",
|
|
343
|
-
wizardBack: "上一步", wizardNext: "下一步", wizardFinish: "建立並啟動",
|
|
344
|
-
wizardBackend: "後端", wizardBackendFound: "在這台主機上找到:{0}", wizardBackendNone: "這台主機上找不到任何後端 CLI —— 請先安裝一個。",
|
|
345
|
-
wizardAgentName: "第一個 agent 名稱", wizardNeedDir: "請輸入絕對路徑的工作目錄。",
|
|
346
|
-
wizardTelegramHint: "用 @BotFather 建立 bot,然後把它加進一個群組。",
|
|
347
|
-
wizardDiscordHint: "在 Discord 開發者後台建立應用程式,並把 bot 邀請到你的伺服器。",
|
|
348
|
-
wizardToken: "Bot token", wizardTokenTelegram: "來自 @BotFather。會寫入 ~/.agend/.env,之後不會再顯示。",
|
|
349
|
-
wizardTokenDiscord: "來自應用程式的 Bot 分頁。會寫入 ~/.agend/.env,之後不會再顯示。",
|
|
350
|
-
wizardVerify: "驗證", wizardVerifying: "正在向平台確認…",
|
|
351
|
-
wizardDetect: "從訊息偵測", wizardDetectHint: "或在群組裡發任一則訊息,然後按「從訊息偵測」。",
|
|
352
|
-
wizardWaitingStart: "等待群組訊息中…", wizardNoStartYet: "還沒收到群組訊息 —— 發一則之後再按一次。",
|
|
353
|
-
wizardGeneralChannel: "General 頻道 id", wizardAdminUser: "你的 user id(會成為第一個允許使用者)",
|
|
354
|
-
wizardNeedVerify: "請先驗證 token 再繼續。",
|
|
355
|
-
wizardPlanLoading: "正在產生方案…", wizardWillWrite: "將寫入:", wizardEnvKeys: "以及寫入 ~/.agend/.env:{0}",
|
|
356
|
-
wizardEnvPerms: "⚠️ ~/.agend/.env 目前其他使用者也讀得到。",
|
|
357
|
-
guildIdField: "Guild id", groupIdField: "群組 id", notSet: "(未設定)", deleteBot: "刪除這個機器人", noAllowedUsers: "沒有允許的使用者",
|
|
358
|
-
searchAll: "🔍 搜尋 agent、機器人、頻道…", accessMovedHint: "誰可以使用 bot 是在連線本身設定 —— 請到「連線與機器人」開啟「{0}」的設定。",
|
|
359
|
-
applyRestartNeeded: "已儲存 —— 需要重新啟動 AgEnD 才會生效", applyNoChange: "無需變更",
|
|
360
|
-
applyRestartHint: "此變更需要重新啟動 AgEnD 本身。可以在這裡重新啟動,或在聊天頻道執行 /restart。",
|
|
361
|
-
applyBusy: "AgEnD 正在重新載入設定。你的變更已儲存,請稍候再按一次套用。",
|
|
362
|
-
restartFleetButton: "重新啟動 AgEnD", restartFleetConfirm: "現在重新啟動 AgEnD?\n\n所有 agent 都會停止再啟動,進行中的工作會被中斷,面板會有最多約一分鐘無法連線。聊天頻道會收到一則通知。",
|
|
363
|
-
restartFleetStarted: "AgEnD 重新啟動中 —— 此頁面會持續確認。",
|
|
364
|
-
restartFleetBusy: "重啟中…", restartFleetTitle: "AgEnD 重新啟動中…",
|
|
365
|
-
restartFleetWaiting: "AgEnD 正在重新啟動。此頁面會持續確認,恢復後會告訴你 —— 不需要再按任何按鈕。",
|
|
366
|
-
working: "執行中…", restartFleetRefused: "重新啟動被拒絕:{0}",
|
|
367
|
-
restartFleetRateLimited: "重新啟動被拒絕:{0}(約 {1} 分鐘後可再試)",
|
|
368
|
-
signatureMismatch: "fleet.yaml 與執行中的設定在這些項目上不一致:{0}。重新啟動無法解決,請先查看 daemon.log。",
|
|
369
|
-
applyStillWorking: "仍在重啟中({0} 秒)—— AgEnD 還在處理,此頁面會持續確認。",
|
|
370
|
-
applyLostJob: "已追蹤不到這次套用(AgEnD 可能重啟過)。請重新整理頁面查看目前狀態。",
|
|
371
|
-
changesPending: "有 {0} 項變更尚未套用", impactNow: "⚡ 立即生效", impactAgent: "🔄 重新啟動此 Agent", impactFleet: "🔄🔄 重新啟動 AgEnD", queued: "已加入待套用:{0}", applyFailed: "套用失敗", changesApplied: "✓ 變更已套用,AgEnD 正在協調受影響的 Agent。", reviewChanges: "檢查變更", backendUnavailable: "{0}(已移除,請選擇其他 backend)", readyApply: "可套用變更", copied: "已複製", expectedMapping: "預期為 mapping", configLoadFailed: "載入設定失敗 — 請重新開啟 Settings 連結。",
|
|
372
|
-
confirmOpenAccess: "Open 模式會允許此頻道中的所有人操作 bot,確定要繼續嗎?", confirmLockedEmpty: "Locked 模式目前沒有允許的使用者,可能導致沒有人能管理 bot,確定要繼續嗎?", accessLockedEmpty: "Locked 模式沒有允許的使用者;建議至少加入一位管理員,避免無法操作。", accessChangeCancelled: "已取消 Access Mode 變更",
|
|
373
|
-
mustNonNegative: "必須為 0 或更大的數值", mustPositive: "必須大於 0", nonnegativeInteger: "必須是非負整數", failoverEmpty: "空白清單會停用模型備援", concurrencyRange: "必須是 1 到 20 的整數", concurrencyWarning: "在較小的主機上可能超出 CPU 負荷", staggerRange: "必須介於 0 到 30000", staggerWarning: "數值太低可能造成啟動尖峰",
|
|
374
|
-
},
|
|
375
|
-
};
|
|
376
|
-
const HELP = {
|
|
377
|
-
en: [["🤖 My Agents", ["[+ New] add an agent (name, backend, working directory)", "[Edit] change settings (backend, model, tags, systemPrompt)", "[Start/Stop] run or stop an agent", "🗑️ delete an agent (shows on hover, needs confirm)", "🔍 search — live-filter the list"]],
|
|
378
|
-
["🔌 Bots", ["[+ Add] add a Discord/TG bot", "🗑️ remove a bot"]],
|
|
379
|
-
["⚙️ General Settings", ["Edit global backend/model, allowed users, ClassicBot admins", "Click [Save]"]],
|
|
380
|
-
["📋 Advanced", ["Edit YAML/JSON directly", "[⬇ fleet.yaml] download a backup"]]],
|
|
381
|
-
"zh-TW": [["🤖 我的 Agent", ["[+ 新增] 新增 agent(name、backend、working directory)", "[編輯] 修改設定(backend、model、tags、systemPrompt)", "[啟動/停止] 啟動或停止 agent", "🗑️ 刪除 agent(hover 顯示,需確認)", "🔍 搜尋 — 即時過濾列表"]],
|
|
382
|
-
["🔌 機器人", ["[+ 加入] 加入 Discord/TG bot", "🗑️ 移除 bot"]],
|
|
383
|
-
["⚙️ 一般設定", ["修改全域 backend/model、允許使用者、ClassicBot 管理員", "點 [儲存]"]],
|
|
384
|
-
["📋 進階", ["直接編輯 YAML/JSON", "[⬇ fleet.yaml] 下載備份"]]],
|
|
385
|
-
};
|
|
386
|
-
const WHATSNEW = [
|
|
387
|
-
"Settings redesign — user-flow sections, white theme, i18n (EN / 繁中)",
|
|
388
|
-
"What's New panel + info help panel + ClassicBot channels list",
|
|
389
|
-
"Same-channel multi-bot ClassicBot; per-bot cancel-button + react fixes",
|
|
390
|
-
"/view avatar & ctx% fixes; config validator + `agend validate` + validate_config MCP tool",
|
|
391
|
-
];
|
|
392
|
-
let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
|
|
393
|
-
if (!I18N[lang]) lang = "en";
|
|
394
|
-
const t = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
|
|
395
|
-
const tf = (k, ...values) => values.reduce((text, value, index) => text.replaceAll(`{${index}}`, String(value)), t(k));
|
|
396
|
-
function applyStaticI18n() {
|
|
397
|
-
document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = t(e.getAttribute("data-i18n")); });
|
|
398
|
-
document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", t(e.getAttribute("data-i18n-ph"))); });
|
|
399
|
-
document.querySelectorAll("[data-i18n-title]").forEach(e => { e.setAttribute("title", t(e.getAttribute("data-i18n-title"))); });
|
|
400
|
-
// toggle labels that carry expand/collapse state
|
|
401
|
-
for (const [id, body] of [["advToggle", "advBody"]]) {
|
|
402
|
-
const b = $(body); if (b) $(id).textContent = b.classList.contains("open") ? t("collapse") : t("expand");
|
|
403
|
-
}
|
|
404
|
-
renderHelp();
|
|
405
|
-
}
|
|
406
|
-
function renderHelp() {
|
|
407
|
-
const host = $("helpContent"); if (!host) return; host.innerHTML = "";
|
|
408
|
-
host.append(el("h3", {}, t("helpTitle")));
|
|
409
|
-
for (const [h, items] of (HELP[lang] || HELP.en)) {
|
|
410
|
-
host.append(el("h4", {}, h));
|
|
411
|
-
const ul = el("ul"); for (const it of items) ul.append(el("li", {}, it)); host.append(ul);
|
|
412
|
-
}
|
|
413
|
-
host.append(el("div", { class: "tip" }, t("tipReload")));
|
|
414
|
-
}
|
|
415
|
-
const CH_TYPES = ["discord", "telegram"];
|
|
416
|
-
|
|
417
|
-
// ── YAML (config subset; round-trip verified) ──
|
|
418
|
-
function toYaml(value, indent = 0) {
|
|
419
|
-
const pad = " ".repeat(indent);
|
|
420
|
-
const scalar = (v) => { if (v === null || v === undefined) return "null"; if (typeof v === "boolean" || typeof v === "number") return String(v); const s = String(v); if (s === "") return '""'; if (/^(true|false|null|~|-?\d+(\.\d+)?)$/i.test(s) || /[:#\[\]{}&*!|>'"%@`]/.test(s) || /^[\s-]/.test(s) || /\s$/.test(s) || s.includes("\n")) return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; return s; };
|
|
421
|
-
if (Array.isArray(value)) { if (!value.length) return "[]"; return value.map(item => { if (item !== null && typeof item === "object") { const d = toYaml(item, indent + 1); const lines = d.split("\n"); return pad + "- " + lines[0].slice((indent + 1) * 2) + (lines.length > 1 ? "\n" + lines.slice(1).join("\n") : ""); } return pad + "- " + scalar(item); }).join("\n"); }
|
|
422
|
-
if (value !== null && typeof value === "object") { const keys = Object.keys(value); if (!keys.length) return "{}"; return keys.map(k => { const v = value[k]; if (v !== null && typeof v === "object" && (Array.isArray(v) ? v.length : Object.keys(v).length)) return pad + k + ":\n" + toYaml(v, indent + 1); if (v !== null && typeof v === "object") return pad + k + ": " + (Array.isArray(v) ? "[]" : "{}"); return pad + k + ": " + scalar(v); }).join("\n"); }
|
|
423
|
-
return pad + scalar(value);
|
|
424
|
-
}
|
|
425
|
-
function parseScalar(s) { s = s.trim(); if (s === "" || s === "~" || s === "null") return null; if (s === "true") return true; if (s === "false") return false; if (/^-?\d+$/.test(s)) return parseInt(s, 10); if (/^-?\d+\.\d+$/.test(s)) return parseFloat(s); if (s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\"); if (s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1); if (s === "[]") return []; if (s === "{}") return {}; return s; }
|
|
426
|
-
function fromYaml(text) {
|
|
427
|
-
const lines = [];
|
|
428
|
-
for (const raw of text.replace(/\r/g, "").split("\n")) { let inS = false, inD = false, out = ""; for (let i = 0; i < raw.length; i++) { const c = raw[i]; if (c === "'" && !inD) inS = !inS; else if (c === '"' && !inS) inD = !inD; else if (c === "#" && !inS && !inD) break; out += c; } if (out.trim() === "") continue; lines.push({ indent: out.match(/^ */)[0].length, text: out.trim() }); }
|
|
429
|
-
let pos = 0;
|
|
430
|
-
function block() { if (pos >= lines.length) return null; const first = lines[pos]; if (first.text.startsWith("- ")) { const arr = []; while (pos < lines.length && lines[pos].indent === first.indent && lines[pos].text.startsWith("- ")) { const at = lines[pos].indent, rest = lines[pos].text.slice(2); if (rest.includes(":") && !/^".*"$/.test(rest) && !rest.startsWith("[") && !rest.startsWith("{")) { lines[pos] = { indent: at + 2, text: rest }; arr.push(map(at + 2)); } else { arr.push(parseScalar(rest)); pos++; } } return arr; } return map(first.indent); }
|
|
431
|
-
function map(indent) { const obj = {}; while (pos < lines.length && lines[pos].indent === indent) { const line = lines[pos].text, ci = line.indexOf(":"); if (ci < 0) { pos++; continue; } const key = line.slice(0, ci).trim(), val = line.slice(ci + 1).trim(); if (val === "") { pos++; if (pos < lines.length && lines[pos].indent > indent) obj[key] = block(); else obj[key] = null; } else { obj[key] = parseScalar(val); pos++; } } return obj; }
|
|
432
|
-
return block();
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// ── API ──
|
|
436
|
-
async function api(path, opts = {}) {
|
|
437
|
-
const headers = Object.assign({}, opts.headers || {});
|
|
438
|
-
if (opts.body) headers["Content-Type"] = "application/json";
|
|
439
|
-
const r = await fetch(path, Object.assign({}, opts, { headers }));
|
|
440
|
-
let body = null; try { body = await r.json(); } catch {}
|
|
441
|
-
return { ok: r.ok, status: r.status, body };
|
|
442
|
-
}
|
|
443
|
-
function resultMsg(elm, res) {
|
|
444
|
-
const b = res.body || {}; let html = "";
|
|
445
|
-
if (b.errors && b.errors.length) html += b.errors.map(e => `<span class="e">✗ ${esc(e.path ? e.path + ": " : "")}${esc(e.message)}</span>`).join(" ");
|
|
446
|
-
if (b.warnings && b.warnings.length) html += " " + b.warnings.map(w => `<span class="w">⚠ ${esc(w.message)}</span>`).join(" ");
|
|
447
|
-
if (res.ok) html = `<span class="s">✓ ${esc(t("saved"))}</span>` + (html ? " " + html : "");
|
|
448
|
-
else if (!html) html = `<span class="e">✗ ${esc((b && b.error) || ("HTTP " + res.status))}</span>`;
|
|
449
|
-
elm.innerHTML = html;
|
|
450
|
-
}
|
|
451
|
-
function showBanner(text, error = false) {
|
|
452
|
-
const b = $("banner");
|
|
453
|
-
b.style.display = "block";
|
|
454
|
-
b.style.background = error ? "#fef2f2" : "#ecfdf5";
|
|
455
|
-
b.style.color = error ? "var(--red)" : "var(--green)";
|
|
456
|
-
b.textContent = text;
|
|
457
|
-
setTimeout(() => { b.style.display = "none"; }, 6000);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// ── State ──
|
|
461
|
-
const state = { schema: { impacts: {}, order: ["now", "instance", "fleet"] }, fleet: {}, classic: {}, classicStatus: {}, classicLive: {}, live: {}, connections: [], providerSecrets: [], fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), version: "", newAgentOpen: false, newBotOpen: false, pending: new Map() };
|
|
462
|
-
const channels = () => state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
|
|
463
|
-
const channelIds = () => channels().map(c => c.id || c.type).filter(Boolean);
|
|
464
|
-
const chLabel = (i) => i === 0 ? "primary" : "persona";
|
|
465
|
-
function chById(id) { return channels().find(c => (c.id || c.type) === id); }
|
|
466
|
-
|
|
467
|
-
function chipList(arr, onChange, placeholder) {
|
|
468
|
-
const box = el("div"); const chips = el("div", { class: "chips" });
|
|
469
|
-
const redraw = () => { chips.innerHTML = ""; arr.forEach((v, i) => chips.append(el("span", { class: "chip" }, String(v), el("span", { class: "x", onclick: () => { arr.splice(i, 1); redraw(); onChange && onChange(); } }, "×")))); };
|
|
470
|
-
const inp = el("input", { type: "text", placeholder: placeholder || "add…" });
|
|
471
|
-
const add = () => { const v = inp.value.trim(); if (v) { arr.push(/^-?\d+$/.test(v) ? Number(v) : v); inp.value = ""; redraw(); onChange && onChange(); } };
|
|
472
|
-
inp.addEventListener("keydown", e => { if (e.key === "Enter") { e.preventDefault(); add(); } });
|
|
473
|
-
box.append(chips, el("div", { class: "chip-add" }, inp, el("button", { class: "sm", onclick: add }, "+ Add")));
|
|
474
|
-
redraw(); return box;
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* The backend picker keeps a current value it does not offer, such as the removed gemini-cli (#1280 review):
|
|
478
|
-
* otherwise the select falls to its first option and an unrelated edit would switch the instance's CLI and
|
|
479
|
-
* account to claude-code without anyone choosing it. The value stays until the user picks another.
|
|
480
|
-
*/
|
|
481
|
-
function backendSelect(value) {
|
|
482
|
-
return select(value, BACKENDS.includes(value) ? BACKENDS : [{ value, label: tf("backendUnavailable", value) }, ...BACKENDS]);
|
|
483
|
-
}
|
|
484
|
-
const LOCALES = [["en", "English"], ["zh-TW", "繁體中文 (zh-TW)"]];
|
|
485
|
-
/**
|
|
486
|
-
* The locale picker adds an explicit "Auto (follow system)" option for unset (#1310):
|
|
487
|
-
* without it the select falls to its first option (en) and any unrelated defaults
|
|
488
|
-
* save writes locale: en, pinning the language and disabling timezone auto-detect.
|
|
489
|
-
* Like backendSelect, it also keeps a value it does not offer until the user picks another.
|
|
490
|
-
*/
|
|
491
|
-
function localeSelect(value) {
|
|
492
|
-
const s = el("select");
|
|
493
|
-
const auto = el("option", {}, t("localeAuto")); auto.value = "";
|
|
494
|
-
if (!value) auto.selected = true;
|
|
495
|
-
s.append(auto);
|
|
496
|
-
if (value && !LOCALES.some(([v]) => v === value)) {
|
|
497
|
-
const unknown = el("option", {}, value); unknown.value = value; unknown.selected = true;
|
|
498
|
-
s.append(unknown);
|
|
499
|
-
}
|
|
500
|
-
for (const [v, lbl] of LOCALES) {
|
|
501
|
-
const o = el("option", {}, lbl); o.value = v; if (v === value) o.selected = true; s.append(o);
|
|
502
|
-
}
|
|
503
|
-
return s;
|
|
504
|
-
}
|
|
505
|
-
function select(value, options) { const s = el("select"); for (const o of options) { const val = typeof o === "string" ? o : o.value; const label = typeof o === "string" ? o : o.label; const opt = el("option", {}, label); opt.value = val; if (val === value) opt.selected = true; s.append(opt); } return s; }
|
|
506
|
-
const impactText = (kind) => t(kind === "now" ? "impactNow" : kind === "fleet" ? "impactFleet" : "impactAgent");
|
|
507
|
-
// The server derives every field's impact from HOT_INSTANCE_CONFIG_KEYS and
|
|
508
|
-
// hands it over at /api/settings/schema, so this page never decides it.
|
|
509
|
-
const impactOf = (field) => state.schema.impacts[field] || "instance";
|
|
510
|
-
const impact = (field) => el("span", { class: "impact" }, impactText(impactOf(field)));
|
|
511
|
-
// A batch costs as much as its worst member, in the order the server ships.
|
|
512
|
-
const batchImpact = (keys, impactFor) => {
|
|
513
|
-
const order = state.schema.order;
|
|
514
|
-
return order[keys.reduce((worst, k) => Math.max(worst, order.indexOf(impactFor(k))), 0)];
|
|
515
|
-
};
|
|
516
|
-
/** One expandable drawer. Advanced settings are collapsed, never removed. */
|
|
517
|
-
function drawer(title, ...children) {
|
|
518
|
-
const box = el("details", { class: "drawer" });
|
|
519
|
-
box.append(el("summary", {}, title), el("div", { class: "drawer-body" }, ...children));
|
|
520
|
-
return box;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// ── Status emojis (#1005) ──
|
|
524
|
-
// The page never resolves or checks an emoji itself: /status-emojis/preview
|
|
525
|
-
// runs the code the bots react with, so what shows here is what gets
|
|
526
|
-
// stamped, and a picked server emoji is stored in the form they react with.
|
|
527
|
-
let statusEmojiCatalog = null;
|
|
528
|
-
async function loadStatusEmojiCatalog() {
|
|
529
|
-
if (statusEmojiCatalog) return statusEmojiCatalog;
|
|
530
|
-
const r = await api("/api/settings/status-emojis");
|
|
531
|
-
statusEmojiCatalog = r.ok ? r.body : { keys: [], builtins: {}, telegram_allowed: [], suggestions: [] };
|
|
532
|
-
return statusEmojiCatalog;
|
|
533
|
-
}
|
|
534
|
-
const guildEmojiLists = new Map();
|
|
535
|
-
function fetchGuildEmojis(channelId, refresh) {
|
|
536
|
-
if (refresh || !guildEmojiLists.has(channelId)) {
|
|
537
|
-
guildEmojiLists.set(channelId, api(`/api/settings/status-emojis/guild-emojis?channel=${encodeURIComponent(channelId)}${refresh ? "&refresh=1" : ""}`)
|
|
538
|
-
.then(r => r.ok ? r.body : { error: r.body?.error || `HTTP ${r.status}` }));
|
|
539
|
-
}
|
|
540
|
-
return guildEmojiLists.get(channelId);
|
|
541
|
-
}
|
|
542
|
-
// no-referrer: the settings URL can carry the web token.
|
|
543
|
-
const emojiImg = (url, alt) => el("img", { src: url, alt, referrerpolicy: "no-referrer", loading: "lazy" });
|
|
544
|
-
const emojiGlyph = (entry) => el("span", { class: "se-glyph" }, entry.image_url ? emojiImg(entry.image_url, entry.display) : entry.value);
|
|
545
|
-
/** Keys in catalog order, blanks dropped — so an unchanged map compares equal. */
|
|
546
|
-
function orderedStatusMap(map, keys) {
|
|
547
|
-
const out = {};
|
|
548
|
-
for (const key of keys) { const v = map?.[key]; if (typeof v === "string" && v.trim()) out[key] = v.trim(); }
|
|
549
|
-
return Object.keys(out).length ? out : null;
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Editor for one status_emojis map. `scope.platform()` and
|
|
553
|
-
* `scope.channelId()` say where it applies; `scope.previewBody(map)` is what
|
|
554
|
-
* the preview resolves (the channel map, or an instance override on top of
|
|
555
|
-
* its channel's).
|
|
556
|
-
*/
|
|
557
|
-
function statusEmojiEditor(own, scope) {
|
|
558
|
-
const values = { ...(own || {}) };
|
|
559
|
-
let keys = [];
|
|
560
|
-
const box = el("div", { class: "se" });
|
|
561
|
-
const preview = el("div", { class: "se-preview" }, el("span", { class: "hint" }, "…"));
|
|
562
|
-
const problems = el("div", { class: "feedback" });
|
|
563
|
-
const rows = el("div", { class: "se-rows" });
|
|
564
|
-
let timer = null, seq = 0;
|
|
565
|
-
const refresh = () => { clearTimeout(timer); timer = setTimeout(runPreview, 150); };
|
|
566
|
-
async function runPreview() {
|
|
567
|
-
const mine = ++seq;
|
|
568
|
-
const r = await api("/api/settings/status-emojis/preview", { method: "POST", body: JSON.stringify(scope.previewBody(orderedStatusMap(values, keys) || {})) });
|
|
569
|
-
if (mine !== seq) return;
|
|
570
|
-
preview.innerHTML = "";
|
|
571
|
-
if (!r.ok) { problems.className = "feedback error"; problems.textContent = `✗ ${r.body?.error || "preview failed"}`; return; }
|
|
572
|
-
for (const entry of r.body.entries) {
|
|
573
|
-
preview.append(el("span", { class: "se-item", title: `${entry.key}: ${entry.value}` }, emojiGlyph(entry),
|
|
574
|
-
el("span", {}, t(`se_${entry.key}`)),
|
|
575
|
-
// Say where every value comes from, defaults included: 👀 is the built-in for three statuses, and
|
|
576
|
-
// an untagged 👀 left under "Processing" after Received is changed reads as if it had moved there.
|
|
577
|
-
el("span", { class: "tag" + (entry.source === "builtin" ? " se-default" : "") }, t(`se_src_${entry.source}`))));
|
|
578
|
-
}
|
|
579
|
-
problems.className = r.body.problems.length ? "feedback error" : "feedback";
|
|
580
|
-
problems.textContent = r.body.problems.map(p => `⚠ ${t(`se_${p.key}`)}: ${p.problem} — ${t("se_fallback")}`).join(" ");
|
|
581
|
-
}
|
|
582
|
-
function openPicker(row, key, input) {
|
|
583
|
-
row.querySelector(".se-picker")?.remove();
|
|
584
|
-
const picker = el("div", { class: "se-picker" });
|
|
585
|
-
const choose = (value) => { input.value = value; values[key] = value; picker.remove(); refresh(); };
|
|
586
|
-
const grid = (items) => { const g = el("div", { class: "se-grid" }); for (const it of items) g.append(it); return g; };
|
|
587
|
-
const unicodeButton = (emoji) => el("button", { type: "button", class: "sm", title: emoji, onclick: () => choose(emoji) }, emoji);
|
|
588
|
-
const platform = scope.platform();
|
|
589
|
-
const cat = statusEmojiCatalog;
|
|
590
|
-
// progress_prefix is message text, so Telegram's reaction set does not bind it.
|
|
591
|
-
const unicode = platform === "telegram" && key !== "progress_prefix" ? cat.telegram_allowed : cat.suggestions;
|
|
592
|
-
picker.append(el("div", { class: "hint" }, platform === "telegram" && key !== "progress_prefix" ? t("se_telegramOnly") : t("se_unicode")), grid(unicode.map(unicodeButton)));
|
|
593
|
-
const channelId = scope.channelId();
|
|
594
|
-
if (platform === "discord" && channelId) {
|
|
595
|
-
const guildBox = el("div");
|
|
596
|
-
const load = async (refreshList) => {
|
|
597
|
-
guildBox.innerHTML = ""; guildBox.append(el("div", { class: "hint" }, "…"));
|
|
598
|
-
const list = await fetchGuildEmojis(channelId, refreshList);
|
|
599
|
-
guildBox.innerHTML = "";
|
|
600
|
-
if (list.error) { guildBox.append(el("div", { class: "feedback error" }, `✗ ${list.error}`)); return; }
|
|
601
|
-
// One group per server the bot can draw on (#1021), primary first;
|
|
602
|
-
// a bot reacts with a custom emoji from any server it is in.
|
|
603
|
-
const groups = list.guilds;
|
|
604
|
-
// Discord checks Use External Emojis in the channel being reacted in.
|
|
605
|
-
if (groups.length > 1) guildBox.append(el("div", { class: "hint" }, t("se_externalEmojiHint")));
|
|
606
|
-
for (const g of groups) {
|
|
607
|
-
if (groups.length > 1) {
|
|
608
|
-
guildBox.append(el("div", { class: "hint se-guild" }, g.name || g.id,
|
|
609
|
-
...(g.primary ? [" ", el("span", { class: "tag" }, t("se_primaryServer"))] : [])));
|
|
610
|
-
}
|
|
611
|
-
if (g.error) { guildBox.append(el("div", { class: "feedback error" }, `✗ ${g.error}`)); continue; }
|
|
612
|
-
if (!g.emojis.length) { guildBox.append(el("div", { class: "hint" }, t("se_noServerEmojis"))); continue; }
|
|
613
|
-
// An unavailable emoji (e.g. the server lost the Boost tier it
|
|
614
|
-
// needs) cannot be reacted with; picking it would preview fine and
|
|
615
|
-
// then fail silently on every status. Shown, but not pickable.
|
|
616
|
-
guildBox.append(grid(g.emojis.map(e => el("button", {
|
|
617
|
-
type: "button", class: "sm" + (e.available ? "" : " off"), title: `:${e.name}:${e.available ? "" : ` (${t("se_unavailable")})`}`,
|
|
618
|
-
...(e.available ? {} : { disabled: "disabled", "aria-disabled": "true" }),
|
|
619
|
-
onclick: () => { if (e.available) choose(e.value); },
|
|
620
|
-
}, emojiImg(e.image_url, `:${e.name}:`)))));
|
|
621
|
-
}
|
|
622
|
-
};
|
|
623
|
-
picker.append(el("div", { class: "hint", style: "display:flex;gap:8px;align-items:center" }, t("se_serverEmojis"),
|
|
624
|
-
el("button", { type: "button", class: "sm ghost", onclick: () => load(true) }, "↻")), guildBox);
|
|
625
|
-
load(false);
|
|
626
|
-
}
|
|
627
|
-
picker.append(el("div", { class: "actions" }, el("button", { type: "button", class: "sm ghost", onclick: () => picker.remove() }, t("close"))));
|
|
628
|
-
row.append(picker);
|
|
629
|
-
}
|
|
630
|
-
(async () => {
|
|
631
|
-
const cat = await loadStatusEmojiCatalog();
|
|
632
|
-
keys = cat.keys;
|
|
633
|
-
const builtins = cat.builtins[scope.platform() === "telegram" ? "telegram" : "discord"] || {};
|
|
634
|
-
for (const key of keys) {
|
|
635
|
-
const input = el("input", { type: "text", value: values[key] || "", placeholder: builtins[key] || "", autocomplete: "off" });
|
|
636
|
-
input.addEventListener("input", () => { values[key] = input.value; refresh(); });
|
|
637
|
-
const row = el("div", { class: "se-row" });
|
|
638
|
-
row.append(el("label", {}, t(`se_${key}`)), input,
|
|
639
|
-
el("button", { type: "button", class: "sm", onclick: () => openPicker(row, key, input) }, t("se_pick")),
|
|
640
|
-
el("button", { type: "button", class: "sm ghost", title: t("se_useDefault"), onclick: () => { input.value = ""; values[key] = ""; refresh(); } }, "×"));
|
|
641
|
-
rows.append(row);
|
|
642
|
-
}
|
|
643
|
-
runPreview();
|
|
644
|
-
})();
|
|
645
|
-
box.append(preview, problems, rows, el("div", { class: "hint" }, t("se_hint")));
|
|
646
|
-
return { box, refresh, value: () => orderedStatusMap(values, keys), baseline: () => orderedStatusMap(own, keys) };
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* One modal edits one object, and closing it stages a change — it never
|
|
651
|
-
* writes. Applying stays where it was: the Apply button on the pending bar.
|
|
652
|
-
*/
|
|
653
|
-
let modalState = null;
|
|
654
|
-
function openModal({ title, subtitle, body, impacts, stage, doneLabel, actions }) {
|
|
655
|
-
modalState = { stage };
|
|
656
|
-
$("modalActions").innerHTML = "";
|
|
657
|
-
// A wizard drives its own footer; an editor uses the standard Cancel/Done.
|
|
658
|
-
$("modalDone").style.display = actions ? "none" : "";
|
|
659
|
-
if (actions) $("modalActions").append(...actions);
|
|
660
|
-
$("modalTitle").textContent = title;
|
|
661
|
-
$("modalSubtitle").textContent = subtitle || "";
|
|
662
|
-
$("modalBody").innerHTML = "";
|
|
663
|
-
$("modalBody").append(body);
|
|
664
|
-
// The footer says what the whole modal will cost, so the impact per field
|
|
665
|
-
// does not have to be added up by eye.
|
|
666
|
-
const kinds = [...new Set((impacts || []).map(impactOf))];
|
|
667
|
-
$("modalImpact").textContent = kinds.length ? kinds.map(impactText).join(" · ") : "";
|
|
668
|
-
$("modalDone").textContent = doneLabel || t("modalDone");
|
|
669
|
-
$("modal").classList.add("open");
|
|
670
|
-
$("modalBackdrop").classList.add("open");
|
|
671
|
-
}
|
|
672
|
-
function closeModal() {
|
|
673
|
-
modalState = null;
|
|
674
|
-
$("modal").classList.remove("open");
|
|
675
|
-
$("modalBackdrop").classList.remove("open");
|
|
676
|
-
$("modalBody").innerHTML = "";
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// ── Setup wizard ──────────────────────────────────────────────────────────
|
|
680
|
-
//
|
|
681
|
-
// The same four steps as `agend quickstart`. There is no second flow for
|
|
682
|
-
// "a fleet already exists": the wizard is one modal that pre-fills from the
|
|
683
|
-
// running configuration and, when it finishes, hands over to the ordinary
|
|
684
|
-
// apply job — the same progress, deadline and restart recovery as every other
|
|
685
|
-
// change the panel makes.
|
|
686
|
-
const WIZARD_STEPS = 4;
|
|
687
|
-
let wiz = null;
|
|
688
|
-
|
|
689
|
-
function wizardField(labelText, input, hint) {
|
|
690
|
-
return el("div", { class: "field" }, el("label", {}, labelText), input,
|
|
691
|
-
hint ? el("div", { class: "sub" }, hint) : null);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
async function openWizard() {
|
|
695
|
-
const envRes = await api("/api/settings/quickstart/environment");
|
|
696
|
-
wiz = {
|
|
697
|
-
step: 1,
|
|
698
|
-
env: envRes.body || { backends: [], channels: [], has_fleet: false },
|
|
699
|
-
backend: (envRes.body?.backends || [])[0] || "claude-code",
|
|
700
|
-
working_directory: "",
|
|
701
|
-
instance_name: "agent-1",
|
|
702
|
-
platform: "telegram",
|
|
703
|
-
token: "",
|
|
704
|
-
token_env: "AGEND_BOT_TOKEN",
|
|
705
|
-
group_id: "",
|
|
706
|
-
guild_id: "",
|
|
707
|
-
general_channel_id: "",
|
|
708
|
-
admin_user_id: "",
|
|
709
|
-
identity: null,
|
|
710
|
-
guilds: [],
|
|
711
|
-
plan: null,
|
|
712
|
-
busy: "",
|
|
713
|
-
};
|
|
714
|
-
renderWizard();
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function renderWizard() {
|
|
718
|
-
const body = el("div", {});
|
|
719
|
-
body.append(el("div", { class: "sub" }, tf("wizardStep", wiz.step, WIZARD_STEPS)));
|
|
720
|
-
body.append(wizardStepBody());
|
|
721
|
-
if (wiz.busy) body.append(el("div", { class: "sub" }, wiz.busy));
|
|
722
|
-
|
|
723
|
-
const back = el("button", { onclick: () => { wiz.step--; renderWizard(); } }, t("wizardBack"));
|
|
724
|
-
back.disabled = wiz.step === 1;
|
|
725
|
-
const next = el("button", { class: "primary", onclick: () => advanceWizard() },
|
|
726
|
-
wiz.step === WIZARD_STEPS ? t("wizardFinish") : t("wizardNext"));
|
|
727
|
-
openModal({
|
|
728
|
-
title: t("wizardTitle"),
|
|
729
|
-
subtitle: wiz.env.has_fleet ? t("wizardRerun") : "",
|
|
730
|
-
body,
|
|
731
|
-
actions: [back, next],
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function wizardStepBody() {
|
|
736
|
-
if (wiz.step === 1) {
|
|
737
|
-
const fBackend = select(wiz.backend, wiz.env.backends.length ? wiz.env.backends : BACKENDS);
|
|
738
|
-
fBackend.onchange = () => { wiz.backend = fBackend.value; };
|
|
739
|
-
const fDir = el("input", { type: "text", value: wiz.working_directory, placeholder: "/home/you/projects/app" });
|
|
740
|
-
fDir.oninput = () => { wiz.working_directory = fDir.value.trim(); };
|
|
741
|
-
const fName = el("input", { type: "text", value: wiz.instance_name });
|
|
742
|
-
fName.oninput = () => { wiz.instance_name = fName.value.trim(); };
|
|
743
|
-
return el("div", {},
|
|
744
|
-
wizardField(t("wizardBackend"), fBackend,
|
|
745
|
-
wiz.env.backends.length ? tf("wizardBackendFound", wiz.env.backends.join(", ")) : t("wizardBackendNone")),
|
|
746
|
-
wizardField(t("workingDir"), fDir),
|
|
747
|
-
wizardField(t("wizardAgentName"), fName));
|
|
748
|
-
}
|
|
749
|
-
if (wiz.step === 2) {
|
|
750
|
-
const pick = (platform) => el("button", { class: wiz.platform === platform ? "primary" : "", onclick: () => { wiz.platform = platform; renderWizard(); } },
|
|
751
|
-
platform === "telegram" ? "Telegram" : "Discord");
|
|
752
|
-
return el("div", {}, el("div", { class: "actions" }, pick("telegram"), pick("discord")),
|
|
753
|
-
el("div", { class: "sub" }, wiz.platform === "telegram" ? t("wizardTelegramHint") : t("wizardDiscordHint")));
|
|
754
|
-
}
|
|
755
|
-
if (wiz.step === 3) return wizardCredentialsStep();
|
|
756
|
-
return wizardReviewStep();
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function wizardCredentialsStep() {
|
|
760
|
-
const fToken = el("input", { type: "password", value: wiz.token, placeholder: "123456:ABC-DEF…" });
|
|
761
|
-
fToken.oninput = () => { wiz.token = fToken.value.trim(); wiz.identity = null; };
|
|
762
|
-
const fEnv = el("input", { type: "text", value: wiz.token_env });
|
|
763
|
-
fEnv.oninput = () => { wiz.token_env = fEnv.value.trim(); };
|
|
764
|
-
const verify = el("button", { onclick: async () => {
|
|
765
|
-
wiz.busy = t("wizardVerifying"); renderWizard();
|
|
766
|
-
const res = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "verify", platform: wiz.platform, token: wiz.token }) });
|
|
767
|
-
wiz.identity = res.body?.identity || { valid: false };
|
|
768
|
-
wiz.busy = "";
|
|
769
|
-
if (wiz.identity.valid && wiz.platform === "discord") {
|
|
770
|
-
const guilds = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "guilds", token: wiz.token }) });
|
|
771
|
-
wiz.guilds = guilds.body?.guilds || [];
|
|
772
|
-
}
|
|
773
|
-
renderWizard();
|
|
774
|
-
} }, t("wizardVerify"));
|
|
775
|
-
|
|
776
|
-
const rows = [
|
|
777
|
-
wizardField(t("wizardToken"), fToken, wiz.platform === "telegram" ? t("wizardTokenTelegram") : t("wizardTokenDiscord")),
|
|
778
|
-
wizardField(t("tokenEnv"), fEnv),
|
|
779
|
-
el("div", { class: "actions" }, verify,
|
|
780
|
-
wiz.identity ? el("span", { class: wiz.identity.valid ? "s" : "e" },
|
|
781
|
-
wiz.identity.valid ? `✓ ${wiz.identity.username || ""}` : `✗ ${wiz.identity.reason || ""}`) : null),
|
|
782
|
-
];
|
|
783
|
-
|
|
784
|
-
// Only the ids that platform actually has.
|
|
785
|
-
if (wiz.platform === "discord") {
|
|
786
|
-
const fGuild = select(wiz.guild_id, ["", ...wiz.guilds.map(g => ({ value: g.id, label: `${g.name} (${g.id})` }))]);
|
|
787
|
-
fGuild.onchange = () => { wiz.guild_id = fGuild.value; };
|
|
788
|
-
const fGeneral = el("input", { type: "text", value: wiz.general_channel_id, placeholder: "General channel id" });
|
|
789
|
-
fGeneral.oninput = () => { wiz.general_channel_id = fGeneral.value.trim(); };
|
|
790
|
-
const fUser = el("input", { type: "text", value: wiz.admin_user_id, placeholder: "your Discord user id" });
|
|
791
|
-
fUser.oninput = () => { wiz.admin_user_id = fUser.value.trim(); };
|
|
792
|
-
rows.push(wizardField(t("guildIdField"), fGuild), wizardField(t("wizardGeneralChannel"), fGeneral), wizardField(t("wizardAdminUser"), fUser));
|
|
793
|
-
} else {
|
|
794
|
-
const fGroup = el("input", { type: "text", value: wiz.group_id, placeholder: "-1001234567890" });
|
|
795
|
-
fGroup.oninput = () => { wiz.group_id = fGroup.value.trim(); };
|
|
796
|
-
const detect = el("button", { onclick: async () => {
|
|
797
|
-
wiz.busy = t("wizardWaitingStart"); renderWizard();
|
|
798
|
-
const res = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "await-telegram-start", token: wiz.token, offset: wiz.offset || 0 }) });
|
|
799
|
-
wiz.busy = "";
|
|
800
|
-
if (!res.ok) { showBanner(res.body?.error || t("applyFailed"), true); renderWizard(); return; }
|
|
801
|
-
wiz.offset = res.body.offset;
|
|
802
|
-
if (res.body.found) {
|
|
803
|
-
wiz.group_id = String(res.body.found.groupId);
|
|
804
|
-
wiz.admin_user_id = String(res.body.found.userId);
|
|
805
|
-
} else showBanner(t("wizardNoStartYet"), true);
|
|
806
|
-
renderWizard();
|
|
807
|
-
} }, t("wizardDetect"));
|
|
808
|
-
const fUser = el("input", { type: "text", value: wiz.admin_user_id, placeholder: "your Telegram user id" });
|
|
809
|
-
fUser.oninput = () => { wiz.admin_user_id = fUser.value.trim(); };
|
|
810
|
-
rows.push(wizardField(t("groupIdField"), fGroup, t("wizardDetectHint")),
|
|
811
|
-
el("div", { class: "actions" }, detect), wizardField(t("wizardAdminUser"), fUser));
|
|
812
|
-
}
|
|
813
|
-
return el("div", {}, ...rows);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function wizardReviewStep() {
|
|
817
|
-
const box = el("div", {});
|
|
818
|
-
if (!wiz.plan) { box.append(el("div", { class: "sub" }, t("wizardPlanLoading"))); return box; }
|
|
819
|
-
box.append(
|
|
820
|
-
el("div", { class: "field" }, el("strong", {}, t("wizardWillWrite"))),
|
|
821
|
-
el("pre", { class: "yaml" }, toYaml({ channels: [wiz.plan.channel], instances: { [wiz.plan.instance.name]: { working_directory: wiz.plan.instance.working_directory, backend: wiz.plan.instance.backend } } })),
|
|
822
|
-
el("div", { class: "field" }, el("div", { class: "sub" }, tf("wizardEnvKeys", wiz.plan.env_keys.join(", ")))),
|
|
823
|
-
);
|
|
824
|
-
for (const warning of wiz.plan.warnings || []) box.append(el("div", { class: "field hint" }, "⚠️ " + warning));
|
|
825
|
-
return box;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
async function advanceWizard() {
|
|
829
|
-
if (wiz.step === 1) {
|
|
830
|
-
if (!wiz.working_directory.startsWith("/")) { showBanner(t("wizardNeedDir"), true); return; }
|
|
831
|
-
wiz.step = 2; renderWizard(); return;
|
|
832
|
-
}
|
|
833
|
-
if (wiz.step === 2) { wiz.step = 3; renderWizard(); return; }
|
|
834
|
-
if (wiz.step === 3) {
|
|
835
|
-
if (!wiz.identity?.valid) { showBanner(t("wizardNeedVerify"), true); return; }
|
|
836
|
-
const res = await api("/api/settings/quickstart/plan", { method: "POST", body: JSON.stringify(wizardInput()) });
|
|
837
|
-
if (!res.ok) { showBanner(res.body?.error || t("applyFailed"), true); return; }
|
|
838
|
-
wiz.plan = res.body; wiz.step = 4; renderWizard(); return;
|
|
839
|
-
}
|
|
840
|
-
await finishWizard();
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
function wizardInput() {
|
|
844
|
-
return {
|
|
845
|
-
platform: wiz.platform, token_env: wiz.token_env, backend: wiz.backend,
|
|
846
|
-
working_directory: wiz.working_directory, instance_name: wiz.instance_name,
|
|
847
|
-
group_id: wiz.group_id || undefined, guild_id: wiz.guild_id || undefined,
|
|
848
|
-
general_channel_id: wiz.general_channel_id || undefined,
|
|
849
|
-
admin_user_id: wiz.admin_user_id || undefined,
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/**
|
|
854
|
-
* Write the files, then start an ordinary apply job.
|
|
855
|
-
*
|
|
856
|
-
* The wizard does not apply anything itself — "create and start" is the same
|
|
857
|
-
* job the Apply button produces, so it has progress, a deadline and recovery
|
|
858
|
-
* across the restart it may cause instead of a silent spinner.
|
|
859
|
-
*/
|
|
860
|
-
async function finishWizard() {
|
|
861
|
-
const commit = await api("/api/settings/quickstart/commit", { method: "POST", body: JSON.stringify({ ...wizardInput(), token: wiz.token }) });
|
|
862
|
-
if (!commit.ok) { showBanner(commit.body?.error || t("applyFailed"), true); return; }
|
|
863
|
-
if (commit.body?.secret_mode_ok === false) showBanner(t("wizardEnvPerms"), true);
|
|
864
|
-
closeModal();
|
|
865
|
-
const key = (crypto.randomUUID ? crypto.randomUUID() : `w-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
866
|
-
const started = await api("/api/settings/apply", {
|
|
867
|
-
method: "POST",
|
|
868
|
-
headers: { "Idempotency-Key": key },
|
|
869
|
-
body: JSON.stringify({ idempotency_key: key }),
|
|
870
|
-
});
|
|
871
|
-
if (started.status === 409) { showBanner(t("applyBusy"), true); return; }
|
|
872
|
-
if (!started.ok || !started.body?.id) { showBanner(started.body?.error || t("applyFailed"), true); return; }
|
|
873
|
-
await watchApplyJob(started.body);
|
|
874
|
-
await reload();
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
function updatePendingBar() {
|
|
878
|
-
const n = state.pending.size;
|
|
879
|
-
$("pendingBar").classList.toggle("open", n > 0);
|
|
880
|
-
$("pendingCount").textContent = tf("changesPending", n);
|
|
881
|
-
const kinds = new Set([...state.pending.values()].map(change => change.impact));
|
|
882
|
-
$("pendingImpact").textContent = [...kinds].map(impactText).join(" · ");
|
|
883
|
-
}
|
|
884
|
-
function stageChange(key, change) {
|
|
885
|
-
state.pending.set(key, change);
|
|
886
|
-
updatePendingBar();
|
|
887
|
-
showBanner(tf("queued", change.label));
|
|
888
|
-
}
|
|
889
|
-
async function applyPendingChanges() {
|
|
890
|
-
const button = $("applyChanges"); button.disabled = true;
|
|
891
|
-
// Complete every safety confirmation before the first write. Otherwise a
|
|
892
|
-
// cancelled access-mode change could leave earlier staged settings applied
|
|
893
|
-
// and later ones pending (a surprising partial apply).
|
|
894
|
-
for (const change of state.pending.values()) {
|
|
895
|
-
if (change.confirm && !change.confirm()) {
|
|
896
|
-
showBanner(change.cancelMessage || t("accessChangeCancelled"), true);
|
|
897
|
-
button.disabled = false; return;
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
for (const [key, change] of [...state.pending]) {
|
|
901
|
-
let result;
|
|
902
|
-
try { result = await change.apply(); }
|
|
903
|
-
catch (error) { result = { ok: false, body: { error: error.message } }; }
|
|
904
|
-
if (!result?.ok) {
|
|
905
|
-
showBanner(`${change.label}: ${result?.body?.error || t("applyFailed")}`, true);
|
|
906
|
-
change.cleanup?.();
|
|
907
|
-
button.disabled = false; return;
|
|
908
|
-
}
|
|
909
|
-
change.cleanup?.();
|
|
910
|
-
state.pending.delete(key); updatePendingBar();
|
|
911
|
-
}
|
|
912
|
-
// The key is generated here, before the first attempt, and reused by every
|
|
913
|
-
// retry: a POST whose response is lost on a phone must rejoin the same job
|
|
914
|
-
// instead of applying everything twice.
|
|
915
|
-
const key = (crypto.randomUUID ? crypto.randomUUID() : `k-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
916
|
-
const started = await api("/api/settings/apply", {
|
|
917
|
-
method: "POST",
|
|
918
|
-
headers: { "Idempotency-Key": key },
|
|
919
|
-
body: JSON.stringify({ idempotency_key: key }),
|
|
920
|
-
});
|
|
921
|
-
if (started.status === 409) {
|
|
922
|
-
// One reconcile at a time. The writes above already landed, so re-applying
|
|
923
|
-
// later is safe and idempotent.
|
|
924
|
-
showBanner(t("applyBusy"), true);
|
|
925
|
-
button.disabled = false;
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
if (!started.ok || !started.body?.id) {
|
|
929
|
-
showBanner(started.body?.error || t("applyFailed"), true);
|
|
930
|
-
button.disabled = false;
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
await watchApplyJob(started.body);
|
|
934
|
-
await reload();
|
|
935
|
-
button.disabled = false;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function applyRow(row) {
|
|
939
|
-
const icon = row.status === "done" ? "✓"
|
|
940
|
-
: row.status === "failed" ? "✗"
|
|
941
|
-
: row.status === "restart-required" ? "🔄🔄"
|
|
942
|
-
: row.status === "running" ? "…" : "·";
|
|
943
|
-
// restart-required is terminal and is NOT success: the value is saved and a
|
|
944
|
-
// running AgEnD cannot adopt it.
|
|
945
|
-
const note = row.target === "fleet" && row.status === "running" ? t("restartFleetTitle")
|
|
946
|
-
: row.status === "restart-required" ? t("applyRestartNeeded")
|
|
947
|
-
: row.settled_by === "no-change" ? t("applyNoChange")
|
|
948
|
-
: row.settled_by === "fleet-restart" ? "(restart)"
|
|
949
|
-
: row.kind === "hot" ? t("impactNow") : t("impactAgent");
|
|
950
|
-
return el("div", { class: "apply-row" },
|
|
951
|
-
el("span", {}, icon),
|
|
952
|
-
el("span", { class: "name" }, row.target === "fleet" ? "AgEnD" : shortName(row.target, {})),
|
|
953
|
-
el("span", { class: "sub" }, note),
|
|
954
|
-
row.error ? el("span", { class: "err" }, row.error) : null);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// One restart at a time, and a button that says so. Without this the panel re-drew a fresh, enabled
|
|
958
|
-
// "Restart AgEnD" button the instant the request was accepted, so a second press asked again.
|
|
959
|
-
let restartingFleet = false;
|
|
960
|
-
async function restartFleet(job, button) {
|
|
961
|
-
if (restartingFleet) return;
|
|
962
|
-
// Its own confirmation, its own key. A single Apply must never be able to
|
|
963
|
-
// carry a full-fleet restart along with it.
|
|
964
|
-
if (!confirm(t("restartFleetConfirm"))) return;
|
|
965
|
-
restartingFleet = true;
|
|
966
|
-
const label = button?.textContent;
|
|
967
|
-
if (button) { button.disabled = true; button.textContent = t("restartFleetBusy"); }
|
|
968
|
-
const key = (crypto.randomUUID ? crypto.randomUUID() : `r-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
969
|
-
let res;
|
|
970
|
-
try {
|
|
971
|
-
res = await api("/api/settings/restart-fleet", {
|
|
972
|
-
method: "POST",
|
|
973
|
-
headers: { "Idempotency-Key": key },
|
|
974
|
-
body: JSON.stringify({ job_id: job.id, confirm: "restart-agend", idempotency_key: key }),
|
|
975
|
-
});
|
|
976
|
-
} catch {
|
|
977
|
-
res = { ok: false, status: 0, body: null };
|
|
978
|
-
}
|
|
979
|
-
if (!res.ok) {
|
|
980
|
-
restartingFleet = false;
|
|
981
|
-
if (button?.isConnected) { button.disabled = false; button.textContent = label; }
|
|
982
|
-
const secs = res.body?.retry_after_seconds;
|
|
983
|
-
showBanner(secs
|
|
984
|
-
? tf("restartFleetRateLimited", res.body?.error || "", Math.ceil(secs / 60))
|
|
985
|
-
: tf("restartFleetRefused", res.body?.error || t("applyFailed")), true);
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
showBanner(t("restartFleetStarted"));
|
|
989
|
-
// The server has moved this job's fleet row to "running" (and will settle it after the restart). Watch
|
|
990
|
-
// *that* job: the one in hand still says "restart-required" and is finished, so watching it ended at once.
|
|
991
|
-
const fresh = await api(`/api/settings/apply/${encodeURIComponent(job.id)}`).catch(() => null);
|
|
992
|
-
const watching = fresh?.ok && fresh.body?.id ? fresh.body : {
|
|
993
|
-
...job, status: "running",
|
|
994
|
-
targets: job.targets.map(row => row.target === "fleet" ? { ...row, status: "running" } : row),
|
|
995
|
-
};
|
|
996
|
-
try {
|
|
997
|
-
await watchApplyJob(watching);
|
|
998
|
-
await reload();
|
|
999
|
-
} finally {
|
|
1000
|
-
restartingFleet = false;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function renderApplyJob(job) {
|
|
1005
|
-
$("applyPanel").classList.add("open");
|
|
1006
|
-
const fleetRestarting = job.status === "running" && job.targets.some(row => row.target === "fleet" && row.status === "running");
|
|
1007
|
-
$("applyTitle").textContent = fleetRestarting ? t("restartFleetTitle")
|
|
1008
|
-
: job.status === "done"
|
|
1009
|
-
? (job.targets.some(r => r.status === "failed") ? t("applyPartial") : t("applyDone"))
|
|
1010
|
-
: job.status === "failed" ? t("applyPartial") : t("applying");
|
|
1011
|
-
const needsRestart = job.targets.some(row => row.status === "restart-required");
|
|
1012
|
-
const mismatch = state.schema.fleet_signature_mismatch;
|
|
1013
|
-
$("applyNote").textContent = fleetRestarting ? t("restartFleetWaiting")
|
|
1014
|
-
: job.overdue
|
|
1015
|
-
? tf("applyStillWorking", Math.round(job.elapsed_ms / 1000))
|
|
1016
|
-
: mismatch && needsRestart ? tf("signatureMismatch", mismatch.join(", "))
|
|
1017
|
-
: needsRestart ? t("applyRestartHint") : "";
|
|
1018
|
-
const rows = $("applyRows"); rows.innerHTML = "";
|
|
1019
|
-
for (const row of job.targets) rows.append(applyRow(row));
|
|
1020
|
-
// A restart that cannot clear the mismatch is worse than no button at all.
|
|
1021
|
-
if (needsRestart && !mismatch) {
|
|
1022
|
-
const restartButton = el("button", { class: "primary", onclick: ev => restartFleet(job, ev.currentTarget) },
|
|
1023
|
-
restartingFleet ? t("restartFleetBusy") : t("restartFleetButton"));
|
|
1024
|
-
// While a restart is under way this stays, disabled and saying so, if anything re-draws the panel.
|
|
1025
|
-
if (restartingFleet) restartButton.disabled = true;
|
|
1026
|
-
rows.append(el("div", { class: "apply-row" }, restartButton));
|
|
1027
|
-
}
|
|
1028
|
-
if (job.error) rows.append(el("div", { class: "apply-row err" }, job.error));
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
/**
|
|
1032
|
-
* GET is the authority. The SSE stream carries no event id, so a reconnect
|
|
1033
|
-
* cannot replay missed frames; it only shortens the wait between polls.
|
|
1034
|
-
*/
|
|
1035
|
-
async function watchApplyJob(job) {
|
|
1036
|
-
renderApplyJob(job);
|
|
1037
|
-
const deadline = Date.now() + 10 * 60_000;
|
|
1038
|
-
let current = job;
|
|
1039
|
-
while (current.status === "running" && Date.now() < deadline) {
|
|
1040
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
1041
|
-
// A restart takes the server down under this request: the fetch rejects until AgEnD is back. That is
|
|
1042
|
-
// the expected middle of a restart, not the end of the watch.
|
|
1043
|
-
let res;
|
|
1044
|
-
try { res = await api(`/api/settings/apply/${encodeURIComponent(job.id)}`); } catch { continue; }
|
|
1045
|
-
if (res.status === 404) {
|
|
1046
|
-
// The job outlives the process that made it, so a 404 means it aged out
|
|
1047
|
-
// of the store — not that it never happened.
|
|
1048
|
-
$("applyNote").textContent = t("applyLostJob");
|
|
1049
|
-
return;
|
|
1050
|
-
}
|
|
1051
|
-
if (!res.ok || !res.body?.id) continue;
|
|
1052
|
-
current = res.body;
|
|
1053
|
-
renderApplyJob(current);
|
|
1054
|
-
}
|
|
1055
|
-
const failed = current.targets.some(r => r.status === "failed");
|
|
1056
|
-
const needsRestart = current.targets.some(r => r.status === "restart-required");
|
|
1057
|
-
showBanner(failed ? t("applyPartial") : needsRestart ? t("applyRestartNeeded") : t("changesApplied"),
|
|
1058
|
-
failed || needsRestart);
|
|
1059
|
-
// A restart reminder has to stay on screen; a clean apply can fade.
|
|
1060
|
-
if (!needsRestart) setTimeout(() => $("applyPanel").classList.remove("open"), 4000);
|
|
1061
|
-
}
|
|
1062
|
-
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
|
|
1063
|
-
const sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1064
|
-
function changedFields(next, baseline) {
|
|
1065
|
-
return Object.fromEntries(Object.entries(next).filter(([key, value]) => !sameValue(value, baseline[key])));
|
|
1066
|
-
}
|
|
1067
|
-
// #1302: cross-instance (bot-to-bot) notices. Unset, or a value the fleet does not know, means full.
|
|
1068
|
-
const VISIBILITY_MODES = ["full", "summary", "hidden"];
|
|
1069
|
-
const visibilityDefault = (defaults) => VISIBILITY_MODES.includes(defaults?.cross_instance_visibility) ? defaults.cross_instance_visibility : "full";
|
|
1070
|
-
function overrideControl(inst, key, input, inheritedValue, impactField = `instance.${key}`) {
|
|
1071
|
-
const inherited = !hasOwn(inst, key);
|
|
1072
|
-
const toggle = el("input", { type: "checkbox" }); toggle.checked = inherited;
|
|
1073
|
-
input.disabled = inherited;
|
|
1074
|
-
if (inherited && inheritedValue !== undefined) {
|
|
1075
|
-
if (input.type === "checkbox") input.checked = !!inheritedValue;
|
|
1076
|
-
else input.value = inheritedValue;
|
|
1077
|
-
}
|
|
1078
|
-
toggle.onchange = () => { input.disabled = toggle.checked; };
|
|
1079
|
-
return { input, toggle, row: el("div", {}, input, el("label", { class: "inherit-row" }, toggle, el("span", {}, t("inheritDefault")), impact(impactField))) };
|
|
1080
|
-
}
|
|
1081
|
-
const replyGuardSupported = (backend, mode, kiroUi = "legacy") => mode === "mcp" && (backend === "claude-code" || (backend === "kiro-cli" && ["legacy", "tui"].includes(kiroUi)));
|
|
1082
|
-
const replyGuardHint = (backend, mode, kiroUi) => el(
|
|
1083
|
-
"div",
|
|
1084
|
-
{ class: "hint" },
|
|
1085
|
-
replyGuardSupported(backend, mode, kiroUi) ? t("replyGuardSupported") : tf("replyGuardUnsupported", backend, mode),
|
|
1086
|
-
);
|
|
1087
|
-
function setValidation(input, feedback, error, warning) {
|
|
1088
|
-
input.classList.toggle("invalid", !!error);
|
|
1089
|
-
feedback.className = "feedback" + (error ? " error" : warning ? " warning" : "");
|
|
1090
|
-
feedback.textContent = error ? `✗ ${error}` : warning ? `⚠ ${warning}` : "";
|
|
1091
|
-
return !error;
|
|
1092
|
-
}
|
|
1093
|
-
function confirmAccessChange(previousAccess, nextMode, nextUsers) {
|
|
1094
|
-
const previousMode = previousAccess?.mode || "locked";
|
|
1095
|
-
const previousUsers = previousAccess?.allowed_users || [];
|
|
1096
|
-
if (previousMode !== "open" && nextMode === "open" && !confirm(t("confirmOpenAccess"))) return false;
|
|
1097
|
-
const newlyEmptyLocked = nextMode === "locked" && nextUsers.length === 0
|
|
1098
|
-
&& (previousMode !== "locked" || previousUsers.length > 0);
|
|
1099
|
-
if (newlyEmptyLocked && !confirm(t("confirmLockedEmpty"))) return false;
|
|
1100
|
-
return true;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// ── My Agents ──
|
|
1104
|
-
function agentChannelLabel(inst) {
|
|
1105
|
-
if (inst.general_topic) return "general";
|
|
1106
|
-
if (!inst.channel_id) return "";
|
|
1107
|
-
const idx = channels().findIndex(c => (c.id || c.type) === inst.channel_id);
|
|
1108
|
-
const ch = channels()[idx];
|
|
1109
|
-
return ch ? `${ch.type} (${chLabel(idx)})` : inst.channel_id;
|
|
1110
|
-
}
|
|
1111
|
-
// Display name: profile display_name → else strip the -t<topicId> suffix.
|
|
1112
|
-
const shortName = (name, inst) => (inst && inst.display_name) || name.replace(/-t\d+$/, "");
|
|
1113
|
-
const STATUS_TEXT = { running: "🟢 Running", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
|
|
1114
|
-
const EXECUTION_TEXT = { idle: "🟢 Idle", working: "🔵 Working", stuck: "🔴 Stuck", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped", running: "🟢 Running" };
|
|
1115
|
-
const STATUS_ORDER = { running: 0, paused: 1, crashed: 2, stopped: 3 };
|
|
1116
|
-
const EFFORT_BACKENDS = new Set(["claude-code", "codex", "kiro-cli", "grok", "antigravity"]);
|
|
1117
|
-
const groupOf = (inst) => (inst.tags && inst.tags[0]) || "Other";
|
|
1118
|
-
function effectiveSummary(name, config, classic = false) {
|
|
1119
|
-
const live = state.live[name] || {};
|
|
1120
|
-
const defaults = classic ? (state.classic?.defaults || {}) : (state.fleet?.defaults || {});
|
|
1121
|
-
const backend = live.backend || config.backend || defaults.backend || state.fleet?.defaults?.backend || "claude-code";
|
|
1122
|
-
const model = live.model_display || config.model || defaults.model || state.fleet?.defaults?.model || "default";
|
|
1123
|
-
const effortSupported = live.effort_supported ?? EFFORT_BACKENDS.has(backend);
|
|
1124
|
-
const effort = effortSupported ? (live.effort || config.effort || state.fleet?.defaults?.effort || "auto") : "—";
|
|
1125
|
-
return { backend, model, effort };
|
|
1126
|
-
}
|
|
1127
|
-
function compactRow(name, inst) {
|
|
1128
|
-
const st = state.live[name]?.status || "stopped";
|
|
1129
|
-
const running = st === "running";
|
|
1130
|
-
const paused = st === "paused";
|
|
1131
|
-
const short = shortName(name, inst);
|
|
1132
|
-
const summary = effectiveSummary(name, inst);
|
|
1133
|
-
const chTag = agentChannelLabel(inst);
|
|
1134
|
-
const item = el("div", { class: "item" });
|
|
1135
|
-
// Single fixed-height line: dot + short name + backend + channel tag + actions.
|
|
1136
|
-
// Full ID lives in the tooltip and the Edit form; working dir only in Edit.
|
|
1137
|
-
item.append(el("div", { class: "item-row" },
|
|
1138
|
-
el("span", { class: "dot " + st, title: STATUS_TEXT[st] || st }),
|
|
1139
|
-
el("span", { class: "name", title: name }, short),
|
|
1140
|
-
el("span", { class: "sub", style: "margin:0", title: summary.backend }, summary.model),
|
|
1141
|
-
el("span", { class: "tag" }, `effort: ${summary.effort}`),
|
|
1142
|
-
chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
|
|
1143
|
-
el("span", { class: "spacer" }),
|
|
1144
|
-
el("button", { class: "sm ghost", onclick: () => {
|
|
1145
|
-
const form = agentEditForm(name, inst);
|
|
1146
|
-
openModal({ title: shortName(name, inst), subtitle: "ID: " + name, body: form.box, impacts: form.impacts, stage: form.stage });
|
|
1147
|
-
} }, t("settingsButton")),
|
|
1148
|
-
running ? el("button", { class: "sm", onclick: ev => pauseWakeAgent(name, "pause", ev.currentTarget) }, t("pause")) : null,
|
|
1149
|
-
paused ? el("button", { class: "sm", onclick: ev => pauseWakeAgent(name, "wake", ev.currentTarget) }, t("wake")) : null,
|
|
1150
|
-
!paused ? el("button", { class: "sm", onclick: ev => toggleAgent(name, running, ev.currentTarget) }, running ? t("stop") : t("start")) : null,
|
|
1151
|
-
el("details", { class: "more-menu" },
|
|
1152
|
-
el("summary", { title: "More actions" }, "⋯"),
|
|
1153
|
-
el("div", { class: "menu" }, el("button", { class: "sm danger", onclick: () => delAgent(name) }, "Delete Agent"))),
|
|
1154
|
-
));
|
|
1155
|
-
return item;
|
|
1156
|
-
}
|
|
1157
|
-
function renderAgents() {
|
|
1158
|
-
const host = $("agents"); host.innerHTML = "";
|
|
1159
|
-
const insts = state.fleet.instances || {};
|
|
1160
|
-
if (!Object.keys(insts).length) { host.className = "card empty"; host.textContent = t("noAgents"); return; }
|
|
1161
|
-
const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
|
|
1162
|
-
let names = Object.keys(insts);
|
|
1163
|
-
if (filter) names = names.filter(n => n.toLowerCase().includes(filter) || shortName(n, insts[n]).toLowerCase().includes(filter));
|
|
1164
|
-
if (!names.length) { host.className = "card empty"; host.textContent = tf("noAgentMatch", filter); return; }
|
|
1165
|
-
host.className = "card";
|
|
1166
|
-
// Group by first tag; "Other" sorts last. Within a group: Running → Crashed
|
|
1167
|
-
// → Stopped, then name.
|
|
1168
|
-
const groups = {};
|
|
1169
|
-
for (const n of names) (groups[groupOf(insts[n])] ??= []).push(n);
|
|
1170
|
-
const gnames = Object.keys(groups).sort((a, b) => a === "Other" ? 1 : b === "Other" ? -1 : a.localeCompare(b));
|
|
1171
|
-
for (const g of gnames) {
|
|
1172
|
-
const members = groups[g].sort((a, b) => {
|
|
1173
|
-
const sa = STATUS_ORDER[state.live[a]?.status || "stopped"], sb = STATUS_ORDER[state.live[b]?.status || "stopped"];
|
|
1174
|
-
return sa !== sb ? sa - sb : a.localeCompare(b);
|
|
1175
|
-
});
|
|
1176
|
-
const collapsed = state.collapsedGroups.has(g) && !filter; // searching expands all
|
|
1177
|
-
host.append(el("div", { class: "group-head", onclick: () => { state.collapsedGroups.has(g) ? state.collapsedGroups.delete(g) : state.collapsedGroups.add(g); renderAgents(); } },
|
|
1178
|
-
el("span", { class: "caret" }, collapsed ? "▶" : "▼"), el("span", {}, g), el("span", { class: "gcount" }, `(${members.length})`)));
|
|
1179
|
-
if (collapsed) continue;
|
|
1180
|
-
for (const name of members) host.append(compactRow(name, insts[name]));
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
function agentEditForm(name, inst) {
|
|
1184
|
-
const box = el("div", { class: "edit" });
|
|
1185
|
-
const defaults = state.fleet.defaults || {};
|
|
1186
|
-
const fBackend = backendSelect(inst.backend || state.fleet.defaults?.backend || "claude-code");
|
|
1187
|
-
const fModel = el("input", { type: "text", value: inst.model || "", placeholder: "(inherit default)" });
|
|
1188
|
-
const fWd = el("input", { type: "text", value: inst.working_directory || "" });
|
|
1189
|
-
const fChan = select(inst.channel_id || "", ["", ...channelIds()]);
|
|
1190
|
-
const fDesc = el("input", { type: "text", value: inst.description || "" });
|
|
1191
|
-
const fSys = el("input", { type: "text", value: inst.systemPrompt || "", placeholder: "e.g. file:prompts/dev.md" });
|
|
1192
|
-
const tags = [...(inst.tags || [])];
|
|
1193
|
-
const fGeneral = el("input", { type: "checkbox" }); fGeneral.checked = !!inst.general_topic;
|
|
1194
|
-
const auto = overrideControl(inst, "auto_pause_after", el("input", { type: "number", min: "0", step: "1", value: inst.auto_pause_after ?? defaults.auto_pause_after ?? 0, placeholder: "0 = disabled" }), defaults.auto_pause_after ?? 0);
|
|
1195
|
-
const hangRaw = inst.hang_detector || {};
|
|
1196
|
-
const hangHolder = hasOwn(hangRaw, "timeout_minutes") ? { value: hangRaw.timeout_minutes } : {};
|
|
1197
|
-
const hang = overrideControl(hangHolder, "value", el("input", { type: "number", min: "0.1", step: "0.5", value: hangRaw.timeout_minutes ?? defaults.hang_detector?.timeout_minutes ?? 15 }), defaults.hang_detector?.timeout_minutes ?? 15, "instance.hang_detector");
|
|
1198
|
-
const agentMode = overrideControl(inst, "agent_mode", select(inst.agent_mode || defaults.agent_mode || "mcp", ["mcp", "cli"]), defaults.agent_mode || "mcp");
|
|
1199
|
-
const toolSet = overrideControl(inst, "tool_set", select(inst.tool_set || defaults.tool_set || "full", ["full", "standard", "minimal"]), defaults.tool_set || "full");
|
|
1200
|
-
const toolProgress = overrideControl(inst, "tool_progress", select(inst.tool_progress ?? defaults.tool_progress ?? "off", ["off", "standard", "verbose"]), defaults.tool_progress ?? "off");
|
|
1201
|
-
// #1302. The baseline is what the picker shows before anyone touches it, so a value it cannot show (a typo in
|
|
1202
|
-
// fleet.yaml, which the fleet reads as unset) is never rewritten by an unrelated edit.
|
|
1203
|
-
const visibility = overrideControl(inst, "cross_instance_visibility", select(inst.cross_instance_visibility ?? visibilityDefault(defaults), VISIBILITY_MODES), visibilityDefault(defaults));
|
|
1204
|
-
const visibilityBaseline = hasOwn(inst, "cross_instance_visibility") ? visibility.input.value : null;
|
|
1205
|
-
const fReplyGuard = el("input", { type: "checkbox" });
|
|
1206
|
-
fReplyGuard.checked = inst.reply_completion_guard ?? defaults.reply_completion_guard ?? true;
|
|
1207
|
-
const replyGuard = overrideControl(inst, "reply_completion_guard", fReplyGuard, defaults.reply_completion_guard ?? true);
|
|
1208
|
-
const kiroUi = inst.kiro_ui ?? defaults.kiro_ui ?? "legacy";
|
|
1209
|
-
const replyHint = replyGuardHint(fBackend.value, agentMode.input.value, kiroUi);
|
|
1210
|
-
const refreshReplyHint = () => {
|
|
1211
|
-
const supported = replyGuardSupported(fBackend.value, agentMode.input.value, kiroUi);
|
|
1212
|
-
replyHint.textContent = supported ? t("replyGuardSupported") : tf("replyGuardUnsupported", fBackend.value, agentMode.input.value);
|
|
1213
|
-
};
|
|
1214
|
-
fBackend.addEventListener("change", refreshReplyHint);
|
|
1215
|
-
agentMode.input.addEventListener("change", refreshReplyHint);
|
|
1216
|
-
const logLevel = overrideControl(inst, "log_level", select(inst.log_level || defaults.log_level || "info", ["trace", "debug", "info", "warn", "error"]), defaults.log_level || "info");
|
|
1217
|
-
const fLightweight = el("input", { type: "checkbox" }); fLightweight.checked = inst.lightweight ?? defaults.lightweight ?? false;
|
|
1218
|
-
const lightweight = overrideControl(inst, "lightweight", fLightweight, defaults.lightweight ?? false);
|
|
1219
|
-
const fallbackValue = Array.isArray(inst.model_failover) ? (inst.model_failover[0] || "") : (defaults.model_failover?.[0] || "");
|
|
1220
|
-
const failover = overrideControl(inst, "model_failover", el("input", { type: "text", value: fallbackValue, placeholder: "e.g. sonnet" }), defaults.model_failover?.[0] || "");
|
|
1221
|
-
const displayName = overrideControl(inst, "display_name", el("input", { type: "text", value: inst.display_name ?? defaults.display_name ?? "", placeholder: name }), defaults.display_name || "");
|
|
1222
|
-
// The override sits on top of the bound channel's map, so the preview is
|
|
1223
|
-
// resolved against whichever channel the form currently binds.
|
|
1224
|
-
const boundChannelId = () => fChan.value || channelIds()[0] || null;
|
|
1225
|
-
const statusEmojis = statusEmojiEditor(inst.status_emojis, {
|
|
1226
|
-
platform: () => chById(boundChannelId())?.type,
|
|
1227
|
-
channelId: boundChannelId,
|
|
1228
|
-
previewBody: map => ({ channel_id: boundChannelId(), instance_config: map }),
|
|
1229
|
-
});
|
|
1230
|
-
fChan.addEventListener("change", statusEmojis.refresh);
|
|
1231
|
-
const autoFeedback = el("div", { class: "feedback" });
|
|
1232
|
-
const hangFeedback = el("div", { class: "feedback" });
|
|
1233
|
-
const failoverFeedback = el("div", { class: "feedback" });
|
|
1234
|
-
const msg = el("span", { class: "msg" });
|
|
1235
|
-
// Never rendered: it carries the validation state and the submit handler
|
|
1236
|
-
// that the modal footer triggers through stage(). The form has no button
|
|
1237
|
-
// of its own, so nothing here can write on its own.
|
|
1238
|
-
const saveBtn = el("button", { hidden: "hidden" });
|
|
1239
|
-
const validate = () => {
|
|
1240
|
-
const autoValue = Number(auto.input.value), hangValue = Number(hang.input.value);
|
|
1241
|
-
const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? t("mustNonNegative") : "");
|
|
1242
|
-
const validHang = setValidation(hang.input, hangFeedback, !hang.toggle.checked && (!Number.isFinite(hangValue) || hangValue <= 0) ? t("mustPositive") : "");
|
|
1243
|
-
setValidation(failover.input, failoverFeedback, "", !failover.toggle.checked && !failover.input.value.trim() ? t("failoverEmpty") : "");
|
|
1244
|
-
saveBtn.disabled = !(validAuto && validHang);
|
|
1245
|
-
return !saveBtn.disabled;
|
|
1246
|
-
};
|
|
1247
|
-
for (const ctl of [auto, hang, agentMode, toolSet, toolProgress, replyGuard, logLevel, lightweight, failover, displayName]) {
|
|
1248
|
-
ctl.input.addEventListener("input", validate); ctl.input.addEventListener("change", validate); ctl.toggle.addEventListener("change", validate);
|
|
1249
|
-
}
|
|
1250
|
-
saveBtn.onclick = async () => {
|
|
1251
|
-
if (!validate()) return;
|
|
1252
|
-
const nextPatch = {
|
|
1253
|
-
backend: fBackend.value, model: fModel.value.trim() || null,
|
|
1254
|
-
working_directory: fWd.value, channel_id: fChan.value || undefined,
|
|
1255
|
-
description: fDesc.value || undefined, systemPrompt: fSys.value || undefined,
|
|
1256
|
-
tags: tags.length ? tags : undefined, general_topic: fGeneral.checked,
|
|
1257
|
-
auto_pause_after: auto.toggle.checked ? null : Number(auto.input.value),
|
|
1258
|
-
hang_detector: { timeout_minutes: hang.toggle.checked ? null : Number(hang.input.value) },
|
|
1259
|
-
agent_mode: agentMode.toggle.checked ? null : agentMode.input.value,
|
|
1260
|
-
tool_set: toolSet.toggle.checked ? null : toolSet.input.value,
|
|
1261
|
-
tool_progress: toolProgress.toggle.checked ? null : toolProgress.input.value,
|
|
1262
|
-
cross_instance_visibility: visibility.toggle.checked ? null : visibility.input.value,
|
|
1263
|
-
reply_completion_guard: replyGuard.toggle.checked ? null : replyGuard.input.checked,
|
|
1264
|
-
log_level: logLevel.toggle.checked ? null : logLevel.input.value,
|
|
1265
|
-
lightweight: lightweight.toggle.checked ? null : lightweight.input.checked,
|
|
1266
|
-
model_failover: failover.toggle.checked ? null : (failover.input.value.trim() ? [failover.input.value.trim()] : []),
|
|
1267
|
-
display_name: displayName.toggle.checked ? null : displayName.input.value.trim(),
|
|
1268
|
-
status_emojis: statusEmojis.value(),
|
|
1269
|
-
};
|
|
1270
|
-
const patch = changedFields(nextPatch, {
|
|
1271
|
-
backend: inst.backend || defaults.backend || "claude-code",
|
|
1272
|
-
model: inst.model?.trim() || null,
|
|
1273
|
-
working_directory: inst.working_directory || "",
|
|
1274
|
-
channel_id: inst.channel_id || undefined,
|
|
1275
|
-
description: inst.description || undefined,
|
|
1276
|
-
systemPrompt: inst.systemPrompt || undefined,
|
|
1277
|
-
tags: inst.tags?.length ? inst.tags : undefined,
|
|
1278
|
-
general_topic: !!inst.general_topic,
|
|
1279
|
-
auto_pause_after: hasOwn(inst, "auto_pause_after") ? inst.auto_pause_after : null,
|
|
1280
|
-
hang_detector: { timeout_minutes: hasOwn(hangRaw, "timeout_minutes") ? hangRaw.timeout_minutes : null },
|
|
1281
|
-
agent_mode: hasOwn(inst, "agent_mode") ? inst.agent_mode : null,
|
|
1282
|
-
tool_set: hasOwn(inst, "tool_set") ? inst.tool_set : null,
|
|
1283
|
-
tool_progress: hasOwn(inst, "tool_progress") ? inst.tool_progress : null,
|
|
1284
|
-
cross_instance_visibility: visibilityBaseline,
|
|
1285
|
-
reply_completion_guard: hasOwn(inst, "reply_completion_guard") ? inst.reply_completion_guard : null,
|
|
1286
|
-
log_level: hasOwn(inst, "log_level") ? inst.log_level : null,
|
|
1287
|
-
lightweight: hasOwn(inst, "lightweight") ? inst.lightweight : null,
|
|
1288
|
-
model_failover: hasOwn(inst, "model_failover") ? inst.model_failover : null,
|
|
1289
|
-
display_name: hasOwn(inst, "display_name") ? inst.display_name : null,
|
|
1290
|
-
status_emojis: statusEmojis.baseline(),
|
|
1291
|
-
});
|
|
1292
|
-
state.fleet.instances[name] = { ...inst, ...patch };
|
|
1293
|
-
for (const [key, value] of Object.entries(patch)) if (value === null) delete state.fleet.instances[name][key];
|
|
1294
|
-
if (patch.hang_detector?.timeout_minutes === null) {
|
|
1295
|
-
state.fleet.instances[name].hang_detector = { ...(inst.hang_detector || {}) };
|
|
1296
|
-
delete state.fleet.instances[name].hang_detector.timeout_minutes;
|
|
1297
|
-
}
|
|
1298
|
-
stageChange(`agent:${name}`, {
|
|
1299
|
-
label: `Update ${shortName(name, state.fleet.instances[name])}`,
|
|
1300
|
-
impact: batchImpact(Object.keys(patch), key => impactOf(`instance.${key}`)),
|
|
1301
|
-
apply: () => api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
|
1302
|
-
});
|
|
1303
|
-
renderAgents();
|
|
1304
|
-
};
|
|
1305
|
-
box.append(
|
|
1306
|
-
el("div", { class: "sub", style: "margin:0 0 8px" }, "ID: " + name),
|
|
1307
|
-
el("div", { class: "grid2" },
|
|
1308
|
-
el("div", {}, el("label", {}, t("displayName"), impact("instance.display_name")), displayName.row),
|
|
1309
|
-
el("div", {}, el("label", {}, t("description"), impact("instance.description")), fDesc),
|
|
1310
|
-
el("div", {}, el("label", {}, t("backend"), impact("instance.backend")), fBackend),
|
|
1311
|
-
el("div", {}, el("label", {}, t("model"), impact("instance.model")), fModel)),
|
|
1312
|
-
el("label", {}, t("workingDir"), impact("instance.working_directory")), fWd,
|
|
1313
|
-
el("div", { class: "grid2" },
|
|
1314
|
-
el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("instance.auto_pause_after")), auto.row, autoFeedback)),
|
|
1315
|
-
drawer(t("advancedSection"),
|
|
1316
|
-
el("div", { class: "grid2" },
|
|
1317
|
-
el("div", {}, el("label", {}, t("channelBinding"), impact("instance.topic_id")), fChan),
|
|
1318
|
-
el("div", {}, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`, impact("instance.hang_detector")), hang.row, hangFeedback),
|
|
1319
|
-
el("div", {}, el("label", {}, t("agentMode"), impact("instance.agent_mode")), agentMode.row),
|
|
1320
|
-
el("div", {}, el("label", {}, t("toolSet"), impact("instance.tool_set")), toolSet.row),
|
|
1321
|
-
el("div", {}, el("label", {}, t("toolProgress"), impact("instance.tool_progress")), toolProgress.row),
|
|
1322
|
-
el("div", {}, el("label", {}, t("crossInstanceVisibility"), impact("instance.cross_instance_visibility")), visibility.row, el("div", { class: "hint" }, t("crossInstanceVisibilityHint"))),
|
|
1323
|
-
el("div", {}, el("label", {}, t("replyCompletionGuard"), impact("instance.reply_completion_guard")), replyGuard.row, replyHint),
|
|
1324
|
-
el("div", {}, el("label", {}, t("logLevel"), impact("instance.log_level")), logLevel.row),
|
|
1325
|
-
el("div", {}, el("label", {}, t("lightweight"), impact("instance.lightweight")), lightweight.row),
|
|
1326
|
-
el("div", {}, el("label", {}, t("modelFailover"), impact("instance.model_failover")), failover.row, failoverFeedback)),
|
|
1327
|
-
el("label", {}, t("systemPrompt"), impact("instance.systemPrompt")), fSys,
|
|
1328
|
-
el("label", {}, t("tags"), impact("instance.tags")), chipList(tags, null, "tag"),
|
|
1329
|
-
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField")), impact("instance.general_topic"))),
|
|
1330
|
-
drawer(t("statusEmojis"),
|
|
1331
|
-
el("label", {}, t("statusEmojisInstance"), impact("instance.status_emojis")), statusEmojis.box),
|
|
1332
|
-
el("div", { class: "actions" }, msg),
|
|
1333
|
-
);
|
|
1334
|
-
validate();
|
|
1335
|
-
return {
|
|
1336
|
-
box,
|
|
1337
|
-
// The modal's footer drives this; nothing in the form submits by itself.
|
|
1338
|
-
stage: () => { if (saveBtn.disabled) return false; saveBtn.onclick(); return true; },
|
|
1339
|
-
impacts: AGENT_MODAL_FIELDS,
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
/** Every field the agent modal shows, for the footer's impact summary. */
|
|
1344
|
-
const AGENT_MODAL_FIELDS = [
|
|
1345
|
-
"instance.display_name", "instance.description", "instance.backend",
|
|
1346
|
-
"instance.model", "instance.working_directory", "instance.auto_pause_after",
|
|
1347
|
-
"instance.topic_id", "instance.hang_detector", "instance.agent_mode",
|
|
1348
|
-
"instance.tool_set", "instance.tool_progress", "instance.cross_instance_visibility",
|
|
1349
|
-
"instance.reply_completion_guard", "instance.log_level",
|
|
1350
|
-
"instance.lightweight", "instance.model_failover", "instance.systemPrompt",
|
|
1351
|
-
"instance.tags", "instance.general_topic", "instance.status_emojis",
|
|
1352
|
-
];
|
|
1353
|
-
// Start, stop, pause and wake take a moment and used to give no sign of it, so they were pressed again.
|
|
1354
|
-
// One action per agent at a time; the row's buttons are disabled and the pressed one says it is working.
|
|
1355
|
-
const busyAgents = new Set();
|
|
1356
|
-
async function runAgentAction(name, button, work) {
|
|
1357
|
-
if (busyAgents.has(name)) return;
|
|
1358
|
-
busyAgents.add(name);
|
|
1359
|
-
const row = button?.closest(".item");
|
|
1360
|
-
const buttons = row ? [...row.querySelectorAll("button")] : (button ? [button] : []);
|
|
1361
|
-
const label = button?.textContent;
|
|
1362
|
-
for (const b of buttons) b.disabled = true;
|
|
1363
|
-
if (button) button.textContent = t("working");
|
|
1364
|
-
try { await work(); }
|
|
1365
|
-
finally {
|
|
1366
|
-
busyAgents.delete(name);
|
|
1367
|
-
// The list is normally redrawn by then; if it was not (a failed reload), give the buttons back.
|
|
1368
|
-
for (const b of buttons) if (b.isConnected) b.disabled = false;
|
|
1369
|
-
if (button?.isConnected) button.textContent = label;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
function toggleAgent(name, running, button) {
|
|
1373
|
-
return runAgentAction(name, button, async () => {
|
|
1374
|
-
const res = running ? await api(`/stop/${encodeURIComponent(name)}`, { method: "POST" })
|
|
1375
|
-
: await api(`/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" });
|
|
1376
|
-
if (!res.ok) showBanner(`${tf("actionFailed", running ? t("stop") : t("start"))}: ${res.body?.error || res.status}`, true);
|
|
1377
|
-
await new Promise(r => setTimeout(r, 800));
|
|
1378
|
-
await reloadLive();
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
function pauseWakeAgent(name, action, button) {
|
|
1382
|
-
return runAgentAction(name, button, async () => {
|
|
1383
|
-
const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" });
|
|
1384
|
-
if (!res.ok) alert(res.body?.error || tf("actionFailed", action));
|
|
1385
|
-
await reloadLive();
|
|
1386
|
-
});
|
|
1387
|
-
}
|
|
1388
|
-
async function delAgent(name) {
|
|
1389
|
-
if (!confirm(tf("deleteAgent", name))) return;
|
|
1390
|
-
delete state.fleet.instances[name];
|
|
1391
|
-
stageChange(`agent:${name}`, {
|
|
1392
|
-
label: `Delete ${name}`, impact: impactOf("instance.delete"),
|
|
1393
|
-
apply: () => api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" }),
|
|
1394
|
-
});
|
|
1395
|
-
renderAgents();
|
|
1396
|
-
}
|
|
1397
|
-
function renderNewAgent(open) {
|
|
1398
|
-
const host = $("newAgent"); host.innerHTML = "";
|
|
1399
|
-
if (!open) return;
|
|
1400
|
-
const box = el("div", { class: "card", style: "margin-bottom:12px" });
|
|
1401
|
-
const fName = el("input", { type: "text", placeholder: "agent name" });
|
|
1402
|
-
const fBackend = select(state.fleet.defaults?.backend || "claude-code", BACKENDS);
|
|
1403
|
-
const fWd = el("input", { type: "text", placeholder: "~/Projects/… (optional)" });
|
|
1404
|
-
const fChan = select("", ["", ...channelIds()]);
|
|
1405
|
-
const msg = el("span", { class: "msg" });
|
|
1406
|
-
box.append(el("div", { class: "edit", style: "border:none" },
|
|
1407
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("nameField")), fName), el("div", {}, el("label", {}, t("backend")), fBackend)),
|
|
1408
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("workingDir")), fWd), el("div", {}, el("label", {}, t("channelBinding")), fChan)),
|
|
1409
|
-
el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
|
|
1410
|
-
const name = fName.value.trim(); if (!name) { msg.innerHTML = `<span class="e">✗ ${esc(t("nameRequired"))}</span>`; return; }
|
|
1411
|
-
const body = { working_directory: fWd.value, backend: fBackend.value, channel_id: fChan.value || undefined };
|
|
1412
|
-
const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "POST", body: JSON.stringify(body) });
|
|
1413
|
-
resultMsg(msg, res); if (res.ok) { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); await reload(); }
|
|
1414
|
-
} }, t("create")), el("button", { class: "sm ghost", onclick: () => { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); } }, t("cancel")), msg),
|
|
1415
|
-
));
|
|
1416
|
-
host.append(box);
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
// ── Bots ──
|
|
1420
|
-
function renderBots() {
|
|
1421
|
-
const host = $("bots"); host.innerHTML = ""; let chs = channels();
|
|
1422
|
-
// One search box for the whole page.
|
|
1423
|
-
const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
|
|
1424
|
-
if (filter) chs = chs.filter(ch => [ch.id, ch.type, ch.group_id, ch.bot_token_env]
|
|
1425
|
-
.some(v => String(v ?? "").toLowerCase().includes(filter)));
|
|
1426
|
-
if (!chs.length) { host.className = "card empty"; host.textContent = filter ? tf("noAgentMatch", filter) : t("noBots"); return; }
|
|
1427
|
-
host.className = "card";
|
|
1428
|
-
chs.forEach((ch, i) => {
|
|
1429
|
-
const item = el("div", { class: "item" });
|
|
1430
|
-
const type = ch.type || "?";
|
|
1431
|
-
item.append(el("div", { class: "item-row conn-row" },
|
|
1432
|
-
el("span", { class: "name" }, type === "telegram" ? "Telegram" : "Discord"),
|
|
1433
|
-
el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, ch.id || chLabel(i)),
|
|
1434
|
-
el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
|
|
1435
|
-
ch.group_id ? el("span", { class: "tag" }, `${type === "telegram" ? "group" : "guild"} ${ch.group_id}`) : null,
|
|
1436
|
-
el("span", { class: "tag" }, `${t("accessMode")}: ${ch.access?.mode || "locked"}`),
|
|
1437
|
-
el("span", { class: "sub", style: "margin:0" },
|
|
1438
|
-
(ch.access?.allowed_users || []).length
|
|
1439
|
-
? (ch.access.allowed_users).slice(0, 3).join(", ") + ((ch.access.allowed_users).length > 3 ? ` +${(ch.access.allowed_users).length - 3}` : "")
|
|
1440
|
-
: t("noAllowedUsers")),
|
|
1441
|
-
el("span", { class: "row-end" },
|
|
1442
|
-
el("span", { class: "tag", title: "secret status" }, state.connections.find(c => (c.id || c.type) === (ch.id || ch.type))?.token_present ? "Token configured" : "Token missing"),
|
|
1443
|
-
el("span", { style: `color:${state.fleetUp ? "var(--green)" : "var(--red)"};font-size:13px` }, state.fleetUp ? "Connected" : "Problem"),
|
|
1444
|
-
el("button", { class: "sm ghost", onclick: () => {
|
|
1445
|
-
const form = botEditForm(ch, i);
|
|
1446
|
-
openModal({ title: type === "telegram" ? "Telegram" : "Discord", subtitle: ch.id || chLabel(i), body: form.box, impacts: form.impacts, stage: form.stage });
|
|
1447
|
-
} }, t("settingsButton")),
|
|
1448
|
-
),
|
|
1449
|
-
));
|
|
1450
|
-
host.append(item);
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// Provider API keys use a separate registry endpoint. Unsupported providers
|
|
1455
|
-
// are intentionally rendered without an input: a key that cannot be
|
|
1456
|
-
// positively verified must never be accepted as an "unverified" write.
|
|
1457
|
-
function providerSecretInputAllowed(spec) { return spec?.verifier === "available"; }
|
|
1458
|
-
function renderProviderSecrets() {
|
|
1459
|
-
const host = $("providerSecrets"); if (!host) return;
|
|
1460
|
-
host.innerHTML = "";
|
|
1461
|
-
const rows = Array.isArray(state.providerSecrets) ? state.providerSecrets : [];
|
|
1462
|
-
if (!rows.length) { host.className = "card empty"; host.textContent = "No provider API-key verifiers available."; return; }
|
|
1463
|
-
host.className = "card";
|
|
1464
|
-
rows.forEach(spec => {
|
|
1465
|
-
const row = el("div", { class: "item" });
|
|
1466
|
-
const feedback = el("div", { class: "feedback" });
|
|
1467
|
-
const present = spec.token_present ? "Key configured" : "Not configured";
|
|
1468
|
-
if (!providerSecretInputAllowed(spec)) {
|
|
1469
|
-
row.append(el("div", { class: "item-row" }, el("span", { class: "name" }, spec.display_name), el("span", { class: "tag" }, "Unsupported verifier")), el("div", { class: "hint" }, "This provider cannot be safely verified here; no key input is offered."));
|
|
1470
|
-
host.append(row); return;
|
|
1471
|
-
}
|
|
1472
|
-
const input = el("input", { type: "password", autocomplete: "new-password", placeholder: present });
|
|
1473
|
-
const button = el("button", { class: "sm primary" }, "Verify & apply");
|
|
1474
|
-
button.onclick = async () => {
|
|
1475
|
-
const secret = input.value;
|
|
1476
|
-
if (!secret) { feedback.className = "feedback error"; feedback.textContent = "Enter a key first."; return; }
|
|
1477
|
-
button.disabled = true; feedback.className = "feedback"; feedback.textContent = "Verifying…";
|
|
1478
|
-
const key = crypto.randomUUID ? crypto.randomUUID() : `provider-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1479
|
-
try {
|
|
1480
|
-
const verified = await api(`/api/settings/secrets/${encodeURIComponent(spec.id)}/verify`, { method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ secret, idempotency_key: key }) });
|
|
1481
|
-
if (!verified.ok) { feedback.className = "feedback error"; feedback.textContent = `✗ ${verified.body?.error || "verification failed"}`; return; }
|
|
1482
|
-
feedback.textContent = "Applying…";
|
|
1483
|
-
let result = await api(`/api/settings/secrets/${encodeURIComponent(spec.id)}/apply`, { method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ verification_id: verified.body?.verification_id, idempotency_key: key }) });
|
|
1484
|
-
let body = result.body || {};
|
|
1485
|
-
for (let n = 0; result.ok && body.job_id && body.result === "applying" && n < 60; n++) {
|
|
1486
|
-
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1487
|
-
const status = await api(`/api/settings/secrets/${encodeURIComponent(spec.id)}/apply/${encodeURIComponent(body.job_id)}`);
|
|
1488
|
-
if (status.ok) body = status.body || body;
|
|
1489
|
-
}
|
|
1490
|
-
const labels = { applied_next_use: "✓ Saved — new processes will use it", reloaded: "✓ Applied", rolled_back: "↩ Rolled back", rollback_failed: "✗ Rollback failed" };
|
|
1491
|
-
const ok = body.result === "applied_next_use" || body.result === "reloaded";
|
|
1492
|
-
feedback.className = `feedback ${ok ? "" : "error"}`;
|
|
1493
|
-
feedback.textContent = labels[body.result] || `✗ ${body.error || "Apply failed"}`;
|
|
1494
|
-
if (ok) { input.value = ""; await reload(); }
|
|
1495
|
-
} finally { input.value = ""; button.disabled = false; }
|
|
1496
|
-
};
|
|
1497
|
-
row.append(el("div", { class: "item-row" }, el("span", { class: "name" }, spec.display_name), el("span", { class: "tag" }, present), el("span", { class: "spacer" }, input), button), feedback);
|
|
1498
|
-
host.append(row);
|
|
1499
|
-
});
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
// Secret rotation is staged through the same Apply bar as every other
|
|
1503
|
-
// Settings mutation. The token is held only in this short-lived closure and
|
|
1504
|
-
// is cleared by the caller after the request settles; it never enters
|
|
1505
|
-
// localStorage, the URL, a label, or the staged change's display text.
|
|
1506
|
-
async function applyConnectionSecret(connectionId, secret, feedback) {
|
|
1507
|
-
const key = crypto.randomUUID ? crypto.randomUUID() : `secret-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1508
|
-
feedback.className = "feedback"; feedback.textContent = "Verifying…";
|
|
1509
|
-
const verified = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/secret/verify`, {
|
|
1510
|
-
method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ secret, idempotency_key: key }),
|
|
1511
|
-
});
|
|
1512
|
-
if (!verified.ok) { feedback.className = "feedback error"; feedback.textContent = `✗ ${verified.body?.error || "verification failed"}`; return verified; }
|
|
1513
|
-
feedback.textContent = "Applying…";
|
|
1514
|
-
const applied = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/secret/apply`, {
|
|
1515
|
-
method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ verification_id: verified.body?.verification_id, idempotency_key: key }),
|
|
1516
|
-
});
|
|
1517
|
-
if (!applied.ok && applied.status !== 409) { feedback.className = "feedback error"; feedback.textContent = `✗ ${applied.body?.error || "apply failed"}`; return applied; }
|
|
1518
|
-
let result = applied.body || {};
|
|
1519
|
-
if (result.job_id && result.result === "applying") {
|
|
1520
|
-
for (let n = 0; n < 60; n++) {
|
|
1521
|
-
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1522
|
-
const status = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/secret/apply/${encodeURIComponent(result.job_id)}`);
|
|
1523
|
-
if (status.ok) { result = status.body || result; if (result.result !== "applying") break; }
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
const labels = { applying: "…Still applying — check status", applied: "✓ Applied", restart_required: "↻ Saved — restart required", rolled_back: "↩ Rolled back", rollback_failed: "✗ Rollback failed" };
|
|
1527
|
-
feedback.className = `feedback ${result.result === "applied" || result.result === "restart_required" ? "" : "error"}`;
|
|
1528
|
-
feedback.textContent = labels[result.result] || "✗ Unknown apply result";
|
|
1529
|
-
return { ok: result.result === "applied" || result.result === "restart_required", status: result.result === "applied" ? 200 : 409, body: result };
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
// Binding changes use a separate verified challenge. They never go through
|
|
1533
|
-
// PUT /fleet/channels: the provider probe proves the target first, and the
|
|
1534
|
-
// apply job updates only group/general coordinates after the replacement
|
|
1535
|
-
// adapter is ready.
|
|
1536
|
-
async function verifyAndApplyConnectionBinding(connectionId, groupId, generalChannelId, feedback) {
|
|
1537
|
-
const key = crypto.randomUUID ? crypto.randomUUID() : `binding-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1538
|
-
const binding = { group_id: String(groupId).trim(), general_channel_id: String(generalChannelId || "").trim() || null };
|
|
1539
|
-
feedback.className = "feedback"; feedback.textContent = "Verifying target…";
|
|
1540
|
-
const verified = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/binding/verify`, {
|
|
1541
|
-
method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ ...binding, idempotency_key: key }),
|
|
1542
|
-
});
|
|
1543
|
-
if (!verified.ok) { feedback.className = "feedback error"; feedback.textContent = `✗ ${verified.body?.error || "binding verification failed"}`; return verified; }
|
|
1544
|
-
const probe = verified.body?.probe || {};
|
|
1545
|
-
const target = [probe.group_name || binding.group_id, probe.channel_name || probe.channel_id].filter(Boolean).join(" / ");
|
|
1546
|
-
const perms = [probe.can_view ? "view" : null, probe.can_send ? "send" : null, probe.can_manage_topics ? "topics" : null].filter(Boolean).join(", ");
|
|
1547
|
-
feedback.textContent = `✓ Verified ${target}${perms ? ` (${perms})` : ""}`;
|
|
1548
|
-
if (!window.confirm(`Rebind this connection to ${target || binding.group_id}?`)) {
|
|
1549
|
-
feedback.textContent = "Verified; not applied.";
|
|
1550
|
-
return { ok: false, status: 409, body: { result: "verified" } };
|
|
1551
|
-
}
|
|
1552
|
-
feedback.textContent = "Applying binding…";
|
|
1553
|
-
const applied = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/binding/apply`, {
|
|
1554
|
-
method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ verification_id: verified.body?.verification_id, idempotency_key: key }),
|
|
1555
|
-
});
|
|
1556
|
-
if (!applied.ok && applied.status !== 409) { feedback.className = "feedback error"; feedback.textContent = `✗ ${applied.body?.error || "binding apply failed"}`; return applied; }
|
|
1557
|
-
let result = applied.body || {};
|
|
1558
|
-
if (result.job_id && result.result === "applying") {
|
|
1559
|
-
for (let n = 0; n < 60; n++) {
|
|
1560
|
-
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1561
|
-
const status = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/binding/apply/${encodeURIComponent(result.job_id)}`);
|
|
1562
|
-
if (status.ok) { result = status.body || result; if (result.result !== "applying") break; }
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
const labels = { applied: "✓ Binding applied", rolled_back: "↩ Binding rolled back", rollback_failed: "✗ Binding rollback failed", applying: "…Still applying — check status" };
|
|
1566
|
-
const ok = result.result === "applied";
|
|
1567
|
-
feedback.className = `feedback ${ok ? "" : "error"}`;
|
|
1568
|
-
feedback.textContent = labels[result.result] || "✗ Unknown binding result";
|
|
1569
|
-
if (ok) await reload();
|
|
1570
|
-
return { ok, status: ok ? 200 : 409, body: result };
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
function botEditForm(ch, i) {
|
|
1574
|
-
ch.access = ch.access || { mode: "locked", allowed_users: [] };
|
|
1575
|
-
ch.access.allowed_users = ch.access.allowed_users || [];
|
|
1576
|
-
const previousAccess = structuredClone(ch.access);
|
|
1577
|
-
const type = ch.type || "telegram";
|
|
1578
|
-
const box = el("div", { class: "edit" });
|
|
1579
|
-
const fMode = select(ch.access.mode || "locked", ["open", "locked", "pairing"]);
|
|
1580
|
-
const users = [...ch.access.allowed_users];
|
|
1581
|
-
const accessFeedback = el("div", { class: "feedback" });
|
|
1582
|
-
const tokenInput = el("input", { type: "password", autocomplete: "new-password", placeholder: "Paste a new bot token to rotate it" });
|
|
1583
|
-
const secretFeedback = el("div", { class: "feedback" });
|
|
1584
|
-
const bindingFeedback = el("div", { class: "feedback" });
|
|
1585
|
-
const fGroup = el("input", { type: "text", value: ch.group_id != null ? String(ch.group_id) : "", autocomplete: "off" });
|
|
1586
|
-
const fGeneral = el("input", { type: "text", value: ch.options?.general_channel_id != null ? String(ch.options.general_channel_id) : "", autocomplete: "off", placeholder: type === "discord" ? "General channel id (optional)" : "Forum topic id (optional)" });
|
|
1587
|
-
const bindingVerify = el("button", { class: "sm primary", onclick: async () => {
|
|
1588
|
-
if (!fGroup.value.trim()) { bindingFeedback.className = "feedback error"; bindingFeedback.textContent = "✗ group/guild id required"; return; }
|
|
1589
|
-
await verifyAndApplyConnectionBinding(connectionId, fGroup.value, fGeneral.value, bindingFeedback);
|
|
1590
|
-
} }, "Verify new binding → Confirm rebind");
|
|
1591
|
-
const connectionId = ch.id || ch.type || chLabel(i);
|
|
1592
|
-
let pendingSecret = "";
|
|
1593
|
-
const stageSecret = () => {
|
|
1594
|
-
const secret = tokenInput.value;
|
|
1595
|
-
if (!secret) { secretFeedback.className = "feedback error"; secretFeedback.textContent = "✗ token required"; return; }
|
|
1596
|
-
pendingSecret = secret;
|
|
1597
|
-
tokenInput.value = "";
|
|
1598
|
-
stageChange(`secret:${connectionId}`, {
|
|
1599
|
-
// Secret rotation is an immediate adapter operation. Use the server's
|
|
1600
|
-
// published impact ordering rather than duplicating an impact literal
|
|
1601
|
-
// in this page (hot/cold parity guards reject such copies).
|
|
1602
|
-
label: `Rotate ${type} token`, impact: state.schema.order[0],
|
|
1603
|
-
apply: async () => {
|
|
1604
|
-
const value = pendingSecret; pendingSecret = "";
|
|
1605
|
-
try { return await applyConnectionSecret(connectionId, value, secretFeedback); }
|
|
1606
|
-
finally { pendingSecret = ""; }
|
|
1607
|
-
},
|
|
1608
|
-
cleanup: () => { pendingSecret = ""; },
|
|
1609
|
-
});
|
|
1610
|
-
secretFeedback.className = "feedback"; secretFeedback.textContent = "Queued — click Apply changes.";
|
|
1611
|
-
};
|
|
1612
|
-
const statusEmojis = statusEmojiEditor(ch.options?.status_emojis, {
|
|
1613
|
-
platform: () => type,
|
|
1614
|
-
channelId: () => connectionId,
|
|
1615
|
-
previewBody: map => ({ channel_id: connectionId, platform: type, channel_config: map }),
|
|
1616
|
-
});
|
|
1617
|
-
const validateAccess = () => setValidation(fMode, accessFeedback, "", fMode.value === "locked" && users.length === 0 ? t("accessLockedEmpty") : "");
|
|
1618
|
-
fMode.addEventListener("change", validateAccess);
|
|
1619
|
-
box.append(
|
|
1620
|
-
el("div", { class: "grid2" },
|
|
1621
|
-
el("div", {}, el("label", {}, t("accessMode"), impact("fleet.channel.access.mode")), fMode, accessFeedback),
|
|
1622
|
-
el("div", {}, el("label", {}, t("allowedUsers"), impact("fleet.channel.access.allowed_users")), chipList(users, validateAccess, "user id"))),
|
|
1623
|
-
el("div", { class: "field" },
|
|
1624
|
-
el("label", {}, type === "discord" ? t("guildIdField") : t("groupIdField"), impact("fleet.channels")),
|
|
1625
|
-
fGroup,
|
|
1626
|
-
el("label", {}, type === "discord" ? "General channel id" : "Forum topic id"), fGeneral,
|
|
1627
|
-
el("div", { class: "hint" }, "The target is checked with the existing bot token before any binding is changed."),
|
|
1628
|
-
el("div", { class: "actions" }, bindingVerify, bindingFeedback)),
|
|
1629
|
-
drawer(t("statusEmojis"),
|
|
1630
|
-
el("label", {}, t("statusEmojisChannel"), impact("fleet.channel.options.status_emojis")), statusEmojis.box),
|
|
1631
|
-
drawer(t("advancedSection"),
|
|
1632
|
-
el("div", { class: "field" }, el("label", {}, t("tokenEnv")), el("div", { class: "sub" }, ch.bot_token_env || "(no token env)")),
|
|
1633
|
-
el("div", { class: "field" },
|
|
1634
|
-
el("label", {}, "Bot token (never stored in the browser)"), tokenInput,
|
|
1635
|
-
el("div", { class: "hint" }, "The token is verified by AgEnD, then applied by replacing the running adapter. It is never echoed."),
|
|
1636
|
-
el("div", { class: "actions" }, el("button", { class: "sm primary", onclick: stageSecret }, "Verify & apply token"), secretFeedback)),
|
|
1637
|
-
el("div", { class: "actions" },
|
|
1638
|
-
el("button", { class: "sm danger", onclick: () => { closeModal(); delBot(i); } }, t("deleteBot")))),
|
|
1639
|
-
);
|
|
1640
|
-
validateAccess();
|
|
1641
|
-
return {
|
|
1642
|
-
box,
|
|
1643
|
-
impacts: ["fleet.channel.access.mode", "fleet.channel.access.allowed_users", "fleet.channels", "fleet.channel.options.status_emojis"],
|
|
1644
|
-
// Staged, not saved: the old form wrote channels straight to the API from
|
|
1645
|
-
// inside the editor, which is the one thing a modal must not do.
|
|
1646
|
-
stage: () => {
|
|
1647
|
-
const originalChannels = structuredClone(channels());
|
|
1648
|
-
const stagedAccessMode = fMode.value;
|
|
1649
|
-
const stagedAllowedUsers = [...users];
|
|
1650
|
-
const nextChannels = structuredClone(originalChannels);
|
|
1651
|
-
nextChannels[i] = { ...nextChannels[i], access: { ...nextChannels[i].access, mode: stagedAccessMode, allowed_users: stagedAllowedUsers } };
|
|
1652
|
-
const accessChanged = !sameValue(nextChannels, originalChannels);
|
|
1653
|
-
const stagedStatusEmojis = statusEmojis.value();
|
|
1654
|
-
const emojisChanged = !sameValue(stagedStatusEmojis, statusEmojis.baseline());
|
|
1655
|
-
if (emojisChanged) {
|
|
1656
|
-
const options = { ...(nextChannels[i].options || {}) };
|
|
1657
|
-
if (stagedStatusEmojis) options.status_emojis = stagedStatusEmojis; else delete options.status_emojis;
|
|
1658
|
-
nextChannels[i] = { ...nextChannels[i], options };
|
|
1659
|
-
}
|
|
1660
|
-
if (!accessChanged && !emojisChanged) return true;
|
|
1661
|
-
stageChange(`connections:${ch.id || chLabel(i)}`, {
|
|
1662
|
-
label: `Update ${ch.id || chLabel(i)} ${accessChanged && emojisChanged ? "access & status emojis" : accessChanged ? "access" : "status emojis"}`,
|
|
1663
|
-
impact: impactOf(accessChanged ? "fleet.channels" : "fleet.channel.options.status_emojis"),
|
|
1664
|
-
// Confirm the immutable staged snapshot, not live form controls
|
|
1665
|
-
// that the user may edit again before pressing global Apply.
|
|
1666
|
-
confirm: () => !accessChanged || confirmAccessChange(previousAccess, stagedAccessMode, stagedAllowedUsers),
|
|
1667
|
-
cancelMessage: t("accessChangeCancelled"),
|
|
1668
|
-
apply: () => api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(nextChannels) }),
|
|
1669
|
-
});
|
|
1670
|
-
return true;
|
|
1671
|
-
},
|
|
1672
|
-
};
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
// ── ClassicBot Channels ──
|
|
1676
|
-
function classicRuntime(instanceName) {
|
|
1677
|
-
const live = state.classicLive[instanceName] || {};
|
|
1678
|
-
const status = live.status || state.classicStatus[instanceName] || "stopped";
|
|
1679
|
-
const execution = status === "paused" ? "paused" : (live.state || status);
|
|
1680
|
-
return { status, execution };
|
|
1681
|
-
}
|
|
1682
|
-
function classicEditForm(c) {
|
|
1683
|
-
const box = el("div", { class: "edit" });
|
|
1684
|
-
const classicDefaults = state.classic?.defaults || {};
|
|
1685
|
-
const fleetDefaults = state.fleet?.defaults || {};
|
|
1686
|
-
const inheritedBackend = classicDefaults.backend || fleetDefaults.backend || "claude-code";
|
|
1687
|
-
const inheritedModel = classicDefaults.model || fleetDefaults.model || "";
|
|
1688
|
-
const inheritedAutoPause = classicDefaults.auto_pause_after ?? fleetDefaults.auto_pause_after ?? 0;
|
|
1689
|
-
const inheritedToolProgress = classicDefaults.tool_progress ?? fleetDefaults.tool_progress ?? "off";
|
|
1690
|
-
const inheritedReplyGuard = classicDefaults.reply_completion_guard ?? fleetDefaults.reply_completion_guard ?? true;
|
|
1691
|
-
const runtime = classicRuntime(c.instanceName);
|
|
1692
|
-
const fBackend = backendSelect(c.backend || inheritedBackend);
|
|
1693
|
-
const fModel = el("input", { type: "text", value: c.model || "", placeholder: inheritedModel || "(inherit default)" });
|
|
1694
|
-
const auto = overrideControl(c, "auto_pause_after", el("input", { type: "number", min: "0", step: "1", value: c.auto_pause_after ?? inheritedAutoPause, placeholder: "0 = disabled" }), inheritedAutoPause, "classic.auto_pause_after");
|
|
1695
|
-
const toolProgress = overrideControl(c, "tool_progress", select(c.tool_progress ?? inheritedToolProgress, ["off", "standard", "verbose"]), inheritedToolProgress, "classic.tool_progress");
|
|
1696
|
-
const fReplyGuard = el("input", { type: "checkbox" });
|
|
1697
|
-
fReplyGuard.checked = c.reply_completion_guard ?? inheritedReplyGuard;
|
|
1698
|
-
const replyGuard = overrideControl(c, "reply_completion_guard", fReplyGuard, inheritedReplyGuard, "classic.reply_completion_guard");
|
|
1699
|
-
const classicMode = fleetDefaults.agent_mode ?? "mcp";
|
|
1700
|
-
const replyHint = replyGuardHint(fBackend.value, classicMode, fleetDefaults.kiro_ui);
|
|
1701
|
-
fBackend.addEventListener("change", () => {
|
|
1702
|
-
const supported = replyGuardSupported(fBackend.value, classicMode, fleetDefaults.kiro_ui);
|
|
1703
|
-
replyHint.textContent = supported ? t("replyGuardSupported") : tf("replyGuardUnsupported", fBackend.value, classicMode);
|
|
1704
|
-
});
|
|
1705
|
-
const fCollab = el("input", { type: "checkbox" }); fCollab.checked = !!c.collab;
|
|
1706
|
-
const fContext = el("input", { type: "number", min: "0", step: "1", value: c.context_lines ?? classicDefaults.context_lines ?? 5 });
|
|
1707
|
-
const autoFeedback = el("div", { class: "feedback" });
|
|
1708
|
-
const contextFeedback = el("div", { class: "feedback" });
|
|
1709
|
-
// Never rendered: it carries the validation state and the submit handler
|
|
1710
|
-
// that the modal footer triggers through stage(). The form has no button
|
|
1711
|
-
// of its own, so nothing here can write on its own.
|
|
1712
|
-
const saveBtn = el("button", { hidden: "hidden" });
|
|
1713
|
-
const validate = () => {
|
|
1714
|
-
const autoValue = Number(auto.input.value);
|
|
1715
|
-
const contextValue = Number(fContext.value);
|
|
1716
|
-
const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? t("mustNonNegative") : "");
|
|
1717
|
-
const validContext = setValidation(fContext, contextFeedback, !Number.isInteger(contextValue) || contextValue < 0 ? t("nonnegativeInteger") : "");
|
|
1718
|
-
saveBtn.disabled = !(validAuto && validContext);
|
|
1719
|
-
return !saveBtn.disabled;
|
|
1720
|
-
};
|
|
1721
|
-
for (const input of [fBackend, fModel, auto.input, auto.toggle, toolProgress.input, toolProgress.toggle, replyGuard.input, replyGuard.toggle, fCollab, fContext]) {
|
|
1722
|
-
input.addEventListener("input", validate); input.addEventListener("change", validate);
|
|
1723
|
-
}
|
|
1724
|
-
saveBtn.onclick = () => {
|
|
1725
|
-
if (!validate()) return;
|
|
1726
|
-
const nextPatch = {
|
|
1727
|
-
backend: fBackend.value,
|
|
1728
|
-
model: fModel.value.trim() || null,
|
|
1729
|
-
auto_pause_after: auto.toggle.checked ? null : Number(auto.input.value),
|
|
1730
|
-
tool_progress: toolProgress.toggle.checked ? null : toolProgress.input.value,
|
|
1731
|
-
reply_completion_guard: replyGuard.toggle.checked ? null : replyGuard.input.checked,
|
|
1732
|
-
collab: fCollab.checked,
|
|
1733
|
-
context_lines: Number(fContext.value),
|
|
1734
|
-
};
|
|
1735
|
-
const patch = changedFields(nextPatch, {
|
|
1736
|
-
backend: c.backend || inheritedBackend,
|
|
1737
|
-
model: c.model?.trim() || null,
|
|
1738
|
-
auto_pause_after: hasOwn(c, "auto_pause_after") ? c.auto_pause_after : null,
|
|
1739
|
-
tool_progress: hasOwn(c, "tool_progress") ? c.tool_progress : null,
|
|
1740
|
-
reply_completion_guard: hasOwn(c, "reply_completion_guard") ? c.reply_completion_guard : null,
|
|
1741
|
-
collab: !!c.collab,
|
|
1742
|
-
context_lines: c.context_lines ?? classicDefaults.context_lines ?? 5,
|
|
1743
|
-
});
|
|
1744
|
-
if (Object.keys(patch).length === 0) { renderClassic(); return; }
|
|
1745
|
-
state.classic.channels[c.key] = { ...state.classic.channels[c.key], ...patch };
|
|
1746
|
-
if (patch.model === null) delete state.classic.channels[c.key].model;
|
|
1747
|
-
if (patch.auto_pause_after === null) delete state.classic.channels[c.key].auto_pause_after;
|
|
1748
|
-
if (patch.tool_progress === null) delete state.classic.channels[c.key].tool_progress;
|
|
1749
|
-
if (patch.reply_completion_guard === null) delete state.classic.channels[c.key].reply_completion_guard;
|
|
1750
|
-
const hotOnly = Object.keys(patch).every(key => impactOf(`classic.${key}`) === "now");
|
|
1751
|
-
stageChange(`classic:${c.key}`, {
|
|
1752
|
-
label: `Update ${c.name || c.instanceName}`,
|
|
1753
|
-
impact: hotOnly ? "now" : "instance",
|
|
1754
|
-
apply: () => api(`/api/settings/classic/channels/${encodeURIComponent(c.key)}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
|
1755
|
-
});
|
|
1756
|
-
renderClassic();
|
|
1757
|
-
};
|
|
1758
|
-
box.append(
|
|
1759
|
-
el("div", { class: "sub", style: "margin:0 0 8px" }, `ID: ${c.instanceName} · ${EXECUTION_TEXT[runtime.execution] || runtime.execution}`),
|
|
1760
|
-
el("div", { class: "grid2" },
|
|
1761
|
-
el("div", {}, el("label", {}, t("backend"), impact("classic.backend")), fBackend),
|
|
1762
|
-
el("div", {}, el("label", {}, t("model"), impact("classic.model")), fModel),
|
|
1763
|
-
el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("classic.auto_pause_after")), auto.row, autoFeedback),
|
|
1764
|
-
el("div", {}, el("label", {}, t("toolProgress"), impact("classic.tool_progress")), toolProgress.row),
|
|
1765
|
-
el("div", {}, el("label", {}, t("replyCompletionGuard"), impact("classic.reply_completion_guard")), replyGuard.row, replyHint),
|
|
1766
|
-
el("div", {}, el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer;margin-top:28px" }, fCollab, el("span", {}, "Collaboration mode"), impact("classic.collab")))),
|
|
1767
|
-
drawer(t("advancedSection"),
|
|
1768
|
-
el("label", {}, "Context lines", impact("classic.context_lines")), fContext, contextFeedback,
|
|
1769
|
-
el("div", { class: "actions" },
|
|
1770
|
-
runtime.status === "running" ? el("button", { class: "sm", onclick: () => { closeModal(); pauseWakeAgent(c.instanceName, "pause"); } }, t("pause")) : null,
|
|
1771
|
-
runtime.status === "paused" ? el("button", { class: "sm", onclick: () => { closeModal(); pauseWakeAgent(c.instanceName, "wake"); } }, t("wake")) : null)),
|
|
1772
|
-
);
|
|
1773
|
-
validate();
|
|
1774
|
-
return {
|
|
1775
|
-
box,
|
|
1776
|
-
stage: () => { if (saveBtn.disabled) return false; saveBtn.onclick(); return true; },
|
|
1777
|
-
impacts: [
|
|
1778
|
-
"classic.backend", "classic.model", "classic.auto_pause_after",
|
|
1779
|
-
"classic.tool_progress", "classic.reply_completion_guard",
|
|
1780
|
-
"classic.collab", "classic.context_lines",
|
|
1781
|
-
],
|
|
1782
|
-
};
|
|
1783
|
-
}
|
|
1784
|
-
function renderClassic() {
|
|
1785
|
-
const host = $("classicList"); if (!host) return; host.innerHTML = "";
|
|
1786
|
-
const chans = Object.entries(state.classic?.channels || {}).map(([key, channel]) => ({ key, ...channel }));
|
|
1787
|
-
if (!chans.length) { host.className = "card empty"; host.textContent = t("noClassic"); return; }
|
|
1788
|
-
const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
|
|
1789
|
-
let list = chans;
|
|
1790
|
-
if (filter) list = list.filter(c => String(c.name || "").toLowerCase().includes(filter) || String(c.instanceName || "").toLowerCase().includes(filter));
|
|
1791
|
-
if (!list.length) { host.className = "card empty"; host.textContent = "—"; return; }
|
|
1792
|
-
host.className = "card";
|
|
1793
|
-
list.sort((a, b) => {
|
|
1794
|
-
const sa = STATUS_ORDER[classicRuntime(a.instanceName).status] ?? 9;
|
|
1795
|
-
const sb = STATUS_ORDER[classicRuntime(b.instanceName).status] ?? 9;
|
|
1796
|
-
return sa !== sb ? sa - sb : String(a.name || "").localeCompare(String(b.name || ""));
|
|
1797
|
-
});
|
|
1798
|
-
for (const c of list) {
|
|
1799
|
-
const runtime = classicRuntime(c.instanceName);
|
|
1800
|
-
const st = runtime.status;
|
|
1801
|
-
const adapter = c.adapterId || "discord";
|
|
1802
|
-
const summary = effectiveSummary(c.instanceName, c, true);
|
|
1803
|
-
const displayName = shortName(c.name || c.instanceName || "?", { display_name: c.display_name });
|
|
1804
|
-
const item = el("div", { class: "item" });
|
|
1805
|
-
item.append(el("div", { class: "item-row" },
|
|
1806
|
-
el("span", { class: "dot " + runtime.execution, title: EXECUTION_TEXT[runtime.execution] || runtime.execution }),
|
|
1807
|
-
el("span", { class: "name", title: c.instanceName || "" }, displayName),
|
|
1808
|
-
el("span", { class: "sub", style: "margin:0", title: summary.backend }, summary.model),
|
|
1809
|
-
el("span", { class: "tag" }, `effort: ${summary.effort}`),
|
|
1810
|
-
el("span", { class: "tag" + (adapter !== "discord" ? " persona" : "") }, adapter),
|
|
1811
|
-
c.collab ? el("span", { class: "tag", style: "background:#ecfdf5;color:#065f46" }, "collab") : null,
|
|
1812
|
-
el("span", { class: "spacer" }),
|
|
1813
|
-
el("span", { class: "tag", title: STATUS_TEXT[st] || st }, EXECUTION_TEXT[runtime.execution] || runtime.execution),
|
|
1814
|
-
el("button", { class: "sm ghost", onclick: () => {
|
|
1815
|
-
const form = classicEditForm(c);
|
|
1816
|
-
openModal({ title: c.name || c.instanceName, subtitle: c.instanceName, body: form.box, impacts: form.impacts, stage: form.stage });
|
|
1817
|
-
} }, t("settingsButton")),
|
|
1818
|
-
st === "running" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "pause") }, t("pause")) : null,
|
|
1819
|
-
st === "paused" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "wake") }, t("wake")) : null,
|
|
1820
|
-
));
|
|
1821
|
-
host.append(item);
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
async function saveChannels(msgEl) {
|
|
1826
|
-
const res = await api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(channels()) });
|
|
1827
|
-
if (msgEl) resultMsg(msgEl, res);
|
|
1828
|
-
return res;
|
|
1829
|
-
}
|
|
1830
|
-
async function delBot(i) {
|
|
1831
|
-
const chs = channels(); const ch = chs[i];
|
|
1832
|
-
if (!confirm(tf("removeBot", `${ch.type} (${chLabel(i)})`))) return;
|
|
1833
|
-
chs.splice(i, 1);
|
|
1834
|
-
state.fleet.channels = chs; delete state.fleet.channel;
|
|
1835
|
-
const res = await saveChannels(); if (res.ok) await reload(); else alert((res.body && res.body.error) || t("failed"));
|
|
1836
|
-
}
|
|
1837
|
-
function renderNewBot(open) {
|
|
1838
|
-
const host = $("newBot"); host.innerHTML = ""; if (!open) return;
|
|
1839
|
-
const box = el("div", { class: "card", style: "margin-bottom:12px" });
|
|
1840
|
-
const fType = select("discord", CH_TYPES);
|
|
1841
|
-
const fId = el("input", { type: "text", placeholder: "adapter id (e.g. persona1)" });
|
|
1842
|
-
const fEnv = el("input", { type: "text", placeholder: "BOT_TOKEN_ENV" });
|
|
1843
|
-
const fGroup = el("input", { type: "text", placeholder: "guild / supergroup id" });
|
|
1844
|
-
const msg = el("span", { class: "msg" });
|
|
1845
|
-
box.append(el("div", { class: "edit", style: "border:none" },
|
|
1846
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("type")), fType), el("div", {}, el("label", {}, t("adapterId")), fId)),
|
|
1847
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("tokenEnv")), fEnv), el("div", {}, el("label", {}, t("groupId")), fGroup)),
|
|
1848
|
-
el("div", { class: "hint" }, t("envHint")),
|
|
1849
|
-
el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
|
|
1850
|
-
if (!fEnv.value.trim()) { msg.innerHTML = `<span class="e">✗ ${esc(t("tokenEnvRequired"))}</span>`; return; }
|
|
1851
|
-
const chs = channels();
|
|
1852
|
-
chs.push({ id: fId.value.trim() || undefined, type: fType.value, bot_token_env: fEnv.value.trim(), group_id: fGroup.value.trim() || undefined, access: { mode: "locked", allowed_users: [] } });
|
|
1853
|
-
state.fleet.channels = chs; delete state.fleet.channel;
|
|
1854
|
-
const res = await saveChannels(msg); if (res.ok) { state.newBotOpen = false; $("newBotBtn").textContent = t("add"); renderNewBot(false); await reload(); }
|
|
1855
|
-
} }, t("save")), el("button", { class: "sm ghost", onclick: () => { state.newBotOpen = false; $("newBotBtn").textContent = t("add"); renderNewBot(false); } }, t("cancel")), msg),
|
|
1856
|
-
));
|
|
1857
|
-
host.append(box);
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
// ── General Settings ──
|
|
1861
|
-
function renderGeneral() {
|
|
1862
|
-
const host = $("general"); host.innerHTML = "";
|
|
1863
|
-
state.fleet.defaults = state.fleet.defaults || {};
|
|
1864
|
-
state.classic.defaults = state.classic.defaults || {};
|
|
1865
|
-
const d = state.fleet.defaults, c = state.classic.defaults;
|
|
1866
|
-
// The controls below mutate their arrays in place. Keep raw snapshots so
|
|
1867
|
-
// changing one hot default does not also stage untouched access settings as
|
|
1868
|
-
// a fleet restart.
|
|
1869
|
-
const originalChannels = structuredClone(channels());
|
|
1870
|
-
const originalClassicDefaults = structuredClone(c);
|
|
1871
|
-
const hang = d.hang_detector || {};
|
|
1872
|
-
const startup = d.startup || {};
|
|
1873
|
-
const primary = channels()[0];
|
|
1874
|
-
const originalPrimaryAccess = structuredClone(primary?.access || { mode: "locked", allowed_users: [] });
|
|
1875
|
-
const allowedUsers = [...(originalPrimaryAccess.allowed_users || [])];
|
|
1876
|
-
c.admin_users = c.admin_users || []; c.allowed_guilds = c.allowed_guilds || [];
|
|
1877
|
-
|
|
1878
|
-
const fBackend = backendSelect(d.backend || "claude-code");
|
|
1879
|
-
const fClassicBackend = backendSelect(c.backend || d.backend || "claude-code");
|
|
1880
|
-
const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
|
|
1881
|
-
const fAgentMode = select(d.agent_mode || "mcp", ["mcp", "cli"]);
|
|
1882
|
-
const fToolSet = select(d.tool_set || "full", ["full", "standard", "minimal"]);
|
|
1883
|
-
const fToolProgress = select(d.tool_progress ?? "off", ["off", "standard", "verbose"]);
|
|
1884
|
-
const fVisibility = select(visibilityDefault(d), VISIBILITY_MODES);
|
|
1885
|
-
const fReplyGuard = el("input", { type: "checkbox" }); fReplyGuard.checked = d.reply_completion_guard ?? true;
|
|
1886
|
-
const fClassicToolProgress = overrideControl(c, "tool_progress", select(c.tool_progress ?? d.tool_progress ?? "off", ["off", "standard", "verbose"]), d.tool_progress ?? "off", "classic_defaults.tool_progress");
|
|
1887
|
-
const fClassicReplyInput = el("input", { type: "checkbox" }); fClassicReplyInput.checked = c.reply_completion_guard ?? d.reply_completion_guard ?? true;
|
|
1888
|
-
const fClassicReplyGuard = overrideControl(c, "reply_completion_guard", fClassicReplyInput, d.reply_completion_guard ?? true, "classic_defaults.reply_completion_guard");
|
|
1889
|
-
const fAccessMode = select(originalPrimaryAccess.mode || "locked", ["locked", "open", "pairing"]);
|
|
1890
|
-
const fLogLevel = select(d.log_level || "info", ["trace", "debug", "info", "warn", "error"]);
|
|
1891
|
-
const fLocale = localeSelect(d.locale || "");
|
|
1892
|
-
const fAutoPause = el("input", { type: "number", min: "0", step: "1", value: d.auto_pause_after ?? 0, placeholder: "0 = disabled" });
|
|
1893
|
-
const fHangEnabled = el("input", { type: "checkbox" }); fHangEnabled.checked = hang.enabled !== false;
|
|
1894
|
-
const fHangTimeout = el("input", { type: "number", min: "0.1", step: "0.5", value: hang.timeout_minutes ?? 15 });
|
|
1895
|
-
const fConcurrency = el("input", { type: "number", min: "1", max: "20", step: "1", value: startup.concurrency ?? 10 });
|
|
1896
|
-
const fStagger = el("input", { type: "number", min: "0", max: "30000", step: "100", value: startup.stagger_delay_ms ?? 500 });
|
|
1897
|
-
const msg = el("span", { class: "msg" });
|
|
1898
|
-
const autoFeedback = el("div", { class: "feedback" });
|
|
1899
|
-
const hangFeedback = el("div", { class: "feedback" });
|
|
1900
|
-
const concurrencyFeedback = el("div", { class: "feedback" });
|
|
1901
|
-
const staggerFeedback = el("div", { class: "feedback" });
|
|
1902
|
-
const accessFeedback = el("div", { class: "feedback" });
|
|
1903
|
-
const saveBtn = el("button", { class: "primary" }, t("save"));
|
|
1904
|
-
const defaultReplyHint = replyGuardHint(fBackend.value, fAgentMode.value, d.kiro_ui);
|
|
1905
|
-
const classicReplyHint = replyGuardHint(fClassicBackend.value, fAgentMode.value, d.kiro_ui);
|
|
1906
|
-
const refreshDefaultReplyHints = () => {
|
|
1907
|
-
const fleetSupported = replyGuardSupported(fBackend.value, fAgentMode.value, d.kiro_ui);
|
|
1908
|
-
defaultReplyHint.textContent = fleetSupported ? t("replyGuardSupported") : tf("replyGuardUnsupported", fBackend.value, fAgentMode.value);
|
|
1909
|
-
const classicSupported = replyGuardSupported(fClassicBackend.value, fAgentMode.value, d.kiro_ui);
|
|
1910
|
-
classicReplyHint.textContent = classicSupported ? t("replyGuardSupported") : tf("replyGuardUnsupported", fClassicBackend.value, fAgentMode.value);
|
|
1911
|
-
};
|
|
1912
|
-
fBackend.addEventListener("change", refreshDefaultReplyHints);
|
|
1913
|
-
fClassicBackend.addEventListener("change", refreshDefaultReplyHints);
|
|
1914
|
-
fAgentMode.addEventListener("change", refreshDefaultReplyHints);
|
|
1915
|
-
const validate = () => {
|
|
1916
|
-
const auto = Number(fAutoPause.value), timeout = Number(fHangTimeout.value);
|
|
1917
|
-
const concurrency = Number(fConcurrency.value), stagger = Number(fStagger.value);
|
|
1918
|
-
const validAuto = setValidation(fAutoPause, autoFeedback, !Number.isFinite(auto) || auto < 0 ? t("mustNonNegative") : "");
|
|
1919
|
-
const validHang = setValidation(fHangTimeout, hangFeedback, !Number.isFinite(timeout) || timeout <= 0 ? t("mustPositive") : "");
|
|
1920
|
-
const validConcurrency = setValidation(fConcurrency, concurrencyFeedback, !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 20 ? t("concurrencyRange") : "", concurrency > 4 ? t("concurrencyWarning") : "");
|
|
1921
|
-
const validStagger = setValidation(fStagger, staggerFeedback, !Number.isFinite(stagger) || stagger < 0 || stagger > 30000 ? t("staggerRange") : "", stagger < 100 ? t("staggerWarning") : "");
|
|
1922
|
-
if (primary) setValidation(fAccessMode, accessFeedback, "", fAccessMode.value === "locked" && allowedUsers.length === 0 ? t("accessLockedEmpty") : "");
|
|
1923
|
-
saveBtn.disabled = !(validAuto && validHang && validConcurrency && validStagger);
|
|
1924
|
-
return !saveBtn.disabled;
|
|
1925
|
-
};
|
|
1926
|
-
for (const input of [fAutoPause, fHangTimeout, fConcurrency, fStagger]) {
|
|
1927
|
-
input.addEventListener("input", validate); input.addEventListener("change", validate);
|
|
1928
|
-
}
|
|
1929
|
-
fAccessMode.addEventListener("change", validate);
|
|
1930
|
-
saveBtn.textContent = t("reviewChanges");
|
|
1931
|
-
saveBtn.onclick = () => {
|
|
1932
|
-
if (!validate()) return;
|
|
1933
|
-
const nextDefaults = {
|
|
1934
|
-
backend: fBackend.value,
|
|
1935
|
-
model: fModel.value || undefined,
|
|
1936
|
-
locale: fLocale.value || null,
|
|
1937
|
-
auto_pause_after: Number(fAutoPause.value),
|
|
1938
|
-
agent_mode: fAgentMode.value,
|
|
1939
|
-
tool_set: fToolSet.value,
|
|
1940
|
-
tool_progress: fToolProgress.value,
|
|
1941
|
-
cross_instance_visibility: fVisibility.value,
|
|
1942
|
-
reply_completion_guard: fReplyGuard.checked,
|
|
1943
|
-
log_level: fLogLevel.value,
|
|
1944
|
-
hang_detector: { enabled: fHangEnabled.checked, timeout_minutes: Number(fHangTimeout.value) },
|
|
1945
|
-
startup: { concurrency: Number(fConcurrency.value), stagger_delay_ms: Number(fStagger.value) },
|
|
1946
|
-
};
|
|
1947
|
-
const defaultsPatch = changedFields(nextDefaults, {
|
|
1948
|
-
backend: d.backend || "claude-code",
|
|
1949
|
-
model: d.model || undefined,
|
|
1950
|
-
locale: d.locale || null,
|
|
1951
|
-
auto_pause_after: d.auto_pause_after ?? 0,
|
|
1952
|
-
agent_mode: d.agent_mode || "mcp",
|
|
1953
|
-
tool_set: d.tool_set || "full",
|
|
1954
|
-
tool_progress: d.tool_progress ?? "off",
|
|
1955
|
-
cross_instance_visibility: visibilityDefault(d),
|
|
1956
|
-
reply_completion_guard: d.reply_completion_guard ?? true,
|
|
1957
|
-
log_level: d.log_level || "info",
|
|
1958
|
-
hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? 15 },
|
|
1959
|
-
startup: { concurrency: startup.concurrency ?? 10, stagger_delay_ms: startup.stagger_delay_ms ?? 500 },
|
|
1960
|
-
});
|
|
1961
|
-
const classicPatch = changedFields(
|
|
1962
|
-
{
|
|
1963
|
-
backend: fClassicBackend.value,
|
|
1964
|
-
tool_progress: fClassicToolProgress.toggle.checked ? null : fClassicToolProgress.input.value,
|
|
1965
|
-
reply_completion_guard: fClassicReplyGuard.toggle.checked ? null : fClassicReplyGuard.input.checked,
|
|
1966
|
-
admin_users: c.admin_users,
|
|
1967
|
-
allowed_guilds: c.allowed_guilds,
|
|
1968
|
-
},
|
|
1969
|
-
{
|
|
1970
|
-
backend: originalClassicDefaults.backend || d.backend || "claude-code",
|
|
1971
|
-
tool_progress: hasOwn(originalClassicDefaults, "tool_progress") ? originalClassicDefaults.tool_progress : null,
|
|
1972
|
-
reply_completion_guard: hasOwn(originalClassicDefaults, "reply_completion_guard") ? originalClassicDefaults.reply_completion_guard : null,
|
|
1973
|
-
admin_users: originalClassicDefaults.admin_users || [],
|
|
1974
|
-
allowed_guilds: originalClassicDefaults.allowed_guilds || [],
|
|
1975
|
-
},
|
|
1976
|
-
);
|
|
1977
|
-
state.fleet.defaults = { ...d, ...defaultsPatch };
|
|
1978
|
-
state.classic.defaults = { ...c, ...classicPatch };
|
|
1979
|
-
for (const [key, value] of Object.entries(classicPatch)) if (value === null) delete state.classic.defaults[key];
|
|
1980
|
-
if (Object.keys(defaultsPatch).length > 0) {
|
|
1981
|
-
stageChange("defaults:fleet", { label: "Update Agent defaults", impact: batchImpact(Object.keys(defaultsPatch), key => impactOf(`defaults.${key}`)), apply: () => api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(defaultsPatch) }) });
|
|
1982
|
-
}
|
|
1983
|
-
if (Object.keys(classicPatch).length > 0) {
|
|
1984
|
-
const classicHotOnly = Object.keys(classicPatch).every(key => impactOf(`classic_defaults.${key}`) === "now");
|
|
1985
|
-
stageChange("defaults:classic", { label: "Update ClassicBot defaults and access", impact: classicHotOnly ? "now" : "fleet", apply: () => api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify(classicPatch) }) });
|
|
1986
|
-
}
|
|
1987
|
-
msg.innerHTML = `<span class="s">${esc(t("readyApply"))}</span>`;
|
|
1988
|
-
};
|
|
1989
|
-
|
|
1990
|
-
const section = (title, open, ...children) => {
|
|
1991
|
-
const details = el("details", { class: "settings-section" }); details.open = open;
|
|
1992
|
-
details.append(el("summary", {}, title), el("div", { class: "section-body" }, ...children));
|
|
1993
|
-
return details;
|
|
1994
|
-
};
|
|
1995
|
-
host.append(
|
|
1996
|
-
section("Defaults", true,
|
|
1997
|
-
el("div", { class: "grid2" },
|
|
1998
|
-
el("div", { class: "field" }, el("label", {}, t("language"), impact("defaults.locale")), fLocale),
|
|
1999
|
-
el("div", { class: "field" }, el("label", {}, t("defaultBackend"), impact("defaults.backend")), fBackend),
|
|
2000
|
-
el("div", { class: "field" }, el("label", {}, t("defaultModel"), impact("defaults.model")), fModel),
|
|
2001
|
-
el("div", { class: "field" }, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("defaults.auto_pause_after")), fAutoPause, autoFeedback),
|
|
2002
|
-
el("div", { class: "field" }, el("label", {}, t("toolProgress"), impact("defaults.tool_progress")), fToolProgress),
|
|
2003
|
-
el("div", { class: "field" }, el("label", {}, t("crossInstanceVisibility"), impact("defaults.cross_instance_visibility")), fVisibility, el("div", { class: "hint" }, t("crossInstanceVisibilityHint"))),
|
|
2004
|
-
el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px" }, fReplyGuard, el("span", {}, t("replyCompletionGuard")), impact("defaults.reply_completion_guard")), defaultReplyHint))),
|
|
2005
|
-
section("ClassicBot", true,
|
|
2006
|
-
el("div", { class: "grid2" },
|
|
2007
|
-
el("div", { class: "field" }, el("label", {}, "Classic default backend", impact("classic_defaults.backend")), fClassicBackend),
|
|
2008
|
-
el("div", { class: "field" }, el("label", {}, t("toolProgress"), impact("classic_defaults.tool_progress")), fClassicToolProgress.row),
|
|
2009
|
-
el("div", { class: "field" }, el("label", {}, t("replyCompletionGuard"), impact("classic_defaults.reply_completion_guard")), fClassicReplyGuard.row, classicReplyHint)),
|
|
2010
|
-
el("div", { class: "field" }, el("strong", {}, "Who can use ClassicBot"),
|
|
2011
|
-
el("div", { class: "grid2" },
|
|
2012
|
-
el("div", {}, el("label", {}, t("classicAdmins"), impact("classic.admin_users")), chipList(c.admin_users, null, "user id")),
|
|
2013
|
-
el("div", {}, el("label", {}, t("allowedGuilds"), impact("classic.allowed_guilds")), chipList(c.allowed_guilds, null, "guild id"))))),
|
|
2014
|
-
drawer(t("advancedSection"),
|
|
2015
|
-
section(t("runtimeResources"), false,
|
|
2016
|
-
el("div", { class: "grid2" },
|
|
2017
|
-
el("div", { class: "field" }, el("label", {}, t("toolSet"), impact("defaults.tool_set")), fToolSet),
|
|
2018
|
-
el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("defaults.log_level")), fLogLevel),
|
|
2019
|
-
el("div", { class: "field" }, el("label", {}, t("agentMode"), impact("defaults.agent_mode")), fAgentMode))),
|
|
2020
|
-
section(t("detection"), false,
|
|
2021
|
-
el("div", { class: "grid2" },
|
|
2022
|
-
el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px" }, fHangEnabled, el("span", {}, t("hangEnabled")), impact("defaults.hang_detector"))),
|
|
2023
|
-
el("div", { class: "field" }, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`, impact("defaults.hang_detector")), fHangTimeout, hangFeedback))),
|
|
2024
|
-
section(t("startup"), false,
|
|
2025
|
-
el("div", { class: "grid2" },
|
|
2026
|
-
el("div", { class: "field" }, el("label", {}, t("concurrency"), impact("fleet.spawn_concurrency")), fConcurrency, concurrencyFeedback),
|
|
2027
|
-
el("div", { class: "field" }, el("label", {}, `${t("staggerDelay")} (ms)`, impact("fleet.spawn_stagger_ms")), fStagger, staggerFeedback))),
|
|
2028
|
-
section("Access", false,
|
|
2029
|
-
// Access belongs to a connection, and one modal edits one object — so
|
|
2030
|
-
// it is edited there, and this is only a pointer to it.
|
|
2031
|
-
primary
|
|
2032
|
-
? el("div", { class: "field hint" }, tf("accessMovedHint", primary.id || chLabel(0)))
|
|
2033
|
-
: el("div", { class: "field hint" }, t("noBots")))),
|
|
2034
|
-
el("div", { class: "field" }, el("div", { class: "actions" }, saveBtn, msg)),
|
|
2035
|
-
);
|
|
2036
|
-
validate();
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
// ── What's New ──
|
|
2040
|
-
function renderWhatsNew() {
|
|
2041
|
-
const host = $("wnBody"); host.innerHTML = "";
|
|
2042
|
-
host.append(el("div", { class: "sub", style: "margin:0 0 8px" }, `${t("currentVersion")}: ${state.version || "?"}`));
|
|
2043
|
-
host.append(el("div", { style: "font-size:13px;margin-bottom:6px" }, t("wnIntro")));
|
|
2044
|
-
const ul = el("ul", { style: "margin:0 0 0 18px" });
|
|
2045
|
-
for (const line of WHATSNEW) ul.append(el("li", { style: "font-size:13px;margin:4px 0" }, line));
|
|
2046
|
-
host.append(ul);
|
|
2047
|
-
// Beta builds have no per-tag release page → link to the latest release.
|
|
2048
|
-
const v = state.version || "";
|
|
2049
|
-
const url = !v || v.includes("-") ? "https://github.com/songsid/AgEnD/releases/latest" : `https://github.com/songsid/AgEnD/releases/tag/v${v}`;
|
|
2050
|
-
host.append(el("div", { style: "margin-top:10px" }, el("a", { href: url, target: "_blank", rel: "noopener", style: "color:var(--accent);font-size:13px;text-decoration:none" }, t("releaseNotes"))));
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
// ── Advanced (YAML/JSON) ──
|
|
2054
|
-
const fleetModel = () => ({ ...(state.fleet.channels ? { channels: state.fleet.channels } : state.fleet.channel ? { channel: state.fleet.channel } : {}), defaults: state.fleet.defaults || {}, instances: state.fleet.instances || {} });
|
|
2055
|
-
function renderAdvanced() {
|
|
2056
|
-
const host = $("advBody"); host.innerHTML = "";
|
|
2057
|
-
const fmt = state.advFmt; const model = fleetModel();
|
|
2058
|
-
const ser = fmt === "yaml" ? toYaml(model) : JSON.stringify(model, null, 2);
|
|
2059
|
-
const msg = el("div", { class: "msg", style: "margin-top:8px" });
|
|
2060
|
-
const toolbar = el("div", { class: "toolbar" },
|
|
2061
|
-
el("div", { class: "seg" },
|
|
2062
|
-
el("button", { class: "sm" + (fmt === "yaml" ? " on" : ""), onclick: () => { state.advFmt = "yaml"; renderAdvanced(); } }, "YAML"),
|
|
2063
|
-
el("button", { class: "sm" + (fmt === "json" ? " on" : ""), onclick: () => { state.advFmt = "json"; renderAdvanced(); } }, "JSON")),
|
|
2064
|
-
el("button", { class: "sm", onclick: () => { state.advEdit = !state.advEdit; renderAdvanced(); } }, state.advEdit ? "View" : "Edit"),
|
|
2065
|
-
el("button", { class: "sm", onclick: () => navigator.clipboard.writeText(ser).then(() => { msg.innerHTML = `<span class="s">${esc(t("copied"))}</span>`; }) }, "Copy"),
|
|
2066
|
-
el("button", { class: "sm", onclick: () => { const blob = new Blob([toYaml(fleetModel())], { type: "text/plain" }); const a = el("a", { href: URL.createObjectURL(blob), download: "fleet.yaml" }); a.click(); URL.revokeObjectURL(a.href); } }, "⬇ fleet.yaml"),
|
|
2067
|
-
);
|
|
2068
|
-
host.append(toolbar);
|
|
2069
|
-
if (state.advEdit) {
|
|
2070
|
-
const ta = el("textarea"); ta.value = ser;
|
|
2071
|
-
host.append(ta, el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
|
|
2072
|
-
let parsed; try { parsed = fmt === "yaml" ? fromYaml(ta.value) : JSON.parse(ta.value || "{}"); } catch (e) { msg.innerHTML = `<span class="e">✗ ${esc(e.message)}</span>`; return; }
|
|
2073
|
-
await applyFullModel(parsed, msg);
|
|
2074
|
-
} }, "Apply & Save"), msg));
|
|
2075
|
-
} else {
|
|
2076
|
-
const pre = el("pre", { class: "preview" }); pre.textContent = ser; host.append(pre, msg);
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2079
|
-
// Apply the edited full model: PUT defaults + channels, and diff instances.
|
|
2080
|
-
async function applyFullModel(m, msg) {
|
|
2081
|
-
if (!m || typeof m !== "object") { msg.innerHTML = `<span class="e">✗ ${esc(t("expectedMapping"))}</span>`; return; }
|
|
2082
|
-
const results = [];
|
|
2083
|
-
if (m.defaults) results.push(await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(m.defaults) }));
|
|
2084
|
-
const chs = m.channels || (m.channel ? [m.channel] : null);
|
|
2085
|
-
if (chs) results.push(await api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(chs) }));
|
|
2086
|
-
if (m.instances && typeof m.instances === "object") {
|
|
2087
|
-
const cur = state.fleet.instances || {};
|
|
2088
|
-
for (const [n, cfg] of Object.entries(m.instances)) {
|
|
2089
|
-
const exists = !!cur[n];
|
|
2090
|
-
results.push(await api(`/api/settings/fleet/instances/${encodeURIComponent(n)}`, { method: exists ? "PATCH" : "POST", body: JSON.stringify(cfg) }));
|
|
2091
|
-
}
|
|
2092
|
-
for (const n of Object.keys(cur)) if (!(n in m.instances)) results.push(await api(`/api/settings/fleet/instances/${encodeURIComponent(n)}`, { method: "DELETE" }));
|
|
2093
|
-
}
|
|
2094
|
-
const bad = results.find(r => !r.ok);
|
|
2095
|
-
resultMsg(msg, bad || { ok: true, body: {} });
|
|
2096
|
-
if (!bad) { state.advEdit = false; await reload(); }
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
// ── Load / render ──
|
|
2100
|
-
function renderAll() { renderAgents(); renderClassic(); renderBots(); renderProviderSecrets(); renderGeneral(); renderAdvanced(); applyStaticI18n(); }
|
|
2101
|
-
async function reloadLive() {
|
|
2102
|
-
const f = await api("/api/fleet");
|
|
2103
|
-
state.fleetUp = f.ok;
|
|
2104
|
-
state.live = {}; state.classicLive = {};
|
|
2105
|
-
if (f.ok && f.body?.instances) for (const i of f.body.instances) {
|
|
2106
|
-
state.live[i.name] = {
|
|
2107
|
-
status: i.status,
|
|
2108
|
-
state: i.state,
|
|
2109
|
-
backend: i.backend,
|
|
2110
|
-
model: i.model,
|
|
2111
|
-
model_display: i.model_display,
|
|
2112
|
-
effort: i.effort,
|
|
2113
|
-
effort_supported: i.effort_supported,
|
|
2114
|
-
};
|
|
2115
|
-
if (i.classic) state.classicLive[i.name] = { status: i.status, state: i.state };
|
|
2116
|
-
}
|
|
2117
|
-
// /api/fleet carries live tri-state for Classic instances. Profiles remain
|
|
2118
|
-
// a fallback roster for a daemon that has not yet appeared in live status.
|
|
2119
|
-
const pr = await api("/api/profiles");
|
|
2120
|
-
state.classicStatus = {}; if (pr.ok && Array.isArray(pr.body)) for (const r of pr.body) state.classicStatus[r.instance_name] = r.status;
|
|
2121
|
-
if (f.ok && f.body?.version) { state.version = f.body.version; $("verBadge").textContent = "v" + state.version; }
|
|
2122
|
-
renderAgents(); renderBots(); renderClassic();
|
|
2123
|
-
}
|
|
2124
|
-
async function reload() {
|
|
2125
|
-
// Forms edit the user-authored config, not defaults-expanded runtime values.
|
|
2126
|
-
const schema = await api("/api/settings/schema");
|
|
2127
|
-
if (schema.ok && schema.body) state.schema = schema.body;
|
|
2128
|
-
const f = await api("/api/settings/fleet/raw");
|
|
2129
|
-
if (!f.ok) { $("banner").style.display = "block"; $("banner").style.background = "#fef2f2"; $("banner").style.color = "var(--red)"; $("banner").textContent = t("configLoadFailed"); return; }
|
|
2130
|
-
state.fleet = f.body || {};
|
|
2131
|
-
const c = await api("/api/settings/classic"); state.classic = c.body || {};
|
|
2132
|
-
const connections = await api("/api/settings/connections");
|
|
2133
|
-
state.connections = connections.ok && Array.isArray(connections.body) ? connections.body : [];
|
|
2134
|
-
const providerSecrets = await api("/api/settings/provider-secrets");
|
|
2135
|
-
const providerHost = $("providerSecrets");
|
|
2136
|
-
if (providerHost) providerHost.style.display = providerSecrets.ok ? "" : "none";
|
|
2137
|
-
state.providerSecrets = providerSecrets.ok && Array.isArray(providerSecrets.body) ? providerSecrets.body : [];
|
|
2138
|
-
await reloadLive();
|
|
2139
|
-
renderAll();
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
const setHelp = (open) => { $("helpPanel").classList.toggle("open", open); $("helpBackdrop").classList.toggle("open", open); };
|
|
2143
|
-
$("helpBtn").onclick = () => setHelp(!$("helpPanel").classList.contains("open"));
|
|
2144
|
-
$("helpClose").onclick = () => setHelp(false);
|
|
2145
|
-
$("helpBackdrop").onclick = () => setHelp(false);
|
|
2146
|
-
$("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); renderAll(); };
|
|
2147
|
-
$("verBadge").onclick = () => window.open("https://github.com/songsid/AgEnD/releases/latest", "_blank", "noopener");
|
|
2148
|
-
$("agentSearch").oninput = () => { renderAgents(); renderClassic(); renderBots(); };
|
|
2149
|
-
$("newAgentBtn").onclick = () => { state.newAgentOpen = !state.newAgentOpen; $("newAgentBtn").textContent = state.newAgentOpen ? t("cancel") : t("new"); renderNewAgent(state.newAgentOpen); };
|
|
2150
|
-
$("newBotBtn").onclick = () => { state.newBotOpen = !state.newBotOpen; $("newBotBtn").textContent = state.newBotOpen ? t("cancel") : t("add"); renderNewBot(state.newBotOpen); };
|
|
2151
|
-
$("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? t("collapse") : t("expand"); };
|
|
2152
|
-
// Closing stages; cancelling discards. Neither writes anything.
|
|
2153
|
-
$("modalDone").onclick = () => { if (modalState?.stage() !== false) closeModal(); };
|
|
2154
|
-
$("modalCancel").onclick = () => closeModal();
|
|
2155
|
-
$("modalBackdrop").onclick = () => closeModal();
|
|
2156
|
-
document.addEventListener("keydown", e => { if (e.key === "Escape" && modalState) closeModal(); });
|
|
2157
|
-
$("wizardBtn").onclick = () => openWizard();
|
|
2158
|
-
$("applyChanges").onclick = applyPendingChanges;
|
|
2159
|
-
$("discardChanges").onclick = async () => { for (const change of state.pending.values()) change.cleanup?.(); state.pending.clear(); updatePendingBar(); await reload(); };
|
|
2160
|
-
|
|
2161
|
-
reload();
|
|
2162
|
-
})();
|
|
2163
|
-
</script>
|
|
2164
|
-
</body>
|
|
2165
|
-
</html>
|