@songsid/agend 2.3.0-beta.7 → 2.3.0-beta.9
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 +2 -1
- package/dist/agent-cli.js +3 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-whats-new.js +2 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/channel/mcp-tools.js +3 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +29 -9
- package/dist/daemon.js.map +1 -1
- package/dist/decision-list.d.ts +25 -0
- package/dist/decision-list.js +30 -0
- package/dist/decision-list.js.map +1 -0
- package/dist/fleet-manager.d.ts +9 -2
- package/dist/fleet-manager.js +44 -12
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-traffic.d.ts +36 -0
- package/dist/fleet-traffic.js +100 -0
- package/dist/fleet-traffic.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +17 -10
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +32 -13
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/outbound-handlers.js +4 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +6 -4
- package/dist/outbound-schemas.js +7 -3
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/settings-api.js +4 -0
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +4 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/tool-permissions.js +9 -42
- package/dist/tool-permissions.js.map +1 -1
- package/dist/ui/app-effects.js +9 -5
- package/dist/ui/chat-ambient.js +8 -7
- package/dist/ui/chat-effect-posters.js +3 -2
- package/dist/ui/chat-effects.js +22 -15
- package/dist/ui/chat-render.js +5 -2
- package/dist/ui/chat-strings.js +6 -4
- package/dist/ui/chat-thread.js +134 -17
- package/dist/ui/effect-trial-controller.js +13 -8
- package/dist/ui/fleet-canvas.js +54 -18
- package/dist/ui/fleet-org.js +3 -3
- package/dist/ui/fleet-traffic.js +156 -37
- package/dist/ui/fleet-usage.js +401 -0
- package/dist/ui/panel-chat.js +101 -47
- package/dist/ui/panel-fleet.js +6 -5
- package/dist/ui/panel-settings.js +6 -1
- package/dist/ui/settings-owner-effects.js +1 -1
- package/dist/ui/settings-strings.js +2 -0
- package/dist/ui/shared/agend-auth.js +1 -1
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-stream.js +3 -2
- package/dist/ui/shared/app.css +89 -9
- package/dist/ui/shared/effect-app-runtime.js +3 -3
- package/dist/ui/shared/effect-chat-runtime.js +1 -1
- package/dist/ui/shared/effect-policy.js +5 -6
- package/dist/ui/shared/effect-profiler.js +31 -9
- package/dist/ui/shared/effect-registry.js +23 -31
- package/dist/ui/shared/effect-stock-snapshot.js +9 -0
- package/dist/ui/shared/effect-stock-trust.js +37 -6
- package/dist/ui/shared/effect-trial-source.js +1 -1
- package/dist/ui/shared/effect-trial-stage.js +27 -21
- package/dist/ui/shared/effect-trial.css +1 -1
- package/dist/ui/shared/fx-motion.js +12 -3
- package/dist/ui/shared/tokens.css +9 -3
- package/dist/ui/shared/usage-dialog.js +8 -0
- package/dist/ui/shared/view-strings.js +4 -0
- package/dist/ui/web-terminal/terminal.css +4 -4
- package/dist/usage-report.d.ts +12 -1
- package/dist/usage-report.js +39 -8
- package/dist/usage-report.js.map +1 -1
- package/dist/web-ambient.js +9 -2
- package/dist/web-ambient.js.map +1 -1
- package/dist/web-api.d.ts +2 -0
- package/dist/web-api.js +8 -3
- package/dist/web-api.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- 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/workflow-templates/default.md +1 -1
- package/package.json +1 -1
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/** #1629 B2: one host-owned Canvas Worker pool. Product adapters are deliberately separate. */
|
|
2
2
|
import { allocateEffectFrames, effectEvent, effectPlaybackReason } from "./effect-policy.js";
|
|
3
|
-
import { captureEffectBaseline } from "./effect-baseline.js";
|
|
4
3
|
import { createOpaqueEffectFrame } from "./effect-frame.js";
|
|
5
4
|
import { createEffectGeometry, hostEffectRect } from "./effect-geometry.js";
|
|
6
|
-
import { effectStockSnapshot, effectStockAllows } from "./effect-stock-snapshot.js";
|
|
5
|
+
import { effectStockSnapshot, effectStockAllows, effectStockRefusal } from "./effect-stock-snapshot.js";
|
|
7
6
|
import { effectTrialStage, effectTrialStageIdentity, watchEffectTrialStage } from "./effect-trial-stage.js";
|
|
8
7
|
import { EffectBudget } from "./effect-profiler.js";
|
|
9
8
|
|
|
10
9
|
const registries = new WeakMap();
|
|
11
10
|
const plain = o => !!o && (Object.getPrototypeOf(o) === Object.prototype || Object.getPrototypeOf(o) === null);
|
|
12
11
|
const finite = v => typeof v === "number" && Number.isFinite(v);
|
|
13
|
-
const FLAGS = ["safe", "publicLink", "reducedMotion", "
|
|
12
|
+
const FLAGS = ["safe", "publicLink", "reducedMotion", "hidden", "enabled", "show", "transportReady", "constrained", "modal"];
|
|
14
13
|
const TYPES = ["state", "reply", "theme"];
|
|
15
14
|
function unsafeSnapshotDescriptor(d) {
|
|
16
15
|
if (!plain(d) || !plain(d.params ?? {})) return null;
|
|
@@ -41,9 +40,9 @@ function snapshotDescriptor(d) { try { return unsafeSnapshotDescriptor(d); } cat
|
|
|
41
40
|
*/
|
|
42
41
|
export function createEffectRegistry({ window: host, origin, boot, load, safety, stockSnapshot, onChange = () => {}, onProfile = () => {}, onMount = () => {}, onRetire = () => {}, clock = () => host.performance.now() } = {}) {
|
|
43
42
|
try { if (!host?.document || !host.document.body || registries.has(host) || typeof load !== "function" || typeof stockSnapshot !== "function" || typeof onChange !== "function" || typeof onProfile !== "function" || typeof onMount !== "function" || typeof onRetire !== "function" || typeof clock !== "function" || !safety || ["allowed", "begin", "finish", "pause", "resume"].some(k => typeof safety[k] !== "function") || ["requestAnimationFrame", "cancelAnimationFrame", "setTimeout", "clearTimeout", "MutationObserver", "ResizeObserver", "AbortController"].some(k => typeof host[k] !== "function")) return { ok: false, reason: "registry_unsupported" }; } catch { return { ok: false, reason: "registry_unsupported" }; }
|
|
44
|
-
let dead = false, blocked = null, revision = 0, last = null, lastView = null, frameSerial = 0, context = null, descriptors = [],
|
|
43
|
+
let dead = false, blocked = null, revision = 0, last = null, lastView = null, frameSerial = 0, context = null, descriptors = [], raf = null, timer = null;
|
|
45
44
|
const slots = new Map(), loads = new Set(), paused = new Map(), offs = [], observed = new Set(), failures = new Map(), results = new Map();
|
|
46
|
-
let readingStock = false;
|
|
45
|
+
let readingStock = false, stockFailure = "stock_evidence_invalid", warned = null;
|
|
47
46
|
let root, geometry, observer, resize, longTasks = null, longTasksSupported = false;
|
|
48
47
|
const time = () => {
|
|
49
48
|
let n; try { n = clock(); } catch { return null; }
|
|
@@ -69,10 +68,14 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
69
68
|
const coverage = (d = null) => {
|
|
70
69
|
if (dead || blocked || readingStock) return null;
|
|
71
70
|
const before = revision; let proof = null; readingStock = true;
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
let failure = "stock_provider_failed";
|
|
72
|
+
try { const raw = stockSnapshot(Object.freeze({ frameRoot: root })); proof = effectStockSnapshot(raw); failure = proof ? null : effectStockRefusal(raw); }
|
|
73
|
+
catch { /* unavailable stays denied, with a local diagnostic */ }
|
|
74
74
|
finally { readingStock = false; }
|
|
75
75
|
if (dead || blocked || before !== revision) return null;
|
|
76
|
+
stockFailure = failure;
|
|
77
|
+
if (failure !== warned) { warned = failure; if (failure) { try { host.console?.warn("[AgEnD effects] Playback blocked:", failure); } catch { /* display only */ } } }
|
|
78
|
+
if (dead || blocked || before !== revision) return null;
|
|
76
79
|
return d === null || permits(d, proof) ? proof : null;
|
|
77
80
|
};
|
|
78
81
|
const position = (d, proof) => d.trialStage ? localStage(d, proof)?.rect ?? null : hostEffectRect(d.element);
|
|
@@ -119,7 +122,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
119
122
|
if (s.request) { s.request.controller.abort(); if (s.request.timer !== null) host.clearTimeout(s.request.timer); }
|
|
120
123
|
// A closed/unknown viewer gate revokes paint, not the effect identity. Fresh known gates
|
|
121
124
|
// may admit it again; resource failures and actual safety pauses retain their latches.
|
|
122
|
-
if (!["disposed", "superseded", "hidden", "human_resume", "
|
|
125
|
+
if (!["disposed", "superseded", "hidden", "human_resume", "geometry_changed", "gates_unknown"].includes(why)) failures.set(s.d.key, { identity: signature(s.d), reason: why });
|
|
123
126
|
s.stageOff?.(); s.stageOff = null;
|
|
124
127
|
s.frame?.remove(why); s.geometry?.remove();
|
|
125
128
|
if (s.loadOff) s.loadOff();
|
|
@@ -138,12 +141,11 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
138
141
|
retire(s, why);
|
|
139
142
|
if (revision !== proposed || dead !== wasDead) return false;
|
|
140
143
|
}
|
|
141
|
-
if (calibration) { calibration.cancel(); calibration = null; }
|
|
142
144
|
return true;
|
|
143
145
|
}
|
|
144
146
|
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); }
|
|
145
147
|
function eligible(d = null) {
|
|
146
|
-
return gatesKnown() && !context.safe && !context.reducedMotion &&
|
|
148
|
+
return gatesKnown() && !context.safe && !context.reducedMotion && context.enabled && context.show && context.transportReady
|
|
147
149
|
&& (!d || !context.publicLink || d.region === "chat" && !d.trialStage);
|
|
148
150
|
}
|
|
149
151
|
const globalAllowed = (d = null) => eligible(d) && host.document.visibilityState === "visible";
|
|
@@ -152,7 +154,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
152
154
|
const shown = why => (why === "gates_unknown" && !eligible() && gateReason()) || why;
|
|
153
155
|
function view() {
|
|
154
156
|
return descriptors.map(d => {
|
|
155
|
-
const r = results.get(d.key) ?? { verdict: "off", reason: blocked ?? (
|
|
157
|
+
const r = results.get(d.key) ?? { verdict: "off", reason: blocked ?? (eligible() ? stockFailure : gateReason()) ?? "runtime_unavailable", queuePos: null }, s = slots.get(d.key);
|
|
156
158
|
// #2055 one display precedence: a global latch, then the sticky safety history, then this page's own pause/failure.
|
|
157
159
|
const n = named.get(d.key), sticky = n && n.identity === signature(d) ? n.reason : null;
|
|
158
160
|
return { key: d.key, id: d.id, ...r, ...(sticky && r.verdict === "queued" ? { verdict: "off", queuePos: null } : {}), phase: s?.frame ? s.ready ? "running" : "starting" : s ? "loading" : null,
|
|
@@ -175,7 +177,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
175
177
|
};
|
|
176
178
|
function observe() {
|
|
177
179
|
const nodes = host.document.querySelectorAll("[data-trust]");
|
|
178
|
-
if (nodes.length >
|
|
180
|
+
if (nodes.length > 4096) throw Error("trust coverage unavailable");
|
|
179
181
|
const wanted = new Set([host.document.documentElement, ...nodes]);
|
|
180
182
|
for (const d of descriptors) if (d.element?.ownerDocument === host.document && d.element.isConnected === true) wanted.add(d.element);
|
|
181
183
|
for (const n of observed) if (!wanted.has(n)) { resize.unobserve(n); observed.delete(n); }
|
|
@@ -188,6 +190,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
188
190
|
if (time() === null) blocked = "clock_unavailable";
|
|
189
191
|
const hidden = context?.hidden === true || host.document.visibilityState !== "visible";
|
|
190
192
|
const stock = eligible() ? coverage() : null;
|
|
193
|
+
if (dead || proposal !== revision) return;
|
|
191
194
|
if (blocked || !eligible() || !stock) {
|
|
192
195
|
if (!stopAll(blocked ?? "gates_unknown")) return;
|
|
193
196
|
results.clear();
|
|
@@ -221,10 +224,9 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
221
224
|
if (!chosen.has(s.d.key) || !d || signature(d) !== signature(s.d) || d.element !== s.d.element || s.device !== device()) retire(s, "superseded");
|
|
222
225
|
if (dead || proposal !== revision) return;
|
|
223
226
|
}
|
|
224
|
-
if (chosen.size === 0) {
|
|
227
|
+
if (chosen.size === 0) { tell(); return; }
|
|
225
228
|
if (hidden) {
|
|
226
229
|
hideGeometry();
|
|
227
|
-
if (calibration) { calibration.cancel(); calibration = null; }
|
|
228
230
|
for (const s of [...slots.values()]) {
|
|
229
231
|
if (!s.frame) retire(s, "hidden");
|
|
230
232
|
else { if (s.hiddenSince === null) { s.hiddenSince = last; s.healthySince = null; } if (s.budget.gates({ hidden: true, reducedMotion: false, saveData: false, show: true }).remove) retire(s, "hidden"); }
|
|
@@ -237,19 +239,9 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
237
239
|
if (s.frame && s.hiddenSince !== null) { s.hiddenSince = null; s.healthySince = last; if (s.budget.gates({ hidden: false, reducedMotion: false, saveData: false, show: true }).remove) retire(s, "hidden"); }
|
|
238
240
|
if (dead || proposal !== revision) return;
|
|
239
241
|
}
|
|
240
|
-
try { observe(); } catch {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (!stopAll("calibrating") || dead || proposal !== revision) return;
|
|
244
|
-
const epoch = ++serial;
|
|
245
|
-
const job = captureEffectBaseline({ window: host, empty: () => !dead && slots.size === 0 && host.document.querySelectorAll("iframe.agend-effect-frame").length === 0 && globalAllowed() && !context.hidden && !!coverage(), clock: nativeClock });
|
|
246
|
-
calibration = job; baselineDevice = device();
|
|
247
|
-
job.promise.then(r => {
|
|
248
|
-
if (dead || calibration !== job || epoch !== serial) return;
|
|
249
|
-
calibration = null;
|
|
250
|
-
if (!r.ok) { blocked = r.reason; tell(); return; }
|
|
251
|
-
baseline = r.baselineFps; reconcile();
|
|
252
|
-
});
|
|
242
|
+
try { observe(); } catch {
|
|
243
|
+
if (dead || proposal !== revision || !stopAll("trust_geometry_unknown")) return;
|
|
244
|
+
for (const d of descriptors) results.set(d.key, { verdict: "off", reason: "trust_geometry_unknown", queuePos: null });
|
|
253
245
|
tell(); return;
|
|
254
246
|
}
|
|
255
247
|
const max = context.constrained ? 1 : 3;
|
|
@@ -321,7 +313,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
321
313
|
if (d.trialStage && !local || !current(s)) { if (current(s)) retire(s, "admission_revoked"); return; }
|
|
322
314
|
if (local) fresh = local.rect;
|
|
323
315
|
s.rect = fresh;
|
|
324
|
-
s.budget = new EffectBudget({ device: s.device,
|
|
316
|
+
s.budget = new EffectBudget({ device: s.device, clock: nativeClock, longTasksSupported });
|
|
325
317
|
const frame = createOpaqueEffectFrame({ window: host, root: local?.root ?? root, origin, boot, clock: nativeClock,
|
|
326
318
|
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) },
|
|
327
319
|
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(); },
|
|
@@ -511,11 +503,11 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
511
503
|
revalidateSafety,
|
|
512
504
|
/** Called only by the host's explicit human Resume UI. A descriptor update never clears this latch. */
|
|
513
505
|
resume(key) {
|
|
514
|
-
if (dead || reentrantStock() || blocked
|
|
506
|
+
if (dead || reentrantStock() || blocked || typeof key !== "string") return false;
|
|
515
507
|
const proposed = revision, d = descriptors.find(d => d.key === key); if (!d || d.trialStage && !coverage(d) || revision !== proposed || receipt(safety.resume, d.id, device()) !== true || revision !== proposed) return false;
|
|
516
508
|
revision++;
|
|
517
509
|
if (!stopAll("human_resume")) return false;
|
|
518
|
-
if (blocked
|
|
510
|
+
if (blocked) return false; paused.delete(key); failures.delete(key); reconcile(); return true;
|
|
519
511
|
},
|
|
520
512
|
/** Host-only content-free signal API. Raw pointer publishing is refused; native pointer metadata is coalesced here. */
|
|
521
513
|
publish(type, fields, instance = null) {
|
|
@@ -531,7 +523,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
531
523
|
return sent;
|
|
532
524
|
},
|
|
533
525
|
dispose() {
|
|
534
|
-
if (dead) return; dead = true; revision++;
|
|
526
|
+
if (dead) return; dead = true; revision++; stopAll("disposed");
|
|
535
527
|
if (raf !== null) host.cancelAnimationFrame(raf); if (timer !== null) host.clearTimeout(timer);
|
|
536
528
|
observer.disconnect(); resize.disconnect(); longTasks?.disconnect(); for (const off of offs) off();
|
|
537
529
|
geometry.dispose(); root.remove(); observed.clear(); if (loads.size === 0 && registries.get(host) === api) registries.delete(host);
|
|
@@ -32,3 +32,12 @@ export function effectStockAllows(snapshot, { layer, by } = {}) {
|
|
|
32
32
|
const proof = effectStockSnapshot(snapshot);
|
|
33
33
|
return !!proof && (layer === "behind" || layer === "front" && proof.front && (by === "human" || proof.agentFront));
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/** Display only. Refused provider metadata never becomes paint evidence or authority. */
|
|
37
|
+
export function effectStockRefusal(value) {
|
|
38
|
+
try {
|
|
39
|
+
if (!plain(value)) return "stock_evidence_invalid";
|
|
40
|
+
const d = Object.getOwnPropertyDescriptor(value, "reason");
|
|
41
|
+
return d && Object.hasOwn(d, "value") && typeof d.value === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(d.value) ? d.value : "stock_evidence_invalid";
|
|
42
|
+
} catch { return "stock_evidence_invalid"; }
|
|
43
|
+
}
|
|
@@ -11,7 +11,7 @@ const SPECS = [
|
|
|
11
11
|
["confirmation", ".shell > .pending-stack, .shell .p-settings > .panel-body > .col > .pending-stack.inline"], ["preview", ".shell .chat-split > .pv-panel, .shell .chat-split > .pv-divider"],
|
|
12
12
|
["preview-menu", ".shell .thread details.pv-menu"], ["palette", ".shell .dock .cmd-palette"],
|
|
13
13
|
["drop", ".shell .p-chat > .drop-overlay"],
|
|
14
|
-
["thread-control", ".shell .p-chat .jump-latest, .shell .thread .msg-tools, .shell .thread .st-more, .shell .thread .cb-head, .shell .thread .cb-more, .shell .thread .tc-head, .shell .thread .tc-more, .shell .thread .pv-head, .shell .thread .pv-note, .shell .thread .pv-banner, .shell .thread .effect-trial-offer, .shell .thread .click-chip"],
|
|
14
|
+
["thread-control", ".shell .chat-main > .amb-note, .shell .p-chat .jump-latest, .shell .thread .msg-tools, .shell .thread .st-more, .shell .thread .cb-head, .shell .thread .cb-more, .shell .thread .tc-head, .shell .thread .tc-more, .shell .thread .pv-head, .shell .thread .pv-note, .shell .thread .pv-banner, .shell .thread .effect-trial-offer, .shell .thread .click-chip"],
|
|
15
15
|
["link", ".shell .thread .md a, .shell .thread .st-text > a.st-link, .shell .dock .prompts .txt > a.st-link"], ["attachment", ".shell .thread .atts"], ["media", ".shell .thread .att-media"],
|
|
16
16
|
["panel-navigation", ".shell .p-fleet > .seg, .shell .p-settings > .seg"],
|
|
17
17
|
["panel-content", ".shell .p-fleet > .panel-body, .shell .p-needs > .panel-body, .shell .p-settings > .panel-body, .shell .p-log > .panel-body, .shell .p-details > .panel-body, .shell .p-view > .panel-body, .shell .p-view > .v-term, .shell .p-view > .v-id, .shell .p-view > .v-card"],
|
|
@@ -20,7 +20,7 @@ const SPECS = [
|
|
|
20
20
|
];
|
|
21
21
|
export const STOCK_TRUST_INVENTORY = Object.freeze(SPECS.map(([kind, selector]) => Object.freeze({ kind, selector })));
|
|
22
22
|
const STRUCTURAL = ["html", "body", "#app", "#app > .shell", ".shell > .main", ".main > .p-chat", ".p-chat > .chat-split", ".chat-split > .chat-main"];
|
|
23
|
-
const CONTENT = [".main > .conn", ".p-chat > .panel-head", ".p-chat > .chat-card", ".p-chat > .aw-chat_header", ".chat-main > .scroller", ".chat-main > .dock", ".sidebar > .side-head", ".sidebar > .side-nav", ".sidebar > .side-section", ".sidebar > .side-widget", ".sidebar > .side-foot", ".u-rail > .u-rail-head", ".u-rail > .u-rail-body"];
|
|
23
|
+
const CONTENT = [".main > .conn", ".p-chat > .panel-head", ".p-chat > .chat-card", ".p-chat > .aw-chat_header", ".chat-main > .amb-note", ".chat-main > .scroller", ".chat-main > .dock", ".sidebar > .side-head", ".sidebar > .side-nav", ".sidebar > .side-section", ".sidebar > .side-widget", ".sidebar > .side-foot", ".u-rail > .u-rail-head", ".u-rail > .u-rail-body"];
|
|
24
24
|
// Closed producer/topology inventory. Bare Outlet loading/error panels never become an effect surface.
|
|
25
25
|
const PANELS = Object.freeze({
|
|
26
26
|
fleet: { required: [".panel-head", ".seg", ".panel-body"], optional: [] },
|
|
@@ -115,13 +115,36 @@ function stockBannerFill(host, el) {
|
|
|
115
115
|
if (b.display !== "block" || el.offsetParent !== box) return null;
|
|
116
116
|
return rect(box);
|
|
117
117
|
}
|
|
118
|
+
function zeroClipMargin(host, style) {
|
|
119
|
+
const margin = style.overflowClipMargin ?? style.getPropertyValue?.("overflow-clip-margin");
|
|
120
|
+
// WebKit versions without this property still implement paint containment. An absent
|
|
121
|
+
// computed value is usable only when the engine explicitly reports it unsupported;
|
|
122
|
+
// unknown capability or a supported but unreadable/expanded margin cannot prove a box.
|
|
123
|
+
return margin === "0px" || [undefined, ""].includes(margin)
|
|
124
|
+
&& host.CSS?.supports?.("overflow-clip-margin", "1px") === false;
|
|
125
|
+
}
|
|
118
126
|
function protectedPaint(host, root, budget) {
|
|
127
|
+
// These two stock toolbar boxes physically clip ALL descendants, including fixed-position
|
|
128
|
+
// children. Their entire box is a hole; descendant ink cannot escape it. Inline, fragmented,
|
|
129
|
+
// expanded-clip or CSS-overridden boxes take the ordinary bounded walk instead.
|
|
130
|
+
const s = host.getComputedStyle(root);
|
|
131
|
+
if (root.matches?.(".shell .thread .msg-tools, .shell .thread .codeblock > .cb-head") && s.contain === "paint"
|
|
132
|
+
&& s.display === "flex" && s.overflowX === "clip" && s.overflowY === "clip" && zeroClipMargin(host, s)
|
|
133
|
+
&& none(s.filter) && none(s.backdropFilter) && s.borderImageSource === "none" && [undefined, "", "visible"].includes(s.contentVisibility) && root.getClientRects().length === 1) {
|
|
134
|
+
if (++budget.n > 4096) { budget.exceeded = true; return null; }
|
|
135
|
+
const r = rect(root), pad = outset(s);
|
|
136
|
+
// Containment clips descendants, not the root's own ink. Do not infer a world-space
|
|
137
|
+
// outset from local CSS lengths under a potentially transformed content ancestor.
|
|
138
|
+
if (!r || pad !== 0) return null;
|
|
139
|
+
return r;
|
|
140
|
+
}
|
|
119
141
|
const nodes = [root, ...root.querySelectorAll("*")]; budget.n += nodes.length;
|
|
120
|
-
if (budget.n > 4096) return null;
|
|
142
|
+
if (budget.n > 4096) { budget.exceeded = true; return null; }
|
|
121
143
|
let union = null;
|
|
122
144
|
for (const el of nodes) {
|
|
123
145
|
if (!visible(host, el)) continue;
|
|
124
146
|
const style = host.getComputedStyle(el), pad = outset(style);
|
|
147
|
+
if (style.borderImageSource !== undefined && style.borderImageSource !== "none" || ![undefined, "", "visible"].includes(style.contentVisibility)) return null;
|
|
125
148
|
const r = none(style.filter) ? rect(el) : stockBannerFill(host, el);
|
|
126
149
|
if (!r || pad === null || !none(style.backdropFilter)) return null;
|
|
127
150
|
const a = { x: r.x - pad, y: r.y - pad, width: r.width + pad * 2, height: r.height + pad * 2 };
|
|
@@ -165,15 +188,23 @@ export function stockEffectTrust(host, { frameRoot = null, ownerCss = "unknown"
|
|
|
165
188
|
if (s.position !== "relative" || s.zIndex !== "1" || !neutral(s)) return fail("stock_order_unknown");
|
|
166
189
|
}
|
|
167
190
|
const marked = [...doc.querySelectorAll("[data-trust]")], expected = new Set(), rects = [], budget = { n: 0 };
|
|
168
|
-
if (marked.length >
|
|
191
|
+
if (marked.length > 4096) return fail("stock_marker_limit");
|
|
169
192
|
for (const { kind, selector } of STOCK_TRUST_INVENTORY) for (const el of doc.querySelectorAll(selector)) {
|
|
170
193
|
if (el.ownerDocument !== doc || el.isConnected !== true || el.getAttribute("data-trust") !== kind) return fail("stock_trust_unknown");
|
|
171
194
|
expected.add(el);
|
|
172
195
|
}
|
|
173
196
|
if (marked.some(el => !expected.has(el)) || surface.kind === "chat" && !chat.querySelector('.dock[data-trust="composer"]') || !surface.panel.querySelector('.panel-head[data-trust="header"]')) return fail("stock_trust_unknown");
|
|
174
197
|
for (const el of marked) {
|
|
175
|
-
const bounds = protectedPaint(host, el, budget); if (!bounds) return fail("stock_paint_unknown");
|
|
176
|
-
|
|
198
|
+
const bounds = protectedPaint(host, el, budget); if (!bounds) return fail(budget.exceeded ? "stock_paint_limit" : "stock_paint_unknown");
|
|
199
|
+
// Off-screen history remains inventoried and paint-checked; it need not consume a
|
|
200
|
+
// viewport hole. Include the clip algorithm's extra 8px protection margin.
|
|
201
|
+
if (bounds.width && bounds.height && (!finite(host.innerHeight) || bounds.x + bounds.width + 8 >= 0 && bounds.y + bounds.height + 8 >= 0 && bounds.x - 8 <= host.innerWidth && bounds.y - 8 <= host.innerHeight)) rects.push(bounds);
|
|
202
|
+
}
|
|
203
|
+
// Keep the existing 128-hole transport bound. Conservative union may hide extra
|
|
204
|
+
// effect pixels; it can never reveal protected pixels, even for a dense viewport.
|
|
205
|
+
if (rects.length > 128) {
|
|
206
|
+
const rest = rects.splice(127), x = Math.min(...rest.map(r => r.x)), y = Math.min(...rest.map(r => r.y));
|
|
207
|
+
rects.push({ x, y, width: Math.max(...rest.map(r => r.x + r.width)) - x, height: Math.max(...rest.map(r => r.y + r.height)) - y });
|
|
177
208
|
}
|
|
178
209
|
if (doc.querySelector(".agend-session-ended")) return fail("session_ended");
|
|
179
210
|
let barrier = [...doc.querySelectorAll(BARRIERS)].some(el => visible(host, el));
|
|
@@ -64,7 +64,7 @@ export function createEffectTrialSource({ window: host, current, ownerCurrent, s
|
|
|
64
64
|
const lease = { identity, reply, stage, owner, current: x.current, onStop: x.onStop, startedAt: x.startedAt, until, dead: false, checking: false, pending: new Set(), timer: null, off: null, binding: null };
|
|
65
65
|
lease.binding = Object.freeze({ kind: "trial", key: identity.key, id: identity.id, source: lease, generation: identity.generation });
|
|
66
66
|
active = lease; used.add(stage.receipt);
|
|
67
|
-
lease.off = watchEffectTrialStage(stage.receipt,
|
|
67
|
+
lease.off = watchEffectTrialStage(stage.receipt, reason => stop(lease, typeof reason === "string" ? reason : "stage_ended"));
|
|
68
68
|
if (typeof lease.off !== "function" || constrainEffectTrialStage(stage.receipt, () => checked(lease)) !== true || !checked(lease)) { stop(lease, "stage_unavailable"); return null; }
|
|
69
69
|
try { arm(lease); } catch { stop(lease, "trial_unavailable"); return null; }
|
|
70
70
|
if (!checked(lease)) return null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** #1629: host-issued LOCAL trial paint receipt. No authority, loader, frame factory or pool is created here. */
|
|
2
2
|
import { effectTrialShadow } from "./effect-trial-shadow.js";
|
|
3
3
|
import { effectHostStyle } from "./effect-host-style.js";
|
|
4
|
-
import { effectStockSnapshot } from "./effect-stock-snapshot.js";
|
|
4
|
+
import { effectStockSnapshot, effectStockRefusal } from "./effect-stock-snapshot.js";
|
|
5
5
|
import { STOCK_EFFECT_BARRIERS } from "./effect-stock-trust.js";
|
|
6
6
|
const receipts = new WeakMap(), owners = new WeakMap();
|
|
7
7
|
let serial = 0;
|
|
@@ -64,7 +64,8 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
|
|
|
64
64
|
if (!own || typeof current !== "function" || typeof onStop !== "function" || !host?.document || !panel || !stage || owners.has(stage)) return null;
|
|
65
65
|
const shell = effectTrialShadow(host, panel), hostSheet = effectHostStyle(host);
|
|
66
66
|
let sheet; try { sheet = shell?.sheet; if (!shell || shell.stage !== stage || !hostSheet || !sheet || host.CSS?.supports("contain", "paint") !== true || host.CSS.supports("overflow", "clip") !== true) return null; } catch { return null; }
|
|
67
|
-
const receipt = Object.freeze(Object.create(null)), frames = new Set(), listeners = new Set(); let dead = false, constraint = null;
|
|
67
|
+
const receipt = Object.freeze(Object.create(null)), frames = new Set(), listeners = new Set(); let dead = false, constraint = null, failure = "stage_unavailable";
|
|
68
|
+
const reject = reason => { failure = reason; return null; };
|
|
68
69
|
const identity = Object.freeze({ key: own.key, id: own.id, instance: own.instance, generation: own.generation, serial: ++serial });
|
|
69
70
|
const invalidate = reason => {
|
|
70
71
|
if (dead) return; dead = true; receipts.delete(receipt); if (owners.get(stage) === receipt) owners.delete(stage);
|
|
@@ -76,45 +77,49 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
|
|
|
76
77
|
};
|
|
77
78
|
const blur = event => { if (event.target === host) invalidate("blur"); };
|
|
78
79
|
function check(stock) {
|
|
80
|
+
failure = "trial_layout_unverified";
|
|
79
81
|
try {
|
|
80
82
|
// This callback may reenter disposal or replace the owner. Recheck identity after it returns.
|
|
81
|
-
if (dead || current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead) return
|
|
83
|
+
if (dead || current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead) return reject("trial_current_changed");
|
|
82
84
|
const proof = effectStockSnapshot(stock), doc = host.document;
|
|
83
|
-
if (
|
|
85
|
+
if (!proof) return reject(effectStockRefusal(stock));
|
|
86
|
+
if (effectHostStyle(host) !== hostSheet || effectTrialShadow(host, panel) !== shell) return reject("trial_style_unavailable");
|
|
84
87
|
// The own panel MUST still suppress ordinary foreground effects. Never synthesize front:true.
|
|
85
|
-
if (
|
|
88
|
+
if (doc.hasFocus?.() !== true) return reject("trial_window_unfocused");
|
|
89
|
+
if (doc.visibilityState !== "visible") return reject("hidden");
|
|
90
|
+
if (proof.front !== false || doc.fullscreenElement || doc.pictureInPictureElement) return reject("trial_overlay_unverified");
|
|
86
91
|
const split = doc.querySelector(".main > .p-chat > .chat-split");
|
|
87
92
|
if (!split || doc.querySelectorAll(".main > .p-chat").length !== 1 || doc.querySelectorAll(".chat-split > .pv-panel").length !== 1
|
|
88
|
-
|| panel.parentElement !== split || panel.ownerDocument !== doc || panel.isConnected !== true || panel.className !== "pv-panel effect-trial-panel" || panel.getAttribute("data-trust") !== "preview") return
|
|
93
|
+
|| panel.parentElement !== split || panel.ownerDocument !== doc || panel.isConnected !== true || panel.className !== "pv-panel effect-trial-panel" || panel.getAttribute("data-trust") !== "preview") return reject("trial_panel_unavailable");
|
|
89
94
|
const children = [...shell.root.children], names = ["effect-trial-style", "effect-trial-head", "effect-trial-controls", "effect-trial-body", "effect-trial-note"];
|
|
90
95
|
if (panel.children.length !== 0 || children.length !== names.length || children.some((el, i) => el.className !== names[i]) || children[3].children.length !== 1 || children[3].firstElementChild !== stage
|
|
91
|
-
|| stage.className !== "effect-trial-stage" || stage.ownerDocument !== doc || stage.isConnected !== true || stage.hasAttribute("tabindex") || stage.hasAttribute("contenteditable")) return
|
|
96
|
+
|| stage.className !== "effect-trial-stage" || stage.ownerDocument !== doc || stage.isConnected !== true || stage.hasAttribute("tabindex") || stage.hasAttribute("contenteditable")) return reject("trial_shadow_unverified");
|
|
92
97
|
for (const control of [shell.head, shell.controls, shell.close, shell.stop, shell.note]) {
|
|
93
98
|
const style = host.getComputedStyle(control);
|
|
94
|
-
if (style.display === "none" || style.visibility !== "visible" || style.opacity !== "1" || !rect(control)) return
|
|
99
|
+
if (style.display === "none" || style.visibility !== "visible" || style.opacity !== "1" || !rect(control)) return reject("trial_controls_unavailable");
|
|
95
100
|
}
|
|
96
101
|
if ([...doc.querySelectorAll(STOCK_EFFECT_BARRIERS)].some(el => el !== panel && visible(host, el))
|
|
97
|
-
|| [...doc.querySelectorAll(".pv-panel")].some(el => el !== panel && visible(host, el))) return
|
|
102
|
+
|| [...doc.querySelectorAll(".pv-panel")].some(el => el !== panel && visible(host, el))) return reject("trial_other_overlay");
|
|
98
103
|
const active = doc.activeElement;
|
|
99
|
-
if (active && active !== doc.body && active !== doc.documentElement && (active.matches("input,select,textarea,[contenteditable]") || active.isContentEditable)) return
|
|
104
|
+
if (active && active !== doc.body && active !== doc.documentElement && (active.matches("input,select,textarea,[contenteditable]") || active.isContentEditable)) return reject("trial_editable_focus");
|
|
100
105
|
let el = stage, depth = 0;
|
|
101
|
-
while (el) { const s = host.getComputedStyle(el); if (++depth > 128 || el.ownerDocument !== doc || !neutral(s) || s.opacity !== "1" || s.visibility !== "visible" || s.display === "none" || s.contain !== (el === stage ? "paint" : "none")) return
|
|
102
|
-
if (el !== doc.documentElement) return
|
|
106
|
+
while (el) { const s = host.getComputedStyle(el); if (++depth > 128 || el.ownerDocument !== doc || !neutral(s) || s.opacity !== "1" || s.visibility !== "visible" || s.display === "none" || s.contain !== (el === stage ? "paint" : "none")) return reject("trial_ancestor_paint"); if (el === doc.documentElement) break; el = el.parentElement ?? (el.getRootNode?.() === shell.root ? panel : null); }
|
|
107
|
+
if (el !== doc.documentElement) return reject("trial_ancestor_paint");
|
|
103
108
|
const s = host.getComputedStyle(stage), r = rect(stage), p = rect(panel);
|
|
104
109
|
if (!r || !p || s.position !== "relative" || s.zIndex !== "0" || s.overflowX !== "clip" || s.overflowY !== "clip"
|
|
105
110
|
|| s.paddingLeft !== "0px" || s.paddingRight !== "0px" || s.paddingTop !== "0px" || s.paddingBottom !== "0px"
|
|
106
111
|
|| ["borderLeftWidth", "borderRightWidth", "borderTopWidth", "borderBottomWidth"].some(k => s[k] !== "0px") || !none(s.boxShadow) || s.outlineStyle !== "none"
|
|
107
112
|
|| r.x < p.x || r.y < p.y || r.x + r.width > p.x + p.width || r.y + r.height > p.y + p.height
|
|
108
|
-
|| r.x < 0 || r.y < 0 || r.x + r.width > host.innerWidth || r.y + r.height > host.innerHeight) return
|
|
109
|
-
if ([...stage.children].some(child => ![...frames].some(f => f.element === child))) return
|
|
113
|
+
|| r.x < 0 || r.y < 0 || r.x + r.width > host.innerWidth || r.y + r.height > host.innerHeight) return reject("trial_clip_unverified");
|
|
114
|
+
if ([...stage.children].some(child => ![...frames].some(f => f.element === child))) return reject("trial_frame_unknown");
|
|
110
115
|
const width = Math.floor(r.width), height = Math.floor(r.height);
|
|
111
|
-
if (width < 1 || height < 1 || width > 4096 || height > 4096 || width * height > 8388608 || dead || owners.get(stage) !== receipt) return
|
|
112
|
-
if (current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead || stage.isConnected !== true || panel.parentElement !== split) return
|
|
116
|
+
if (width < 1 || height < 1 || width > 4096 || height > 4096 || width * height > 8388608 || dead || owners.get(stage) !== receipt) return reject("trial_size_limit");
|
|
117
|
+
if (current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead || stage.isConnected !== true || panel.parentElement !== split) return reject("trial_current_changed");
|
|
113
118
|
return { x: r.x, y: r.y, width, height };
|
|
114
|
-
} catch { return
|
|
119
|
+
} catch { return reject("trial_check_failed"); }
|
|
115
120
|
}
|
|
116
121
|
function project(stock) {
|
|
117
|
-
const r = check(stock); if (!r) { invalidate(
|
|
122
|
+
const r = check(stock); if (!r) { invalidate(failure); return null; }
|
|
118
123
|
return Object.freeze({ root: stage, rect: Object.freeze(r), identity, bind });
|
|
119
124
|
}
|
|
120
125
|
function bind(frame, { layer } = {}, stock) {
|
|
@@ -131,7 +136,7 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
|
|
|
131
136
|
};
|
|
132
137
|
const handle = { ok: true, element: frame, remove, hide,
|
|
133
138
|
sync(proof) {
|
|
134
|
-
const r = check(proof); if (!r || removed) { invalidate(
|
|
139
|
+
const r = check(proof); if (!r || removed) { invalidate(failure); return false; }
|
|
135
140
|
try {
|
|
136
141
|
const matches = visibility => {
|
|
137
142
|
const s = host.getComputedStyle(frame), actual = rect(frame);
|
|
@@ -146,7 +151,7 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
|
|
|
146
151
|
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");
|
|
147
152
|
if (!matches("hidden")) throw Error("local geometry lost");
|
|
148
153
|
rule.style.setProperty("visibility", "visible", "important"); shown = true; return true;
|
|
149
|
-
} catch { invalidate("
|
|
154
|
+
} catch { invalidate("trial_frame_geometry"); return false; }
|
|
150
155
|
},
|
|
151
156
|
};
|
|
152
157
|
frames.add(handle);
|
|
@@ -154,7 +159,8 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
|
|
|
154
159
|
const i = sheet.insertRule(`.${token}{}`, sheet.cssRules.length); rule = sheet.cssRules[i];
|
|
155
160
|
for (const [k, v] of Object.entries({ position: "absolute", visibility: "hidden", "pointer-events": "none", "z-index": "0", opacity: layer === "front" ? "0.85" : "1", border: "0px", margin: "0px", padding: "0px", "box-sizing": "border-box", "min-width": "0px", "min-height": "0px", "max-width": "none", "max-height": "none", outline: "none", "box-shadow": "none", "background-color": "transparent", animation: "none", transition: "none" })) rule.style.setProperty(k, v, "important");
|
|
156
161
|
frame.classList.add(token);
|
|
157
|
-
if (host.getComputedStyle(frame).visibility !== "hidden"
|
|
162
|
+
if (host.getComputedStyle(frame).visibility !== "hidden") throw Error("initial local hiding lost");
|
|
163
|
+
if (!check(stock)) { remove(); invalidate(failure); return { ok: false }; }
|
|
158
164
|
} catch { remove(); invalidate("stage_unavailable"); return { ok: false }; }
|
|
159
165
|
return handle;
|
|
160
166
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
overflow: clip !important;
|
|
17
17
|
background: #ffffff !important;
|
|
18
18
|
color: #1b1b1f !important;
|
|
19
|
-
font: 400 16px/1.4 system-ui, sans-serif !important;
|
|
19
|
+
font: 400 16px/1.4 system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji !important;
|
|
20
20
|
direction: ltr !important;
|
|
21
21
|
unicode-bidi: isolate !important;
|
|
22
22
|
text-align: start !important;
|
|
@@ -5,19 +5,27 @@ export const FX_FOLLOW_REDUCED_MOTION_KEY = "agend_fx_follow_reduced_motion";
|
|
|
5
5
|
/** The one-time chat note "your system asks for reduced motion; effects are on" was shown on this device. */
|
|
6
6
|
export const FX_MOTION_NOTE_KEY = "agend_fx_motion_note_seen";
|
|
7
7
|
const listeners = new Set();
|
|
8
|
+
let pageChoice = null, noteShown = false;
|
|
8
9
|
|
|
9
10
|
/** Whether this device asked effects to follow the system's reduced-motion setting (default: no). */
|
|
10
11
|
export function followsReducedMotion() {
|
|
12
|
+
if (pageChoice !== null) return pageChoice;
|
|
11
13
|
try { return globalThis.localStorage.getItem(FX_FOLLOW_REDUCED_MOTION_KEY) === "1"; } catch { return false; }
|
|
12
14
|
}
|
|
13
15
|
export function setFollowsReducedMotion(on) {
|
|
16
|
+
pageChoice = !!on;
|
|
14
17
|
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
18
|
for (const fn of [...listeners]) { try { fn(); } catch { /* a listener's own */ } }
|
|
16
19
|
}
|
|
17
20
|
/** `fn()` when the switch or the system's answer moves (this tab, another tab, the OS). Returns the unsubscribe. */
|
|
18
21
|
export function onMotionChange(fn) {
|
|
19
22
|
listeners.add(fn);
|
|
20
|
-
const onStorage = (e) => {
|
|
23
|
+
const onStorage = (e) => {
|
|
24
|
+
if (!e || e.key === FX_FOLLOW_REDUCED_MOTION_KEY || e.key === null) {
|
|
25
|
+
pageChoice = e?.key === FX_FOLLOW_REDUCED_MOTION_KEY ? e.newValue === "1" : null;
|
|
26
|
+
fn();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
21
29
|
let q = null;
|
|
22
30
|
try { q = typeof globalThis.matchMedia === "function" ? globalThis.matchMedia("(prefers-reduced-motion: reduce)") : null; } catch { q = null; }
|
|
23
31
|
try { globalThis.addEventListener && globalThis.addEventListener("storage", onStorage); } catch { /* none */ }
|
|
@@ -37,10 +45,11 @@ export function systemReducesMotion(win = globalThis) {
|
|
|
37
45
|
* note was not shown here before. Showing it marks it shown.
|
|
38
46
|
*/
|
|
39
47
|
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
|
|
48
|
+
if (noteShown || followsReducedMotion() || !systemReducesMotion(win)) return false;
|
|
49
|
+
try { return globalThis.localStorage.getItem(FX_MOTION_NOTE_KEY) !== "1"; } catch { return true; }
|
|
42
50
|
}
|
|
43
51
|
export function markMotionNoteShown() {
|
|
52
|
+
noteShown = true;
|
|
44
53
|
try { globalThis.localStorage.setItem(FX_MOTION_NOTE_KEY, "1"); } catch { /* shown again next time: harmless */ }
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
* surfaces it is used on, and focus rings and strong borders meet 3:1 (tests/web-tokens-1408.test.ts).
|
|
10
10
|
*
|
|
11
11
|
* Inter is bundled (/assets/inter.woff2, SIL OFL 1.1 — see fonts/README.md): the CSP allows fonts from 'self' only,
|
|
12
|
-
* and nothing is loaded from another site. Chinese falls back to the system's CJK fonts, in the order below.
|
|
12
|
+
* and nothing is loaded from another site. Chinese falls back to the system's CJK fonts, in the order below.
|
|
13
|
+
*
|
|
14
|
+
* Emoji (#1918 follow-up, the user's tofu boxes): every stack ends with the platforms' colour emoji fonts and the
|
|
15
|
+
* `emoji` generic (--font-emoji), so an emoji the text fonts lack is drawn by Windows' Segoe UI Emoji, macOS's Apple
|
|
16
|
+
* Color Emoji or Linux's Noto Color Emoji rather than as a box — for the stock stacks and each theme font
|
|
17
|
+
* (app.css: .th-font-system, .th-font-mono). A browser tries the families in order for each character. */
|
|
13
18
|
|
|
14
19
|
@font-face {
|
|
15
20
|
font-family: "Inter";
|
|
@@ -22,8 +27,9 @@
|
|
|
22
27
|
|
|
23
28
|
:root {
|
|
24
29
|
/* ── Type ── */
|
|
25
|
-
--font-
|
|
26
|
-
--font-
|
|
30
|
+
--font-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji;
|
|
31
|
+
--font-ui: Inter, "PingFang TC", "PingFang SC", "Noto Sans TC", "Noto Sans SC", "Noto Sans CJK TC", "Noto Sans CJK SC", "Microsoft JhengHei", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif, var(--font-emoji);
|
|
32
|
+
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, var(--font-emoji);
|
|
27
33
|
--fs-xs: 12px; --lh-xs: 16px;
|
|
28
34
|
--fs-sm: 13px; --lh-sm: 18px;
|
|
29
35
|
--fs-md: 15px; --lh-md: 24px; /* message text */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "./app-html.js";
|
|
5
5
|
import { t } from "./app-i18n.js";
|
|
6
6
|
import { useLease } from "./app-ctx.js";
|
|
7
|
+
import { guestPage } from "./app-store.js";
|
|
7
8
|
import { readStream } from "./read-stream.js";
|
|
8
9
|
import { Dialog } from "./ui-dialog.js";
|
|
9
10
|
import { ErrorState, Skeleton } from "./ui-states.js";
|
|
@@ -159,11 +160,18 @@ export function UsageBody({ owner }) {
|
|
|
159
160
|
${!p.stale && p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : !(p.metrics || []).length ? html`<p class="note">${tn("usage.no_data")}</p>` : null}`}
|
|
160
161
|
</section>`)}
|
|
161
162
|
<div class="u-foot"><span class="note" title=${new Date(data.fetchedAt).toLocaleString()}>${updatedAgo(data.fetchedAt)}${data.refreshing ? html` · <span class="u-refreshing">${tn("usageRefreshing")}</span>` : null}</span>
|
|
163
|
+
${byAgent() ? html`<a class="btn btn-ghost btn-sm u-by-agent" href="/ui/fleet/usage" title=${tn("usageByAgentTitle")}>${tn("usageByAgent")}</a>` : null}
|
|
162
164
|
<button type="button" class="btn btn-sm" onClick=${() => load(true)}>${tn("usageRefresh")}</button></div>`;
|
|
163
165
|
}
|
|
164
166
|
return content;
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
/**
|
|
170
|
+
* #2122: the plans above are per login; each agent's own tokens and API-equivalent are Fleet → Usage — linked from the
|
|
171
|
+
* app (not View's anonymous reader, not a guest, who has no Fleet).
|
|
172
|
+
*/
|
|
173
|
+
const byAgent = () => !guestPage() && typeof location !== "undefined" && location.pathname.startsWith("/ui");
|
|
174
|
+
|
|
167
175
|
/** `owner`: the navigation that opened it — its reads and the minute's timer live and end with it. */
|
|
168
176
|
export function UsageDialog({ onClose, owner = "" }) {
|
|
169
177
|
return html`<${Dialog} title=${tn("usage.title")} onClose=${onClose} wide=${true}><${UsageBody} owner=${`usage-dialog:${owner}`} /></${Dialog}>`;
|
|
@@ -40,6 +40,8 @@ register("view", {
|
|
|
40
40
|
"usageMinutes": "{n} min",
|
|
41
41
|
"usageHours": "{n} h",
|
|
42
42
|
"usageRefresh": "Refresh",
|
|
43
|
+
"usageByAgent": "By agent →",
|
|
44
|
+
"usageByAgentTitle": "Each agent's tokens and API-equivalent (Fleet → Usage)",
|
|
43
45
|
"usageHigh": "high",
|
|
44
46
|
"usageNear": "near limit",
|
|
45
47
|
"usageMoveUp": "Move up",
|
|
@@ -248,6 +250,8 @@ register("view", {
|
|
|
248
250
|
"usageMinutes": "{n} 分鐘",
|
|
249
251
|
"usageHours": "{n} 小時",
|
|
250
252
|
"usageRefresh": "重新整理",
|
|
253
|
+
"usageByAgent": "依 agent →",
|
|
254
|
+
"usageByAgentTitle": "每個 agent 的 token 與 API 等值(Fleet → 用量)",
|
|
251
255
|
"usageHigh": "偏高",
|
|
252
256
|
"usageNear": "接近上限",
|
|
253
257
|
"usageMoveUp": "上移",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
html, body { margin: 0; height: 100%; background: #111; color: #ddd; font: 14px system-ui, sans-serif; }
|
|
1
|
+
html, body { margin: 0; height: 100%; background: #111; color: #ddd; font: 14px system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji; }
|
|
2
2
|
body { display: flex; flex-direction: column; }
|
|
3
3
|
#bar { display: flex; align-items: center; gap: 12px; padding: 6px 10px; background: #1c1c1c; border-bottom: 1px solid #333; }
|
|
4
4
|
#bar #title { font-weight: 600; }
|
|
@@ -6,18 +6,18 @@ body { display: flex; flex-direction: column; }
|
|
|
6
6
|
#bar button { background: #402; color: #fbb; border: 1px solid #733; border-radius: 4px; padding: 3px 10px; cursor: pointer; }
|
|
7
7
|
#gate { max-width: 560px; margin: 40px auto; padding: 0 16px; line-height: 1.5; }
|
|
8
8
|
#gate form { display: flex; gap: 8px; margin-top: 12px; }
|
|
9
|
-
#gate input { flex: 1; font: 16px ui-monospace, monospace; letter-spacing: 1px; padding: 8px; background: #000; color: #fff; border: 1px solid #555; border-radius: 4px; }
|
|
9
|
+
#gate input { flex: 1; font: 16px ui-monospace, monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji; letter-spacing: 1px; padding: 8px; background: #000; color: #fff; border: 1px solid #555; border-radius: 4px; }
|
|
10
10
|
#gate button { padding: 8px 16px; background: #246; color: #fff; border: 0; border-radius: 4px; cursor: pointer; }
|
|
11
11
|
#gate-msg { color: #f88; min-height: 1.5em; }
|
|
12
12
|
#term { flex: 1; min-height: 0; padding: 4px; overflow: hidden; } /* never drawn over the rows below it (#1242) */
|
|
13
13
|
#code-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 10px; background: #1c1c1c; border-top: 1px solid #333; }
|
|
14
14
|
#code-row[hidden] { display: none; }
|
|
15
|
-
#code-row input { flex: 1; min-width: 12em; font: 16px ui-monospace, monospace; padding: 8px; background: #000; color: #fff; border: 1px solid #555; border-radius: 4px; }
|
|
15
|
+
#code-row input { flex: 1; min-width: 12em; font: 16px ui-monospace, monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji; padding: 8px; background: #000; color: #fff; border: 1px solid #555; border-radius: 4px; }
|
|
16
16
|
#code-row button { padding: 8px 16px; background: #246; color: #fff; border: 0; border-radius: 4px; cursor: pointer; }
|
|
17
17
|
#code-msg { color: #9ad; min-height: 1.2em; }
|
|
18
18
|
#code-hint { flex-basis: 100%; margin: 0; color: #aaa; font-size: 13px; }
|
|
19
19
|
#keys { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px; background: #1c1c1c; border-top: 1px solid #333; }
|
|
20
|
-
#keys button { background: #222; color: #ddd; border: 1px solid #444; border-radius: 4px; padding: 6px 10px; font: 14px ui-monospace, monospace; cursor: pointer; }
|
|
20
|
+
#keys button { background: #222; color: #ddd; border: 1px solid #444; border-radius: 4px; padding: 6px 10px; font: 14px ui-monospace, monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji; cursor: pointer; }
|
|
21
21
|
#done { padding: 10px; background: #1c1c1c; border-top: 1px solid #333; }
|
|
22
22
|
#done.ok { color: #8d8; } #done.bad { color: #f88; }
|
|
23
23
|
@media (min-width: 900px) { #keys { display: none; } }
|
package/dist/usage-report.d.ts
CHANGED
|
@@ -22,7 +22,10 @@ export type UsageRange = {
|
|
|
22
22
|
};
|
|
23
23
|
/** What every amount is, in words — the page, the CSV and the agent tool all carry it. */
|
|
24
24
|
export declare const usageBasis: (checked?: string) => string;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Daily rows returned at most (the CSV and the trend): one per day, source and model. Beyond it the report keeps the
|
|
27
|
+
* latest whole days and says from which date (`dailyFrom`); the rows and totals always cover the whole range.
|
|
28
|
+
*/
|
|
26
29
|
export declare const USAGE_DAILY_MAX = 20000;
|
|
27
30
|
export interface UsageInstanceInput {
|
|
28
31
|
name: string;
|
|
@@ -54,6 +57,8 @@ export interface UsageInput {
|
|
|
54
57
|
pendingBytes: number;
|
|
55
58
|
pendingFiles: number;
|
|
56
59
|
};
|
|
60
|
+
/** Daily rows at most (default USAGE_DAILY_MAX; tests lower it to reach the same path). */
|
|
61
|
+
dailyMax?: number;
|
|
57
62
|
}
|
|
58
63
|
export interface UsageTotals {
|
|
59
64
|
/** Uncached input. */
|
|
@@ -111,7 +116,13 @@ export interface UsageReport {
|
|
|
111
116
|
rows: UsageRow[];
|
|
112
117
|
totals: UsageRow;
|
|
113
118
|
daily: UsageDaily[];
|
|
119
|
+
/**
|
|
120
|
+
* The daily rows (and the messages by day) are not the whole range: they start at `dailyFrom` (earlier days were too
|
|
121
|
+
* many rows to send), or — `dailyFrom` null — there are none at all (the latest day alone was more than the cap: a
|
|
122
|
+
* part of a day is never sent as if it were the day). The rows and totals always cover the whole range.
|
|
123
|
+
*/
|
|
114
124
|
dailyTruncated: boolean;
|
|
125
|
+
dailyFrom: string | null;
|
|
115
126
|
messagesDaily: Array<{
|
|
116
127
|
date: string;
|
|
117
128
|
instance: string;
|