@songsid/agend 2.3.0-beta.5 → 2.3.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-cli.js +21 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +4 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.js +68 -8
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-whats-new.js +4 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.js +3 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +38 -0
- package/dist/authority/authority-state.js +114 -0
- package/dist/authority/authority-state.js.map +1 -0
- package/dist/authority/session-authorizations.d.ts +43 -0
- package/dist/authority/session-authorizations.js +102 -0
- package/dist/authority/session-authorizations.js.map +1 -0
- package/dist/authority/web-authority.d.ts +31 -11
- package/dist/authority/web-authority.js +39 -12
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/cache-ledger.d.ts +16 -3
- package/dist/cache-ledger.js +33 -13
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +55 -3
- package/dist/cache-service.js +152 -16
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/mcp-tools.js +12 -6
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +2 -2
- package/dist/classic-channel-manager.js +2 -6
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/config-validator.js +10 -4
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +2 -0
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -0
- package/dist/delivery-outbox.js +7 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +109 -2
- package/dist/fleet-manager.js +280 -12
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +2 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +81 -25
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +55 -8
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/human-delivery-receipts.d.ts +14 -0
- package/dist/human-delivery-receipts.js +42 -6
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +8 -1
- package/dist/instructions.js +9 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +52 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.d.ts +6 -0
- package/dist/outbound-handlers.js +45 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +57 -1
- package/dist/outbound-schemas.js +33 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +8 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/rollout-index.d.ts +5 -0
- package/dist/rollout-index.js +26 -5
- package/dist/rollout-index.js.map +1 -1
- package/dist/settings-api.d.ts +6 -0
- package/dist/settings-api.js +50 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +27 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +8 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/tool-permissions.d.ts +0 -7
- package/dist/tool-permissions.js +73 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +2 -1
- package/dist/topic-commands.js +2 -4
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +13 -2
- package/dist/ui/app-effects.js +43 -25
- package/dist/ui/chat-ambient.js +306 -0
- package/dist/ui/chat-effect-posters.js +6 -7
- package/dist/ui/chat-effects.js +38 -15
- package/dist/ui/chat-strings.js +17 -4
- package/dist/ui/panel-chat.js +15 -7
- package/dist/ui/panel-settings.js +4 -1
- package/dist/ui/settings-appearance.js +8 -0
- package/dist/ui/settings-mods.js +19 -6
- package/dist/ui/settings-owner-effects.js +3 -3
- package/dist/ui/settings-strings.js +28 -0
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/app-session.js +2 -0
- package/dist/ui/shared/app-shell.js +2 -1
- package/dist/ui/shared/app-themes.js +24 -6
- package/dist/ui/shared/app.css +42 -8
- package/dist/ui/shared/effect-app-runtime.js +67 -18
- package/dist/ui/shared/effect-chat-runtime.js +16 -3
- package/dist/ui/shared/effect-frame.js +17 -3
- package/dist/ui/shared/effect-policy.js +18 -4
- package/dist/ui/shared/effect-registry.js +11 -18
- package/dist/ui/shared/effect-stock-trust.js +2 -2
- package/dist/ui/shared/effect-trial-stage.js +18 -7
- package/dist/ui/shared/fx-motion.js +58 -0
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/usage-report.d.ts +147 -0
- package/dist/usage-report.js +195 -0
- package/dist/usage-report.js.map +1 -0
- package/dist/usage-rollup.d.ts +51 -0
- package/dist/usage-rollup.js +168 -0
- package/dist/usage-rollup.js.map +1 -0
- package/dist/web-ambient.d.ts +72 -0
- package/dist/web-ambient.js +154 -0
- package/dist/web-ambient.js.map +1 -0
- package/dist/web-api.d.ts +32 -0
- package/dist/web-api.js +143 -18
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +3 -0
- package/dist/web-appearance-tools.js +18 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +14 -2
- package/dist/web-appearance-view.js +32 -5
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +15 -1
- package/dist/web-appearance.js +77 -1
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +2 -2
- package/dist/web-asset-parts.js +3 -3
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +3 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-effect-bootstrap.d.ts +12 -0
- package/dist/web-effect-bootstrap.js +49 -0
- package/dist/web-effect-bootstrap.js.map +1 -0
- package/dist/web-effect-bundle.js +46 -1
- package/dist/web-effect-bundle.js.map +1 -1
- package/dist/web-effect-playback.d.ts +30 -0
- package/dist/web-effect-playback.js +71 -0
- package/dist/web-effect-playback.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +4 -4
- package/dist/web-effect-service.d.ts +11 -4
- package/dist/web-effect-service.js +25 -4
- package/dist/web-effect-service.js.map +1 -1
- package/dist/web-effect-tools.d.ts +1 -0
- package/dist/web-effect-tools.js +2 -1
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-worker.d.ts +1 -1
- package/dist/web-effect-worker.js +2 -2
- package/dist/web-effect-worker.js.map +1 -1
- package/dist/web-guest-access.js +3 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-agent.js +5 -1
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +9 -1
- package/dist/web-mod-bundle.js +62 -3
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-export.d.ts +9 -0
- package/dist/web-mod-export.js +21 -0
- package/dist/web-mod-export.js.map +1 -1
- package/dist/web-mod-store.d.ts +2 -1
- package/dist/web-mod-store.js +13 -6
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-session-authorization.d.ts +132 -0
- package/dist/web-session-authorization.js +193 -0
- package/dist/web-session-authorization.js.map +1 -0
- package/dist/web-session.d.ts +5 -0
- package/dist/web-session.js +16 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +24 -2
- package/dist/web-settings-patch.js +47 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +8 -0
- package/dist/web-settings-tools.js +14 -1
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-theme-store.d.ts +39 -0
- package/dist/web-theme-store.js +122 -11
- package/dist/web-theme-store.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** #1629 B2: one host-owned Canvas Worker pool. Product adapters are deliberately separate. */
|
|
2
|
-
import { allocateEffectFrames, effectEvent } from "./effect-policy.js";
|
|
2
|
+
import { allocateEffectFrames, effectEvent, effectPlaybackReason } from "./effect-policy.js";
|
|
3
3
|
import { captureEffectBaseline } from "./effect-baseline.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createOpaqueEffectFrame } from "./effect-frame.js";
|
|
5
5
|
import { createEffectGeometry, hostEffectRect } from "./effect-geometry.js";
|
|
6
6
|
import { effectStockSnapshot, effectStockAllows } from "./effect-stock-snapshot.js";
|
|
7
7
|
import { effectTrialStage, effectTrialStageIdentity, watchEffectTrialStage } from "./effect-trial-stage.js";
|
|
@@ -10,7 +10,7 @@ import { EffectBudget } from "./effect-profiler.js";
|
|
|
10
10
|
const registries = new WeakMap();
|
|
11
11
|
const plain = o => !!o && (Object.getPrototypeOf(o) === Object.prototype || Object.getPrototypeOf(o) === null);
|
|
12
12
|
const finite = v => typeof v === "number" && Number.isFinite(v);
|
|
13
|
-
const FLAGS = ["safe", "publicLink", "reducedMotion", "saveData", "hidden", "enabled", "show", "
|
|
13
|
+
const FLAGS = ["safe", "publicLink", "reducedMotion", "saveData", "hidden", "enabled", "show", "transportReady", "constrained", "modal"];
|
|
14
14
|
const TYPES = ["state", "reply", "theme"];
|
|
15
15
|
function unsafeSnapshotDescriptor(d) {
|
|
16
16
|
if (!plain(d) || !plain(d.params ?? {})) return null;
|
|
@@ -105,12 +105,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
105
105
|
return true;
|
|
106
106
|
};
|
|
107
107
|
// Display only: the closed global gate a row is waiting on, when the host knows it (else the caller's unknown).
|
|
108
|
-
const gateReason = () =>
|
|
109
|
-
if (!gatesKnown()) return null;
|
|
110
|
-
const c = context;
|
|
111
|
-
return c.safe ? "safe_mode" : c.publicLink ? "public_link" : c.reducedMotion ? "reduced_motion" : c.saveData ? "save_data"
|
|
112
|
-
: !(c.enabled && c.show) ? "disabled" : !(c.previewReady && c.optIn) ? "preview_unavailable" : null;
|
|
113
|
-
};
|
|
108
|
+
const gateReason = () => effectPlaybackReason(context);
|
|
114
109
|
const clearMarker = (s, why) => {
|
|
115
110
|
if (!s.token) return true;
|
|
116
111
|
const token = s.token; s.token = null;
|
|
@@ -122,7 +117,9 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
122
117
|
s.live = false;
|
|
123
118
|
if (slots.get(s.d.key) === s) slots.delete(s.d.key);
|
|
124
119
|
if (s.request) { s.request.controller.abort(); if (s.request.timer !== null) host.clearTimeout(s.request.timer); }
|
|
125
|
-
|
|
120
|
+
// A closed/unknown viewer gate revokes paint, not the effect identity. Fresh known gates
|
|
121
|
+
// may admit it again; resource failures and actual safety pauses retain their latches.
|
|
122
|
+
if (!["disposed", "superseded", "hidden", "human_resume", "calibrating", "geometry_changed", "gates_unknown"].includes(why)) failures.set(s.d.key, { identity: signature(s.d), reason: why });
|
|
126
123
|
s.stageOff?.(); s.stageOff = null;
|
|
127
124
|
s.frame?.remove(why); s.geometry?.remove();
|
|
128
125
|
if (s.loadOff) s.loadOff();
|
|
@@ -145,13 +142,9 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
145
142
|
return true;
|
|
146
143
|
}
|
|
147
144
|
function gatesKnown() { return context && FLAGS.every(k => typeof context[k] === "boolean") && (context.openInstance === null || typeof context.openInstance === "string" && context.openInstance.length >= 1 && context.openInstance.length <= 128); }
|
|
148
|
-
function consent(d) {
|
|
149
|
-
const trial = d?.trialStage && effectTrialStageIdentity(d.trialStage);
|
|
150
|
-
return trial ? context.trialReady === true : context.previewReady === true && context.optIn === true;
|
|
151
|
-
}
|
|
152
145
|
function eligible(d = null) {
|
|
153
|
-
return gatesKnown() && !context.safe && !context.
|
|
154
|
-
&& (d
|
|
146
|
+
return gatesKnown() && !context.safe && !context.reducedMotion && !context.saveData && context.enabled && context.show && context.transportReady
|
|
147
|
+
&& (!d || !context.publicLink || d.region === "chat" && !d.trialStage);
|
|
155
148
|
}
|
|
156
149
|
const globalAllowed = (d = null) => eligible(d) && host.document.visibilityState === "visible";
|
|
157
150
|
const signature = d => d.identity;
|
|
@@ -329,7 +322,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
329
322
|
if (local) fresh = local.rect;
|
|
330
323
|
s.rect = fresh;
|
|
331
324
|
s.budget = new EffectBudget({ device: s.device, baselineFps: baseline, clock: nativeClock, longTasksSupported });
|
|
332
|
-
const frame =
|
|
325
|
+
const frame = createOpaqueEffectFrame({ window: host, root: local?.root ?? root, origin, boot, clock: nativeClock,
|
|
333
326
|
render: { code: resource.code, params: resource.params, assets: resource.assets, wants: resource.wants, instance: d.instance, width: fresh.width, height: fresh.height, dpr: dpr(s.device) },
|
|
334
327
|
onReady() { if (!current(s)) return; if (!globalAllowed(d) || context.hidden || !coverage(d)) { retire(s, "hidden"); tell(); return; } const n = time(); if (n === null) { blocked = "clock_unavailable"; stopAll(blocked); tell(); return; } s.ready = true; s.readyAt = n; s.healthySince = n; publishEnv(s); schedule(); tell(); },
|
|
335
328
|
onRemove(why) { if (current(s)) { retire(s, why); tell(); } },
|
|
@@ -510,7 +503,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
510
503
|
descriptors = ds.map(snapshotDescriptor).filter(Boolean);
|
|
511
504
|
for (const [key, identity] of paused) if (!descriptors.some(d => d.key === key && signature(d) === identity)) paused.delete(key);
|
|
512
505
|
for (const [key, f] of failures) if (!descriptors.some(d => d.key === key && signature(d) === f.identity)) failures.delete(key);
|
|
513
|
-
context = Object.fromEntries([...FLAGS, "
|
|
506
|
+
context = Object.fromEntries([...FLAGS, "openInstance"].map(k => [k, ctx[k]]));
|
|
514
507
|
reconcile(); return view();
|
|
515
508
|
},
|
|
516
509
|
snapshot: view,
|
|
@@ -178,9 +178,9 @@ export function stockEffectTrust(host, { frameRoot = null, ownerCss = "unknown"
|
|
|
178
178
|
if (doc.querySelector(".agend-session-ended")) return fail("session_ended");
|
|
179
179
|
let barrier = [...doc.querySelectorAll(BARRIERS)].some(el => visible(host, el));
|
|
180
180
|
if (host.innerWidth < 900 && [...doc.querySelectorAll(".pv-panel")].some(el => visible(host, el))) barrier = true;
|
|
181
|
-
const active = doc.activeElement;
|
|
182
181
|
if (doc.fullscreenElement || doc.pictureInPictureElement) barrier = true;
|
|
183
|
-
|
|
182
|
+
// Focus alone does not change protected paint bounds. Composer/controls stay clipped; editable focus
|
|
183
|
+
// separately suppresses pointer delivery in the registry, and keyboard events never enter a Worker.
|
|
184
184
|
return { ok: true, rects, front: !barrier, reason: barrier ? "stock_overlay" : "stock", agentFront: !barrier && ownerCss === "off", agentReason: barrier ? "stock_overlay" : ownerCss === "off" ? "stock" : "owner_css", settingsHref: "/settings/appearance" };
|
|
185
185
|
} catch { return fail("stock_trust_unknown"); }
|
|
186
186
|
}
|
|
@@ -120,21 +120,32 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
|
|
|
120
120
|
function bind(frame, { layer } = {}, stock) {
|
|
121
121
|
if (frames.size || !["front", "behind"].includes(layer) || frame?.ownerDocument !== host.document || frame.parentElement !== stage || frame.tagName !== "IFRAME") { frame?.remove?.(); return { ok: false }; }
|
|
122
122
|
// The new frame is temporarily known to this owner before the topology check; it remains hidden until sync.
|
|
123
|
-
const token = `agend-effect-trial-${identity.serial}`; let rule, removed = false;
|
|
123
|
+
const token = `agend-effect-trial-${identity.serial}`; let rule, removed = false, shown = false;
|
|
124
124
|
const remove = () => {
|
|
125
125
|
if (removed) return; removed = true; frame.remove(); frames.delete(handle);
|
|
126
126
|
try { frame.classList.remove(token); const i = [...sheet.cssRules].indexOf(rule); if (i >= 0) sheet.deleteRule(i); } catch { /* physically removed first */ }
|
|
127
127
|
};
|
|
128
|
-
const hide = () => {
|
|
128
|
+
const hide = () => {
|
|
129
|
+
shown = false;
|
|
130
|
+
try { rule.style.setProperty("visibility", "hidden", "important"); if (frame.isConnected && host.getComputedStyle(frame).visibility !== "hidden") { remove(); return false; } return !removed; } catch { remove(); return false; }
|
|
131
|
+
};
|
|
129
132
|
const handle = { ok: true, element: frame, remove, hide,
|
|
130
133
|
sync(proof) {
|
|
131
|
-
|
|
134
|
+
const r = check(proof); if (!r || removed) { invalidate("stage_unavailable"); return false; }
|
|
132
135
|
try {
|
|
136
|
+
const matches = visibility => {
|
|
137
|
+
const s = host.getComputedStyle(frame), actual = rect(frame);
|
|
138
|
+
return !!actual && s.position === "absolute" && s.visibility === visibility && s.pointerEvents === "none" && s.zIndex === "0" && s.opacity === (layer === "front" ? "0.85" : "1") && neutral(s)
|
|
139
|
+
&& ["x", "y", "width", "height"].every(k => Math.abs(actual[k] - r[k]) < 0.02) && actual.x >= r.x && actual.y >= r.y && actual.x + actual.width <= r.x + r.width && actual.y + actual.height <= r.y + r.height
|
|
140
|
+
&& frame.isConnected === true && frame.ownerDocument === host.document && frame.parentElement === stage && !dead && !removed && owners.get(stage) === receipt;
|
|
141
|
+
};
|
|
142
|
+
// Reprove the current stage, clipping and actual frame every tick. Hiding/revealing an
|
|
143
|
+
// unchanged iframe each rAF can starve Firefox's OffscreenCanvas presentation.
|
|
144
|
+
if (shown && matches("visible")) return true;
|
|
145
|
+
if (!hide()) throw Error("local hiding lost");
|
|
133
146
|
for (const [k, v] of Object.entries({ left: "0px", top: "0px", width: `${r.width}px`, height: `${r.height}px` })) rule.style.setProperty(k, v, "important");
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|| !["x", "y", "width", "height"].every(k => Math.abs(actual[k] - r[k]) < 0.02) || actual.x < r.x || actual.y < r.y || actual.x + actual.width > r.x + r.width || actual.y + actual.height > r.y + r.height) throw Error("local geometry lost");
|
|
137
|
-
rule.style.setProperty("visibility", "visible", "important"); return true;
|
|
147
|
+
if (!matches("hidden")) throw Error("local geometry lost");
|
|
148
|
+
rule.style.setProperty("visibility", "visible", "important"); shown = true; return true;
|
|
138
149
|
} catch { invalidate("stage_unavailable"); return false; }
|
|
139
150
|
},
|
|
140
151
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// #2117 (the user's direction, hmsg-3c166c26): agents' effects play whatever the system's reduced-motion setting says,
|
|
2
|
+
// unless this device chose "Effects: follow the system's reduced-motion setting". One per-device switch (not synced:
|
|
3
|
+
// it is about this screen), shared by ambient effects and Canvas Worker effects; it also works on the public link.
|
|
4
|
+
export const FX_FOLLOW_REDUCED_MOTION_KEY = "agend_fx_follow_reduced_motion";
|
|
5
|
+
/** The one-time chat note "your system asks for reduced motion; effects are on" was shown on this device. */
|
|
6
|
+
export const FX_MOTION_NOTE_KEY = "agend_fx_motion_note_seen";
|
|
7
|
+
const listeners = new Set();
|
|
8
|
+
|
|
9
|
+
/** Whether this device asked effects to follow the system's reduced-motion setting (default: no). */
|
|
10
|
+
export function followsReducedMotion() {
|
|
11
|
+
try { return globalThis.localStorage.getItem(FX_FOLLOW_REDUCED_MOTION_KEY) === "1"; } catch { return false; }
|
|
12
|
+
}
|
|
13
|
+
export function setFollowsReducedMotion(on) {
|
|
14
|
+
try { if (on) globalThis.localStorage.setItem(FX_FOLLOW_REDUCED_MOTION_KEY, "1"); else globalThis.localStorage.removeItem(FX_FOLLOW_REDUCED_MOTION_KEY); } catch { /* this page only */ }
|
|
15
|
+
for (const fn of [...listeners]) { try { fn(); } catch { /* a listener's own */ } }
|
|
16
|
+
}
|
|
17
|
+
/** `fn()` when the switch or the system's answer moves (this tab, another tab, the OS). Returns the unsubscribe. */
|
|
18
|
+
export function onMotionChange(fn) {
|
|
19
|
+
listeners.add(fn);
|
|
20
|
+
const onStorage = (e) => { if (!e || e.key === FX_FOLLOW_REDUCED_MOTION_KEY || e.key === null) fn(); };
|
|
21
|
+
let q = null;
|
|
22
|
+
try { q = typeof globalThis.matchMedia === "function" ? globalThis.matchMedia("(prefers-reduced-motion: reduce)") : null; } catch { q = null; }
|
|
23
|
+
try { globalThis.addEventListener && globalThis.addEventListener("storage", onStorage); } catch { /* none */ }
|
|
24
|
+
try { if (q && q.addEventListener) q.addEventListener("change", fn); } catch { /* read again on the next render */ }
|
|
25
|
+
return () => {
|
|
26
|
+
listeners.delete(fn);
|
|
27
|
+
try { globalThis.removeEventListener && globalThis.removeEventListener("storage", onStorage); } catch { /* gone */ }
|
|
28
|
+
try { if (q && q.removeEventListener) q.removeEventListener("change", fn); } catch { /* gone */ }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Whether the system asks for reduced motion (false when it can't say: the note is only for a clear yes). */
|
|
32
|
+
export function systemReducesMotion(win = globalThis) {
|
|
33
|
+
try { return typeof win.matchMedia === "function" && win.matchMedia("(prefers-reduced-motion: reduce)").matches === true; } catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The one-time note (the user's direction): the system asks for reduced motion, this device does not follow it, and the
|
|
37
|
+
* note was not shown here before. Showing it marks it shown.
|
|
38
|
+
*/
|
|
39
|
+
export function motionNoteDue(win = globalThis) {
|
|
40
|
+
if (followsReducedMotion() || !systemReducesMotion(win)) return false;
|
|
41
|
+
try { return globalThis.localStorage.getItem(FX_MOTION_NOTE_KEY) !== "1"; } catch { return false; }
|
|
42
|
+
}
|
|
43
|
+
export function markMotionNoteShown() {
|
|
44
|
+
try { globalThis.localStorage.setItem(FX_MOTION_NOTE_KEY, "1"); } catch { /* shown again next time: harmless */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True when effects stay still here: this device follows the system's setting and the system asks for reduced motion
|
|
49
|
+
* (or cannot say — then still, as asked for). With the switch off (the default), never.
|
|
50
|
+
*/
|
|
51
|
+
export function motionBlocked(win = globalThis) {
|
|
52
|
+
if (!followsReducedMotion()) return false;
|
|
53
|
+
try {
|
|
54
|
+
if (typeof win.matchMedia !== "function") return true;
|
|
55
|
+
const q = win.matchMedia("(prefers-reduced-motion: reduce)");
|
|
56
|
+
return !q || typeof q.matches !== "boolean" || q.matches;
|
|
57
|
+
} catch { return true; }
|
|
58
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// #1939 S2b (design §0.1): "Authorize this browser". The app shows this browser's state — verified (until when), a
|
|
2
|
+
// Confirm card waiting in the account's Discord or Telegram DM (with its countdown), or why it can't be authorized here —
|
|
3
|
+
// and asks for the card. The card itself is the gate: nothing here can authorize anything.
|
|
4
|
+
import { html, useEffect, useRef, useState } from "./app-html.js";
|
|
5
|
+
import { register, t } from "./app-i18n.js";
|
|
6
|
+
import { guestPage } from "./app-store.js";
|
|
7
|
+
|
|
8
|
+
register("webauth", {
|
|
9
|
+
en: {
|
|
10
|
+
verified: "Verified · speaks as you until {0}",
|
|
11
|
+
notVerified: "Not verified",
|
|
12
|
+
authorize: "Authorize this browser",
|
|
13
|
+
waiting: "Waiting for your Confirm in Discord or Telegram ({0})",
|
|
14
|
+
sent: "Sent — confirm it in your DM within 5 minutes.",
|
|
15
|
+
publicLink: "Not on the temporary public link: open the dashboard directly to authorize.",
|
|
16
|
+
reason_disabled: "Web authority is turned off.",
|
|
17
|
+
reason_loopback_only: "Only this computer can authorize, and this browser is not on it.",
|
|
18
|
+
reason_no_account: "This browser was not signed in as an account (agend web): sign in with /web to authorize.",
|
|
19
|
+
reason_not_admin: "Your account is not a fleet admin here.",
|
|
20
|
+
reason_not_admin_session: "Guests can't authorize.",
|
|
21
|
+
reason_token_rotated: "The dashboard's token changed: sign in again.",
|
|
22
|
+
reason_public_link: "Not on the temporary public link.",
|
|
23
|
+
reason_not_session: "Sign in to authorize.",
|
|
24
|
+
reason_other: "This browser can't be authorized here.",
|
|
25
|
+
refusal_pending: "A request is already waiting in your DM.",
|
|
26
|
+
refusal_rate_limited: "Three requests this hour — try again in {0} min.",
|
|
27
|
+
refusal_dm_unavailable: "Couldn't send you a DM. Open your DMs with the bot, then try again.",
|
|
28
|
+
refusal_other: "That didn't work: {0}",
|
|
29
|
+
settings: "Settings",
|
|
30
|
+
},
|
|
31
|
+
"zh-TW": {
|
|
32
|
+
verified: "已驗證 · 以你的身分發言到 {0}",
|
|
33
|
+
notVerified: "未驗證",
|
|
34
|
+
authorize: "授權這個瀏覽器",
|
|
35
|
+
waiting: "等你在 Discord 或 Telegram 按下確認({0})",
|
|
36
|
+
sent: "已送出——請在 5 分鐘內到私訊確認。",
|
|
37
|
+
publicLink: "臨時公開連結上無法授權:請直接開啟儀表板再授權。",
|
|
38
|
+
reason_disabled: "網頁授權已關閉。",
|
|
39
|
+
reason_loopback_only: "只有這台電腦可以授權,而這個瀏覽器不在這台電腦上。",
|
|
40
|
+
reason_no_account: "這個瀏覽器不是用帳號登入的(agend web):請用 /web 登入後再授權。",
|
|
41
|
+
reason_not_admin: "你的帳號在這裡不是 fleet 管理者。",
|
|
42
|
+
reason_not_admin_session: "訪客無法授權。",
|
|
43
|
+
reason_token_rotated: "儀表板的 token 已變更:請重新登入。",
|
|
44
|
+
reason_public_link: "臨時公開連結上無法授權。",
|
|
45
|
+
reason_not_session: "登入後才能授權。",
|
|
46
|
+
reason_other: "這個瀏覽器在這裡無法授權。",
|
|
47
|
+
refusal_pending: "已經有一個請求在你的私訊中等待。",
|
|
48
|
+
refusal_rate_limited: "這一小時已請求三次——請 {0} 分鐘後再試。",
|
|
49
|
+
refusal_dm_unavailable: "無法私訊給你。請開放與 bot 的私訊後再試一次。",
|
|
50
|
+
refusal_other: "沒有成功:{0}",
|
|
51
|
+
settings: "設定",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
const tn = (k, ...v) => t(`webauth.${k}`, ...v);
|
|
55
|
+
const REASONS = ["disabled", "loopback_only", "no_account", "not_admin", "not_admin_session", "token_rotated", "public_link", "not_session"];
|
|
56
|
+
// Until when: the time, with the day when it isn't today (an authorization lasts up to 48 h).
|
|
57
|
+
const clock = (ms) => {
|
|
58
|
+
const d = new Date(ms), today = new Date().toDateString() === d.toDateString();
|
|
59
|
+
return today ? d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
|
60
|
+
: d.toLocaleString([], { weekday: "short", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
61
|
+
};
|
|
62
|
+
const mmss = (ms) => { const s = Math.max(0, Math.ceil(ms / 1000)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; };
|
|
63
|
+
const publicLink = () => globalThis.document?.body?.dataset?.publicLink === "1";
|
|
64
|
+
|
|
65
|
+
// One read per page load, shared by every line that shows it (the composer's, the session menu's): coming back to a chat
|
|
66
|
+
// reads nothing (#1374). It is read again only after an ask, and while a card is pending.
|
|
67
|
+
// An answer of any kind counts as this page's read (one that isn't a state hides the line); only a network failure lets a
|
|
68
|
+
// later mount try again. Signed out (401: the session ended, e.g. Not me), the line goes.
|
|
69
|
+
let shared = null, answered = false, inflight = null, signedOut = false;
|
|
70
|
+
async function readStatus() {
|
|
71
|
+
inflight ??= (async () => {
|
|
72
|
+
try {
|
|
73
|
+
const r = await fetch("/ui/authorization", { credentials: "same-origin" });
|
|
74
|
+
answered = true;
|
|
75
|
+
if (r.status === 401) { signedOut = true; shared = null; }
|
|
76
|
+
const body = r.ok ? await r.json().catch(() => null) : null;
|
|
77
|
+
if (body && typeof body.state === "string") shared = body;
|
|
78
|
+
} catch { /* the line stays as it was; a later mount may try again */ }
|
|
79
|
+
finally { inflight = null; }
|
|
80
|
+
return shared;
|
|
81
|
+
})();
|
|
82
|
+
return inflight;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** This browser's authorization, kept fresh while a card is pending (every 3 s, until it is decided or expires). */
|
|
86
|
+
export function useWebAuthorization() {
|
|
87
|
+
const [st, setSt] = useState(shared), [notice, setNotice] = useState(null);
|
|
88
|
+
const [deadline, setDeadline] = useState(0), [now, setNow] = useState(() => Date.now());
|
|
89
|
+
const alive = useRef(true);
|
|
90
|
+
const load = async () => {
|
|
91
|
+
if (publicLink() || guestPage()) return;
|
|
92
|
+
const body = await readStatus();
|
|
93
|
+
if (!alive.current) return;
|
|
94
|
+
if (signedOut) { setSt(null); setNotice(null); return; }
|
|
95
|
+
if (!body) return;
|
|
96
|
+
setSt(body);
|
|
97
|
+
if (body.state !== "pending") setNotice((n) => (n === tn("sent") ? null : n)); // "Sent —" was about the card
|
|
98
|
+
if (body.state === "pending" && typeof body.expiresInMs === "number") setDeadline(Date.now() + body.expiresInMs);
|
|
99
|
+
};
|
|
100
|
+
useEffect(() => { alive.current = true; if (!answered) load(); return () => { alive.current = false; }; }, []);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!st || st.state !== "pending") return undefined;
|
|
103
|
+
const tick = setInterval(() => setNow(Date.now()), 1000), poll = setInterval(load, 3000);
|
|
104
|
+
return () => { clearInterval(tick); clearInterval(poll); };
|
|
105
|
+
}, [st && st.state]);
|
|
106
|
+
// "Verified · until X" is read again at X (a page left open must not keep saying verified after it ends).
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!st || st.state !== "verified" || typeof st.until !== "number") return undefined;
|
|
109
|
+
const at = setTimeout(() => { resetWebAuthorization(); load(); }, Math.max(0, Math.min(st.until - Date.now() + 1000, 2 ** 31 - 1)));
|
|
110
|
+
return () => clearTimeout(at);
|
|
111
|
+
}, [st && st.state, st && st.until]);
|
|
112
|
+
const ask = async () => {
|
|
113
|
+
setNotice(null);
|
|
114
|
+
let r = null, body = null;
|
|
115
|
+
try { r = await fetch("/ui/authorization", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: "{}" }); body = await r.json(); } catch { body = null; }
|
|
116
|
+
if (r && r.ok && body && body.ok) { setNotice(tn("sent")); setSt(shared = { state: "pending", expiresInMs: body.expiresInMs }); setDeadline(Date.now() + body.expiresInMs); return; }
|
|
117
|
+
const refusal = body && body.refusal;
|
|
118
|
+
if (refusal === "rate_limited") setNotice(tn("refusal_rate_limited", Math.max(1, Math.ceil((body.retryAfterMs || 0) / 60_000))));
|
|
119
|
+
else if (refusal === "pending" || refusal === "dm_unavailable") setNotice(tn(`refusal_${refusal}`));
|
|
120
|
+
else if (REASONS.includes(refusal)) setNotice(tn(`reason_${refusal}`));
|
|
121
|
+
else setNotice(tn("refusal_other", String(refusal || (r && r.status) || "?")));
|
|
122
|
+
load();
|
|
123
|
+
};
|
|
124
|
+
return { st, notice, ask, left: deadline - now };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Forget the page's one read, so the next line that mounts reads again: Settings → Web calls it when it shows (a revoke
|
|
129
|
+
* or a changed switch there changes what the line should say). Tests use it too: each case is its own page load.
|
|
130
|
+
*/
|
|
131
|
+
export function resetWebAuthorization() { shared = null; answered = false; inflight = null; signedOut = false; }
|
|
132
|
+
|
|
133
|
+
/** The line under the composer: verified, waiting, or "Not verified" with the button (or why not, with the fix). */
|
|
134
|
+
export function AuthorizeChip() {
|
|
135
|
+
const a = useWebAuthorization();
|
|
136
|
+
if (guestPage()) return null;
|
|
137
|
+
if (publicLink()) return html`<p class="webauth-chip note" data-state="public">${tn("publicLink")}</p>`;
|
|
138
|
+
if (!a.st) return null;
|
|
139
|
+
return html`<div class="webauth-chip" data-state=${a.st.state}>${body(a)}${a.notice ? html` <span class="note" role="status">${a.notice}</span>` : null}</div>`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function body(a) {
|
|
143
|
+
const st = a.st;
|
|
144
|
+
if (st.state === "verified") return html`<span class="webauth-ok">✓ ${tn("verified", clock(st.until))}</span>`;
|
|
145
|
+
if (st.state === "pending") return html`<span class="note">${tn("waiting", mmss(a.left))}</span>`;
|
|
146
|
+
if (st.reason === "not_authorized" || st.reason === "authorization_expired")
|
|
147
|
+
return html`<span class="note">${tn("notVerified")} ·</span> <button type="button" class="btn btn-sm" onClick=${a.ask}>${tn("authorize")}</button>`;
|
|
148
|
+
const why = REASONS.includes(st.reason) ? tn(`reason_${st.reason}`) : tn("reason_other");
|
|
149
|
+
// The fix, where there is one the person can take here: the switch lives in Settings → Web.
|
|
150
|
+
return html`<span class="note">${tn("notVerified")}: ${why}</span>${st.reason === "disabled" || st.reason === "loopback_only" ? html` <a href="/settings/web">${tn("settings")}</a>` : null}`;
|
|
151
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2122: the usage report — tokens, requests, delivered messages and their API-equivalent at list price, per instance
|
|
3
|
+
* (or by backend, model, credential profile or team), over a range of days. Pure: it reads the ledgers' hours after the
|
|
4
|
+
* rollup's checkpoint and the rollup's days up to it (usage-rollup.ts), and the delivered-message counts; it never reads
|
|
5
|
+
* a transcript. docs/design/2122-usage-analysis.md.
|
|
6
|
+
*
|
|
7
|
+
* Honest by construction: an amount is "API-equivalent at list price (checked <date>) — not money spent"; an unpriced
|
|
8
|
+
* model's tokens are shown and its amount is not; Codex's cache writes are "not reported" (every CLI seen records 0),
|
|
9
|
+
* never priced as uncached; an instance whose backend AgEnD cannot measure is listed with its reason, never as $0.
|
|
10
|
+
*/
|
|
11
|
+
import type { Ledger } from "./cache-ledger.js";
|
|
12
|
+
import { type UsageDays } from "./usage-rollup.js";
|
|
13
|
+
export declare const USAGE_GROUPS: readonly ["instance", "backend", "model", "profile", "team"];
|
|
14
|
+
export type UsageGroup = typeof USAGE_GROUPS[number];
|
|
15
|
+
export declare const USAGE_RANGES: readonly ["today", "7d", "month", "all"];
|
|
16
|
+
export type UsageRange = {
|
|
17
|
+
kind: typeof USAGE_RANGES[number];
|
|
18
|
+
} | {
|
|
19
|
+
kind: "custom";
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
};
|
|
23
|
+
/** What every amount is, in words — the page, the CSV and the agent tool all carry it. */
|
|
24
|
+
export declare const usageBasis: (checked?: string) => string;
|
|
25
|
+
/** Daily rows returned at most (the CSV and the trend); beyond it the report says it is truncated. */
|
|
26
|
+
export declare const USAGE_DAILY_MAX = 20000;
|
|
27
|
+
export interface UsageInstanceInput {
|
|
28
|
+
name: string;
|
|
29
|
+
/** Its backend now: what it is collected with from here on (each slice of its history carries its own). */
|
|
30
|
+
backend: string;
|
|
31
|
+
/** The team it is in now (fleet.yaml), for the one grouping built from current settings. */
|
|
32
|
+
team: string | null;
|
|
33
|
+
/** Whether it is collected now; what it measured before (`ledger`, `days`) is reported either way. */
|
|
34
|
+
status: "ok" | "no_data" | "credit_billed" | "unsupported" | "shared";
|
|
35
|
+
with?: string[];
|
|
36
|
+
/** A snapshot taken together with `days` (cache-service.ts usageData): read-only here. */
|
|
37
|
+
ledger?: Ledger;
|
|
38
|
+
days?: UsageDays;
|
|
39
|
+
}
|
|
40
|
+
export interface UsageInput {
|
|
41
|
+
now: number;
|
|
42
|
+
zone: string;
|
|
43
|
+
range: UsageRange;
|
|
44
|
+
group: UsageGroup;
|
|
45
|
+
instances: UsageInstanceInput[];
|
|
46
|
+
messages: Array<{
|
|
47
|
+
instance: string;
|
|
48
|
+
hour: number;
|
|
49
|
+
count: number;
|
|
50
|
+
}>;
|
|
51
|
+
scanning: {
|
|
52
|
+
active: boolean;
|
|
53
|
+
caughtUp: boolean;
|
|
54
|
+
pendingBytes: number;
|
|
55
|
+
pendingFiles: number;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export interface UsageTotals {
|
|
59
|
+
/** Uncached input. */
|
|
60
|
+
input: number;
|
|
61
|
+
output: number;
|
|
62
|
+
cacheRead: number;
|
|
63
|
+
cacheWrite5m: number;
|
|
64
|
+
cacheWrite1h: number;
|
|
65
|
+
/** OpenAI's one write column (Codex records 0: see `flags`). */
|
|
66
|
+
cacheWrite: number;
|
|
67
|
+
requests: number;
|
|
68
|
+
}
|
|
69
|
+
export interface UsageRow extends UsageTotals {
|
|
70
|
+
key: string;
|
|
71
|
+
total: number;
|
|
72
|
+
/** Delivered messages; null where a message has no such key (by model, by profile). */
|
|
73
|
+
messages: number | null;
|
|
74
|
+
/** The API-equivalent at list price of what is priced; null when nothing in it is. */
|
|
75
|
+
amount: number | null;
|
|
76
|
+
priced: "all" | "partial" | "none";
|
|
77
|
+
/**
|
|
78
|
+
* "cache_write_not_reported" (Codex), "not_priced" (with `unpriced`), "source_unknown" (hours counted before #2122:
|
|
79
|
+
* which backend and profile read them was not recorded).
|
|
80
|
+
*/
|
|
81
|
+
flags: string[];
|
|
82
|
+
unpriced: string[];
|
|
83
|
+
instances: string[];
|
|
84
|
+
backends: string[];
|
|
85
|
+
models: string[];
|
|
86
|
+
profiles: string[];
|
|
87
|
+
}
|
|
88
|
+
export interface UsageDaily extends UsageTotals {
|
|
89
|
+
date: string;
|
|
90
|
+
zone: string;
|
|
91
|
+
instance: string;
|
|
92
|
+
backend: string;
|
|
93
|
+
model: string;
|
|
94
|
+
tier: "long" | "standard";
|
|
95
|
+
profile: string;
|
|
96
|
+
amount: number | null;
|
|
97
|
+
flags: string[];
|
|
98
|
+
}
|
|
99
|
+
export interface UsageReport {
|
|
100
|
+
range: {
|
|
101
|
+
kind: UsageRange["kind"];
|
|
102
|
+
from: string;
|
|
103
|
+
to: string;
|
|
104
|
+
};
|
|
105
|
+
zone: string;
|
|
106
|
+
/** Days are made of whole UTC hours (§5.3): in a half-hour zone a day's edge falls on the hour that holds midnight. */
|
|
107
|
+
granularity: "hour";
|
|
108
|
+
basis: string;
|
|
109
|
+
pricesChecked: string;
|
|
110
|
+
group: UsageGroup;
|
|
111
|
+
rows: UsageRow[];
|
|
112
|
+
totals: UsageRow;
|
|
113
|
+
daily: UsageDaily[];
|
|
114
|
+
dailyTruncated: boolean;
|
|
115
|
+
messagesDaily: Array<{
|
|
116
|
+
date: string;
|
|
117
|
+
instance: string;
|
|
118
|
+
count: number;
|
|
119
|
+
}>;
|
|
120
|
+
notMeasured: Array<{
|
|
121
|
+
name: string;
|
|
122
|
+
backend: string;
|
|
123
|
+
reason: "credit_billed" | "unsupported" | "no_data";
|
|
124
|
+
}>;
|
|
125
|
+
shared: Array<{
|
|
126
|
+
name: string;
|
|
127
|
+
backend: string;
|
|
128
|
+
with: string[];
|
|
129
|
+
}>;
|
|
130
|
+
/** Per measured instance: the merged UTC intervals its transcripts were read over (§5.5) — never inferred. */
|
|
131
|
+
coverage: Array<{
|
|
132
|
+
name: string;
|
|
133
|
+
intervals: Array<[number, number]>;
|
|
134
|
+
}>;
|
|
135
|
+
scanning: UsageInput["scanning"];
|
|
136
|
+
}
|
|
137
|
+
/** A range as asked (query string or tool argument); anything else is the last 7 days. */
|
|
138
|
+
export declare function usageRange(kind: unknown, from?: unknown, to?: unknown): UsageRange;
|
|
139
|
+
export declare const usageGroup: (g: unknown) => UsageGroup;
|
|
140
|
+
/** A local date (YYYY-MM-DD) moved by `n` calendar days — a date's arithmetic, never 24-hour steps (a DST day is 23 or 25). */
|
|
141
|
+
export declare const shiftDate: (date: string, n: number) => string;
|
|
142
|
+
/** The range's first and last local dates (inclusive) in `zone`: "7d" is today and the 6 calendar days before it. */
|
|
143
|
+
export declare function rangeDates(range: UsageRange, now: number, zone: string): {
|
|
144
|
+
from: string;
|
|
145
|
+
to: string;
|
|
146
|
+
};
|
|
147
|
+
export declare function buildUsageReport(input: UsageInput): UsageReport;
|