@songsid/agend 2.2.0 → 2.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +25 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +35 -0
- package/dist/agent-file-read.js +144 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +8 -0
- 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.js +17 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.js +3 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.js +5 -1
- 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 +8 -0
- package/dist/backend/types.js +10 -0
- package/dist/backend/types.js.map +1 -1
- 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-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 +15 -0
- package/dist/cache-service.js +26 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +17 -2
- 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 +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +15 -0
- package/dist/cli.js +7 -3
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +22 -2
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +244 -20
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-manager.d.ts +92 -0
- package/dist/fleet-manager.js +410 -29
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +7 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +367 -0
- package/dist/instance-avatar.js +3 -0
- 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/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 +7 -0
- package/dist/outbound-handlers.js +65 -10
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +74 -0
- package/dist/outbound-schemas.js +56 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +14 -0
- package/dist/public-web-gateway.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/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.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/tool-permissions.d.ts +5 -0
- package/dist/tool-permissions.js +61 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +10 -6
- package/dist/topic-commands.js +21 -17
- 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 +13 -0
- package/dist/ui/app-effects.js +187 -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 +4 -4
- package/dist/ui/chat-store.js +9 -3
- package/dist/ui/chat-strings.js +15 -0
- package/dist/ui/chat-thread.js +47 -21
- package/dist/ui/chat-tour.js +1 -1
- 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/fleet-cache.js +23 -1
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +85 -17
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +3 -3
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +3 -3
- package/dist/ui/panel-settings.js +8 -5
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +245 -0
- 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 +2 -0
- 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 +12 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +225 -3
- 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 +1 -1
- 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 +37 -19
- 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 +81 -8
- package/dist/ui/shared/signin.js +2 -0
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +2 -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 +14 -2
- package/dist/ui/shared/view-strings.js +36 -2
- package/dist/ui/signin.html +1 -0
- package/dist/usage/providers.d.ts +3 -0
- package/dist/usage/providers.js +4 -3
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +188 -8
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +42 -0
- package/dist/web-api.js +898 -36
- 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 +93 -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 +13 -0
- package/dist/web-chat-history.js +23 -2
- package/dist/web-chat-history.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 +314 -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 +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-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-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +205 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +149 -0
- package/dist/web-mod-bundle.js +470 -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 +625 -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 +48 -0
- package/dist/web-prefs.js +209 -33
- 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-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-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/package.json +1 -1
- package/templates/systemd.service.ejs +8 -0
|
@@ -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 = {}) {
|
|
@@ -222,6 +224,8 @@ function writeFacetsOpen(open) { try { localStorage.setItem(FACETS_KEY, JSON.str
|
|
|
222
224
|
|
|
223
225
|
/** The Favorites group's key among the tag groups: no tag can be it (a group name is never a control character). */
|
|
224
226
|
const FAV_GROUP = "\u0000favorites";
|
|
227
|
+
/** #1936: the open instance, pinned on top while a filter is on. */
|
|
228
|
+
const PINNED_GROUP = "\u0000pinned";
|
|
225
229
|
function persistOrder(groupNames, instByGroup) {
|
|
226
230
|
const rows = [];
|
|
227
231
|
groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
|
|
@@ -280,9 +284,16 @@ const CLI_MONOGRAM = { "claude-code": "C", codex: "X", "kiro-cli": "K", grok: "G
|
|
|
280
284
|
// One path segment under /ui/img/<kind>/ — no further slash or backslash, and not a dot segment ("." / ".." or their
|
|
281
285
|
// %2e spellings, which a browser resolves too): the picture can only ever be one of those routes (#1642 review).
|
|
282
286
|
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
|
-
|
|
287
|
+
&& (/^\/ui\/img\/(bot|avatar|emoji)\/(?!(?:\.|%2[eE]){1,2}(?:\?|$))[^/\\?#\s]+(\?v=[0-9A-Za-z%._-]*)?$/.test(src)
|
|
288
|
+
// #1629: a theme's picture — one name segment, then its 32-hex content id.
|
|
289
|
+
|| /^\/ui\/img\/theme\/(?!(?:\.|%2[eE]){1,2}\/)[^/\\?#\s]+\/[0-9a-f]{32}$/.test(src));
|
|
290
|
+
/**
|
|
291
|
+
* The avatar to draw: the server's, if it is one the page may load; else the first character. #1629: the agent's theme
|
|
292
|
+
* picture first, while this viewer shows agents' themes (fx.show, not safe mode).
|
|
293
|
+
*/
|
|
285
294
|
export function avatarOf(it) {
|
|
295
|
+
const th = it && themesShown() ? it.theme_avatar : null;
|
|
296
|
+
if (th && th.kind === "image" && sameOrigin(th.src)) return { kind: "image", src: th.src };
|
|
286
297
|
const a = it && it.avatar;
|
|
287
298
|
if (a && a.kind === "image" && sameOrigin(a.src)) return { kind: "image", src: a.src };
|
|
288
299
|
if (a && a.kind === "emoji" && sameOrigin(a.src)) return { kind: "emoji", src: a.src, alt: typeof a.alt === "string" ? a.alt : "" };
|
|
@@ -290,7 +301,6 @@ export function avatarOf(it) {
|
|
|
290
301
|
// #1722: the initial is the leading name's ("Names"), not the server's (which is always the nickname's).
|
|
291
302
|
return { kind: "initial", text: firstCharacter(namesOf(it).primary) || (a && a.kind === "initial" && typeof a.text === "string" ? a.text : "") };
|
|
292
303
|
}
|
|
293
|
-
const ACCENT = /^#[0-9a-f]{6}$/i;
|
|
294
304
|
/** The status dot on the avatar: needs you, crashed, paused, stopped, looks stuck, working, idle — colour AND a label. */
|
|
295
305
|
export function dotState(it, exec, awaiting) {
|
|
296
306
|
if (awaiting != null) return "needs";
|
|
@@ -306,22 +316,19 @@ const DOT = {
|
|
|
306
316
|
};
|
|
307
317
|
export function dotLabel(state) { return (DOT[state] || DOT.stopped)[1](); }
|
|
308
318
|
|
|
309
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* An instance's avatar with its status dot. #1958: its ring is the identity colour, painted by the row it sits in
|
|
321
|
+
* (identity-color.js: `.idc.id-ring`), for every face.
|
|
322
|
+
*/
|
|
310
323
|
export function InstanceAvatar({ it, state, cli = false }) {
|
|
324
|
+
useStore(themesStore); // #1629: redraw when the viewer turns agents' themes on or off
|
|
311
325
|
const a = avatarOf(it);
|
|
312
326
|
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
327
|
const src = a.kind !== "initial" && a.src && broken !== a.src ? a.src : "";
|
|
321
328
|
const text = a.kind === "emoji" && a.text ? a.text : a.kind === "initial" ? a.text : firstCharacter(namesOf(it).primary);
|
|
322
329
|
const [cls] = DOT[state] || DOT.stopped;
|
|
323
330
|
const label = dotLabel(state);
|
|
324
|
-
return html`<span
|
|
331
|
+
return html`<span class=${`av${a.kind === "emoji" ? " av-emoji" : ""}`}>
|
|
325
332
|
${src ? html`<img src=${src} alt="" decoding="async" onError=${() => setBroken(src)} />` : html`<span class="av-text" aria-hidden="true">${text}</span>`}
|
|
326
333
|
<span class=${`av-dot dot ${cls}`} role="img" aria-label=${label} title=${label}></span>${cli
|
|
327
334
|
? html`<span class="av-cli" aria-hidden="true" title=${backendLabel(it.backend)}>${CLI_MONOGRAM[it.backend] || "·"}</span>` : null}</span>`;
|
|
@@ -341,6 +348,7 @@ const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet ===
|
|
|
341
348
|
export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting = {}, viewOnly = false, onPick }) {
|
|
342
349
|
const nav = useStore(navStore);
|
|
343
350
|
const names = useStore(namesStore); // #1722: which name leads
|
|
351
|
+
useIdentityColors(); // #1958: repaint when the host's colour settings or the themes switch change
|
|
344
352
|
const filterRef = useRef(null);
|
|
345
353
|
const list = useRef(null);
|
|
346
354
|
const drag = useRef(null);
|
|
@@ -350,13 +358,12 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
350
358
|
// #1664: "☆ Edit" — the stars on every row (on a phone the only way to them) and ↑ ↓ on the favorites — only on the
|
|
351
359
|
// full list, as drag: in a filtered one the row above may not be the one above in the list (#1677 review).
|
|
352
360
|
const [editing, setEditing] = useState(false);
|
|
353
|
-
// Where a row goes: the view the page is in (#1523 §3.1). View → View; Details,
|
|
354
|
-
//
|
|
361
|
+
// Where a row goes: the view the page is in (#1523 §3.1). View → View; Details → Details; everywhere else, a Fleet tab
|
|
362
|
+
// included (#1917: the user's call, replacing Q4 = A), → Chat. The anonymous reader only ever has View.
|
|
355
363
|
const rowPath = viewOnly || (route && route.panel === "view") ? viewPath
|
|
356
364
|
: route && route.panel === "log" ? logPath // #1218: Log → Log
|
|
357
|
-
: route &&
|
|
365
|
+
: route && route.panel === "details" ? detailsPath : chatPath;
|
|
358
366
|
const active = route && (route.panel === "chat" || route.panel === "view" || route.panel === "log" || route.panel === "details") ? route.instance : null;
|
|
359
|
-
useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded]);
|
|
360
367
|
const own = (map, name) => Object.prototype.hasOwnProperty.call(map, name);
|
|
361
368
|
const byName = new Map(items.map((it) => [nameOf(it), it]));
|
|
362
369
|
const facetOf = (name) => statusFacet(byName.get(name), own(exec, name) ? exec[name] : null, own(awaiting, name) ? awaiting[name] : null);
|
|
@@ -373,6 +380,12 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
373
380
|
if (favs.length) shownInsts.set(FAV_GROUP, favs);
|
|
374
381
|
const f = nav.filter;
|
|
375
382
|
const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
|
|
383
|
+
// #1936: while a filter is on, the instance you have open stays on top, pinned and marked "Current", whether or not
|
|
384
|
+
// it matches; a match is not repeated below. "N / M" still counts the filter's matches (the pinned row counts only
|
|
385
|
+
// when it is one). Clearing the filter puts it back in its place, and the list scrolls to it again.
|
|
386
|
+
const pinned = shown.active && active && byName.has(active) ? active : null;
|
|
387
|
+
const listed = pinned ? shown.groups.map((x) => ({ ...x, names: x.names.filter((n) => n !== pinned) })).filter((x) => x.names.length) : shown.groups;
|
|
388
|
+
useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded, shown.active]);
|
|
376
389
|
const backends = [...new Set(items.map((it) => it.backend).filter((b) => typeof b === "string" && b))].sort((a, b) => backendLabel(a).localeCompare(backendLabel(b)));
|
|
377
390
|
const toggle = (g) => { const c = new Set(navStore.get().collapsed); if (c.has(g)) c.delete(g); else c.add(g); setCollapsed(c); };
|
|
378
391
|
// From the store, not this render's copy: two clicks before the next render must both count.
|
|
@@ -431,8 +444,10 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
431
444
|
const waiting = own(awaiting, name) ? awaiting[name] : null;
|
|
432
445
|
const on = name === active;
|
|
433
446
|
const fav = g === FAV_GROUP, starred = favSet.has(name);
|
|
447
|
+
// #1958: its identity colour — the ring, a left bar and/or the name, as resolved; painted through the CSSOM.
|
|
448
|
+
const id = identityOf(it);
|
|
434
449
|
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)}
|
|
450
|
+
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
451
|
aria-current=${on ? "page" : undefined} onClick=${onPick} ...${fav ? {} : dragProps({ type: "instance", name, group: g })}>
|
|
437
452
|
<${InstanceAvatar} it=${it} state=${dotState(it, own(exec, name) ? exec[name] : null, waiting)} cli=${true} />
|
|
438
453
|
<span class="inst-text"><span class="inst-name" title=${label}>${label}</span>
|
|
@@ -450,12 +465,15 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
450
465
|
};
|
|
451
466
|
// The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
|
|
452
467
|
return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
|
|
453
|
-
<div class="side-section" id="instanceList" ref=${list}>
|
|
468
|
+
<div data-trust="navigation" class="side-section" id="instanceList" ref=${list}>
|
|
454
469
|
<div class="inst-head"><h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
455
470
|
${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
|
|
456
471
|
${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
|
|
457
472
|
${!loaded ? html`<${Skeleton} lines=${4} />` : !items.length ? html`<p class="side-empty">${t("app.noInstances")}</p>` : null}
|
|
458
|
-
${
|
|
473
|
+
${pinned ? html`<div key=${PINNED_GROUP} class="v-group v-pinned" role="group" aria-label=${tn("pinnedLabel")}>
|
|
474
|
+
<div class="v-group-head pinned-head"><span class="grow">${tn("pinnedCurrent")}</span></div>
|
|
475
|
+
<ul class="inst-list">${row(pinned, PINNED_GROUP, 0, 1)}</ul></div>` : null}
|
|
476
|
+
${listed.map(({ group: g, names }) => {
|
|
459
477
|
// #1664: Favorites — always open, not dragged (↑ ↓ in Edit), named in the page's language.
|
|
460
478
|
if (g === FAV_GROUP) return html`<div key=${g} class="v-group v-fav" role="group" aria-label=${tn("favorites")}>
|
|
461
479
|
<div class="v-group-head fav-head"><${Icon} name="star" size=${14} cls="fav-mark" /><span class="grow">${tn("favorites")}</span><span class="count">${names.length}</span></div>
|
|
@@ -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
|
+
}
|