@songsid/agend 2.2.0-beta.9 → 2.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -24
- package/README.zh-TW.md +40 -24
- package/dist/agent-cli.js +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +28 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +20 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +3 -1
- package/dist/auth-api.js +32 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +33 -0
- package/dist/backend/antigravity.js +129 -21
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +9 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +3 -0
- package/dist/backend/grok.js +9 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -1
- package/dist/backend/kiro.js +7 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +24 -0
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +14 -0
- package/dist/channel/adapters/discord.js +66 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +26 -1
- package/dist/channel/adapters/telegram.js +98 -7
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +1 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +31 -0
- package/dist/cli.js +124 -24
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +5 -2
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.d.ts +86 -0
- package/dist/compact-watch.js +288 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/config-validator.js +55 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.js +394 -29
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -1
- package/dist/delivery-outbox.js +6 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.js +7 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +12 -0
- package/dist/fleet-manager.d.ts +247 -9
- package/dist/fleet-manager.js +1227 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/install-lock.d.ts +18 -1
- package/dist/install-lock.js +30 -3
- package/dist/install-lock.js.map +1 -1
- package/dist/instance-avatar.d.ts +5 -4
- package/dist/instance-avatar.js +15 -8
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +2 -2
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/launchd-handoff.d.ts +43 -0
- package/dist/launchd-handoff.js +56 -0
- package/dist/launchd-handoff.js.map +1 -0
- package/dist/locale.js +202 -30
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +10 -2
- package/dist/log-service.js.map +1 -1
- package/dist/login-shell.d.ts +21 -0
- package/dist/login-shell.js +98 -0
- package/dist/login-shell.js.map +1 -0
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +32 -0
- package/dist/outbound-handlers.js +150 -45
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +76 -0
- package/dist/outbound-schemas.js +65 -4
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +29 -0
- package/dist/provider-probe.js +198 -2
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +25 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +8 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +12 -0
- package/dist/quickstart.js +36 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +86 -5
- package/dist/reply-buttons-controller.js +271 -7
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +0 -2
- package/dist/restart-progress.js +0 -2
- package/dist/restart-progress.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/sd-notify.d.ts +22 -1
- package/dist/sd-notify.js +63 -10
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +42 -26
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +24 -0
- package/dist/service-installer.js +49 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +11 -0
- package/dist/service-plan.js +23 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tool-permissions.js +53 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +72 -2
- package/dist/topic-commands.js +156 -38
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/transcript-jsonl.d.ts +7 -2
- package/dist/transcript-jsonl.js +13 -7
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-sources.js +6 -1
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +59 -14
- package/dist/ui/chat-store.js +78 -8
- package/dist/ui/chat-strings.js +43 -16
- package/dist/ui/chat-thread.js +175 -31
- package/dist/ui/chat-tour.js +18 -15
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +101 -27
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +18 -5
- package/dist/ui/panel-log.js +10 -10
- package/dist/ui/panel-needs.js +13 -10
- package/dist/ui/panel-settings.js +66 -12
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-dialogs.js +11 -6
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-model.js +3 -1
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +269 -10
- package/dist/ui/settings-token.js +7 -0
- package/dist/ui/settings-wizard.js +5 -3
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +3 -1
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +15 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +266 -11
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +115 -0
- package/dist/ui/shared/effect-policy.js +137 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +35 -15
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/instance-switch.js +5 -2
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +96 -12
- package/dist/ui/shared/signin.js +17 -9
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/view-strings.js +40 -2
- package/dist/ui/signin.html +9 -3
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-launch.d.ts +48 -1
- package/dist/update-launch.js +159 -5
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/providers.d.ts +28 -8
- package/dist/usage/providers.js +96 -41
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +214 -17
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +86 -2
- package/dist/web-api.js +1160 -92
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +39 -1
- package/dist/web-chat-history.js +69 -9
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +39 -3
- package/dist/web-commands.js +22 -5
- package/dist/web-commands.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +29 -0
- package/dist/web-effect-upload.js +190 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +180 -0
- package/dist/web-owner-css-store.js +384 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +66 -0
- package/dist/web-prefs.js +243 -35
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-session.d.ts +14 -0
- package/dist/web-session.js +10 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +119 -4
- package/dist/web-upload.js +252 -39
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/launcher/install-admission.cjs +61 -6
- package/package.json +3 -3
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// #1958: an instance's identity colour on the page — the cue people scan the sidebar by — and where it shows (the
|
|
2
|
+
// name, a bar at the row's left edge, the avatar's ring). The server sends each instance's tiers in the status frame
|
|
3
|
+
// (`color`, `color_show`); this module resolves them as src/web-identity-color.ts does (a test pins the two equal),
|
|
4
|
+
// paints the sidebar row through the CSSOM (no style attribute, #1300), and holds the Color dialog.
|
|
5
|
+
// colour: a person's pick > the agent's (themes shown) > its own bot's role colour > automatic (a few soft colours)
|
|
6
|
+
// places: a person's choice > the agent's (themes shown) > this host's default (color.show) > ring + bar
|
|
7
|
+
// Settings → Web appearance can turn colours off for the host (color.off): nothing is painted, every pick is kept.
|
|
8
|
+
import { html, useEffect, useState } from "./app-html.js";
|
|
9
|
+
import { createStore, useStore } from "./app-store.js";
|
|
10
|
+
import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
|
|
11
|
+
import { register, t } from "./app-i18n.js";
|
|
12
|
+
import { themesShown, themesStore } from "./app-themes.js";
|
|
13
|
+
import { Dialog } from "./ui-dialog.js";
|
|
14
|
+
import { toast } from "./ui-toast.js";
|
|
15
|
+
|
|
16
|
+
// ── The palettes (src/web-identity-palette.ts: the same values, pinned by a test) ──
|
|
17
|
+
|
|
18
|
+
export const IDENTITY_PALETTE = [
|
|
19
|
+
{ id: "red", light: "#cf222e", dark: "#ff7b72" }, { id: "orange", light: "#bc4c00", dark: "#ffa657" },
|
|
20
|
+
{ id: "amber", light: "#8a5d00", dark: "#e3b341" }, { id: "lime", light: "#4d7c0f", dark: "#a3e635" },
|
|
21
|
+
{ id: "green", light: "#1a7f37", dark: "#56d364" }, { id: "teal", light: "#0f766e", dark: "#2dd4bf" },
|
|
22
|
+
{ id: "cyan", light: "#0e7490", dark: "#22d3ee" }, { id: "blue", light: "#0a62c2", dark: "#7ab7ff" },
|
|
23
|
+
{ id: "indigo", light: "#4f46e5", dark: "#a5b4fc" }, { id: "violet", light: "#8250df", dark: "#d2a8ff" },
|
|
24
|
+
{ id: "pink", light: "#bf3989", dark: "#f778ba" }, { id: "slate", light: "#57606a", dark: "#b1bac4" },
|
|
25
|
+
];
|
|
26
|
+
export const AUTO_PALETTE = [
|
|
27
|
+
{ id: "sand", light: "#8a6d3b", dark: "#d4b98a" }, { id: "clay", light: "#9c5a4a", dark: "#d9a294" },
|
|
28
|
+
{ id: "rose", light: "#9b4f6a", dark: "#d99ab0" }, { id: "plum", light: "#7a5a8c", dark: "#c3a6d6" },
|
|
29
|
+
{ id: "dusk", light: "#5a628f", dark: "#a9b0db" }, { id: "sea", light: "#3f7286", dark: "#92c2d4" },
|
|
30
|
+
{ id: "moss", light: "#5b7340", dark: "#a9c48d" }, { id: "slate", light: "#5f6b78", dark: "#aeb8c4" },
|
|
31
|
+
];
|
|
32
|
+
export const PLACEMENTS = ["name", "bar", "ring"];
|
|
33
|
+
export const DEFAULT_PLACEMENTS = ["bar", "ring"];
|
|
34
|
+
const HEX = /^#[0-9a-f]{6}$/;
|
|
35
|
+
const presetOf = (c) => (typeof c === "string" && c.startsWith("preset:") ? IDENTITY_PALETTE.find((p) => p.id === c.slice(7)) : undefined);
|
|
36
|
+
const autoOf = (c) => (typeof c === "string" && c.startsWith("auto:") ? AUTO_PALETTE.find((p) => p.id === c.slice(5)) : undefined);
|
|
37
|
+
/** A colour as the server sends it (preset:<id>, auto:<id>, #rrggbb), checked; null for anything else. */
|
|
38
|
+
export function colorOk(c) {
|
|
39
|
+
if (typeof c !== "string") return null;
|
|
40
|
+
if (presetOf(c) || autoOf(c) || HEX.test(c)) return c;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** A place list, in PLACEMENTS order; null for anything that is not one. */
|
|
44
|
+
export function placesOk(v) {
|
|
45
|
+
if (!Array.isArray(v) || v.length > 3 || !v.every((p) => PLACEMENTS.includes(p)) || new Set(v).size !== v.length) return null;
|
|
46
|
+
return PLACEMENTS.filter((p) => v.includes(p));
|
|
47
|
+
}
|
|
48
|
+
/** The colour drawn for `c` on a page in `mode`. */
|
|
49
|
+
export function colorValue(c, mode) {
|
|
50
|
+
const p = presetOf(c) || autoOf(c);
|
|
51
|
+
return p ? p[mode] : c;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Contrast: name text reads at 4.5:1 on the page's and the sidebar's background (tokens.css) ──
|
|
55
|
+
|
|
56
|
+
const BG = { light: ["#ffffff", "#f9f9f9"], dark: ["#212121", "#171717"] };
|
|
57
|
+
const TEXT = { light: "#0d0d0d", dark: "#ececec" };
|
|
58
|
+
function luminance(hex) {
|
|
59
|
+
const c = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((x) => (x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4));
|
|
60
|
+
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
|
61
|
+
}
|
|
62
|
+
export function contrast(a, b) {
|
|
63
|
+
const [l1, l2] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
64
|
+
return (l1 + 0.05) / (l2 + 0.05);
|
|
65
|
+
}
|
|
66
|
+
/** The shade of `hex` name text uses: itself if readable, else the nearest step toward black (light) or white (dark). */
|
|
67
|
+
export function readableShade(hex, mode) {
|
|
68
|
+
if (!HEX.test(hex)) return TEXT[mode];
|
|
69
|
+
const toward = mode === "light" ? 0 : 255;
|
|
70
|
+
const ch = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
|
71
|
+
for (let step = 0; step <= 20; step++) {
|
|
72
|
+
const shade = `#${ch.map((c) => Math.round(c + (toward - c) * (step / 20)).toString(16).padStart(2, "0")).join("")}`;
|
|
73
|
+
if (BG[mode].every((bg) => contrast(shade, bg) >= 4.5)) return shade;
|
|
74
|
+
}
|
|
75
|
+
return TEXT[mode];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Resolution ──
|
|
79
|
+
|
|
80
|
+
/** The colour in force and its source, from the frame's tiers. */
|
|
81
|
+
export function resolveColor(tiers, showThemes) {
|
|
82
|
+
const t2 = tiers && typeof tiers === "object" ? tiers : {};
|
|
83
|
+
const person = colorOk(t2.person);
|
|
84
|
+
if (person) return { color: person, source: "person" };
|
|
85
|
+
const agent = showThemes ? colorOk(t2.agent) : null;
|
|
86
|
+
if (agent && !autoOf(agent)) return { color: agent, source: "agent" };
|
|
87
|
+
const role = typeof t2.role === "string" && HEX.test(t2.role) ? t2.role : null;
|
|
88
|
+
if (role) return { color: role, source: "role" };
|
|
89
|
+
return { color: autoOf(t2.auto) ? t2.auto : "auto:sand", source: "auto" };
|
|
90
|
+
}
|
|
91
|
+
/** Where it shows, and whose choice that is. */
|
|
92
|
+
export function resolvePlaces(tiers, showThemes, hostDefault) {
|
|
93
|
+
const t2 = tiers && typeof tiers === "object" ? tiers : {};
|
|
94
|
+
const person = placesOk(t2.person);
|
|
95
|
+
if (person) return { show: person, source: "person" };
|
|
96
|
+
const agent = showThemes ? placesOk(t2.agent) : null;
|
|
97
|
+
if (agent) return { show: agent, source: "agent" };
|
|
98
|
+
return { show: placesOk(hostDefault) || [...DEFAULT_PLACEMENTS], source: "host" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── This host's settings (web prefs color.show / color.off) ──
|
|
102
|
+
|
|
103
|
+
export const colorPrefsStore = createStore({ n: 0 });
|
|
104
|
+
const bump = () => colorPrefsStore.set((s) => ({ n: s.n + 1 }));
|
|
105
|
+
onPrefsChanged((changed) => { if (changed.has("color.show") || changed.has("color.off")) bump(); });
|
|
106
|
+
const readLocal = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
|
|
107
|
+
/** Where colours show by default on this host (Settings → Web appearance). */
|
|
108
|
+
export function defaultPlaces() {
|
|
109
|
+
const f = prefInForce("color.show");
|
|
110
|
+
let v = f ? f.value : undefined;
|
|
111
|
+
if (!f) { try { v = JSON.parse(readLocal("agend_color_show") || "null"); } catch { v = null; } }
|
|
112
|
+
return placesOk(v) || [...DEFAULT_PLACEMENTS];
|
|
113
|
+
}
|
|
114
|
+
export function colorsOff() {
|
|
115
|
+
const f = prefInForce("color.off");
|
|
116
|
+
return f ? f.value === true : readLocal("agend_color_off") === "on";
|
|
117
|
+
}
|
|
118
|
+
/** The default places (null: back to ring + bar). */
|
|
119
|
+
export function setDefaultPlaces(list) {
|
|
120
|
+
const v = list === null ? null : placesOk(list);
|
|
121
|
+
const same = v && v.length === DEFAULT_PLACEMENTS.length && v.every((p, i) => p === DEFAULT_PLACEMENTS[i]);
|
|
122
|
+
savePref("color.show", v === null || same ? null : v);
|
|
123
|
+
bump();
|
|
124
|
+
}
|
|
125
|
+
export function setColorsOff(off) {
|
|
126
|
+
savePref("color.off", off ? true : null);
|
|
127
|
+
bump();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** What one instance's row shows: its colour per mode, name shades, places, and whether anything is painted at all. */
|
|
131
|
+
export function identityOf(it, opts = {}) {
|
|
132
|
+
const showThemes = opts.showThemes ?? themesShown();
|
|
133
|
+
const c = resolveColor(it && it.color, showThemes);
|
|
134
|
+
const p = resolvePlaces(it && it.color_show, showThemes, opts.hostDefault ?? defaultPlaces());
|
|
135
|
+
const light = colorValue(c.color, "light"), dark = colorValue(c.color, "dark");
|
|
136
|
+
return {
|
|
137
|
+
color: c.color, source: c.source, show: p.show, showSource: p.source, off: opts.off ?? colorsOff(),
|
|
138
|
+
light, dark, nameLight: readableShade(light, "light"), nameDark: readableShade(dark, "dark"),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** The row's classes for its identity colour ("" when colours are off or shown nowhere). */
|
|
142
|
+
export function identityClasses(id) {
|
|
143
|
+
if (!id || id.off || !id.show.length) return "";
|
|
144
|
+
return ["idc", ...id.show.map((p) => `id-${p}`)].join(" ");
|
|
145
|
+
}
|
|
146
|
+
/** Paint an element (a callback ref): the colour's values for both modes as custom properties; the stylesheet picks. */
|
|
147
|
+
export function paintIdentity(el, id) {
|
|
148
|
+
if (!el || !el.style || typeof el.style.setProperty !== "function") return;
|
|
149
|
+
const on = id && !id.off && id.show.length;
|
|
150
|
+
for (const [k, v] of [["--id-light", id && id.light], ["--id-dark", id && id.dark], ["--id-name-light", id && id.nameLight], ["--id-name-dark", id && id.nameDark]]) {
|
|
151
|
+
if (on && v) el.style.setProperty(k, v); else el.style.removeProperty(k);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Redraw when this host's colour settings or the viewer's themes switch change. */
|
|
155
|
+
export function useIdentityColors() {
|
|
156
|
+
useStore(colorPrefsStore);
|
|
157
|
+
useStore(themesStore);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── The Color dialog (Chat ⋯, Details ⋯, View's Edit profile) ──
|
|
161
|
+
|
|
162
|
+
export const colorDialogStore = createStore({ name: null, it: null });
|
|
163
|
+
export function openColorDialog(name, it) { colorDialogStore.set({ name, it: it || null }); }
|
|
164
|
+
const closeColorDialog = () => colorDialogStore.set({ name: null, it: null });
|
|
165
|
+
|
|
166
|
+
/** The ⋯ entry: on the public link too (decision 3: colours change wherever the profile does). */
|
|
167
|
+
export function colorMenuItem(name, it) {
|
|
168
|
+
return { key: "identity-color", label: t("color.menu"), icon: "palette", onSelect: () => openColorDialog(name, it) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function ColorDialogHost() {
|
|
172
|
+
const s = useStore(colorDialogStore);
|
|
173
|
+
return s.name ? html`<${ColorDialog} key=${s.name} name=${s.name} it=${s.it} onClose=${closeColorDialog} />` : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ColorDialog({ name, it, onClose }) {
|
|
177
|
+
useIdentityColors();
|
|
178
|
+
const [loaded, setLoaded] = useState(null); // the person's choice as stored: { color, color_show }
|
|
179
|
+
const [color, setColor] = useState(null); // the colour picked here (null: none — the next tier)
|
|
180
|
+
const [hex, setHex] = useState("");
|
|
181
|
+
const [places, setPlaces] = useState(null); // null: the default places
|
|
182
|
+
const [busy, setBusy] = useState(false);
|
|
183
|
+
const [error, setError] = useState("");
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
let live = true;
|
|
186
|
+
fetch(`/api/profile/${encodeURIComponent(name)}`).then((r) => (r.ok ? r.json() : null)).then((row) => {
|
|
187
|
+
if (!live) return;
|
|
188
|
+
const c = row ? colorOk(row.color) : null, s = row ? placesOk(row.color_show) : null;
|
|
189
|
+
setLoaded({ color: c, show: s }); setColor(c); setHex(c && HEX.test(c) ? c : ""); setPlaces(s);
|
|
190
|
+
}).catch(() => { if (live) setLoaded({ color: null, show: null }); });
|
|
191
|
+
return () => { live = false; };
|
|
192
|
+
}, [name]);
|
|
193
|
+
// The preview: the instance as it would be drawn with the choice here.
|
|
194
|
+
const preview = identityOf({ ...(it || {}), color: { ...((it && it.color) || {}), person: color || undefined }, color_show: { ...((it && it.color_show) || {}), person: places || undefined } }, { off: false });
|
|
195
|
+
const pick = (c) => { setColor(c); setHex(c && HEX.test(c) ? c : ""); setError(""); };
|
|
196
|
+
const togglePlace = (p) => {
|
|
197
|
+
const base = places || preview.show;
|
|
198
|
+
setPlaces(base.includes(p) ? base.filter((x) => x !== p) : PLACEMENTS.filter((x) => x === p || base.includes(x)));
|
|
199
|
+
};
|
|
200
|
+
async function save(body) {
|
|
201
|
+
setBusy(true); setError("");
|
|
202
|
+
try {
|
|
203
|
+
const res = await fetch(`/api/profile/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
204
|
+
const r = await res.json().catch(() => ({}));
|
|
205
|
+
if (!res.ok) { setError(t("color.failed", (r && (r.detail || r.error)) || `HTTP ${res.status}`)); setBusy(false); return; }
|
|
206
|
+
toast(t("color.saved", name));
|
|
207
|
+
onClose();
|
|
208
|
+
} catch (err) { setError(t("color.failed", (err && err.message) || "network")); setBusy(false); }
|
|
209
|
+
}
|
|
210
|
+
const onHex = (v) => { setHex(v); const c = /^#[0-9a-fA-F]{6}$/.test(v) ? v.toLowerCase() : null; if (c) { setColor(c); setError(""); } };
|
|
211
|
+
const off = colorsOff();
|
|
212
|
+
return html`<${Dialog} title=${t("color.title", name)} onClose=${onClose} busy=${busy}
|
|
213
|
+
actions=${html`<button type="button" class="btn" disabled=${busy || !loaded} onClick=${() => save({ color: null, color_show: null })}>${t("color.reset")}</button>
|
|
214
|
+
<span class="dlg-spacer"></span>
|
|
215
|
+
<button type="button" class="btn" disabled=${busy} onClick=${onClose}>${t("color.cancel")}</button>
|
|
216
|
+
<button type="button" class="btn btn-primary" disabled=${busy || !loaded} onClick=${() => save({ color, color_show: places })}>${t("color.save")}</button>`}>
|
|
217
|
+
${off ? html`<p class="note" role="status">${t("color.offNote")}</p>` : null}
|
|
218
|
+
<p class="sub">${t(`color.src_${preview.source}`)}</p>
|
|
219
|
+
<fieldset class="idc-swatches"><legend>${t("color.colour")}</legend>
|
|
220
|
+
${IDENTITY_PALETTE.map((p) => html`<button type="button" key=${p.id} class=${`idc-swatch${color === `preset:${p.id}` ? " on" : ""}`}
|
|
221
|
+
aria-pressed=${color === `preset:${p.id}` ? "true" : "false"} aria-label=${t(`color.p_${p.id}`)} title=${t(`color.p_${p.id}`)}
|
|
222
|
+
ref=${(el) => { if (el && el.style) { el.style.setProperty("--sw-light", p.light); el.style.setProperty("--sw-dark", p.dark); } }}
|
|
223
|
+
onClick=${() => pick(`preset:${p.id}`)}></button>`)}
|
|
224
|
+
<label class="idc-custom">${t("color.custom")}
|
|
225
|
+
<input type="color" value=${HEX.test(hex) ? hex : "#808080"} onInput=${(e) => onHex(e.currentTarget.value)} aria-label=${t("color.custom")} />
|
|
226
|
+
<input type="text" class="idc-hex" inputmode="text" spellcheck=${false} maxlength="7" placeholder="#rrggbb" value=${hex} onInput=${(e) => onHex(e.currentTarget.value.trim())} aria-label=${t("color.hex")} /></label>
|
|
227
|
+
<button type="button" class="btn btn-sm" disabled=${!color} onClick=${() => pick(null)}>${t("color.noPick")}</button>
|
|
228
|
+
</fieldset>
|
|
229
|
+
<fieldset class="idc-places"><legend>${t("color.places")}</legend>
|
|
230
|
+
${PLACEMENTS.map((p) => html`<label key=${p} class="check"><input type="checkbox" checked=${preview.show.includes(p)} onChange=${() => togglePlace(p)} /> ${t(`color.place_${p}`)}</label>`)}
|
|
231
|
+
<label class="check"><input type="checkbox" checked=${places === null} onChange=${() => setPlaces(places === null ? [...preview.show] : null)} /> ${t("color.useDefault")}</label>
|
|
232
|
+
</fieldset>
|
|
233
|
+
<div class="idc-preview" aria-hidden="true"><span class=${`inst v-inst ${identityClasses(preview)}`} ref=${(el) => paintIdentity(el, preview)}>
|
|
234
|
+
<span class="av"><span class="av-text">${(name || "?").slice(0, 1).toUpperCase()}</span></span>
|
|
235
|
+
<span class="inst-text"><span class="inst-name">${name}</span></span></span></div>
|
|
236
|
+
${error ? html`<p class="err" role="alert">${error}</p>` : null}
|
|
237
|
+
</${Dialog}>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
register("color", {
|
|
241
|
+
en: {
|
|
242
|
+
menu: "Colour…", title: "Colour for {0}", colour: "Colour", custom: "Custom", hex: "Hex colour", noPick: "Not chosen",
|
|
243
|
+
places: "Where it shows", place_name: "Name", place_bar: "Left bar", place_ring: "Ring", useDefault: "Use this host's default places",
|
|
244
|
+
src_person: "Shown now: picked by a person.", src_agent: "Shown now: the agent's own colour.", src_role: "Shown now: the colour of its bot's Discord role.",
|
|
245
|
+
src_auto: "Shown now: an automatic colour.", save: "Save", cancel: "Cancel", reset: "Reset",
|
|
246
|
+
saved: "Colour saved for {0}", failed: "Couldn't save the colour: {0}",
|
|
247
|
+
offNote: "Colours are turned off on this host (Settings → Web appearance). What you pick here is kept for when they're on.",
|
|
248
|
+
p_red: "Red", p_orange: "Orange", p_amber: "Amber", p_lime: "Lime", p_green: "Green", p_teal: "Teal", p_cyan: "Cyan", p_blue: "Blue",
|
|
249
|
+
p_indigo: "Indigo", p_violet: "Violet", p_pink: "Pink", p_slate: "Slate",
|
|
250
|
+
},
|
|
251
|
+
"zh-TW": {
|
|
252
|
+
menu: "顏色…", title: "{0} 的顏色", colour: "顏色", custom: "自訂", hex: "色碼", noPick: "不指定",
|
|
253
|
+
places: "顯示位置", place_name: "名稱", place_bar: "左側色條", place_ring: "頭像外框", useDefault: "使用這台主機的預設位置",
|
|
254
|
+
src_person: "目前顯示:有人挑選的顏色。", src_agent: "目前顯示:agent 自己的顏色。", src_role: "目前顯示:它的 bot 在 Discord 身分組的顏色。",
|
|
255
|
+
src_auto: "目前顯示:自動配色。", save: "儲存", cancel: "取消", reset: "重設",
|
|
256
|
+
saved: "已儲存 {0} 的顏色", failed: "無法儲存顏色:{0}",
|
|
257
|
+
offNote: "這台主機已關閉顏色(設定 → 網頁外觀)。在這裡挑的顏色會保留,重新開啟後就會出現。",
|
|
258
|
+
p_red: "紅", p_orange: "橙", p_amber: "琥珀", p_lime: "萊姆", p_green: "綠", p_teal: "青綠", p_cyan: "青", p_blue: "藍",
|
|
259
|
+
p_indigo: "靛", p_violet: "紫", p_pink: "粉紅", p_slate: "石板灰",
|
|
260
|
+
},
|
|
261
|
+
});
|
|
@@ -16,6 +16,8 @@ import { Skeleton } from "./ui-states.js";
|
|
|
16
16
|
import { viewStore } from "./view-roster-store.js";
|
|
17
17
|
import { onPrefsChanged, prefInForce, prefsWritable, savePref } from "./app-prefs.js";
|
|
18
18
|
import "./view-strings.js";
|
|
19
|
+
import { themesShown, themesStore } from "./app-themes.js";
|
|
20
|
+
import { identityClasses, identityOf, paintIdentity, useIdentityColors } from "./identity-color.js";
|
|
19
21
|
|
|
20
22
|
/** view.* text with named values. */
|
|
21
23
|
function tn(key, values = {}) {
|
|
@@ -280,9 +282,16 @@ const CLI_MONOGRAM = { "claude-code": "C", codex: "X", "kiro-cli": "K", grok: "G
|
|
|
280
282
|
// One path segment under /ui/img/<kind>/ — no further slash or backslash, and not a dot segment ("." / ".." or their
|
|
281
283
|
// %2e spellings, which a browser resolves too): the picture can only ever be one of those routes (#1642 review).
|
|
282
284
|
const sameOrigin = (src) => typeof src === "string"
|
|
283
|
-
&& /^\/ui\/img\/(bot|avatar|emoji)\/(?!(?:\.|%2[eE]){1,2}(?:\?|$))[^/\\?#\s]+(\?v=[0-9A-Za-z%._-]*)?$/.test(src)
|
|
284
|
-
|
|
285
|
+
&& (/^\/ui\/img\/(bot|avatar|emoji)\/(?!(?:\.|%2[eE]){1,2}(?:\?|$))[^/\\?#\s]+(\?v=[0-9A-Za-z%._-]*)?$/.test(src)
|
|
286
|
+
// #1629: a theme's picture — one name segment, then its 32-hex content id.
|
|
287
|
+
|| /^\/ui\/img\/theme\/(?!(?:\.|%2[eE]){1,2}\/)[^/\\?#\s]+\/[0-9a-f]{32}$/.test(src));
|
|
288
|
+
/**
|
|
289
|
+
* The avatar to draw: the server's, if it is one the page may load; else the first character. #1629: the agent's theme
|
|
290
|
+
* picture first, while this viewer shows agents' themes (fx.show, not safe mode).
|
|
291
|
+
*/
|
|
285
292
|
export function avatarOf(it) {
|
|
293
|
+
const th = it && themesShown() ? it.theme_avatar : null;
|
|
294
|
+
if (th && th.kind === "image" && sameOrigin(th.src)) return { kind: "image", src: th.src };
|
|
286
295
|
const a = it && it.avatar;
|
|
287
296
|
if (a && a.kind === "image" && sameOrigin(a.src)) return { kind: "image", src: a.src };
|
|
288
297
|
if (a && a.kind === "emoji" && sameOrigin(a.src)) return { kind: "emoji", src: a.src, alt: typeof a.alt === "string" ? a.alt : "" };
|
|
@@ -290,7 +299,6 @@ export function avatarOf(it) {
|
|
|
290
299
|
// #1722: the initial is the leading name's ("Names"), not the server's (which is always the nickname's).
|
|
291
300
|
return { kind: "initial", text: firstCharacter(namesOf(it).primary) || (a && a.kind === "initial" && typeof a.text === "string" ? a.text : "") };
|
|
292
301
|
}
|
|
293
|
-
const ACCENT = /^#[0-9a-f]{6}$/i;
|
|
294
302
|
/** The status dot on the avatar: needs you, crashed, paused, stopped, looks stuck, working, idle — colour AND a label. */
|
|
295
303
|
export function dotState(it, exec, awaiting) {
|
|
296
304
|
if (awaiting != null) return "needs";
|
|
@@ -306,22 +314,19 @@ const DOT = {
|
|
|
306
314
|
};
|
|
307
315
|
export function dotLabel(state) { return (DOT[state] || DOT.stopped)[1](); }
|
|
308
316
|
|
|
309
|
-
/**
|
|
317
|
+
/**
|
|
318
|
+
* An instance's avatar with its status dot. #1958: its ring is the identity colour, painted by the row it sits in
|
|
319
|
+
* (identity-color.js: `.idc.id-ring`), for every face.
|
|
320
|
+
*/
|
|
310
321
|
export function InstanceAvatar({ it, state, cli = false }) {
|
|
322
|
+
useStore(themesStore); // #1629: redraw when the viewer turns agents' themes on or off
|
|
311
323
|
const a = avatarOf(it);
|
|
312
324
|
const [broken, setBroken] = useState("");
|
|
313
|
-
const el = useRef(null);
|
|
314
|
-
const accent = typeof it.accent === "string" && ACCENT.test(it.accent) && a.kind === "image" ? it.accent : "";
|
|
315
|
-
useLayoutEffect(() => {
|
|
316
|
-
const node = el.current;
|
|
317
|
-
if (!node || !node.style) return;
|
|
318
|
-
if (accent) node.style.setProperty("--ring", accent); else node.style.removeProperty("--ring");
|
|
319
|
-
}, [accent]);
|
|
320
325
|
const src = a.kind !== "initial" && a.src && broken !== a.src ? a.src : "";
|
|
321
326
|
const text = a.kind === "emoji" && a.text ? a.text : a.kind === "initial" ? a.text : firstCharacter(namesOf(it).primary);
|
|
322
327
|
const [cls] = DOT[state] || DOT.stopped;
|
|
323
328
|
const label = dotLabel(state);
|
|
324
|
-
return html`<span
|
|
329
|
+
return html`<span class=${`av${a.kind === "emoji" ? " av-emoji" : ""}`}>
|
|
325
330
|
${src ? html`<img src=${src} alt="" decoding="async" onError=${() => setBroken(src)} />` : html`<span class="av-text" aria-hidden="true">${text}</span>`}
|
|
326
331
|
<span class=${`av-dot dot ${cls}`} role="img" aria-label=${label} title=${label}></span>${cli
|
|
327
332
|
? html`<span class="av-cli" aria-hidden="true" title=${backendLabel(it.backend)}>${CLI_MONOGRAM[it.backend] || "·"}</span>` : null}</span>`;
|
|
@@ -341,6 +346,7 @@ const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet ===
|
|
|
341
346
|
export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting = {}, viewOnly = false, onPick }) {
|
|
342
347
|
const nav = useStore(navStore);
|
|
343
348
|
const names = useStore(namesStore); // #1722: which name leads
|
|
349
|
+
useIdentityColors(); // #1958: repaint when the host's colour settings or the themes switch change
|
|
344
350
|
const filterRef = useRef(null);
|
|
345
351
|
const list = useRef(null);
|
|
346
352
|
const drag = useRef(null);
|
|
@@ -431,8 +437,10 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
431
437
|
const waiting = own(awaiting, name) ? awaiting[name] : null;
|
|
432
438
|
const on = name === active;
|
|
433
439
|
const fav = g === FAV_GROUP, starred = favSet.has(name);
|
|
440
|
+
// #1958: its identity colour — the ring, a left bar and/or the name, as resolved; painted through the CSSOM.
|
|
441
|
+
const id = identityOf(it);
|
|
434
442
|
const full = !starred && favs.length >= MAX_FAVORITES;
|
|
435
|
-
return html`<li key=${name} class="v-li"><a class=${`inst v-inst${on ? " active" : ""}`} href=${rowPath(name)} title=${instanceTooltip(it)}
|
|
443
|
+
return html`<li key=${name} class="v-li"><a class=${`inst v-inst${on ? " active" : ""} ${identityClasses(id)}`} ref=${(el) => paintIdentity(el, id)} href=${rowPath(name)} title=${instanceTooltip(it)}
|
|
436
444
|
aria-current=${on ? "page" : undefined} onClick=${onPick} ...${fav ? {} : dragProps({ type: "instance", name, group: g })}>
|
|
437
445
|
<${InstanceAvatar} it=${it} state=${dotState(it, own(exec, name) ? exec[name] : null, waiting)} cli=${true} />
|
|
438
446
|
<span class="inst-text"><span class="inst-name" title=${label}>${label}</span>
|
|
@@ -450,7 +458,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
450
458
|
};
|
|
451
459
|
// The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
|
|
452
460
|
return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
|
|
453
|
-
<div class="side-section" id="instanceList" ref=${list}>
|
|
461
|
+
<div data-trust="navigation" class="side-section" id="instanceList" ref=${list}>
|
|
454
462
|
<div class="inst-head"><h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
455
463
|
${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
|
|
456
464
|
${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
|
|
@@ -6,10 +6,13 @@ import { html } from "./app-html.js";
|
|
|
6
6
|
import { t } from "./app-i18n.js";
|
|
7
7
|
import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* current: "chat" | "view" | "log" | "details". #1218: Log sits between View and Details. `data-label`: each tab is as
|
|
11
|
+
* wide as its label in the current tab's weight (app.css), so the bar keeps its size whichever tab is current.
|
|
12
|
+
*/
|
|
10
13
|
export function InstanceSwitch({ name, current }) {
|
|
11
14
|
const seg = (key, href, label) => html`<a key=${key} class=${`seg-item${current === key ? " active" : ""}`} href=${href}
|
|
12
|
-
aria-current=${current === key ? "page" : undefined}>${label}</a>`;
|
|
15
|
+
aria-current=${current === key ? "page" : undefined} data-label=${label}>${label}</a>`;
|
|
13
16
|
return html`<nav class="inst-switch seg" aria-label=${t("app.instanceViews", name)}>
|
|
14
17
|
${seg("chat", chatPath(name), t("app.chat"))}${seg("view", viewPath(name), t("app.view"))}${seg("log", logPath(name), t("app.log"))}${seg("details", detailsPath(name), t("app.details"))}
|
|
15
18
|
</nav>`;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// #1629 r4 §3: owner CSS on this page — the signed-in person's own stylesheet, linked last in <head> so it wins over
|
|
2
|
+
// app.css. It is a same-origin <link> (style-src 'self' admits it): no inline CSS, no nonce, no style attribute and no
|
|
3
|
+
// constructed stylesheet. Never linked:
|
|
4
|
+
// - on Settings (removed while it is open, put back on leaving: the way out is always rendered stock);
|
|
5
|
+
// - on the public link, for the anonymous View reader, and wherever the server named no version (data-owner-css);
|
|
6
|
+
// - on a device that turned it off (agend_owner_css_off), and for a load with ?safe=1 (safe mode).
|
|
7
|
+
// Sign-in, resend-code and setup are other pages: they never load this module.
|
|
8
|
+
import { navStore } from "./app-nav.js";
|
|
9
|
+
import { appStore, createStore, useStore } from "./app-store.js";
|
|
10
|
+
import { html, useEffect, useState } from "./app-html.js";
|
|
11
|
+
import { t } from "./app-i18n.js";
|
|
12
|
+
|
|
13
|
+
export const OWNER_CSS_OFF_KEY = "agend_owner_css_off";
|
|
14
|
+
const LINK_ID = "owner-css";
|
|
15
|
+
|
|
16
|
+
// Read when this module loads, so it holds for whatever asks first (themes start before installOwnerCss runs).
|
|
17
|
+
const readSafe = () => { try { return new URLSearchParams(location.search).get("safe") === "1"; } catch { return false; } };
|
|
18
|
+
let safe = readSafe();
|
|
19
|
+
let active = null;
|
|
20
|
+
let installed = false;
|
|
21
|
+
// B2: admission uses positive observed lifecycle, never active===null as proof of absence.
|
|
22
|
+
const lifeStore = createStore(Object.freeze({ state: "unknown", generation: 0, reason: "unverified" }));
|
|
23
|
+
export const ownerCssLifecycle = Object.freeze({ get: lifeStore.get, subscribe: lifeStore.subscribe });
|
|
24
|
+
let lifeGeneration = 0, linkGeneration = 0, ownedLink = null, linkState = "unknown", sourceState = "unknown";
|
|
25
|
+
function life(state, reason) { lifeStore.set(() => Object.freeze({ state, generation: ++lifeGeneration, reason })); }
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/** This load is in safe mode (?safe=1): no owner CSS — and, as they come, no widgets or effects and default themes. */
|
|
29
|
+
export function safeMode() { return safe; }
|
|
30
|
+
|
|
31
|
+
/** This device turned owner CSS off (a per-device switch: not synced, so a broken phone layout is fixed on the phone). */
|
|
32
|
+
export function ownerCssOffHere() {
|
|
33
|
+
try { return localStorage.getItem(OWNER_CSS_OFF_KEY) === "1"; } catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
export function setOwnerCssOffHere(off) {
|
|
36
|
+
try { if (off) localStorage.setItem(OWNER_CSS_OFF_KEY, "1"); else localStorage.removeItem(OWNER_CSS_OFF_KEY); } catch { /* this page only */ }
|
|
37
|
+
sync();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The person's version in force, as this page knows it (null: none). */
|
|
41
|
+
export function ownerCssActive() { return active; }
|
|
42
|
+
|
|
43
|
+
function eligiblePage() {
|
|
44
|
+
const b = document.body && document.body.dataset;
|
|
45
|
+
return !!b && b.mode === "full" && b.publicLink !== "1";
|
|
46
|
+
}
|
|
47
|
+
function wanted() {
|
|
48
|
+
if (!eligiblePage() || safe || !active || ownerCssOffHere()) return false;
|
|
49
|
+
const r = navStore.get().route;
|
|
50
|
+
return !(r && r.panel === "settings");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Revoke admission before a CSS/route mutation; a missing element alone is never an off verdict. */
|
|
54
|
+
function sync() {
|
|
55
|
+
life("unknown", "changing");
|
|
56
|
+
const el = document.getElementById(LINK_ID);
|
|
57
|
+
if (!wanted()) {
|
|
58
|
+
++linkGeneration;
|
|
59
|
+
if (el) el.remove();
|
|
60
|
+
if (ownedLink && ownedLink !== el) ownedLink.remove();
|
|
61
|
+
ownedLink = null; linkState = "unknown";
|
|
62
|
+
const absent = document.querySelectorAll(`#${LINK_ID}`).length === 0;
|
|
63
|
+
const route = navStore.get().route;
|
|
64
|
+
const disabled = !eligiblePage() || safe || ownerCssOffHere() || route?.panel === "settings";
|
|
65
|
+
life(absent && (disabled || sourceState === "off") ? "off" : "unknown", absent ? disabled ? "disabled_here" : sourceState === "off" ? "host_off" : "unverified" : "removal_failed");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const href = `/ui/owner-css.css?v=${encodeURIComponent(active)}`;
|
|
69
|
+
if (el && el !== ownedLink) { life("unknown", "unowned_link"); return; }
|
|
70
|
+
if (ownedLink && el === ownedLink && el.getAttribute("href") === href) { life(linkState, linkState === "on" ? "loaded" : linkState === "loading" ? "loading" : "load_failed"); return; }
|
|
71
|
+
// A new owned element fences late events from the previous version, including href changes.
|
|
72
|
+
const epoch = ++linkGeneration, sha = active;
|
|
73
|
+
if (ownedLink) ownedLink.remove();
|
|
74
|
+
const link = document.createElement("link"); ownedLink = link; linkState = "loading";
|
|
75
|
+
link.setAttribute("rel", "stylesheet"); link.setAttribute("id", LINK_ID); link.setAttribute("href", href);
|
|
76
|
+
const current = () => ownedLink === link && epoch === linkGeneration && active === sha && wanted()
|
|
77
|
+
&& link.isConnected === true && document.getElementById(LINK_ID) === link && link.getAttribute("href") === href;
|
|
78
|
+
link.addEventListener("load", () => {
|
|
79
|
+
if (!current()) return;
|
|
80
|
+
let valid = false;
|
|
81
|
+
try { valid = link.disabled !== true && !!link.sheet && link.sheet.ownerNode === link && link.sheet.disabled !== true && link.sheet.href === new URL(href, location.href).href; } catch { /* unknown stylesheet */ }
|
|
82
|
+
linkState = valid ? "on" : "unknown"; life(linkState, valid ? "loaded" : "load_unverified");
|
|
83
|
+
});
|
|
84
|
+
link.addEventListener("error", () => { if (!current()) return; linkState = "unknown"; life("unknown", "load_failed"); });
|
|
85
|
+
life("loading", "loading");
|
|
86
|
+
document.head.appendChild(link); // last in head; no style text, inline attribute or constructed stylesheet
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// #1883 r2: every change this page makes, and every read it starts, moves `gen`. A host answer is taken only if nothing
|
|
90
|
+
// newer happened after it was asked for — an older /state answer never overwrites a newer read, save, keep or revert.
|
|
91
|
+
let gen = 0;
|
|
92
|
+
const local = () => ++gen;
|
|
93
|
+
|
|
94
|
+
/** This page saved, restored or turned off a version: show it at once (null: none). */
|
|
95
|
+
export function setOwnerCssActive(sha) {
|
|
96
|
+
local();
|
|
97
|
+
showActive(sha, sha === null ? "off" : "on");
|
|
98
|
+
}
|
|
99
|
+
function showActive(sha, evidence = "unknown") {
|
|
100
|
+
active = typeof sha === "string" && /^[0-9a-f]{32}$/.test(sha) ? sha : null;
|
|
101
|
+
sourceState = evidence === "off" && sha === null ? "off" : evidence === "on" && active ? "on" : "unknown";
|
|
102
|
+
sync();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Keep or revert (r4 §3): a saved version is on trial. "Keep" within 15 s keeps it; anything else reverts it — the
|
|
106
|
+
// page that saved it at 15 s, and the host at its own deadline (a closed tab, or CSS that hid this bar). The bar is the
|
|
107
|
+
// shell's, on every page and every device of the person's, so they can look at the result before answering.
|
|
108
|
+
// A trial has an id of its own (the same text saved twice is two trials): keep and revert name it, and the host acts
|
|
109
|
+
// only while that trial still is the one on trial — one kept, reverted or replaced meanwhile, anywhere, stays.
|
|
110
|
+
export const KEEP_SECONDS = 15;
|
|
111
|
+
/** The host's whole trial budget (20 s) and a second: the longest a page that did not save a trial waits to ask again. */
|
|
112
|
+
const HOST_TRIAL_SECONDS = 21;
|
|
113
|
+
/** { id, sha, previous, deadline (performance.now ms), local } while a version is on trial, else null. */
|
|
114
|
+
export const ownerCssTrial = createStore(null);
|
|
115
|
+
let trialTimer = null;
|
|
116
|
+
// set(fn) replaces the state; set(object) would merge into it (createStore), and null merges as nothing.
|
|
117
|
+
const dropTrial = () => { if (trialTimer) clearTimeout(trialTimer); trialTimer = null; ownerCssTrial.set(() => null); };
|
|
118
|
+
|
|
119
|
+
async function post(path, body) {
|
|
120
|
+
const res = await fetch(path, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
121
|
+
let json = null;
|
|
122
|
+
try { json = await res.json(); } catch { /* none */ }
|
|
123
|
+
return { ok: res.ok, status: res.status, body: json };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Trial `id` (of version `sha`) is in force: show it, and ask. `local`: this page saved it — at the end of `seconds` it
|
|
128
|
+
* reverts it (by its id). Otherwise (another device's trial, or this page loaded during one) `seconds` is the host's
|
|
129
|
+
* own remaining time (never the device's clock), and at its end the page asks the host again, which has settled it.
|
|
130
|
+
*/
|
|
131
|
+
export function startOwnerCssTrial(id, sha, previous, seconds = KEEP_SECONDS, isLocal = true) {
|
|
132
|
+
local();
|
|
133
|
+
if (trialTimer) clearTimeout(trialTimer);
|
|
134
|
+
showActive(sha, "on");
|
|
135
|
+
const s = Math.max(0, Math.min(isLocal ? KEEP_SECONDS : HOST_TRIAL_SECONDS, Number.isFinite(seconds) ? seconds : 0));
|
|
136
|
+
const trial = { id, sha, previous: previous || null, deadline: performance.now() + s * 1000, local: isLocal };
|
|
137
|
+
ownerCssTrial.set(() => trial);
|
|
138
|
+
trialTimer = setTimeout(() => {
|
|
139
|
+
if (ownerCssTrial.get() !== trial) return;
|
|
140
|
+
if (isLocal) revertOwnerCss(); else { dropTrial(); reread(); }
|
|
141
|
+
}, s * 1000);
|
|
142
|
+
}
|
|
143
|
+
export async function keepOwnerCss() {
|
|
144
|
+
const trial = ownerCssTrial.get();
|
|
145
|
+
if (!trial) return false;
|
|
146
|
+
local();
|
|
147
|
+
dropTrial();
|
|
148
|
+
const r = await post("/ui/owner-css/keep", { trial: trial.id });
|
|
149
|
+
if (!r.ok) { await reread(); return false; } // too late: the host reverted it — show what is in force
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
export async function revertOwnerCss() {
|
|
153
|
+
const trial = ownerCssTrial.get();
|
|
154
|
+
if (!trial) return;
|
|
155
|
+
local();
|
|
156
|
+
dropTrial();
|
|
157
|
+
showActive(trial.previous); // at once; the host's answer below is what stays
|
|
158
|
+
await post("/ui/owner-css/revert", { trial: trial.id }).catch(() => null);
|
|
159
|
+
await reread();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The shell's bar: "Keep this CSS?", the seconds left, Keep and Revert. */
|
|
163
|
+
export function OwnerCssKeepBar() {
|
|
164
|
+
const trial = useStore(ownerCssTrial);
|
|
165
|
+
const [, tick] = useState(0);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!trial) return undefined;
|
|
168
|
+
const id = setInterval(() => tick((n) => n + 1), 500);
|
|
169
|
+
return () => clearInterval(id);
|
|
170
|
+
}, [trial]);
|
|
171
|
+
if (!trial) return null;
|
|
172
|
+
const left = Math.max(0, Math.ceil((trial.deadline - performance.now()) / 1000));
|
|
173
|
+
return html`<div class="oc-keep" role="alertdialog" aria-live="assertive" aria-label=${t("app.ownerCssKeep")} data-trust="keep">
|
|
174
|
+
<span class="oc-keep-text"><span class="oc-keep-q">${t("app.ownerCssKeep")}</span><span class="oc-keep-left">${t("app.ownerCssKeepLeft", left)}</span></span>
|
|
175
|
+
<button type="button" class="btn btn-primary btn-sm" onClick=${() => { keepOwnerCss(); }}>${t("app.ownerCssKeepYes")}</button>
|
|
176
|
+
<button type="button" class="btn btn-sm" onClick=${() => { revertOwnerCss(); }}>${t("app.ownerCssKeepNo")}</button>
|
|
177
|
+
</div>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Ask the host which version is in force (another device changed it, or a trial ended), and whether one is on trial. */
|
|
181
|
+
async function reread() {
|
|
182
|
+
if (!eligiblePage()) return;
|
|
183
|
+
const mine = local();
|
|
184
|
+
sourceState = "unknown"; life("unknown", "refreshing");
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetch("/ui/owner-css/state", { credentials: "same-origin" });
|
|
187
|
+
if (!res.ok) return;
|
|
188
|
+
const body = await res.json();
|
|
189
|
+
if (body && mine === gen) reconcile(body); // nothing newer happened since this was asked
|
|
190
|
+
} catch { /* the next change tries again */ }
|
|
191
|
+
}
|
|
192
|
+
/** Match the host: the version in force, and whether one is on trial (another device's included) — or none any more. */
|
|
193
|
+
function reconcile(state) {
|
|
194
|
+
const pending = state.pending && state.pending.sha === state.active && typeof state.pending.id === "string" ? state.pending : null;
|
|
195
|
+
const trial = ownerCssTrial.get();
|
|
196
|
+
if (!pending) { if (trial) dropTrial(); showActive(state.active, state.effectState); return; }
|
|
197
|
+
if (trial && trial.id === pending.id) return; // already asking about this one
|
|
198
|
+
// The host's own remaining time and a second (never this device's clock): it has settled the trial by then.
|
|
199
|
+
const remaining = typeof pending.remaining_ms === "number" && Number.isFinite(pending.remaining_ms) ? pending.remaining_ms : 0;
|
|
200
|
+
startOwnerCssTrial(pending.id, pending.sha, pending.previous, remaining / 1000 + 1, false);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Once per page load, before the first panel paints. */
|
|
204
|
+
export function installOwnerCss() {
|
|
205
|
+
if (installed) return;
|
|
206
|
+
installed = true;
|
|
207
|
+
safe = readSafe();
|
|
208
|
+
const b = document.body && document.body.dataset;
|
|
209
|
+
showActive(b?.ownerCss ?? null, b?.ownerCssState);
|
|
210
|
+
navStore.subscribe(sync);
|
|
211
|
+
// What the host says now: a trial in progress (this page reloaded during one, or another device's) shows the bar;
|
|
212
|
+
// one the host has settled since the page was served shows its outcome.
|
|
213
|
+
if (eligiblePage()) reread();
|
|
214
|
+
let rev = appStore.get().prefsRev;
|
|
215
|
+
appStore.subscribe((s) => {
|
|
216
|
+
if (s.prefsRev === rev) return;
|
|
217
|
+
const first = rev === null || rev === undefined;
|
|
218
|
+
rev = s.prefsRev;
|
|
219
|
+
if (!first) reread();
|
|
220
|
+
});
|
|
221
|
+
}
|