@songsid/agend 2.2.0 → 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/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 +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 +13 -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/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/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 +34 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +15 -0
- package/dist/config-validator.js +17 -1
- 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 +89 -0
- package/dist/fleet-manager.js +401 -25
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -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 +5 -0
- package/dist/outbound-handlers.js +30 -9
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +64 -0
- package/dist/outbound-schemas.js +50 -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 +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.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.js +50 -0
- 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/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 +214 -1
- 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 +22 -14
- 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/view-strings.js +30 -0
- package/dist/ui/signin.html +1 -0
- 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 +896 -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 +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 +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 +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-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 +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
|
@@ -33,13 +33,32 @@ export const SYNCED = {
|
|
|
33
33
|
"nav.favorites": { ls: "agend_favorites", ...json }, // #1664
|
|
34
34
|
"preview.optIn": { ls: "agend_html_preview", toLocal: (v) => (v ? "on" : null), fromLocal: (s) => (s === "on" ? true : undefined) },
|
|
35
35
|
"names.primary": { ls: "agend_names_primary", toLocal: (v) => (v === "instance" ? v : null), fromLocal: (s) => (s === "instance" ? s : undefined) }, // #1722
|
|
36
|
+
// #1629 r2 (d): "Show agents' themes and effects" — on unless turned off (only "off" is cached).
|
|
37
|
+
"fx.show": { ls: "agend_fx_show", toLocal: (v) => (v === false ? "off" : null), fromLocal: (s) => (s === "off" ? false : undefined) },
|
|
38
|
+
// #1629 r4.1 Q3 (a): the identity card's size — every card's default (M unless chosen), and the cards set apart.
|
|
39
|
+
"card.size": { ls: "agend_card_size", toLocal: (v) => (v === "s" || v === "l" ? v : null), fromLocal: (s) => (s === "s" || s === "l" ? s : undefined) },
|
|
40
|
+
"card.sizeByInstance": { ls: "agend_card_sizes", ...json },
|
|
41
|
+
// #1629 r4-4: the person's own sidebar widget ({ spec, data }), drawn by the page's own components.
|
|
42
|
+
"widget.sidebar": { ls: "agend_widget_sidebar", ...json },
|
|
43
|
+
// #1958: where identity colours show by default (ring + bar unless chosen), and the host's switch to turn them off.
|
|
44
|
+
"color.show": { ls: "agend_color_show", ...json },
|
|
45
|
+
"color.off": { ls: "agend_color_off", toLocal: (v) => (v === true ? "on" : null), fromLocal: (s) => (s === "on" ? true : undefined) },
|
|
36
46
|
};
|
|
47
|
+
// #1629: fixed personal scope. A separate session-bound owner-effect store synchronizes these, never this host cache.
|
|
48
|
+
export const OWNER_EFFECT_PREF_KEYS = Object.freeze(["fx.sidebar", "fx.header", "fx.page", "fx.box", "fx.cursor"]);
|
|
49
|
+
for (const spec of Object.values(SYNCED)) spec.scope = "host";
|
|
50
|
+
for (const key of OWNER_EFFECT_PREF_KEYS) SYNCED[key] = Object.freeze({ scope: "person", ls: null });
|
|
51
|
+
for (const spec of Object.values(SYNCED)) Object.freeze(spec);
|
|
52
|
+
Object.freeze(SYNCED);
|
|
53
|
+
const HOST_KEYS = Object.keys(SYNCED).filter(key => SYNCED[key].scope === "host");
|
|
54
|
+
const hostKey = key => own(SYNCED, key) && SYNCED[key].scope === "host";
|
|
55
|
+
const hostValues = values => Object.fromEntries(HOST_KEYS.filter(key => own(values, key)).map(key => [key, values[key]]));
|
|
37
56
|
const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
38
57
|
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
39
58
|
|
|
40
59
|
function readLocal(key) { try { return localStorage.getItem(key); } catch { return null; } }
|
|
41
60
|
function writeLocal(key, s) { try { if (s == null) localStorage.removeItem(key); else localStorage.setItem(key, s); } catch { /* this page only */ } }
|
|
42
|
-
function cache(key, value) { const k = SYNCED[key]; writeLocal(k.ls, value == null ? null : k.toLocal(value)); }
|
|
61
|
+
function cache(key, value) { if (!hostKey(key)) return; const k = SYNCED[key]; writeLocal(k.ls, value == null ? null : k.toLocal(value)); }
|
|
43
62
|
const inStep = () => readLocal(SYNCED_FLAG) === "1";
|
|
44
63
|
|
|
45
64
|
function pageHost() {
|
|
@@ -51,7 +70,7 @@ function pageHost() {
|
|
|
51
70
|
|
|
52
71
|
const initial = typeof document !== "undefined" ? pageHost() : null;
|
|
53
72
|
let rev = initial ? initial.rev : null;
|
|
54
|
-
let host = initial ? initial.values : null;
|
|
73
|
+
let host = initial ? hostValues(initial.values) : null;
|
|
55
74
|
const pending = new Map(); // key → value chosen here, not sent yet
|
|
56
75
|
const mine = new Map(); // key → value chosen here whose write is not over (pending or out)
|
|
57
76
|
const inflight = new Map(); // key → writes sent and not answered yet
|
|
@@ -63,7 +82,7 @@ const listeners = new Set();
|
|
|
63
82
|
// the host does not have: the default, on a device in step with the host; this device's own (to upload) otherwise.
|
|
64
83
|
if (host) {
|
|
65
84
|
const step = inStep();
|
|
66
|
-
for (const key of
|
|
85
|
+
for (const key of HOST_KEYS) {
|
|
67
86
|
if (own(host, key)) cache(key, host[key]);
|
|
68
87
|
else if (step) cache(key, null);
|
|
69
88
|
}
|
|
@@ -80,14 +99,14 @@ export function prefsWritable() {
|
|
|
80
99
|
* in step) — the caller reads this browser's copy.
|
|
81
100
|
*/
|
|
82
101
|
export function prefInForce(key) {
|
|
83
|
-
if (!
|
|
102
|
+
if (!hostKey(key)) return null;
|
|
84
103
|
if (mine.has(key)) return { value: mine.get(key) };
|
|
85
104
|
if (host && own(host, key)) return { value: host[key] };
|
|
86
105
|
if (initial && inStep()) return { value: undefined };
|
|
87
106
|
return null;
|
|
88
107
|
}
|
|
89
108
|
/** The host's value for a key as this page last saw it (undefined: not set there, or not known). */
|
|
90
|
-
export function hostPref(key) { return host && own(host, key) ? host[key] : undefined; }
|
|
109
|
+
export function hostPref(key) { return hostKey(key) && host && own(host, key) ? host[key] : undefined; }
|
|
91
110
|
/** Called with the keys another device changed, after they are cached and applied here. Returns the unsubscribe. */
|
|
92
111
|
export function onPrefsChanged(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
|
93
112
|
|
|
@@ -97,7 +116,7 @@ const isView = (body) => !!body && typeof body.rev === "number" && !!body.values
|
|
|
97
116
|
function take(body) {
|
|
98
117
|
if (!isView(body) || (rev !== null && body.rev < rev)) return false;
|
|
99
118
|
rev = body.rev;
|
|
100
|
-
host = body.values;
|
|
119
|
+
host = hostValues(body.values);
|
|
101
120
|
return true;
|
|
102
121
|
}
|
|
103
122
|
|
|
@@ -106,7 +125,7 @@ function take(body) {
|
|
|
106
125
|
* (the public link, the anonymous reader): this browser only, as before.
|
|
107
126
|
*/
|
|
108
127
|
export function savePref(key, value) {
|
|
109
|
-
if (!
|
|
128
|
+
if (!hostKey(key)) return;
|
|
110
129
|
cache(key, value);
|
|
111
130
|
if (!prefsWritable()) return;
|
|
112
131
|
pending.set(key, value);
|
|
@@ -165,8 +184,8 @@ export async function refresh() {
|
|
|
165
184
|
body = res.ok ? await res.json() : null;
|
|
166
185
|
} catch { body = null; }
|
|
167
186
|
if (!isView(body)) return false;
|
|
168
|
-
const before = new Map(
|
|
169
|
-
if (take(body)) align(
|
|
187
|
+
const before = new Map(HOST_KEYS.map((key) => [key, own(host, key) ? SYNCED[key].toLocal(host[key]) : null]));
|
|
188
|
+
if (take(body)) align(HOST_KEYS, { before });
|
|
170
189
|
return true;
|
|
171
190
|
}
|
|
172
191
|
|
|
@@ -179,6 +198,7 @@ export async function refresh() {
|
|
|
179
198
|
function align(keys, opts = {}) {
|
|
180
199
|
const changed = new Set();
|
|
181
200
|
for (const key of keys) {
|
|
201
|
+
if (!hostKey(key)) continue;
|
|
182
202
|
if (pending.has(key) || inflight.has(key)) continue;
|
|
183
203
|
const k = SYNCED[key];
|
|
184
204
|
const want = own(host, key) ? k.toLocal(host[key]) : null;
|
|
@@ -216,7 +236,7 @@ function apply(changed) {
|
|
|
216
236
|
*/
|
|
217
237
|
export async function migratePrefs() {
|
|
218
238
|
if (!prefsWritable() || inStep()) return false;
|
|
219
|
-
for (const key of
|
|
239
|
+
for (const key of HOST_KEYS) {
|
|
220
240
|
if (own(host, key)) continue;
|
|
221
241
|
const local = SYNCED[key].fromLocal(readLocal(SYNCED[key].ls));
|
|
222
242
|
if (local === undefined) continue;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// a test runs both over one list of cases). Pure: no DOM, no history.
|
|
3
3
|
|
|
4
4
|
export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "config"];
|
|
5
|
-
export const SETTINGS_SECTIONS = ["device", "agents", "bots", "classic", "general", "web", "advanced"]; // #1604: device first
|
|
5
|
+
export const SETTINGS_SECTIONS = ["device", "appearance", "agents", "bots", "classic", "general", "web", "advanced"]; // #1604: device first
|
|
6
6
|
const MAX_INSTANCE_NAME = 128;
|
|
7
7
|
const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
|
|
8
8
|
|
|
@@ -62,6 +62,28 @@ export function routePath(route) {
|
|
|
62
62
|
if (route.panel === "log") return logPath(route.instance);
|
|
63
63
|
return route.instance ? chatPath(route.instance) : "/ui";
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* #1990: a text with the in-app links the server put on it (ranges of the text: { offset, length, url }) as segments —
|
|
67
|
+
* { text } or { text, href }. Only a route this table parses as an instance's chat or View becomes a link (its href
|
|
68
|
+
* rebuilt here); a range out of order, out of the text, or anything else stays text. No target: the router opens it.
|
|
69
|
+
*/
|
|
70
|
+
export function textLinkSegments(text, links) {
|
|
71
|
+
const s = String(text == null ? "" : text);
|
|
72
|
+
const out = [];
|
|
73
|
+
let at = 0;
|
|
74
|
+
for (const l of Array.isArray(links) ? links.slice(0, 16) : []) {
|
|
75
|
+
if (!l || !Number.isInteger(l.offset) || !Number.isInteger(l.length) || typeof l.url !== "string") continue;
|
|
76
|
+
if (l.offset < at || l.length < 1 || l.offset + l.length > s.length || !/^\/[^?#\s]*$/.test(l.url)) continue;
|
|
77
|
+
const route = parseRoute(l.url);
|
|
78
|
+
if (!route || !route.instance || (route.panel !== "chat" && route.panel !== "view")) continue;
|
|
79
|
+
if (l.offset > at) out.push({ text: s.slice(at, l.offset) });
|
|
80
|
+
out.push({ text: s.slice(l.offset, l.offset + l.length), href: routePath(route) });
|
|
81
|
+
at = l.offset + l.length;
|
|
82
|
+
}
|
|
83
|
+
if (at < s.length || !out.length) out.push({ text: s.slice(at) });
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
/** One string per route: a change of it is a new navigation (a new lease for the panel, §4). */
|
|
66
88
|
export function routeKey(route) {
|
|
67
89
|
if (!route) return "none";
|
|
@@ -49,7 +49,7 @@ export function SessionMenu() {
|
|
|
49
49
|
<button ref=${btn} type="button" class="side-row" aria-haspopup="dialog" aria-expanded=${open ? "true" : "false"}
|
|
50
50
|
onClick=${() => { const next = !open; setOpen(next); if (next) refresh(); }}>
|
|
51
51
|
<span class=${`dot${me ? " ok" : ""}`} aria-hidden="true"></span><span class="grow">${label}</span>${me ? html`<span class="meta">${me.label}</span>` : null}</button>
|
|
52
|
-
${open ? html`<div ref=${pop} class="pop" role="dialog" aria-label=${t("app.session")} onKeyDown=${onKey}>
|
|
52
|
+
${open ? html`<div data-trust="session" ref=${pop} class="pop" role="dialog" aria-label=${t("app.session")} onKeyDown=${onKey}>
|
|
53
53
|
${!me ? html`<h3>${t("app.signedOut")}</h3><a class="btn" href=${`/signin?next=${encodeURIComponent(location.pathname)}`}>${t("app.signIn")}</a>` : html`
|
|
54
54
|
<h3>${t("app.signedIn")} · ${me.label}</h3>
|
|
55
55
|
<p class="sub">${t("app.expires", clock(me.expiresAt), span(me.idleMs))}</p>
|
|
@@ -5,6 +5,7 @@ import { html, useEffect, useLayoutEffect, useRef, useState } from "./app-html.j
|
|
|
5
5
|
import { Icon } from "./ui-icons.js";
|
|
6
6
|
import { Toasts } from "./ui-toast.js";
|
|
7
7
|
import { ConfirmHost } from "./ui-confirm.js";
|
|
8
|
+
import { ColorDialogHost } from "./identity-color.js";
|
|
8
9
|
import { t, lang, setLang, onLang } from "./app-i18n.js";
|
|
9
10
|
import { appStore, createStore, useStore } from "./app-store.js";
|
|
10
11
|
import { navStore } from "./app-nav.js";
|
|
@@ -14,6 +15,8 @@ import { savePref } from "./app-prefs.js";
|
|
|
14
15
|
import { ErrorState, Skeleton } from "./ui-states.js";
|
|
15
16
|
import { InstanceNav, RosterNav, namesStore, setNamesPrimary } from "./instance-nav.js";
|
|
16
17
|
import { UsageRail, usageRailStore } from "./header-tools.js";
|
|
18
|
+
import { OwnerCssKeepBar } from "./owner-css.js";
|
|
19
|
+
import { AgentWidget, fleetBinds, sidebarWidget, sidebarWidgetStore } from "./agent-widget.js";
|
|
17
20
|
|
|
18
21
|
/** Shell state shared with panels: the phone drawer, the collapsed sidebar, extensions a panel module adds. */
|
|
19
22
|
export const shellStore = createStore({ drawer: false, collapsed: readCollapsed(), keyboard: false, dialog: null, side: null });
|
|
@@ -175,7 +178,7 @@ function SidebarResize() {
|
|
|
175
178
|
handle.addEventListener("pointercancel", cancel);
|
|
176
179
|
handle.addEventListener("lostpointercapture", cancel);
|
|
177
180
|
};
|
|
178
|
-
return html`<div class="side-resize" role="separator" aria-orientation="vertical" tabindex="0" aria-controls="sidebar"
|
|
181
|
+
return html`<div data-trust="navigation" class="side-resize" role="separator" aria-orientation="vertical" tabindex="0" aria-controls="sidebar"
|
|
179
182
|
aria-label=${t("app.resizeSidebar")} title=${t("app.resizeSidebarHint")} aria-valuemin=${String(SIDEBAR_W.min)}
|
|
180
183
|
aria-valuemax=${String(clampSidebarWidth(Infinity, mainBudget()))} aria-valuenow=${String(now)}
|
|
181
184
|
onKeyDown=${onKey} onPointerDown=${onDown} ondblclick=${() => setSidebarWidth(null)}></div>`;
|
|
@@ -255,13 +258,13 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
255
258
|
// View-only (an anonymous reader, #1408 §3): View and a way to sign in — nothing that needs a session is rendered.
|
|
256
259
|
if (viewOnly) {
|
|
257
260
|
return html`<aside id="sidebar" class="sidebar" aria-label=${t("app.menu")}>
|
|
258
|
-
<div class="side-head">
|
|
261
|
+
<div class="side-head" data-trust="navigation">
|
|
259
262
|
<a class="brand" href="/view" onClick=${closeDrawer}>${t("app.brand")}</a>
|
|
260
263
|
<button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
|
|
261
264
|
</div>
|
|
262
|
-
<nav class="side-nav" aria-label=${t("app.menu")}>${navLink("view", "/view", "view", t("app.view"))}</nav>
|
|
265
|
+
<nav data-trust="navigation" class="side-nav" aria-label=${t("app.menu")}>${navLink("view", "/view", "view", t("app.view"))}</nav>
|
|
263
266
|
${shell.side ? html`<${shell.side.Component} />` : html`<${ViewOnlyNav} route=${route} />`}
|
|
264
|
-
<div class="side-foot">
|
|
267
|
+
<div class="side-foot" data-trust="navigation">
|
|
265
268
|
<a class="side-row" href=${signInHref()}><${Icon} name="user" /><span>${t("app.signIn")}</span></a>
|
|
266
269
|
<${PrefsPopover} />
|
|
267
270
|
</div>
|
|
@@ -274,12 +277,12 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
274
277
|
const overview = html`<a class=${`side-row side-overview${onFleet ? " active" : ""}`} href=${fleetPath("org")}
|
|
275
278
|
aria-current=${onFleet ? "page" : undefined} onClick=${closeDrawer}><${Icon} name="org" /><span class="grow">${t("app.fleetOverview")}</span></a>`;
|
|
276
279
|
return html`<aside id="sidebar" class="sidebar" aria-label=${t("app.menu")}>
|
|
277
|
-
<div class="side-head">
|
|
280
|
+
<div class="side-head" data-trust="navigation">
|
|
278
281
|
<a class="brand" href="/ui" onClick=${closeDrawer}>${t("app.brand")}</a>
|
|
279
282
|
<button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
|
|
280
283
|
<button type="button" class="icon-btn" onClick=${onNewInstance} aria-label=${t("app.newInstance")} title=${t("app.newInstance")}><${Icon} name="plus" /></button>
|
|
281
284
|
</div>
|
|
282
|
-
<nav class="side-nav" aria-label=${t("app.menu")}>
|
|
285
|
+
<nav data-trust="navigation" class="side-nav" aria-label=${t("app.menu")}>
|
|
283
286
|
${overview}
|
|
284
287
|
${navLink("needs", NEEDS_PATH, "inbox", t("app.needsNav"), needsCount)}
|
|
285
288
|
${navLink("fleet", openInstance ? detailsPath(openInstance) : "/ui/fleet", "fleet", t("app.fleet"))}
|
|
@@ -287,7 +290,8 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
287
290
|
</nav>
|
|
288
291
|
${shell.side ? html`<${shell.side.Component} />` : html`<${InstanceNav} route=${route} items=${app.instances} loaded=${app.ready !== false}
|
|
289
292
|
exec=${app.exec} awaiting=${app.awaiting} onPick=${closeDrawer} />`}
|
|
290
|
-
|
|
293
|
+
<${SidebarWidget} app=${app} />
|
|
294
|
+
<div class="side-foot" data-trust="navigation">
|
|
291
295
|
${navLink("settings", settingsPath(), "settings", t("app.settings"))}
|
|
292
296
|
<${SessionMenu} />
|
|
293
297
|
</div>
|
|
@@ -295,6 +299,13 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
295
299
|
</aside>`;
|
|
296
300
|
}
|
|
297
301
|
|
|
302
|
+
/** #1629 r4-4: the person's own sidebar widget (their synced preference), with the fleet's counts. */
|
|
303
|
+
function SidebarWidget({ app }) {
|
|
304
|
+
useStore(sidebarWidgetStore);
|
|
305
|
+
const widget = sidebarWidget();
|
|
306
|
+
return widget ? html`<div class="side-widget" data-trust="widget"><${AgentWidget} slot="sidebar" widget=${widget} binds=${fleetBinds(app.instances, app.exec, app.awaiting)} /></div>` : null;
|
|
307
|
+
}
|
|
308
|
+
|
|
298
309
|
/** The anonymous View reader's list: View's roster read (no status frames without a session), the same component. */
|
|
299
310
|
function ViewOnlyNav({ route }) {
|
|
300
311
|
return html`<${RosterNav} route=${route} viewOnly=${true} onPick=${closeDrawer} />`;
|
|
@@ -328,7 +339,7 @@ export function DevicePrefs({ labels = false } = {}) {
|
|
|
328
339
|
function PrefsPopover() {
|
|
329
340
|
return html`<details class="prefs-pop">
|
|
330
341
|
<summary class="icon-btn" aria-label=${t("app.devicePrefs")} title=${t("app.devicePrefs")}><${Icon} name="sun" /></summary>
|
|
331
|
-
<div class="pop" role="group" aria-label=${t("app.devicePrefs")}><${DevicePrefs} labels=${true} /></div>
|
|
342
|
+
<div data-trust="popover" class="pop" role="group" aria-label=${t("app.devicePrefs")}><${DevicePrefs} labels=${true} /></div>
|
|
332
343
|
</details>`;
|
|
333
344
|
}
|
|
334
345
|
|
|
@@ -338,7 +349,7 @@ function PrefsPopover() {
|
|
|
338
349
|
*/
|
|
339
350
|
export function PanelHeader({ title, sub, children, headingRef, nav }) {
|
|
340
351
|
const shell = useStore(shellStore);
|
|
341
|
-
return html`<header class=${`panel-head${shell.collapsed ? " collapsed" : ""}`}>
|
|
352
|
+
return html`<header data-trust="header" class=${`panel-head${shell.collapsed ? " collapsed" : ""}`}>
|
|
342
353
|
<button type="button" id="sbOpen" class="icon-btn sb-open" onClick=${() => (narrow() ? openDrawer() : toggleSidebar())}
|
|
343
354
|
aria-label=${narrow() ? t("app.openMenu") : t("app.expand")} title=${narrow() ? t("app.openMenu") : t("app.expand")} aria-controls="sidebar" aria-expanded=${shell.drawer ? "true" : "false"}><${Icon} name="menu" /></button>
|
|
344
355
|
<div class="panel-title"><h1 ref=${headingRef} tabindex="-1">${title}</h1>${sub ? html`<div class="panel-sub">${sub}</div>` : null}</div>
|
|
@@ -368,12 +379,12 @@ function BottomTabs({ route, viewOnly }) {
|
|
|
368
379
|
try { lastView = localStorage.getItem("agend_last_view"); } catch { /* none */ }
|
|
369
380
|
const viewHref = route && (route.panel === "view" || route.panel === "log" || route.panel === "details") && route.instance ? viewPath(route.instance) : viewPath(lastView);
|
|
370
381
|
if (viewOnly) {
|
|
371
|
-
return html`<nav class="tabs" aria-label=${t("app.menu")}>
|
|
382
|
+
return html`<nav data-trust="navigation" class="tabs" aria-label=${t("app.menu")}>
|
|
372
383
|
${tab(viewHref, "view", t("app.view"), route && route.panel === "view")}
|
|
373
384
|
${tab(signInHref(), "user", t("app.signIn"), false)}
|
|
374
385
|
</nav>`;
|
|
375
386
|
}
|
|
376
|
-
return html`<nav class="tabs" aria-label=${t("app.menu")}>
|
|
387
|
+
return html`<nav data-trust="navigation" class="tabs" aria-label=${t("app.menu")}>
|
|
377
388
|
${tab(chatHref, "chat", t("app.chat"), route && route.panel === "chat", anyAwaiting)}
|
|
378
389
|
${tab(NEEDS_PATH, "inbox", t("app.needsTab"), route && route.panel === "needs", (app.needs || []).length, t("app.needsCount", (app.needs || []).length))}
|
|
379
390
|
${tab(open ? detailsPath(open) : "/ui/fleet", "fleet", t("app.fleet"), route && (route.panel === "fleet" || route.panel === "details"))}
|
|
@@ -386,7 +397,7 @@ function ConnectionLine() {
|
|
|
386
397
|
const { connection, hydration, retryHydration } = useStore(appStore);
|
|
387
398
|
// A late catch-up that is not done: the chat may be missing what was open before it loaded (#1425 review).
|
|
388
399
|
if (hydration === "retrying" || hydration === "failed") {
|
|
389
|
-
return html`<div class="conn" role="status">${hydration === "failed" ? t("app.hydrateFailed") : t("app.hydrating")}
|
|
400
|
+
return html`<div data-trust="status" class="conn" role="status">${hydration === "failed" ? t("app.hydrateFailed") : t("app.hydrating")}
|
|
390
401
|
${hydration === "failed" && retryHydration ? html` <button type="button" class="btn btn-sm" onClick=${() => retryHydration()}>${t("app.retry")}</button>` : null}</div>`;
|
|
391
402
|
}
|
|
392
403
|
if (connection === "reconnecting") return html`<${Reconnecting} />`;
|
|
@@ -400,7 +411,7 @@ function Reconnecting() {
|
|
|
400
411
|
const { retryAt } = useStore(appStore);
|
|
401
412
|
useTick(true);
|
|
402
413
|
const s = typeof retryAt === "number" ? Math.max(0, Math.ceil((retryAt - NOW()) / 1000)) : 0;
|
|
403
|
-
return html`<div class="conn reconnecting" role="status">${s > 0 ? t("app.connReconnectingIn", s) : t("app.connReconnecting")}</div>`;
|
|
414
|
+
return html`<div data-trust="status" class="conn reconnecting" role="status">${s > 0 ? t("app.connReconnectingIn", s) : t("app.connReconnecting")}</div>`;
|
|
404
415
|
}
|
|
405
416
|
|
|
406
417
|
const NOW = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
@@ -425,7 +436,7 @@ export function PendingChanges({ inline = false } = {}) {
|
|
|
425
436
|
const list = pendingChanges || [];
|
|
426
437
|
useTick(list.some(p => p.state === "pending"));
|
|
427
438
|
if (!list.length) return null;
|
|
428
|
-
return html`<div class=${`pending-stack${inline ? " inline" : ""}`} role="region" aria-label=${t("app.pendingTitle")}>${list.map(p => html`<section key=${p.id} class=${`pending-card ${p.state}`} role="status">
|
|
439
|
+
return html`<div data-trust="confirmation" class=${`pending-stack${inline ? " inline" : ""}`} role="region" aria-label=${t("app.pendingTitle")}>${list.map(p => html`<section key=${p.id} class=${`pending-card ${p.state}`} role="status">
|
|
429
440
|
<div class="pending-head"><${Icon} name=${p.state === "applied" ? "check" : p.state === "pending" || p.state === "applying" ? "clock" : "alert"} size=${16} />
|
|
430
441
|
<strong>${t(`app.pending_${p.state}`)}</strong>
|
|
431
442
|
${p.state === "pending" ? html`<span class="pending-left">${t("app.pendingLeft", mmss(p.deadline - NOW()))}</span>` : null}
|
|
@@ -450,7 +461,7 @@ function OperationLine({ route }) {
|
|
|
450
461
|
: op.phase === "posting" || op.phase === "watching" ? t("app.opApplying")
|
|
451
462
|
: op.phase === "failed" ? t("app.opFailed") : restart ? t("app.opRestart") : failedRows ? t("app.opPartial") : t("app.opDone");
|
|
452
463
|
const busy = op.phase === "writing" || op.phase === "posting" || op.phase === "watching";
|
|
453
|
-
return html`<div class=${`conn op-line${busy ? "" : op.phase === "failed" || restart || failedRows ? " bad" : " ok"}`} role="status">
|
|
464
|
+
return html`<div data-trust="status" class=${`conn op-line${busy ? "" : op.phase === "failed" || restart || failedRows ? " bad" : " ok"}`} role="status">
|
|
454
465
|
<span>${text}</span> <a class="btn btn-sm" href=${settingsPath()}>${t("app.openSettings")}</a></div>`;
|
|
455
466
|
}
|
|
456
467
|
|
|
@@ -499,7 +510,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
499
510
|
}, []);
|
|
500
511
|
const cls = ["shell", shell.collapsed ? "sb-collapsed" : "", shell.drawer ? "sb-open" : "", shell.keyboard ? "kb-open" : ""].filter(Boolean).join(" ");
|
|
501
512
|
return html`<div class=${cls}>
|
|
502
|
-
<a class="skip" href="#main">${t("app.skip")}</a>
|
|
513
|
+
<a data-trust="navigation" class="skip" href="#main">${t("app.skip")}</a>
|
|
503
514
|
<${Sidebar} route=${nav.route} onNewInstance=${onNewInstance} viewOnly=${viewOnly} />
|
|
504
515
|
<div class="scrim" onClick=${closeDrawer} aria-hidden="true"></div>
|
|
505
516
|
<main id="main" class="main" tabindex="-1" inert=${shell.drawer && narrow() ? true : undefined}>
|
|
@@ -512,6 +523,8 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
512
523
|
${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
|
|
513
524
|
${viewOnly || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
|
|
514
525
|
<${ConfirmHost} />
|
|
526
|
+
<${ColorDialogHost} />
|
|
527
|
+
${viewOnly ? null : html`<${OwnerCssKeepBar} />`}
|
|
515
528
|
<${Toasts} />
|
|
516
529
|
</div>`;
|
|
517
530
|
}
|
|
@@ -39,6 +39,8 @@ export function applyStatus(d) {
|
|
|
39
39
|
connections: typeof d.connections === "number" ? d.connections : null,
|
|
40
40
|
// #1665: the host preferences' revision (app-prefs.js re-reads them when it moves).
|
|
41
41
|
prefsRev: typeof d.prefs_rev === "number" ? d.prefs_rev : null,
|
|
42
|
+
// #1629: the agents' themes' revision (app-themes.js re-reads them when it moves).
|
|
43
|
+
themesRev: typeof d.themes_rev === "number" ? d.themes_rev : null,
|
|
42
44
|
// #1685: host memory pressure slowing or holding new agents ("elevated" | "critical"), else null.
|
|
43
45
|
hostMemory: d.host_memory === "elevated" || d.host_memory === "critical" ? d.host_memory : null });
|
|
44
46
|
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// - suspend()/resume(): the page is leaving (pagehide) / came back from the back-forward cache.
|
|
31
31
|
|
|
32
32
|
// #1218: steps_tick — an instance's transcript moved while a Log watches it (SSE only: a poll page asks the Log itself).
|
|
33
|
-
const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "steps_tick", "clock"];
|
|
33
|
+
const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "steps_tick", "clock"];
|
|
34
34
|
const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
|
|
35
35
|
const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
|
|
36
36
|
const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
|
|
@@ -68,17 +68,20 @@ export function createStream(opts) {
|
|
|
68
68
|
const set = listeners.get(name);
|
|
69
69
|
if (set) for (const fn of [...set]) { try { fn(data, extra); } catch (e) { if (env.console) env.console.error(e); } }
|
|
70
70
|
}
|
|
71
|
+
function sseIngress() {
|
|
72
|
+
try { return Object.freeze({ source: "sse", receivedAt: now() }); } catch { return undefined; }
|
|
73
|
+
}
|
|
71
74
|
function setConnection(c) { if (state.connection !== c) { state.connection = c; emit("connection", c); } }
|
|
72
75
|
function setHydration(h) { if (state.hydration !== h) { state.hydration = h; emit("hydration", h); } }
|
|
73
76
|
|
|
74
77
|
/** A live event: delivered now, or held (in order) while a catch-up snapshot is on its way. */
|
|
75
|
-
function live(name, data, cursor) {
|
|
78
|
+
function live(name, data, cursor, extra) {
|
|
76
79
|
if (held) {
|
|
77
|
-
if (held.length < MAX_HELD) held.push([name, data, cursor]); else held.overflow = true;
|
|
80
|
+
if (held.length < MAX_HELD) held.push([name, data, cursor, extra]); else held.overflow = true;
|
|
78
81
|
return;
|
|
79
82
|
}
|
|
80
83
|
if (cursor) state.cursor = cursor;
|
|
81
|
-
emit(name, data);
|
|
84
|
+
emit(name, data, extra);
|
|
82
85
|
// A full set of open prompts arrives on every (re)connect: whatever a failed catch-up missed is current again.
|
|
83
86
|
if (name === "prompts" && (state.hydration === "failed" || state.hydration === "retrying") && !catching) setHydration("ok");
|
|
84
87
|
}
|
|
@@ -86,7 +89,7 @@ export function createStream(opts) {
|
|
|
86
89
|
const list = held;
|
|
87
90
|
held = null;
|
|
88
91
|
if (!list) return false;
|
|
89
|
-
for (const [name, data, cursor] of list) { if (cursor) state.cursor = cursor; emit(name, data); }
|
|
92
|
+
for (const [name, data, cursor, extra] of list) { if (cursor) state.cursor = cursor; emit(name, data, extra); }
|
|
90
93
|
return !!list.overflow;
|
|
91
94
|
}
|
|
92
95
|
|
|
@@ -116,7 +119,7 @@ export function createStream(opts) {
|
|
|
116
119
|
if (closed || mine !== pollSeq) return false;
|
|
117
120
|
if (typeof d.now === "number") emit("clock", { now: d.now }); // #1823: read just now, handed over at once
|
|
118
121
|
if (d.status) emit("status", d.status);
|
|
119
|
-
for (const m of d.messages || []) emit("message", m);
|
|
122
|
+
for (const m of d.messages || []) emit("message", m, { source: "poll" });
|
|
120
123
|
if (typeof d.cursor === "string") state.cursor = d.cursor;
|
|
121
124
|
if (Array.isArray(d.deliveries)) emit("deliveries", d.deliveries);
|
|
122
125
|
if (Array.isArray(d.prompts)) emit("prompts", d.prompts);
|
|
@@ -206,13 +209,15 @@ export function createStream(opts) {
|
|
|
206
209
|
for (const name of EVENTS) {
|
|
207
210
|
es.addEventListener(name, (e) => {
|
|
208
211
|
if (es !== sse) return; // a stream this page already let go of
|
|
212
|
+
// Snapshot before parsing or any synchronous connection/hydration observer can consume time.
|
|
213
|
+
const ingress = name === "message" || name === "agent_reply_live" ? sseIngress() : undefined;
|
|
209
214
|
let data;
|
|
210
215
|
try { data = JSON.parse(e.data); } catch { return; }
|
|
211
216
|
if (!spoke) { spoke = true; alive(); if (again) catchUp(); }
|
|
212
217
|
else alive();
|
|
213
218
|
// #1823: a clock reading is about now, not about order: it is never held behind a catch-up (it would be stale).
|
|
214
219
|
if (name === "clock") { emit(name, data); return; }
|
|
215
|
-
live(name, data, name === "message" ? e.lastEventId : null);
|
|
220
|
+
live(name, data, name === "message" ? e.lastEventId : null, ingress);
|
|
216
221
|
});
|
|
217
222
|
}
|
|
218
223
|
es.onerror = () => {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// #1629 (first design §5, r2 (b)(d)): agents' web themes on the page. Each applies to THAT agent's own chat surface
|
|
2
|
+
// (and its avatar ring) only — never the shell, the sidebar chrome or another instance. What the host sends is
|
|
3
|
+
// canonical already; this checks it again (a value that fails is dropped, never passed on) and applies it through the
|
|
4
|
+
// CSSOM (custom properties) and fixed class tokens. No style attribute, no CSS text, no url() built in CSS.
|
|
5
|
+
// Not shown: with "Show agents' themes, effects and widgets" off (fx.show, synced), and in safe mode (?safe=1).
|
|
6
|
+
import { appStore, createStore } from "./app-store.js";
|
|
7
|
+
import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
|
|
8
|
+
import { safeMode } from "./owner-css.js";
|
|
9
|
+
import { t } from "./app-i18n.js";
|
|
10
|
+
import { toast } from "./ui-toast.js";
|
|
11
|
+
import { confirmDialog } from "./ui-confirm.js";
|
|
12
|
+
|
|
13
|
+
/** { rev, themes: { <name>: theme }, locked: [<name>], widgets: { <name>: { <slot>: widget } } } as GET /ui/themes said (rev -1: not read yet). */
|
|
14
|
+
export const themesStore = createStore({ rev: -1, themes: {}, locked: [], widgets: {}, shown: true });
|
|
15
|
+
const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
16
|
+
|
|
17
|
+
const COLOR = /^#[0-9a-f]{6}$/;
|
|
18
|
+
const ASSET = /^[0-9a-f]{32}$/;
|
|
19
|
+
const PALETTE = { bg: "--bg", surface: "--surface", text: "--text", text2: "--text-2", accent: "--accent", userBubble: "--user-bubble", agentBubble: "--agent-bubble" };
|
|
20
|
+
const FONTS = ["inter", "system", "mono"], BORDERS = ["none", "thin", "accent"], FITS = ["cover", "contain"];
|
|
21
|
+
// The label's rule, as the host's (web-theme.ts canonicalText): 1..24 graphemes on one line, no control, format or
|
|
22
|
+
// separator character but the zero-width joiner, and at least one character that draws something (#1888 review).
|
|
23
|
+
const FORBIDDEN = /(?!\u200d)[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
24
|
+
const VISIBLE = /[^\p{M}\p{C}\p{Z}\u115f\u1160\u3164\uffa0\u2800]/u;
|
|
25
|
+
let graphemes = null;
|
|
26
|
+
function labelOk(s) {
|
|
27
|
+
if (typeof s !== "string" || FORBIDDEN.test(s) || s.trim() !== s || !VISIBLE.test(s)) return false;
|
|
28
|
+
try { graphemes ??= new Intl.Segmenter(undefined, { granularity: "grapheme" }); } catch { return false; }
|
|
29
|
+
const n = [...graphemes.segment(s)].length;
|
|
30
|
+
return n >= 1 && n <= 24;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Public sessions cannot write host prefs. Keep their explicit switch apart from boot-cached host defaults.
|
|
34
|
+
const PUBLIC_FX_SHOW = "agend_public_fx_show";
|
|
35
|
+
let publicFxChoice = (() => { try { const v = localStorage.getItem(PUBLIC_FX_SHOW); return v === "on" ? true : v === "off" ? false : null; } catch { return null; } })();
|
|
36
|
+
|
|
37
|
+
/** Whether this page shows agents' themes now: the viewer's switch (default on), and never in safe mode. */
|
|
38
|
+
export function themesShown() {
|
|
39
|
+
if (safeMode()) return false;
|
|
40
|
+
if (publicLink() && publicFxChoice !== null) return publicFxChoice;
|
|
41
|
+
try {
|
|
42
|
+
const f = prefInForce("fx.show"); // this page's choice, else the host's, else the default
|
|
43
|
+
if (f) return f.value !== false;
|
|
44
|
+
return localStorage.getItem("agend_fx_show") !== "off"; // not known here (the public link): this browser's copy
|
|
45
|
+
} catch { return true; }
|
|
46
|
+
}
|
|
47
|
+
/** The viewer's switch (Settings → Appearance): kept with the person's preferences, and in force on this page at once. */
|
|
48
|
+
export function setThemesShown(on) {
|
|
49
|
+
if (publicLink()) {
|
|
50
|
+
publicFxChoice = !!on;
|
|
51
|
+
try { localStorage.setItem(PUBLIC_FX_SHOW, on ? "on" : "off"); } catch { /* the in-memory choice still applies */ }
|
|
52
|
+
} else savePref("fx.show", on ? null : false);
|
|
53
|
+
themesStore.set({ shown: themesShown() });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The mode the page is drawn in: the chosen one, or the system's for "system". */
|
|
57
|
+
export function resolvedMode() {
|
|
58
|
+
const T = globalThis.AgendTheme;
|
|
59
|
+
const chosen = T && typeof T.get === "function" ? T.get() : "system";
|
|
60
|
+
if (chosen === "light" || chosen === "dark") return chosen;
|
|
61
|
+
try { return globalThis.matchMedia && globalThis.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"; } catch { return "dark"; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The theme to draw for `name`, re-checked; null when none (or not shown). */
|
|
65
|
+
export function themeFor(name) {
|
|
66
|
+
const s = themesStore.get();
|
|
67
|
+
if (!s.shown || !themesShown() || !own(s.themes, name)) return null;
|
|
68
|
+
const t = s.themes[name];
|
|
69
|
+
if (!t || typeof t !== "object") return null;
|
|
70
|
+
const out = {};
|
|
71
|
+
const palette = (p) => { const o = {}; if (p && typeof p === "object") for (const k of Object.keys(PALETTE)) if (typeof p[k] === "string" && COLOR.test(p[k])) o[k] = p[k]; return o; };
|
|
72
|
+
if (t.colors && typeof t.colors === "object") out.colors = { light: palette(t.colors.light), dark: palette(t.colors.dark) };
|
|
73
|
+
if (typeof t.accent === "string" && COLOR.test(t.accent)) out.accent = t.accent;
|
|
74
|
+
if (t.bubble && typeof t.bubble === "object") {
|
|
75
|
+
out.bubble = {};
|
|
76
|
+
if (Number.isInteger(t.bubble.radius) && t.bubble.radius >= 0 && t.bubble.radius <= 24) out.bubble.radius = t.bubble.radius;
|
|
77
|
+
if (BORDERS.includes(t.bubble.border)) out.bubble.border = t.bubble.border;
|
|
78
|
+
}
|
|
79
|
+
if (FONTS.includes(t.font)) out.font = t.font;
|
|
80
|
+
const bg = t.background;
|
|
81
|
+
if (bg && typeof bg.image === "string" && ASSET.test(bg.image) && FITS.includes(bg.fit) && typeof bg.opacity === "number" && bg.opacity >= 0.05 && bg.opacity <= 1) {
|
|
82
|
+
out.background = { image: bg.image, fit: bg.fit, opacity: bg.opacity };
|
|
83
|
+
}
|
|
84
|
+
if (labelOk(t.workingLabel)) out.workingLabel = t.workingLabel;
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isThemeLocked(name) { return (themesStore.get().locked || []).includes(name); }
|
|
89
|
+
|
|
90
|
+
const TOKENS = ["th", "th-font-inter", "th-font-system", "th-font-mono", "th-bubble-none", "th-bubble-thin", "th-bubble-accent", "th-agent-bubble"];
|
|
91
|
+
/**
|
|
92
|
+
* Draw `theme` (from themeFor) on one agent's chat root `el` for `mode`; null clears it. Colours are custom properties
|
|
93
|
+
* set through the CSSOM; choices are class tokens from the fixed list above.
|
|
94
|
+
*/
|
|
95
|
+
export function applyTheme(el, theme, mode) {
|
|
96
|
+
if (!el || !el.style) return;
|
|
97
|
+
for (const prop of [...Object.values(PALETTE), "--bubble-radius"]) el.style.removeProperty(prop);
|
|
98
|
+
for (const c of TOKENS) el.classList.remove(c);
|
|
99
|
+
if (!theme) return;
|
|
100
|
+
const pal = { ...(theme.accent ? { accent: theme.accent } : {}), ...((theme.colors && theme.colors[mode]) || {}) };
|
|
101
|
+
for (const [k, prop] of Object.entries(PALETTE)) if (typeof pal[k] === "string" && COLOR.test(pal[k])) el.style.setProperty(prop, pal[k]);
|
|
102
|
+
el.classList.add("th");
|
|
103
|
+
if (theme.font) el.classList.add(`th-font-${theme.font}`);
|
|
104
|
+
if (theme.bubble && theme.bubble.border) el.classList.add(`th-bubble-${theme.bubble.border}`);
|
|
105
|
+
if (theme.bubble && Number.isInteger(theme.bubble.radius)) el.style.setProperty("--bubble-radius", `${theme.bubble.radius}px`);
|
|
106
|
+
if (typeof pal.agentBubble === "string") el.classList.add("th-agent-bubble");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The theme picture's same-origin URL (the id is its content hash). */
|
|
110
|
+
export function themeImageUrl(name, id) { return `/ui/img/theme/${encodeURIComponent(name)}/${id}`; }
|
|
111
|
+
|
|
112
|
+
let reading = false, wanted = -1, started = false;
|
|
113
|
+
async function read(rev) {
|
|
114
|
+
wanted = rev;
|
|
115
|
+
if (reading) return;
|
|
116
|
+
reading = true;
|
|
117
|
+
try {
|
|
118
|
+
while (themesStore.get().rev !== wanted) {
|
|
119
|
+
const want = wanted;
|
|
120
|
+
let body = null;
|
|
121
|
+
try { const r = await fetch("/ui/themes", { credentials: "same-origin" }); body = r.ok ? await r.json() : null; } catch { body = null; }
|
|
122
|
+
if (!body || typeof body !== "object") break;
|
|
123
|
+
themesStore.set({ rev: typeof body.rev === "number" ? body.rev : want, themes: body.themes && typeof body.themes === "object" ? body.themes : {},
|
|
124
|
+
locked: Array.isArray(body.locked) ? body.locked.filter((n) => typeof n === "string") : [],
|
|
125
|
+
widgets: body.widgets && typeof body.widgets === "object" && !Array.isArray(body.widgets) ? body.widgets : {} });
|
|
126
|
+
if (want === wanted) break;
|
|
127
|
+
}
|
|
128
|
+
} finally { reading = false; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Once per page load (the full app): read the themes when the status frame's themes_rev moves; follow fx.show. */
|
|
132
|
+
export function startThemes() {
|
|
133
|
+
if (started) return;
|
|
134
|
+
started = true;
|
|
135
|
+
themesStore.set({ shown: themesShown() });
|
|
136
|
+
let last = null;
|
|
137
|
+
appStore.subscribe((s) => {
|
|
138
|
+
const rev = typeof s.themesRev === "number" ? s.themesRev : null;
|
|
139
|
+
if (rev === null || rev === last) return;
|
|
140
|
+
last = rev;
|
|
141
|
+
read(rev);
|
|
142
|
+
});
|
|
143
|
+
onPrefsChanged((changed) => { if (changed.has("fx.show")) themesStore.set({ shown: themesShown() }); });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const publicLink = () => typeof document !== "undefined" && !!document.body && document.body.dataset.publicLink === "1";
|
|
147
|
+
|
|
148
|
+
/** A person's Reset or Lock of an agent's theme (an admin session; never over the public link). */
|
|
149
|
+
async function themeAction(name, what, body) {
|
|
150
|
+
let r;
|
|
151
|
+
try {
|
|
152
|
+
const res = await fetch(`/ui/themes/${encodeURIComponent(name)}/${what}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body || {}) });
|
|
153
|
+
r = await res.json().catch(() => ({}));
|
|
154
|
+
if (!res.ok) r = { error: r && r.error ? r.error : `HTTP ${res.status}` };
|
|
155
|
+
} catch (err) { r = { error: err && err.message ? err.message : "network" }; }
|
|
156
|
+
if (r && r.error) toast(t("chat.themeFailed", r.error), false);
|
|
157
|
+
else toast(t(what === "reset" ? "chat.themeResetDone" : body && body.locked ? "chat.themeLocked" : "chat.themeUnlocked", name));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A person's removal of an agent's widget from one slot (r4-4). */
|
|
161
|
+
async function removeWidget(name, slot) {
|
|
162
|
+
let r;
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch(`/ui/themes/${encodeURIComponent(name)}/widget`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ slot }) });
|
|
165
|
+
r = await res.json().catch(() => ({}));
|
|
166
|
+
if (!res.ok) r = { error: r && r.error ? r.error : `HTTP ${res.status}` };
|
|
167
|
+
} catch (err) { r = { error: err && err.message ? err.message : "network" }; }
|
|
168
|
+
if (r && r.error) toast(t("chat.themeFailed", r.error), false);
|
|
169
|
+
else toast(t("chat.widgetRemoved", name));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The chat's and Details' ⋯ entries for an agent's theme: Lock/Unlock, Reset… and its widgets' Remove — none over the public link. */
|
|
173
|
+
export function themeMenuItems(name) {
|
|
174
|
+
if (publicLink()) return [];
|
|
175
|
+
const locked = isThemeLocked(name);
|
|
176
|
+
const w = themesStore.get().widgets;
|
|
177
|
+
const placed = w && own(w, name) && w[name] && typeof w[name] === "object" ? w[name] : {};
|
|
178
|
+
return [
|
|
179
|
+
...["chat_header", "details"].filter((slot) => own(placed, slot)).map((slot) => ({
|
|
180
|
+
key: `widget-remove-${slot}`, label: t(slot === "chat_header" ? "chat.widgetRemoveHeader" : "chat.widgetRemoveDetails"), icon: "trash",
|
|
181
|
+
onSelect: () => removeWidget(name, slot),
|
|
182
|
+
})),
|
|
183
|
+
{ key: "theme-lock", label: t(locked ? "chat.themeUnlock" : "chat.themeLock"), icon: "key", onSelect: () => themeAction(name, "lock", { locked: !locked }) },
|
|
184
|
+
{ key: "theme-reset", label: t("chat.themeReset"), icon: "restart", onSelect: async () => {
|
|
185
|
+
if (await confirmDialog({ message: t("chat.themeResetConfirm", name), confirmLabel: t("chat.themeResetGo"), danger: true })) themeAction(name, "reset");
|
|
186
|
+
} },
|
|
187
|
+
];
|
|
188
|
+
}
|