@songsid/agend 2.3.0-beta.6 → 2.3.0-beta.8
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 +13 -7
- 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.d.ts +2 -0
- package/dist/daemon.js +31 -9
- 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 +89 -30
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +62 -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-config-impact.js +1 -0
- package/dist/instance-config-impact.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 +49 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +57 -1
- package/dist/outbound-schemas.js +34 -2
- 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 +54 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +31 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +10 -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 +46 -28
- package/dist/ui/chat-ambient.js +307 -0
- package/dist/ui/chat-effect-posters.js +8 -8
- package/dist/ui/chat-effects.js +51 -22
- package/dist/ui/chat-strings.js +21 -6
- package/dist/ui/chat-thread.js +1 -1
- package/dist/ui/fleet-usage.js +401 -0
- package/dist/ui/panel-chat.js +22 -9
- package/dist/ui/panel-fleet.js +6 -5
- package/dist/ui/panel-settings.js +10 -2
- 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 +30 -0
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/app-route.js +1 -1
- 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 +95 -0
- package/dist/ui/shared/effect-app-runtime.js +68 -19
- 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 -5
- package/dist/ui/shared/effect-profiler.js +31 -9
- package/dist/ui/shared/effect-registry.js +17 -41
- package/dist/ui/shared/effect-stock-trust.js +4 -4
- package/dist/ui/shared/effect-trial-stage.js +18 -7
- package/dist/ui/shared/fx-motion.js +67 -0
- package/dist/ui/shared/usage-dialog.js +8 -0
- package/dist/ui/shared/view-strings.js +4 -0
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/usage-report.d.ts +158 -0
- package/dist/usage-report.js +226 -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 +33 -0
- package/dist/web-api.js +148 -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 +26 -2
- package/dist/web-settings-patch.js +50 -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-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +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,6 @@
|
|
|
1
1
|
/** #1629 B2: one host-owned Canvas Worker pool. Product adapters are deliberately separate. */
|
|
2
|
-
import { allocateEffectFrames, effectEvent } from "./effect-policy.js";
|
|
3
|
-
import {
|
|
4
|
-
import { createEffectFrame } from "./effect-frame.js";
|
|
2
|
+
import { allocateEffectFrames, effectEvent, effectPlaybackReason } from "./effect-policy.js";
|
|
3
|
+
import { createOpaqueEffectFrame } from "./effect-frame.js";
|
|
5
4
|
import { createEffectGeometry, hostEffectRect } from "./effect-geometry.js";
|
|
6
5
|
import { effectStockSnapshot, effectStockAllows } from "./effect-stock-snapshot.js";
|
|
7
6
|
import { effectTrialStage, effectTrialStageIdentity, watchEffectTrialStage } from "./effect-trial-stage.js";
|
|
@@ -10,7 +9,7 @@ import { EffectBudget } from "./effect-profiler.js";
|
|
|
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,7 +40,7 @@ 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
45
|
let readingStock = false;
|
|
47
46
|
let root, geometry, observer, resize, longTasks = null, longTasksSupported = false;
|
|
@@ -105,12 +104,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
105
104
|
return true;
|
|
106
105
|
};
|
|
107
106
|
// 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
|
-
};
|
|
107
|
+
const gateReason = () => effectPlaybackReason(context);
|
|
114
108
|
const clearMarker = (s, why) => {
|
|
115
109
|
if (!s.token) return true;
|
|
116
110
|
const token = s.token; s.token = null;
|
|
@@ -122,7 +116,9 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
122
116
|
s.live = false;
|
|
123
117
|
if (slots.get(s.d.key) === s) slots.delete(s.d.key);
|
|
124
118
|
if (s.request) { s.request.controller.abort(); if (s.request.timer !== null) host.clearTimeout(s.request.timer); }
|
|
125
|
-
|
|
119
|
+
// A closed/unknown viewer gate revokes paint, not the effect identity. Fresh known gates
|
|
120
|
+
// may admit it again; resource failures and actual safety pauses retain their latches.
|
|
121
|
+
if (!["disposed", "superseded", "hidden", "human_resume", "geometry_changed", "gates_unknown"].includes(why)) failures.set(s.d.key, { identity: signature(s.d), reason: why });
|
|
126
122
|
s.stageOff?.(); s.stageOff = null;
|
|
127
123
|
s.frame?.remove(why); s.geometry?.remove();
|
|
128
124
|
if (s.loadOff) s.loadOff();
|
|
@@ -141,17 +137,12 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
141
137
|
retire(s, why);
|
|
142
138
|
if (revision !== proposed || dead !== wasDead) return false;
|
|
143
139
|
}
|
|
144
|
-
if (calibration) { calibration.cancel(); calibration = null; }
|
|
145
140
|
return true;
|
|
146
141
|
}
|
|
147
142
|
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
143
|
function eligible(d = null) {
|
|
153
|
-
return gatesKnown() && !context.safe && !context.
|
|
154
|
-
&& (d
|
|
144
|
+
return gatesKnown() && !context.safe && !context.reducedMotion && context.enabled && context.show && context.transportReady
|
|
145
|
+
&& (!d || !context.publicLink || d.region === "chat" && !d.trialStage);
|
|
155
146
|
}
|
|
156
147
|
const globalAllowed = (d = null) => eligible(d) && host.document.visibilityState === "visible";
|
|
157
148
|
const signature = d => d.identity;
|
|
@@ -228,10 +219,9 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
228
219
|
if (!chosen.has(s.d.key) || !d || signature(d) !== signature(s.d) || d.element !== s.d.element || s.device !== device()) retire(s, "superseded");
|
|
229
220
|
if (dead || proposal !== revision) return;
|
|
230
221
|
}
|
|
231
|
-
if (chosen.size === 0) {
|
|
222
|
+
if (chosen.size === 0) { tell(); return; }
|
|
232
223
|
if (hidden) {
|
|
233
224
|
hideGeometry();
|
|
234
|
-
if (calibration) { calibration.cancel(); calibration = null; }
|
|
235
225
|
for (const s of [...slots.values()]) {
|
|
236
226
|
if (!s.frame) retire(s, "hidden");
|
|
237
227
|
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"); }
|
|
@@ -245,20 +235,6 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
245
235
|
if (dead || proposal !== revision) return;
|
|
246
236
|
}
|
|
247
237
|
try { observe(); } catch { stopAll("trust_geometry_unknown"); tell(); return; }
|
|
248
|
-
if (baseline === null || baselineDevice !== device()) {
|
|
249
|
-
if (calibration && baselineDevice === device()) { tell(); return; }
|
|
250
|
-
if (!stopAll("calibrating") || dead || proposal !== revision) return;
|
|
251
|
-
const epoch = ++serial;
|
|
252
|
-
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 });
|
|
253
|
-
calibration = job; baselineDevice = device();
|
|
254
|
-
job.promise.then(r => {
|
|
255
|
-
if (dead || calibration !== job || epoch !== serial) return;
|
|
256
|
-
calibration = null;
|
|
257
|
-
if (!r.ok) { blocked = r.reason; tell(); return; }
|
|
258
|
-
baseline = r.baselineFps; reconcile();
|
|
259
|
-
});
|
|
260
|
-
tell(); return;
|
|
261
|
-
}
|
|
262
238
|
const max = context.constrained ? 1 : 3;
|
|
263
239
|
for (const d of descriptors) {
|
|
264
240
|
if (!chosen.has(d.key) || slots.has(d.key)) continue;
|
|
@@ -328,8 +304,8 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
328
304
|
if (d.trialStage && !local || !current(s)) { if (current(s)) retire(s, "admission_revoked"); return; }
|
|
329
305
|
if (local) fresh = local.rect;
|
|
330
306
|
s.rect = fresh;
|
|
331
|
-
s.budget = new EffectBudget({ device: s.device,
|
|
332
|
-
const frame =
|
|
307
|
+
s.budget = new EffectBudget({ device: s.device, clock: nativeClock, longTasksSupported });
|
|
308
|
+
const frame = createOpaqueEffectFrame({ window: host, root: local?.root ?? root, origin, boot, clock: nativeClock,
|
|
333
309
|
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
310
|
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
311
|
onRemove(why) { if (current(s)) { retire(s, why); tell(); } },
|
|
@@ -510,7 +486,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
510
486
|
descriptors = ds.map(snapshotDescriptor).filter(Boolean);
|
|
511
487
|
for (const [key, identity] of paused) if (!descriptors.some(d => d.key === key && signature(d) === identity)) paused.delete(key);
|
|
512
488
|
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, "
|
|
489
|
+
context = Object.fromEntries([...FLAGS, "openInstance"].map(k => [k, ctx[k]]));
|
|
514
490
|
reconcile(); return view();
|
|
515
491
|
},
|
|
516
492
|
snapshot: view,
|
|
@@ -518,11 +494,11 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
518
494
|
revalidateSafety,
|
|
519
495
|
/** Called only by the host's explicit human Resume UI. A descriptor update never clears this latch. */
|
|
520
496
|
resume(key) {
|
|
521
|
-
if (dead || reentrantStock() || blocked
|
|
497
|
+
if (dead || reentrantStock() || blocked || typeof key !== "string") return false;
|
|
522
498
|
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;
|
|
523
499
|
revision++;
|
|
524
500
|
if (!stopAll("human_resume")) return false;
|
|
525
|
-
if (blocked
|
|
501
|
+
if (blocked) return false; paused.delete(key); failures.delete(key); reconcile(); return true;
|
|
526
502
|
},
|
|
527
503
|
/** Host-only content-free signal API. Raw pointer publishing is refused; native pointer metadata is coalesced here. */
|
|
528
504
|
publish(type, fields, instance = null) {
|
|
@@ -538,7 +514,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
538
514
|
return sent;
|
|
539
515
|
},
|
|
540
516
|
dispose() {
|
|
541
|
-
if (dead) return; dead = true; revision++;
|
|
517
|
+
if (dead) return; dead = true; revision++; stopAll("disposed");
|
|
542
518
|
if (raf !== null) host.cancelAnimationFrame(raf); if (timer !== null) host.clearTimeout(timer);
|
|
543
519
|
observer.disconnect(); resize.disconnect(); longTasks?.disconnect(); for (const off of offs) off();
|
|
544
520
|
geometry.dispose(); root.remove(); observed.clear(); if (loads.size === 0 && registries.get(host) === api) registries.delete(host);
|
|
@@ -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: [] },
|
|
@@ -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,67 @@
|
|
|
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
|
+
let pageChoice = null, noteShown = false;
|
|
9
|
+
|
|
10
|
+
/** Whether this device asked effects to follow the system's reduced-motion setting (default: no). */
|
|
11
|
+
export function followsReducedMotion() {
|
|
12
|
+
if (pageChoice !== null) return pageChoice;
|
|
13
|
+
try { return globalThis.localStorage.getItem(FX_FOLLOW_REDUCED_MOTION_KEY) === "1"; } catch { return false; }
|
|
14
|
+
}
|
|
15
|
+
export function setFollowsReducedMotion(on) {
|
|
16
|
+
pageChoice = !!on;
|
|
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 */ }
|
|
18
|
+
for (const fn of [...listeners]) { try { fn(); } catch { /* a listener's own */ } }
|
|
19
|
+
}
|
|
20
|
+
/** `fn()` when the switch or the system's answer moves (this tab, another tab, the OS). Returns the unsubscribe. */
|
|
21
|
+
export function onMotionChange(fn) {
|
|
22
|
+
listeners.add(fn);
|
|
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
|
+
};
|
|
29
|
+
let q = null;
|
|
30
|
+
try { q = typeof globalThis.matchMedia === "function" ? globalThis.matchMedia("(prefers-reduced-motion: reduce)") : null; } catch { q = null; }
|
|
31
|
+
try { globalThis.addEventListener && globalThis.addEventListener("storage", onStorage); } catch { /* none */ }
|
|
32
|
+
try { if (q && q.addEventListener) q.addEventListener("change", fn); } catch { /* read again on the next render */ }
|
|
33
|
+
return () => {
|
|
34
|
+
listeners.delete(fn);
|
|
35
|
+
try { globalThis.removeEventListener && globalThis.removeEventListener("storage", onStorage); } catch { /* gone */ }
|
|
36
|
+
try { if (q && q.removeEventListener) q.removeEventListener("change", fn); } catch { /* gone */ }
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Whether the system asks for reduced motion (false when it can't say: the note is only for a clear yes). */
|
|
40
|
+
export function systemReducesMotion(win = globalThis) {
|
|
41
|
+
try { return typeof win.matchMedia === "function" && win.matchMedia("(prefers-reduced-motion: reduce)").matches === true; } catch { return false; }
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The one-time note (the user's direction): the system asks for reduced motion, this device does not follow it, and the
|
|
45
|
+
* note was not shown here before. Showing it marks it shown.
|
|
46
|
+
*/
|
|
47
|
+
export function motionNoteDue(win = globalThis) {
|
|
48
|
+
if (noteShown || followsReducedMotion() || !systemReducesMotion(win)) return false;
|
|
49
|
+
try { return globalThis.localStorage.getItem(FX_MOTION_NOTE_KEY) !== "1"; } catch { return true; }
|
|
50
|
+
}
|
|
51
|
+
export function markMotionNoteShown() {
|
|
52
|
+
noteShown = true;
|
|
53
|
+
try { globalThis.localStorage.setItem(FX_MOTION_NOTE_KEY, "1"); } catch { /* shown again next time: harmless */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* True when effects stay still here: this device follows the system's setting and the system asks for reduced motion
|
|
58
|
+
* (or cannot say — then still, as asked for). With the switch off (the default), never.
|
|
59
|
+
*/
|
|
60
|
+
export function motionBlocked(win = globalThis) {
|
|
61
|
+
if (!followsReducedMotion()) return false;
|
|
62
|
+
try {
|
|
63
|
+
if (typeof win.matchMedia !== "function") return true;
|
|
64
|
+
const q = win.matchMedia("(prefers-reduced-motion: reduce)");
|
|
65
|
+
return !q || typeof q.matches !== "boolean" || q.matches;
|
|
66
|
+
} catch { return true; }
|
|
67
|
+
}
|
|
@@ -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": "上移",
|
|
@@ -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
|
+
}
|