@songsid/agend 2.2.0 → 2.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli-instructions.md +5 -2
- package/dist/agent-cli.js +284 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +29 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +38 -0
- package/dist/agent-file-read.js +317 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.d.ts +11 -0
- package/dist/agent-whats-new.js +34 -2
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +61 -12
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +38 -0
- package/dist/authority/authority-state.js +114 -0
- package/dist/authority/authority-state.js.map +1 -0
- package/dist/authority/session-authorizations.d.ts +43 -0
- package/dist/authority/session-authorizations.js +102 -0
- package/dist/authority/session-authorizations.js.map +1 -0
- package/dist/authority/web-authority.d.ts +120 -0
- package/dist/authority/web-authority.js +157 -0
- package/dist/authority/web-authority.js.map +1 -0
- package/dist/backend/antigravity.js +3 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -2
- package/dist/backend/claude-code.js +10 -5
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/launch-effort.js +1 -1
- package/dist/backend/launch-effort.js.map +1 -1
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +14 -0
- package/dist/backend/types.js +25 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +120 -0
- package/dist/backend-availability.js +396 -0
- package/dist/backend-availability.js.map +1 -0
- package/dist/backend-command.d.ts +21 -3
- package/dist/backend-command.js +23 -4
- package/dist/backend-command.js.map +1 -1
- package/dist/bundled-skills.d.ts +14 -0
- package/dist/bundled-skills.js +29 -0
- package/dist/bundled-skills.js.map +1 -0
- package/dist/cache-analysis.d.ts +47 -1
- package/dist/cache-analysis.js +68 -8
- package/dist/cache-analysis.js.map +1 -1
- package/dist/cache-ledger.d.ts +16 -3
- package/dist/cache-ledger.js +33 -13
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-report-tool.d.ts +17 -0
- package/dist/cache-report-tool.js +40 -0
- package/dist/cache-report-tool.js.map +1 -0
- package/dist/cache-service.d.ts +70 -3
- package/dist/cache-service.js +178 -17
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord-client-retirement.d.ts +5 -0
- package/dist/channel/adapters/discord-client-retirement.js +57 -0
- package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +66 -6
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +8 -1
- package/dist/channel/adapters/telegram.js +74 -4
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +48 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/classic-channel-manager.d.ts +10 -5
- package/dist/classic-channel-manager.js +12 -9
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +142 -110
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +119 -4
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +10 -0
- package/dist/daemon.js +327 -31
- package/dist/daemon.js.map +1 -1
- package/dist/decision-list.d.ts +25 -0
- package/dist/decision-list.js +30 -0
- package/dist/decision-list.js.map +1 -0
- package/dist/delivery-outbox.d.ts +10 -0
- package/dist/delivery-outbox.js +12 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +15 -0
- package/dist/fleet-manager.d.ts +514 -12
- package/dist/fleet-manager.js +1885 -163
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +18 -0
- package/dist/fleet-process-handlers.js +69 -0
- package/dist/fleet-process-handlers.js.map +1 -0
- package/dist/fleet-traffic.d.ts +74 -0
- package/dist/fleet-traffic.js +196 -0
- package/dist/fleet-traffic.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +492 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +482 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/human-delivery-receipts.d.ts +24 -1
- package/dist/human-delivery-receipts.js +64 -11
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/instance-avatar.js +3 -0
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +6 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +20 -10
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +12 -1
- package/dist/instructions.js +13 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +100 -8
- package/dist/locale.js.map +1 -1
- package/dist/ls-attach-resolve.d.ts +95 -0
- package/dist/ls-attach-resolve.js +174 -0
- package/dist/ls-attach-resolve.js.map +1 -0
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +15 -0
- package/dist/outbound-handlers.js +152 -11
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +228 -4
- package/dist/outbound-schemas.js +147 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +21 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -0
- package/dist/public-web-link.js +8 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +10 -1
- package/dist/quickstart-api.js +12 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +4 -0
- package/dist/reply-buttons-controller.js +2 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/restart-progress.d.ts +65 -1
- package/dist/restart-progress.js +236 -10
- package/dist/restart-progress.js.map +1 -1
- package/dist/rollout-index.d.ts +5 -0
- package/dist/rollout-index.js +26 -5
- package/dist/rollout-index.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +70 -1
- package/dist/scheduler/scheduler.js +256 -17
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/service-installer.d.ts +2 -1
- package/dist/service-installer.js +18 -3
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +23 -0
- package/dist/settings-api.js +233 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +112 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +32 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.js +16 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +4 -6
- package/dist/tool-permissions.js +100 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +17 -7
- package/dist/topic-commands.js +39 -21
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/types.d.ts +61 -1
- package/dist/ui/app-effects.js +245 -0
- package/dist/ui/chat-ambient.js +307 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +160 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +25 -7
- package/dist/ui/chat-store.js +41 -13
- package/dist/ui/chat-strings.js +86 -2
- package/dist/ui/chat-thread.js +204 -37
- package/dist/ui/chat-tour.js +1 -1
- package/dist/ui/effect-trial-controller.js +124 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +40 -0
- package/dist/ui/fleet-cache.js +23 -1
- package/dist/ui/fleet-canvas-layout.js +111 -0
- package/dist/ui/fleet-canvas.js +525 -0
- package/dist/ui/fleet-org.js +27 -12
- package/dist/ui/fleet-traffic.js +343 -0
- package/dist/ui/fleet-usage.js +401 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +252 -58
- package/dist/ui/panel-details.js +20 -6
- package/dist/ui/panel-fleet.js +45 -19
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +3 -3
- package/dist/ui/panel-settings.js +58 -13
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/preview.js +3 -2
- package/dist/ui/settings-appearance.js +349 -0
- package/dist/ui/settings-catalog.js +92 -0
- package/dist/ui/settings-dialogs.js +40 -12
- package/dist/ui/settings-effects.js +202 -0
- package/dist/ui/settings-guests.js +117 -0
- package/dist/ui/settings-learn.js +25 -0
- package/dist/ui/settings-model.js +15 -6
- package/dist/ui/settings-mods.js +271 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +335 -10
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/agend-auth.js +28 -4
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +6 -2
- package/dist/ui/shared/app-prefs.js +34 -13
- package/dist/ui/shared/app-route.js +42 -2
- package/dist/ui/shared/app-session.js +3 -1
- package/dist/ui/shared/app-shell.js +119 -41
- package/dist/ui/shared/app-store.js +28 -1
- package/dist/ui/shared/app-stream.js +14 -7
- package/dist/ui/shared/app-themes.js +206 -0
- package/dist/ui/shared/app.css +566 -5
- package/dist/ui/shared/app.js +23 -10
- package/dist/ui/shared/card-strings.js +32 -0
- package/dist/ui/shared/effect-app-runtime.js +297 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +167 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +146 -0
- package/dist/ui/shared/effect-geometry.js +137 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +122 -0
- package/dist/ui/shared/effect-policy.js +151 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +184 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +543 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +43 -0
- package/dist/ui/shared/effect-stock-trust.js +217 -0
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +170 -0
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/fx-motion.js +67 -0
- package/dist/ui/shared/header-tools.js +1 -1
- package/dist/ui/shared/identity-card.js +273 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +215 -41
- package/dist/ui/shared/owner-css.js +250 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +103 -10
- package/dist/ui/shared/signin.js +2 -0
- package/dist/ui/shared/tokens.css +9 -3
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/usage-dialog.js +22 -2
- package/dist/ui/shared/view-strings.js +98 -2
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/ui/signin.html +1 -0
- package/dist/ui/web-terminal/terminal.css +4 -4
- package/dist/usage/providers.d.ts +28 -0
- package/dist/usage/providers.js +126 -36
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage-report.d.ts +158 -0
- package/dist/usage-report.js +226 -0
- package/dist/usage-report.js.map +1 -0
- package/dist/usage-rollup.d.ts +51 -0
- package/dist/usage-rollup.js +168 -0
- package/dist/usage-rollup.js.map +1 -0
- package/dist/view-api.d.ts +68 -0
- package/dist/view-api.js +249 -8
- package/dist/view-api.js.map +1 -1
- package/dist/web-ambient.d.ts +72 -0
- package/dist/web-ambient.js +161 -0
- package/dist/web-ambient.js.map +1 -0
- package/dist/web-api.d.ts +117 -7
- package/dist/web-api.js +1283 -70
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +64 -0
- package/dist/web-appearance-policy.js +87 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +53 -0
- package/dist/web-appearance-tools.js +124 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +124 -0
- package/dist/web-appearance-view.js +278 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +93 -0
- package/dist/web-appearance.js +373 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +93 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +20 -2
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +110 -0
- package/dist/web-card.js +266 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-channel-echo.d.ts +7 -1
- package/dist/web-channel-echo.js +6 -2
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +63 -0
- package/dist/web-chat-history.js +109 -6
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +1 -0
- package/dist/web-effect-bootstrap.d.ts +12 -0
- package/dist/web-effect-bootstrap.js +49 -0
- package/dist/web-effect-bootstrap.js.map +1 -0
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +171 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-playback.d.ts +30 -0
- package/dist/web-effect-playback.js +71 -0
- package/dist/web-effect-playback.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +93 -0
- package/dist/web-effect-service.js +335 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +48 -0
- package/dist/web-effect-tools.js +129 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +27 -0
- package/dist/web-effect-upload.js +185 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +103 -0
- package/dist/web-effect.js +153 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-guest-access.d.ts +125 -0
- package/dist/web-guest-access.js +234 -0
- package/dist/web-guest-access.js.map +1 -0
- package/dist/web-guest-rooms.d.ts +82 -0
- package/dist/web-guest-rooms.js +135 -0
- package/dist/web-guest-rooms.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +38 -0
- package/dist/web-image-meta.js +262 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +47 -0
- package/dist/web-login.js +115 -0
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +207 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +157 -0
- package/dist/web-mod-bundle.js +529 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +68 -0
- package/dist/web-mod-export.js +100 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +289 -0
- package/dist/web-mod-store.js +638 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +222 -0
- package/dist/web-owner-css-store.js +440 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +62 -0
- package/dist/web-prefs.js +232 -36
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +15 -3
- package/dist/web-preview.js +28 -9
- package/dist/web-preview.js.map +1 -1
- package/dist/web-returning-device.d.ts +41 -0
- package/dist/web-returning-device.js +57 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session-authorization.d.ts +132 -0
- package/dist/web-session-authorization.js +193 -0
- package/dist/web-session-authorization.js.map +1 -0
- package/dist/web-session.d.ts +70 -10
- package/dist/web-session.js +137 -22
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +67 -2
- package/dist/web-settings-patch.js +196 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +89 -0
- package/dist/web-settings-tools.js +273 -0
- package/dist/web-settings-tools.js.map +1 -0
- package/dist/web-shell-routes.d.ts +2 -2
- package/dist/web-shell-routes.js +2 -2
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +257 -0
- package/dist/web-theme-store.js +627 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +123 -0
- package/dist/web-theme.js +416 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +13 -0
- package/dist/web-upload.js +39 -3
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +8 -0
|
@@ -27,6 +27,20 @@
|
|
|
27
27
|
|
|
28
28
|
const nativeFetch = window.fetch.bind(window);
|
|
29
29
|
let sessionPending = null;
|
|
30
|
+
// Observation only: no server authority, owner/CSRF data or way to restore this page's episode.
|
|
31
|
+
let ended = null;
|
|
32
|
+
const endListeners = new Set();
|
|
33
|
+
function endSession(reason) {
|
|
34
|
+
if (ended) return;
|
|
35
|
+
ended = Object.freeze({ reason });
|
|
36
|
+
const listeners = [...endListeners]; endListeners.clear();
|
|
37
|
+
for (const fn of listeners) { try { fn(ended); } catch { /* one observer cannot delay another's cleanup */ } }
|
|
38
|
+
}
|
|
39
|
+
function onSessionEnd(fn) {
|
|
40
|
+
if (typeof fn !== "function") return () => {};
|
|
41
|
+
if (ended) { try { fn(ended); } catch { /* late observers fail closed */ } return () => {}; }
|
|
42
|
+
endListeners.add(fn); return () => endListeners.delete(fn);
|
|
43
|
+
}
|
|
30
44
|
|
|
31
45
|
/**
|
|
32
46
|
* This page's sign-in, read once from GET /auth/session: { csrf, handle } or null (no session, or the read failed —
|
|
@@ -36,8 +50,8 @@
|
|
|
36
50
|
function session() {
|
|
37
51
|
if (!sessionPending) {
|
|
38
52
|
sessionPending = nativeFetch("/auth/session", { credentials: "same-origin", cache: "no-store" })
|
|
39
|
-
.then((r) => (r.ok ? r.json() : null)
|
|
40
|
-
.then((j) => (j && typeof j.csrf === "string" ? { csrf: j.csrf, handle: typeof j.handle === "string" ? j.handle : null } : null))
|
|
53
|
+
.then((r) => { if (r.status === 401) endSession("unauthorized"); return r.ok ? r.json() : null; })
|
|
54
|
+
.then((j) => (j && typeof j.csrf === "string" ? { csrf: j.csrf, handle: typeof j.handle === "string" ? j.handle : null, canWrite: j.tier === "admin", localAdmin: j.tier === "admin" && j.surface === "local" } : null))
|
|
41
55
|
.catch(() => null)
|
|
42
56
|
.then((value) => { if (!value) sessionPending = null; return value; });
|
|
43
57
|
}
|
|
@@ -51,7 +65,9 @@
|
|
|
51
65
|
bannerShown = true;
|
|
52
66
|
const bar = document.createElement("div");
|
|
53
67
|
bar.setAttribute("role", "alert");
|
|
54
|
-
bar.
|
|
68
|
+
bar.setAttribute("data-trust", "session-ended");
|
|
69
|
+
bar.className = "agend-session-ended";
|
|
70
|
+
bar.style.cssText = "position:fixed;top:0;left:0;right:0;z-index:2147483647;padding:10px 16px;background:#f85149;color:#fff;font:14px system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\",emoji;text-align:center";
|
|
55
71
|
const zh = /^zh/i.test(navigator.language || "");
|
|
56
72
|
bar.append(document.createTextNode(zh ? "登入已結束。" : "Your session has ended. "));
|
|
57
73
|
const link = document.createElement("a");
|
|
@@ -77,15 +93,23 @@
|
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
95
|
const response = await nativeFetch(input, init);
|
|
80
|
-
if (sameOrigin && response.status === 401 && !target.pathname.startsWith("/auth/")) sessionEndedBanner();
|
|
96
|
+
if (sameOrigin && response.status === 401 && !target.pathname.startsWith("/auth/")) { endSession("unauthorized"); sessionEndedBanner(); }
|
|
81
97
|
return response;
|
|
82
98
|
};
|
|
83
99
|
|
|
84
100
|
window.AgendAuth = {
|
|
85
101
|
csrf,
|
|
102
|
+
/** Host runtimes subscribe before admission. Late subscribers observe the same closed episode immediately. */
|
|
103
|
+
onSessionEnd,
|
|
104
|
+
/** Display hint from the existing session read. Every write still requires server authorization. */
|
|
105
|
+
canWrite() { return session().then(s => !ended && s?.canWrite === true); },
|
|
106
|
+
/** Display hint from the same server session response; Start still reauthorizes. No CSRF is exposed here. */
|
|
107
|
+
trialSession() { return session().then(s => !ended && s?.handle && /^[a-f0-9]{16}$/.test(s.handle) ? Object.freeze({ epoch: s.handle, localAdmin: s.localAdmin === true }) : null); },
|
|
108
|
+
sessionEnded() { return ended !== null; },
|
|
86
109
|
/** #1568: the session handle of this page's sign-in, or null when there is none. */
|
|
87
110
|
sessionHandle() { return session().then((s) => (s && s.handle ? s.handle : null)); },
|
|
88
111
|
async signOut() {
|
|
112
|
+
endSession("sign_out");
|
|
89
113
|
const value = await csrf();
|
|
90
114
|
const r = await nativeFetch("/auth/logout", { method: "POST", credentials: "same-origin", headers: value ? { "X-Agend-CSRF": value } : {} });
|
|
91
115
|
// A sign-out the server could not make durable is not quietly reported as done: say so before leaving.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// #1629 r4-4 (option A): widgets — an agent's in its own chat header and Details page, a person's in their sidebar —
|
|
2
|
+
// drawn by the page's own components from a closed declarative spec and its values (src/web-widget.ts). Nothing from a
|
|
3
|
+
// mod becomes markup, script, a handler, a URL or a style: text is text nodes, a number fills a <progress> or an SVG
|
|
4
|
+
// polyline's points, and tones are fixed class tokens. Colours are the theme's tokens (and a person's own CSS). The box
|
|
5
|
+
// always carries the page's own ✦ chip, so a widget never reads as AgEnD's chrome, and nothing in it can be clicked.
|
|
6
|
+
//
|
|
7
|
+
// What the host sends is checked again here with the same rules; a widget that fails is not drawn, and a value that
|
|
8
|
+
// fails shows "—". A `bind` is a value the page already has (the instance's own state, context and cost; the fleet's
|
|
9
|
+
// counts in the person's sidebar only) — nothing new is fetched for it.
|
|
10
|
+
import { html } from "./app-html.js";
|
|
11
|
+
import { register, t } from "./app-i18n.js";
|
|
12
|
+
import { statusFacet } from "./instance-nav.js";
|
|
13
|
+
import { themesShown, themesStore } from "./app-themes.js";
|
|
14
|
+
import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
|
|
15
|
+
import { createStore } from "./app-store.js";
|
|
16
|
+
import { safeMode } from "./owner-css.js";
|
|
17
|
+
|
|
18
|
+
register("widget", {
|
|
19
|
+
en: {
|
|
20
|
+
label: "Widget: {0}",
|
|
21
|
+
chip: "A widget — drawn by AgEnD from its owner's data",
|
|
22
|
+
byHuman: "placed by a person",
|
|
23
|
+
none: "—",
|
|
24
|
+
},
|
|
25
|
+
"zh-TW": {
|
|
26
|
+
label: "小工具:{0}",
|
|
27
|
+
chip: "小工具——由 AgEnD 依擁有者的資料繪製",
|
|
28
|
+
byHuman: "由人放置",
|
|
29
|
+
none: "—",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const WIDGET_ITEM_TYPES = ["stat", "progress", "text", "list", "badge", "spark"];
|
|
34
|
+
export const WIDGET_TONES = ["ok", "warn", "bad", "info", "muted"];
|
|
35
|
+
const INSTANCE_BINDS = ["state", "context_pct", "cost_usd"];
|
|
36
|
+
const FLEET_BINDS = ["fleet.idle", "fleet.working", "fleet.needs_you", "fleet.stopped"];
|
|
37
|
+
export const SLOT_RULES = {
|
|
38
|
+
chat_header: { maxItems: 4, types: ["stat", "progress", "text", "badge"], binds: INSTANCE_BINDS },
|
|
39
|
+
details: { maxItems: 8, types: WIDGET_ITEM_TYPES, binds: INSTANCE_BINDS },
|
|
40
|
+
sidebar: { maxItems: 8, types: WIDGET_ITEM_TYPES, binds: FLEET_BINDS },
|
|
41
|
+
};
|
|
42
|
+
const BINDS_FOR = {
|
|
43
|
+
stat: ["context_pct", "cost_usd", ...FLEET_BINDS], progress: ["context_pct"], text: ["state"], badge: ["state"], list: [], spark: [],
|
|
44
|
+
};
|
|
45
|
+
const ITEM_KEYS = {
|
|
46
|
+
stat: ["type", "label", "key", "bind", "unit"], progress: ["type", "label", "key", "bind", "max"], text: ["type", "label", "key", "bind"],
|
|
47
|
+
list: ["type", "label", "key"], badge: ["type", "label", "key", "bind", "tone", "tones"], spark: ["type", "label", "key"],
|
|
48
|
+
};
|
|
49
|
+
const LABEL_REQUIRED = ["stat", "progress", "spark"];
|
|
50
|
+
const KEY = /^[a-z][a-z0-9_]{0,31}$/;
|
|
51
|
+
const NUMBER_MAX = 1e15;
|
|
52
|
+
const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
53
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
54
|
+
|
|
55
|
+
// One line of text, as the host's canonicalText: NFC, no control, format or separator character but the zero-width
|
|
56
|
+
// joiner, at least one character that draws something, 1..max graphemes.
|
|
57
|
+
const FORBIDDEN = /(?!)[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
58
|
+
const VISIBLE = /[^\p{M}\p{C}\p{Z}ᅟᅠㅤᅠ⠀]/u;
|
|
59
|
+
let graphemes = null;
|
|
60
|
+
export function lineOk(s, max) {
|
|
61
|
+
if (typeof s !== "string" || s.normalize("NFC") !== s || FORBIDDEN.test(s) || s.trim() !== s || !VISIBLE.test(s)) return false;
|
|
62
|
+
try { graphemes ??= new Intl.Segmenter(undefined, { granularity: "grapheme" }); } catch { return false; }
|
|
63
|
+
const n = [...graphemes.segment(s)].length;
|
|
64
|
+
return n >= 1 && n <= max;
|
|
65
|
+
}
|
|
66
|
+
const isNumber = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= NUMBER_MAX;
|
|
67
|
+
|
|
68
|
+
/** Whether `v` is a value an item of `type` can show (web-widget.ts valueFits). */
|
|
69
|
+
export function valueFits(type, v) {
|
|
70
|
+
switch (type) {
|
|
71
|
+
case "stat": return isNumber(v) || lineOk(v, 24);
|
|
72
|
+
case "progress": return isNumber(v);
|
|
73
|
+
case "text": return lineOk(v, 120);
|
|
74
|
+
case "badge": return lineOk(v, 24);
|
|
75
|
+
case "list": return Array.isArray(v) && v.length >= 1 && v.length <= 6 && v.every((s) => lineOk(s, 60));
|
|
76
|
+
case "spark": return Array.isArray(v) && v.length >= 2 && v.length <= 48 && v.every(isNumber);
|
|
77
|
+
default: return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A widget `{ spec, data, by? }` for `slot`, checked again: `{ widget }`, or `{ problem }` (where, and what) when its
|
|
83
|
+
* spec fails — then the whole widget is not drawn. A data value that fails is left out (shown as "—").
|
|
84
|
+
*/
|
|
85
|
+
export function explainWidget(slot, w) {
|
|
86
|
+
const no = (problem) => ({ problem });
|
|
87
|
+
const rules = own(SLOT_RULES, slot) ? SLOT_RULES[slot] : null;
|
|
88
|
+
if (!rules) return no("slot");
|
|
89
|
+
if (!isObj(w) || !isObj(w.spec)) return no("spec: an object { v: 1, title, items }");
|
|
90
|
+
const s = w.spec;
|
|
91
|
+
const extra = Object.keys(s).find((k) => !["v", "title", "items"].includes(k));
|
|
92
|
+
if (extra) return no(`spec.${extra}: unknown key`);
|
|
93
|
+
if (s.v !== 1) return no("spec.v: must be 1");
|
|
94
|
+
if (!lineOk(s.title, 24)) return no("spec.title: 1..24 characters on one line");
|
|
95
|
+
if (!Array.isArray(s.items) || s.items.length < 1 || s.items.length > rules.maxItems) return no(`spec.items: 1..${rules.maxItems} items`);
|
|
96
|
+
const items = [];
|
|
97
|
+
for (const [i, it] of s.items.entries()) {
|
|
98
|
+
const at = `spec.items.${i}`;
|
|
99
|
+
if (!isObj(it) || !rules.types.includes(it.type)) return no(`${at}.type: ${rules.types.join(", ")}`);
|
|
100
|
+
const unknown = Object.keys(it).find((k) => !ITEM_KEYS[it.type].includes(k));
|
|
101
|
+
if (unknown) return no(`${at}.${unknown}: unknown key`);
|
|
102
|
+
if ((it.label !== undefined || LABEL_REQUIRED.includes(it.type)) && !lineOk(it.label, 24)) return no(`${at}.label: 1..24 characters`);
|
|
103
|
+
if ((it.key === undefined) === (it.bind === undefined)) return no(`${at}: exactly one of key or bind`);
|
|
104
|
+
if (it.key !== undefined && !(typeof it.key === "string" && KEY.test(it.key))) return no(`${at}.key: a-z, 0-9 and _`);
|
|
105
|
+
if (it.bind !== undefined && !(BINDS_FOR[it.type].includes(it.bind) && rules.binds.includes(it.bind))) return no(`${at}.bind: ${BINDS_FOR[it.type].filter((b) => rules.binds.includes(b)).join(", ") || "none here"}`);
|
|
106
|
+
if (it.unit !== undefined && !lineOk(it.unit, 8)) return no(`${at}.unit: 1..8 characters`);
|
|
107
|
+
if (it.max !== undefined && !(isNumber(it.max) && it.max > 0)) return no(`${at}.max: a number > 0`);
|
|
108
|
+
if (it.tone !== undefined && !WIDGET_TONES.includes(it.tone)) return no(`${at}.tone: ${WIDGET_TONES.join(", ")}`);
|
|
109
|
+
let tones;
|
|
110
|
+
if (it.tones !== undefined) {
|
|
111
|
+
if (!isObj(it.tones)) return no(`${at}.tones: { value: tone }`);
|
|
112
|
+
tones = new Map();
|
|
113
|
+
for (const [k, v] of Object.entries(it.tones)) { if (!lineOk(k, 24) || !WIDGET_TONES.includes(v)) return no(`${at}.tones: { value: ${WIDGET_TONES.join(" | ")} }`); tones.set(k, v); }
|
|
114
|
+
if (tones.size < 1 || tones.size > 8) return no(`${at}.tones: 1..8 entries`);
|
|
115
|
+
}
|
|
116
|
+
items.push({ type: it.type, label: it.label, key: it.key, bind: it.bind, unit: it.unit, max: it.max, tone: it.tone, tones });
|
|
117
|
+
}
|
|
118
|
+
const data = new Map();
|
|
119
|
+
if (isObj(w.data)) items.forEach((it, i) => { if (it.key && own(w.data, it.key) && valueFits(it.type, w.data[it.key])) data.set(`${i}`, w.data[it.key]); });
|
|
120
|
+
return { widget: { title: s.title, items, data, by: w.by === "human" ? "human" : "agent" } };
|
|
121
|
+
}
|
|
122
|
+
/** explainWidget's widget, or null. */
|
|
123
|
+
export function checkWidget(slot, w) { return explainWidget(slot, w).widget || null; }
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* What the host would refuse in a person's `{ spec, data }` for `slot` (its whole check, web-widget.ts and web-prefs.ts),
|
|
127
|
+
* or null — so the editor can say why before anything is saved.
|
|
128
|
+
*/
|
|
129
|
+
export function widgetProblem(slot, v) {
|
|
130
|
+
if (!isObj(v) || Object.keys(v).length !== 2 || !own(v, "spec") || !own(v, "data")) return "{ \"spec\": …, \"data\": … }";
|
|
131
|
+
const r = explainWidget(slot, v);
|
|
132
|
+
if (r.problem) return r.problem;
|
|
133
|
+
if (!isObj(v.data)) return "data: an object { key: value }";
|
|
134
|
+
for (const [k, val] of Object.entries(v.data)) {
|
|
135
|
+
const users = r.widget.items.filter((it) => it.key === k);
|
|
136
|
+
if (!users.length) return `data.${k}: no item shows this key`;
|
|
137
|
+
const bad = users.find((it) => !valueFits(it.type, val));
|
|
138
|
+
if (bad) return `data.${k}: not a value a ${bad.type} shows`;
|
|
139
|
+
}
|
|
140
|
+
const bytes = (x) => new TextEncoder().encode(JSON.stringify(x)).length;
|
|
141
|
+
if (bytes(v.spec) > 2048) return "spec: at most 2048 bytes";
|
|
142
|
+
if (bytes(v.data) > 4096) return "data: at most 4096 bytes";
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The values the page has for one instance: its state word (a sidebar facet), context % and cost. */
|
|
147
|
+
export function instanceBinds(inst, exec, awaiting) {
|
|
148
|
+
if (!inst) return {};
|
|
149
|
+
return {
|
|
150
|
+
state: statusFacet(inst, exec, awaiting),
|
|
151
|
+
context_pct: isNumber(inst.context_pct) ? Math.round(Math.max(0, Math.min(100, inst.context_pct))) : undefined,
|
|
152
|
+
cost_usd: isNumber(inst.cost) ? inst.cost : undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** The fleet's counts, for the person's sidebar widget only: how many instances are idle, working, need you, stopped. */
|
|
156
|
+
export function fleetBinds(instances, exec, awaiting) {
|
|
157
|
+
const n = { "fleet.idle": 0, "fleet.working": 0, "fleet.needs_you": 0, "fleet.stopped": 0 };
|
|
158
|
+
for (const it of Array.isArray(instances) ? instances : []) {
|
|
159
|
+
const f = statusFacet(it, exec && exec[it.name], awaiting && own(awaiting, it.name) ? awaiting[it.name] : null);
|
|
160
|
+
n[f === "needs" ? "fleet.needs_you" : f === "working" ? "fleet.working" : f === "idle" ? "fleet.idle" : "fleet.stopped"] += 1;
|
|
161
|
+
}
|
|
162
|
+
return n;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let numbers = null;
|
|
166
|
+
function fmt(v) {
|
|
167
|
+
try { numbers ??= new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }); return numbers.format(v); } catch { return String(v); }
|
|
168
|
+
}
|
|
169
|
+
/** An item's value: its data, or its bind; undefined when there is none (shown as "—"). */
|
|
170
|
+
function valueOf(item, i, w, binds) {
|
|
171
|
+
if (item.key) return w.data.get(`${i}`);
|
|
172
|
+
const v = binds && own(binds, item.bind) ? binds[item.bind] : undefined;
|
|
173
|
+
return item.bind === "state" ? (lineOk(v, 24) ? v : undefined) : isNumber(v) ? v : undefined;
|
|
174
|
+
}
|
|
175
|
+
function shown(item, v) {
|
|
176
|
+
if (v === undefined) return t("widget.none");
|
|
177
|
+
if (item.bind === "cost_usd") return `$${v.toFixed(2)}`;
|
|
178
|
+
if (item.bind === "context_pct") return `${v}%`;
|
|
179
|
+
return typeof v === "number" ? fmt(v) : v;
|
|
180
|
+
}
|
|
181
|
+
/** The sparkline's points in a 100×24 box (numbers only; nothing else reaches the SVG). */
|
|
182
|
+
export function sparkPoints(values) {
|
|
183
|
+
const lo = Math.min(...values), hi = Math.max(...values), span = hi - lo || 1;
|
|
184
|
+
return values.map((v, i) => `${((i / (values.length - 1)) * 100).toFixed(2)},${(22 - ((v - lo) / span) * 20).toFixed(2)}`).join(" ");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Item({ item, v }) {
|
|
188
|
+
const label = item.label ? html`<span class="aw-label">${item.label}</span>` : null;
|
|
189
|
+
switch (item.type) {
|
|
190
|
+
case "stat":
|
|
191
|
+
return html`<div class="aw-item aw-stat">${label}<span class="aw-value">${shown(item, v)}${v !== undefined && item.unit ? html`<span class="aw-unit">${item.unit}</span>` : null}</span></div>`;
|
|
192
|
+
case "progress": {
|
|
193
|
+
const max = item.max ?? 100;
|
|
194
|
+
const pct = v === undefined ? null : Math.round((Math.max(0, Math.min(max, v)) / max) * 100);
|
|
195
|
+
// No value: no bar at all (a <progress> without one is the browser's animated "busy" bar), only "—".
|
|
196
|
+
return html`<div class="aw-item aw-progress">${label}${pct === null ? null : html`<progress class="aw-bar" max=${max} value=${Math.max(0, Math.min(max, v))} aria-label=${item.label}></progress>`}<span class="aw-value">${pct === null ? t("widget.none") : `${pct}%`}</span></div>`;
|
|
197
|
+
}
|
|
198
|
+
case "text":
|
|
199
|
+
return html`<div class="aw-item aw-text">${label}<span class="aw-value">${shown(item, v)}</span></div>`;
|
|
200
|
+
case "badge": {
|
|
201
|
+
const tone = v !== undefined && item.tones && item.tones.has(v) ? item.tones.get(v) : item.tone || "muted";
|
|
202
|
+
return html`<div class="aw-item aw-badge-item">${label}<span class=${`aw-badge aw-tone-${WIDGET_TONES.includes(tone) ? tone : "muted"}`}>${shown(item, v)}</span></div>`;
|
|
203
|
+
}
|
|
204
|
+
case "list":
|
|
205
|
+
return html`<div class="aw-item aw-list">${label}${v === undefined ? html`<span class="aw-value">${t("widget.none")}</span>` : html`<ul>${v.map((line, i) => html`<li key=${i}>${line}</li>`)}</ul>`}</div>`;
|
|
206
|
+
case "spark":
|
|
207
|
+
return html`<div class="aw-item aw-spark-item">${label}${v === undefined ? html`<span class="aw-value">${t("widget.none")}</span>`
|
|
208
|
+
: html`<svg class="aw-spark" viewBox="0 0 100 24" preserveAspectRatio="none" aria-hidden="true"><polyline points=${sparkPoints(v)} /></svg><span class="aw-value">${fmt(v[v.length - 1])}</span>`}</div>`;
|
|
209
|
+
default: return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** One widget, checked (checkWidget's result), in `slot`. `binds`: instanceBinds or fleetBinds. */
|
|
214
|
+
export function AgentWidget({ slot, widget, binds, tools = null }) {
|
|
215
|
+
if (!widget) return null;
|
|
216
|
+
return html`<section data-trust="widget" class=${`aw aw-${slot}`} aria-label=${t("widget.label", widget.title)} data-widget-slot=${slot}>
|
|
217
|
+
<div class="aw-head"><span class="aw-chip" title=${t("widget.chip")}>✦ ${widget.title}</span>
|
|
218
|
+
${widget.by === "human" ? html`<span class="aw-by">${t("widget.byHuman")}</span>` : null}${tools}</div>
|
|
219
|
+
<div class="aw-items">${widget.items.map((item, i) => html`<${Item} key=${i} item=${item} v=${valueOf(item, i, widget, binds)} />`)}</div>
|
|
220
|
+
</section>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** An agent's widget in `slot` ("chat_header" | "details"), checked; null when none, or agents' looks are not shown here. */
|
|
224
|
+
export function widgetFor(name, slot) {
|
|
225
|
+
const s = themesStore.get();
|
|
226
|
+
if (!s.shown || !themesShown() || !own(s.widgets, name)) return null;
|
|
227
|
+
const slots = s.widgets[name];
|
|
228
|
+
return isObj(slots) && own(slots, slot) ? checkWidget(slot, slots[slot]) : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const SIDEBAR_KEY = "widget.sidebar";
|
|
232
|
+
const publicLink = () => typeof document !== "undefined" && !!document.body && document.body.dataset.publicLink === "1";
|
|
233
|
+
/** The person's own sidebar widget (a synced preference), raw; null when none. Never on the public link or in safe mode. */
|
|
234
|
+
export function sidebarWidgetRaw() {
|
|
235
|
+
if (publicLink() || safeMode()) return null;
|
|
236
|
+
const f = prefInForce(SIDEBAR_KEY);
|
|
237
|
+
let v = f ? f.value : undefined;
|
|
238
|
+
if (!f) { try { v = JSON.parse(localStorage.getItem("agend_widget_sidebar") || "null"); } catch { v = null; } }
|
|
239
|
+
return isObj(v) ? v : null;
|
|
240
|
+
}
|
|
241
|
+
/** The person's sidebar widget, checked; null when none. */
|
|
242
|
+
export function sidebarWidget() { const v = sidebarWidgetRaw(); return v ? checkWidget("sidebar", v) : null; }
|
|
243
|
+
/** Bumped when the person's sidebar widget changes (here, or on another device), so the sidebar redraws. */
|
|
244
|
+
export const sidebarWidgetStore = createStore({ n: 0 });
|
|
245
|
+
const bump = () => sidebarWidgetStore.set((s) => ({ n: s.n + 1 }));
|
|
246
|
+
onPrefsChanged((changed) => { if (changed.has(SIDEBAR_KEY)) bump(); });
|
|
247
|
+
/** Set (or with null, remove) the person's sidebar widget on all their devices. */
|
|
248
|
+
export function setSidebarWidget(value) { savePref(SIDEBAR_KEY, value); bump(); }
|
|
@@ -60,7 +60,7 @@ export function t(key, ...values) {
|
|
|
60
60
|
|
|
61
61
|
register("app", {
|
|
62
62
|
en: {
|
|
63
|
-
brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet
|
|
63
|
+
brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet", view: "View", settings: "Settings", instances: "Instances", newInstance: "New instance", newAgent: "New agent",
|
|
64
64
|
collapse: "Hide the sidebar", expand: "Show the sidebar", openMenu: "Open the sidebar", closeMenu: "Close the sidebar",
|
|
65
65
|
resizeSidebar: "Sidebar width", resizeSidebarHint: "Drag to resize · double-click to reset",
|
|
66
66
|
names: "Names", namesNickname: "Nickname first", namesInstance: "Real name first",
|
|
@@ -68,11 +68,13 @@ register("app", {
|
|
|
68
68
|
previews: "Allow HTML previews", tour: "Tour", tourTitle: "Show the web chat tour again",
|
|
69
69
|
needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", log: "Log", instanceViews: "Views of {0}",
|
|
70
70
|
textSize: "Text size: {0}", textSize_s: "S", textSize_m: "M", textSize_l: "L", textSize_fit: "Fit", usage: "Usage", usageHide: "Hide usage",
|
|
71
|
+
ownerCssKeep: "Keep this custom CSS?", ownerCssKeepLeft: "Reverts in {0} s", ownerCssKeepYes: "Keep", ownerCssKeepNo: "Revert",
|
|
71
72
|
statusRunning: "running", statusStopped: "stopped", statusPaused: "paused", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
|
|
72
73
|
connReconnecting: "Can't reach AgEnD — it may be restarting. Reconnecting…", connReconnectingIn: "Can't reach AgEnD — it may be restarting. Trying again in {0} s.",
|
|
73
74
|
hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
|
|
74
75
|
skip: "Skip to the conversation", loadFailed: "This could not be loaded.", retry: "Try again", loading: "Loading…",
|
|
75
76
|
noInstances: "No instances yet", noInstancesHint: "Create one to start chatting.",
|
|
77
|
+
guestChip: "Guest", guestAs: "Guest · {0}", guestAsOn: "Guest · {0} ({1})", guestHint: "You see only the agents you can already use in chat.",
|
|
76
78
|
session: "Session", signIn: "Sign in", signedIn: "Signed in", signedOut: "Not signed in", thisDevice: "This device", devicePrefs: "Theme and language",
|
|
77
79
|
lastActive: "active {0}", justNow: "just now", minAgo: "{0} min ago", hrAgo: "{0} h ago", signOut: "Sign out",
|
|
78
80
|
signOutAll: "Sign out everywhere", devices: "Signed-in devices", expires: "Ends by {0} at the latest, or after {1} unused",
|
|
@@ -91,7 +93,7 @@ register("app", {
|
|
|
91
93
|
needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed",
|
|
92
94
|
},
|
|
93
95
|
"zh-TW": {
|
|
94
|
-
brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance",
|
|
96
|
+
brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
|
|
95
97
|
collapse: "隱藏側邊欄", expand: "顯示側邊欄", openMenu: "打開側邊欄", closeMenu: "關閉側邊欄",
|
|
96
98
|
resizeSidebar: "側邊欄寬度", resizeSidebarHint: "拖曳調整寬度 · 按兩下恢復預設",
|
|
97
99
|
names: "名稱", namesNickname: "暱稱優先", namesInstance: "實際名稱優先",
|
|
@@ -99,11 +101,13 @@ register("app", {
|
|
|
99
101
|
previews: "允許預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
|
|
100
102
|
needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", log: "紀錄", instanceViews: "{0} 的檢視",
|
|
101
103
|
textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "填滿", usage: "用量", usageHide: "隱藏用量",
|
|
104
|
+
ownerCssKeep: "保留這份自訂 CSS?", ownerCssKeepLeft: "{0} 秒後還原", ownerCssKeepYes: "保留", ownerCssKeepNo: "還原",
|
|
102
105
|
statusRunning: "執行中", statusStopped: "已停止", statusPaused: "已暫停", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
|
|
103
106
|
connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
|
|
104
107
|
hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
|
|
105
108
|
skip: "跳到對話", loadFailed: "無法載入。", retry: "再試一次", loading: "載入中…",
|
|
106
109
|
noInstances: "還沒有 instance", noInstancesHint: "建立一個就能開始聊天。",
|
|
110
|
+
guestChip: "訪客", guestAs: "訪客 · {0}", guestAsOn: "訪客 · {0}({1})", guestHint: "你只會看到在聊天中本來就能用的 agent。",
|
|
107
111
|
session: "登入狀態", signIn: "登入", signedIn: "已登入", signedOut: "尚未登入", thisDevice: "這台裝置", devicePrefs: "主題與語言",
|
|
108
112
|
lastActive: "{0}活動", justNow: "剛剛", minAgo: "{0} 分鐘前", hrAgo: "{0} 小時前", signOut: "登出",
|
|
109
113
|
signOutAll: "全部登出", devices: "已登入的裝置", expires: "最晚 {0} 結束;閒置 {1} 也會結束",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// - The status frame's prefs_rev: another device changed something; the host's values are read again and applied. A
|
|
19
19
|
// revision counts as seen only once its read succeeded (a failed one is tried again, at most every few seconds).
|
|
20
20
|
import { deviceLanguage, lang, setLang } from "./app-i18n.js";
|
|
21
|
-
import { appStore } from "./app-store.js";
|
|
21
|
+
import { appStore, guestPage } from "./app-store.js";
|
|
22
22
|
|
|
23
23
|
const SYNCED_FLAG = "agend_prefs_synced";
|
|
24
24
|
const RETRY_MS = 5000;
|
|
@@ -31,15 +31,35 @@ export const SYNCED = {
|
|
|
31
31
|
"nav.collapsed": { ls: "agend_instance_collapsed", ...json },
|
|
32
32
|
"nav.filter": { ls: "agend_instance_filter", ...json },
|
|
33
33
|
"nav.favorites": { ls: "agend_favorites", ...json }, // #1664
|
|
34
|
+
"nav.sort": { ls: "agend_nav_sort", ...json }, // #2179
|
|
34
35
|
"preview.optIn": { ls: "agend_html_preview", toLocal: (v) => (v ? "on" : null), fromLocal: (s) => (s === "on" ? true : undefined) },
|
|
35
36
|
"names.primary": { ls: "agend_names_primary", toLocal: (v) => (v === "instance" ? v : null), fromLocal: (s) => (s === "instance" ? s : undefined) }, // #1722
|
|
37
|
+
// #1629 r2 (d): "Show agents' themes and effects" — on unless turned off (only "off" is cached).
|
|
38
|
+
"fx.show": { ls: "agend_fx_show", toLocal: (v) => (v === false ? "off" : null), fromLocal: (s) => (s === "off" ? false : undefined) },
|
|
39
|
+
// #1629 r4.1 Q3 (a): the identity card's size — every card's default (M unless chosen), and the cards set apart.
|
|
40
|
+
"card.size": { ls: "agend_card_size", toLocal: (v) => (v === "s" || v === "l" ? v : null), fromLocal: (s) => (s === "s" || s === "l" ? s : undefined) },
|
|
41
|
+
"card.sizeByInstance": { ls: "agend_card_sizes", ...json },
|
|
42
|
+
// #1629 r4-4: the person's own sidebar widget ({ spec, data }), drawn by the page's own components.
|
|
43
|
+
"widget.sidebar": { ls: "agend_widget_sidebar", ...json },
|
|
44
|
+
// #1958: where identity colours show by default (#2056: name, bar and ring unless chosen), and the host's switch to turn them off.
|
|
45
|
+
"color.show": { ls: "agend_color_show", ...json },
|
|
46
|
+
"color.off": { ls: "agend_color_off", toLocal: (v) => (v === true ? "on" : null), fromLocal: (s) => (s === "on" ? true : undefined) },
|
|
36
47
|
};
|
|
48
|
+
// #1629: fixed personal scope. A separate session-bound owner-effect store synchronizes these, never this host cache.
|
|
49
|
+
export const OWNER_EFFECT_PREF_KEYS = Object.freeze(["fx.sidebar", "fx.header", "fx.page", "fx.box", "fx.cursor"]);
|
|
50
|
+
for (const spec of Object.values(SYNCED)) spec.scope = "host";
|
|
51
|
+
for (const key of OWNER_EFFECT_PREF_KEYS) SYNCED[key] = Object.freeze({ scope: "person", ls: null });
|
|
52
|
+
for (const spec of Object.values(SYNCED)) Object.freeze(spec);
|
|
53
|
+
Object.freeze(SYNCED);
|
|
54
|
+
const HOST_KEYS = Object.keys(SYNCED).filter(key => SYNCED[key].scope === "host");
|
|
55
|
+
const hostKey = key => own(SYNCED, key) && SYNCED[key].scope === "host";
|
|
56
|
+
const hostValues = values => Object.fromEntries(HOST_KEYS.filter(key => own(values, key)).map(key => [key, values[key]]));
|
|
37
57
|
const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
38
58
|
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
39
59
|
|
|
40
60
|
function readLocal(key) { try { return localStorage.getItem(key); } catch { return null; } }
|
|
41
61
|
function writeLocal(key, s) { try { if (s == null) localStorage.removeItem(key); else localStorage.setItem(key, s); } catch { /* this page only */ } }
|
|
42
|
-
function cache(key, value) { const k = SYNCED[key]; writeLocal(k.ls, value == null ? null : k.toLocal(value)); }
|
|
62
|
+
function cache(key, value) { if (!hostKey(key)) return; const k = SYNCED[key]; writeLocal(k.ls, value == null ? null : k.toLocal(value)); }
|
|
43
63
|
const inStep = () => readLocal(SYNCED_FLAG) === "1";
|
|
44
64
|
|
|
45
65
|
function pageHost() {
|
|
@@ -51,7 +71,7 @@ function pageHost() {
|
|
|
51
71
|
|
|
52
72
|
const initial = typeof document !== "undefined" ? pageHost() : null;
|
|
53
73
|
let rev = initial ? initial.rev : null;
|
|
54
|
-
let host = initial ? initial.values : null;
|
|
74
|
+
let host = initial ? hostValues(initial.values) : null;
|
|
55
75
|
const pending = new Map(); // key → value chosen here, not sent yet
|
|
56
76
|
const mine = new Map(); // key → value chosen here whose write is not over (pending or out)
|
|
57
77
|
const inflight = new Map(); // key → writes sent and not answered yet
|
|
@@ -63,15 +83,15 @@ const listeners = new Set();
|
|
|
63
83
|
// the host does not have: the default, on a device in step with the host; this device's own (to upload) otherwise.
|
|
64
84
|
if (host) {
|
|
65
85
|
const step = inStep();
|
|
66
|
-
for (const key of
|
|
86
|
+
for (const key of HOST_KEYS) {
|
|
67
87
|
if (own(host, key)) cache(key, host[key]);
|
|
68
88
|
else if (step) cache(key, null);
|
|
69
89
|
}
|
|
70
90
|
}
|
|
71
91
|
|
|
72
|
-
/** This page may change the host's preferences: the signed-in app, not over the public link. */
|
|
92
|
+
/** This page may change the host's preferences: the signed-in app, not over the public link, not a guest's (#1920). */
|
|
73
93
|
export function prefsWritable() {
|
|
74
|
-
return !!initial && !(typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1");
|
|
94
|
+
return !!initial && !guestPage() && !(typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1");
|
|
75
95
|
}
|
|
76
96
|
/**
|
|
77
97
|
* The value in force for a key, in memory — never only in this browser's storage (a full storage must not bring back
|
|
@@ -80,14 +100,14 @@ export function prefsWritable() {
|
|
|
80
100
|
* in step) — the caller reads this browser's copy.
|
|
81
101
|
*/
|
|
82
102
|
export function prefInForce(key) {
|
|
83
|
-
if (!
|
|
103
|
+
if (!hostKey(key)) return null;
|
|
84
104
|
if (mine.has(key)) return { value: mine.get(key) };
|
|
85
105
|
if (host && own(host, key)) return { value: host[key] };
|
|
86
106
|
if (initial && inStep()) return { value: undefined };
|
|
87
107
|
return null;
|
|
88
108
|
}
|
|
89
109
|
/** The host's value for a key as this page last saw it (undefined: not set there, or not known). */
|
|
90
|
-
export function hostPref(key) { return host && own(host, key) ? host[key] : undefined; }
|
|
110
|
+
export function hostPref(key) { return hostKey(key) && host && own(host, key) ? host[key] : undefined; }
|
|
91
111
|
/** Called with the keys another device changed, after they are cached and applied here. Returns the unsubscribe. */
|
|
92
112
|
export function onPrefsChanged(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
|
93
113
|
|
|
@@ -97,7 +117,7 @@ const isView = (body) => !!body && typeof body.rev === "number" && !!body.values
|
|
|
97
117
|
function take(body) {
|
|
98
118
|
if (!isView(body) || (rev !== null && body.rev < rev)) return false;
|
|
99
119
|
rev = body.rev;
|
|
100
|
-
host = body.values;
|
|
120
|
+
host = hostValues(body.values);
|
|
101
121
|
return true;
|
|
102
122
|
}
|
|
103
123
|
|
|
@@ -106,7 +126,7 @@ function take(body) {
|
|
|
106
126
|
* (the public link, the anonymous reader): this browser only, as before.
|
|
107
127
|
*/
|
|
108
128
|
export function savePref(key, value) {
|
|
109
|
-
if (!
|
|
129
|
+
if (!hostKey(key)) return;
|
|
110
130
|
cache(key, value);
|
|
111
131
|
if (!prefsWritable()) return;
|
|
112
132
|
pending.set(key, value);
|
|
@@ -165,8 +185,8 @@ export async function refresh() {
|
|
|
165
185
|
body = res.ok ? await res.json() : null;
|
|
166
186
|
} catch { body = null; }
|
|
167
187
|
if (!isView(body)) return false;
|
|
168
|
-
const before = new Map(
|
|
169
|
-
if (take(body)) align(
|
|
188
|
+
const before = new Map(HOST_KEYS.map((key) => [key, own(host, key) ? SYNCED[key].toLocal(host[key]) : null]));
|
|
189
|
+
if (take(body)) align(HOST_KEYS, { before });
|
|
170
190
|
return true;
|
|
171
191
|
}
|
|
172
192
|
|
|
@@ -179,6 +199,7 @@ export async function refresh() {
|
|
|
179
199
|
function align(keys, opts = {}) {
|
|
180
200
|
const changed = new Set();
|
|
181
201
|
for (const key of keys) {
|
|
202
|
+
if (!hostKey(key)) continue;
|
|
182
203
|
if (pending.has(key) || inflight.has(key)) continue;
|
|
183
204
|
const k = SYNCED[key];
|
|
184
205
|
const want = own(host, key) ? k.toLocal(host[key]) : null;
|
|
@@ -216,7 +237,7 @@ function apply(changed) {
|
|
|
216
237
|
*/
|
|
217
238
|
export async function migratePrefs() {
|
|
218
239
|
if (!prefsWritable() || inStep()) return false;
|
|
219
|
-
for (const key of
|
|
240
|
+
for (const key of HOST_KEYS) {
|
|
220
241
|
if (own(host, key)) continue;
|
|
221
242
|
const local = SYNCED[key].fromLocal(readLocal(SYNCED[key].ls));
|
|
222
243
|
if (local === undefined) continue;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// #1408 §3: the app's routes on the client. The same table as the server's classifier (src/web-shell-routes.ts;
|
|
2
2
|
// a test runs both over one list of cases). Pure: no DOM, no history.
|
|
3
3
|
|
|
4
|
-
export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "config"];
|
|
5
|
-
export const SETTINGS_SECTIONS = ["device", "agents", "bots", "classic", "general", "web", "advanced"]; // #1604: device first
|
|
4
|
+
export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "usage", "config"];
|
|
5
|
+
export const SETTINGS_SECTIONS = ["device", "appearance", "agents", "bots", "classic", "general", "web", "advanced"]; // #1604: device first
|
|
6
6
|
const MAX_INSTANCE_NAME = 128;
|
|
7
7
|
const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
|
|
8
8
|
|
|
@@ -50,6 +50,24 @@ export function viewPath(instance) { return instance ? `/view/${encodeURICompone
|
|
|
50
50
|
export function detailsPath(instance) { return `/ui/fleet/agent/${encodeURIComponent(instance)}`; }
|
|
51
51
|
export function logPath(instance) { return `/ui/log/${encodeURIComponent(instance)}`; }
|
|
52
52
|
export function fleetPath(tab) { return tab && tab !== "tasks" ? `/ui/fleet/${tab}` : "/ui/fleet"; }
|
|
53
|
+
/** #2183: this device's last Fleet tab (Fleet remembers it; the Org chart the first time). */
|
|
54
|
+
export const LAST_FLEET_TAB_KEY = "agend_last_fleet_tab";
|
|
55
|
+
export function lastFleetTab() {
|
|
56
|
+
let tab = null;
|
|
57
|
+
try { tab = localStorage.getItem(LAST_FLEET_TAB_KEY); } catch { /* storage blocked: the default */ }
|
|
58
|
+
return FLEET_TABS.includes(tab) ? tab : "org";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* #2183: where Fleet (the sidebar's entry, the phone's tab) goes — on a Fleet tab, that tab (the route, not storage: the
|
|
62
|
+
* panel stores it in an effect after this render, and a storage write re-renders nothing); anywhere else, the last one.
|
|
63
|
+
*/
|
|
64
|
+
export function fleetEntryPath(route) {
|
|
65
|
+
return fleetPath(route && route.panel === "fleet" ? route.tab : lastFleetTab()); // parseRoute names only FLEET_TABS
|
|
66
|
+
}
|
|
67
|
+
export function rememberFleetTab(tab) {
|
|
68
|
+
if (!FLEET_TABS.includes(tab)) return;
|
|
69
|
+
try { localStorage.setItem(LAST_FLEET_TAB_KEY, tab); } catch { /* this page only */ }
|
|
70
|
+
}
|
|
53
71
|
export const NEEDS_PATH = "/ui/needs";
|
|
54
72
|
export function settingsPath(section) { return section && section !== "agents" ? `/settings/${section}` : "/settings"; }
|
|
55
73
|
export function routePath(route) {
|
|
@@ -62,6 +80,28 @@ export function routePath(route) {
|
|
|
62
80
|
if (route.panel === "log") return logPath(route.instance);
|
|
63
81
|
return route.instance ? chatPath(route.instance) : "/ui";
|
|
64
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* #1990: a text with the in-app links the server put on it (ranges of the text: { offset, length, url }) as segments —
|
|
85
|
+
* { text } or { text, href }. Only a route this table parses as an instance's chat or View becomes a link (its href
|
|
86
|
+
* rebuilt here); a range out of order, out of the text, or anything else stays text. No target: the router opens it.
|
|
87
|
+
*/
|
|
88
|
+
export function textLinkSegments(text, links) {
|
|
89
|
+
const s = String(text == null ? "" : text);
|
|
90
|
+
const out = [];
|
|
91
|
+
let at = 0;
|
|
92
|
+
for (const l of Array.isArray(links) ? links.slice(0, 16) : []) {
|
|
93
|
+
if (!l || !Number.isInteger(l.offset) || !Number.isInteger(l.length) || typeof l.url !== "string") continue;
|
|
94
|
+
if (l.offset < at || l.length < 1 || l.offset + l.length > s.length || !/^\/[^?#\s]*$/.test(l.url)) continue;
|
|
95
|
+
const route = parseRoute(l.url);
|
|
96
|
+
if (!route || !route.instance || (route.panel !== "chat" && route.panel !== "view")) continue;
|
|
97
|
+
if (l.offset > at) out.push({ text: s.slice(at, l.offset) });
|
|
98
|
+
out.push({ text: s.slice(l.offset, l.offset + l.length), href: routePath(route) });
|
|
99
|
+
at = l.offset + l.length;
|
|
100
|
+
}
|
|
101
|
+
if (at < s.length || !out.length) out.push({ text: s.slice(at) });
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
65
105
|
/** One string per route: a change of it is a new navigation (a new lease for the panel, §4). */
|
|
66
106
|
export function routeKey(route) {
|
|
67
107
|
if (!route) return "none";
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { html, useEffect, useRef, useState } from "./app-html.js";
|
|
5
5
|
import { Icon } from "./ui-icons.js";
|
|
6
6
|
import { t, lang } from "./app-i18n.js";
|
|
7
|
+
import { AuthorizeChip } from "./web-authorize.js";
|
|
7
8
|
|
|
8
9
|
async function getJson(url) {
|
|
9
10
|
try {
|
|
@@ -49,10 +50,11 @@ export function SessionMenu() {
|
|
|
49
50
|
<button ref=${btn} type="button" class="side-row" aria-haspopup="dialog" aria-expanded=${open ? "true" : "false"}
|
|
50
51
|
onClick=${() => { const next = !open; setOpen(next); if (next) refresh(); }}>
|
|
51
52
|
<span class=${`dot${me ? " ok" : ""}`} aria-hidden="true"></span><span class="grow">${label}</span>${me ? html`<span class="meta">${me.label}</span>` : null}</button>
|
|
52
|
-
${open ? html`<div ref=${pop} class="pop" role="dialog" aria-label=${t("app.session")} onKeyDown=${onKey}>
|
|
53
|
+
${open ? html`<div data-trust="session" ref=${pop} class="pop" role="dialog" aria-label=${t("app.session")} onKeyDown=${onKey}>
|
|
53
54
|
${!me ? html`<h3>${t("app.signedOut")}</h3><a class="btn" href=${`/signin?next=${encodeURIComponent(location.pathname)}`}>${t("app.signIn")}</a>` : html`
|
|
54
55
|
<h3>${t("app.signedIn")} · ${me.label}</h3>
|
|
55
56
|
<p class="sub">${t("app.expires", clock(me.expiresAt), span(me.idleMs))}</p>
|
|
57
|
+
<${AuthorizeChip} />
|
|
56
58
|
${rows.length > 1 ? html`<h3>${t("app.devices")}</h3><ul class="devices">${rows.map(s => html`<li key=${s.handle}>
|
|
57
59
|
<span class="grow"><span class="name">${s.label}${s.current ? ` · ${t("app.thisDevice")}` : ""}</span><span class="when">${t("app.lastActive", ago(s.lastSeen))}</span></span>
|
|
58
60
|
${s.current ? null : html`<button type="button" class="btn btn-sm danger" onClick=${() => act("DELETE", `/auth/sessions/${encodeURIComponent(s.handle)}`, refresh)}>${t("app.signOut")}</button>`}</li>`)}</ul>` : null}
|