@songsid/agend 2.3.0-beta.5 → 2.3.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-cli.js +21 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +4 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.js +68 -8
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-whats-new.js +4 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.js +3 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +38 -0
- package/dist/authority/authority-state.js +114 -0
- package/dist/authority/authority-state.js.map +1 -0
- package/dist/authority/session-authorizations.d.ts +43 -0
- package/dist/authority/session-authorizations.js +102 -0
- package/dist/authority/session-authorizations.js.map +1 -0
- package/dist/authority/web-authority.d.ts +31 -11
- package/dist/authority/web-authority.js +39 -12
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/cache-ledger.d.ts +16 -3
- package/dist/cache-ledger.js +33 -13
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +55 -3
- package/dist/cache-service.js +152 -16
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/mcp-tools.js +12 -6
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +2 -2
- package/dist/classic-channel-manager.js +2 -6
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/config-validator.js +10 -4
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +2 -0
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -0
- package/dist/delivery-outbox.js +7 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +109 -2
- package/dist/fleet-manager.js +280 -12
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +2 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +81 -25
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +55 -8
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/human-delivery-receipts.d.ts +14 -0
- package/dist/human-delivery-receipts.js +42 -6
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +8 -1
- package/dist/instructions.js +9 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +52 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.d.ts +6 -0
- package/dist/outbound-handlers.js +45 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +57 -1
- package/dist/outbound-schemas.js +33 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +8 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/rollout-index.d.ts +5 -0
- package/dist/rollout-index.js +26 -5
- package/dist/rollout-index.js.map +1 -1
- package/dist/settings-api.d.ts +6 -0
- package/dist/settings-api.js +50 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +27 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +8 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/tool-permissions.d.ts +0 -7
- package/dist/tool-permissions.js +73 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +2 -1
- package/dist/topic-commands.js +2 -4
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +13 -2
- package/dist/ui/app-effects.js +43 -25
- package/dist/ui/chat-ambient.js +306 -0
- package/dist/ui/chat-effect-posters.js +6 -7
- package/dist/ui/chat-effects.js +38 -15
- package/dist/ui/chat-strings.js +17 -4
- package/dist/ui/panel-chat.js +15 -7
- package/dist/ui/panel-settings.js +4 -1
- package/dist/ui/settings-appearance.js +8 -0
- package/dist/ui/settings-mods.js +19 -6
- package/dist/ui/settings-owner-effects.js +3 -3
- package/dist/ui/settings-strings.js +28 -0
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/app-session.js +2 -0
- package/dist/ui/shared/app-shell.js +2 -1
- package/dist/ui/shared/app-themes.js +24 -6
- package/dist/ui/shared/app.css +42 -8
- package/dist/ui/shared/effect-app-runtime.js +67 -18
- package/dist/ui/shared/effect-chat-runtime.js +16 -3
- package/dist/ui/shared/effect-frame.js +17 -3
- package/dist/ui/shared/effect-policy.js +18 -4
- package/dist/ui/shared/effect-registry.js +11 -18
- package/dist/ui/shared/effect-stock-trust.js +2 -2
- package/dist/ui/shared/effect-trial-stage.js +18 -7
- package/dist/ui/shared/fx-motion.js +58 -0
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/usage-report.d.ts +147 -0
- package/dist/usage-report.js +195 -0
- package/dist/usage-report.js.map +1 -0
- package/dist/usage-rollup.d.ts +51 -0
- package/dist/usage-rollup.js +168 -0
- package/dist/usage-rollup.js.map +1 -0
- package/dist/web-ambient.d.ts +72 -0
- package/dist/web-ambient.js +154 -0
- package/dist/web-ambient.js.map +1 -0
- package/dist/web-api.d.ts +32 -0
- package/dist/web-api.js +143 -18
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +3 -0
- package/dist/web-appearance-tools.js +18 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +14 -2
- package/dist/web-appearance-view.js +32 -5
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +15 -1
- package/dist/web-appearance.js +77 -1
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +2 -2
- package/dist/web-asset-parts.js +3 -3
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +3 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-effect-bootstrap.d.ts +12 -0
- package/dist/web-effect-bootstrap.js +49 -0
- package/dist/web-effect-bootstrap.js.map +1 -0
- package/dist/web-effect-bundle.js +46 -1
- package/dist/web-effect-bundle.js.map +1 -1
- package/dist/web-effect-playback.d.ts +30 -0
- package/dist/web-effect-playback.js +71 -0
- package/dist/web-effect-playback.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +4 -4
- package/dist/web-effect-service.d.ts +11 -4
- package/dist/web-effect-service.js +25 -4
- package/dist/web-effect-service.js.map +1 -1
- package/dist/web-effect-tools.d.ts +1 -0
- package/dist/web-effect-tools.js +2 -1
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-worker.d.ts +1 -1
- package/dist/web-effect-worker.js +2 -2
- package/dist/web-effect-worker.js.map +1 -1
- package/dist/web-guest-access.js +3 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-agent.js +5 -1
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +9 -1
- package/dist/web-mod-bundle.js +62 -3
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-export.d.ts +9 -0
- package/dist/web-mod-export.js +21 -0
- package/dist/web-mod-export.js.map +1 -1
- package/dist/web-mod-store.d.ts +2 -1
- package/dist/web-mod-store.js +13 -6
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-session-authorization.d.ts +132 -0
- package/dist/web-session-authorization.js +193 -0
- package/dist/web-session-authorization.js.map +1 -0
- package/dist/web-session.d.ts +5 -0
- package/dist/web-session.js +16 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +24 -2
- package/dist/web-settings-patch.js +47 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +8 -0
- package/dist/web-settings-tools.js +14 -1
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-theme-store.d.ts +39 -0
- package/dist/web-theme-store.js +122 -11
- package/dist/web-theme-store.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// #1939 S2a: Settings → Web → Web authority — whether a signed-in admin's web messages carry their account's authority
|
|
2
|
+
// in delivery receipts (docs/design/1939-verified-web-sessions.md §0, §0.1, §3, §10): the server switch (on by default),
|
|
3
|
+
// "Only this computer can authorize", how long a session's authorization lasts, the sessions authorized now, and "Revoke
|
|
4
|
+
// web authority". Changes go through Review changes → Apply like every Settings edit, and apply at once (§0.1: none of
|
|
5
|
+
// them grants anything by itself — a session still needs its account's Confirm in Discord or Telegram).
|
|
6
|
+
import { html, useEffect, useState } from "/assets/app-html.js";
|
|
7
|
+
import { t } from "/assets/app-i18n.js";
|
|
8
|
+
import { toast } from "/assets/ui-toast.js";
|
|
9
|
+
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
10
|
+
import { api } from "./settings-confirm.js";
|
|
11
|
+
import { Feedback } from "./settings-dialogs.js";
|
|
12
|
+
import { resetWebAuthorization } from "/assets/web-authorize.js";
|
|
13
|
+
import "./settings-strings.js";
|
|
14
|
+
|
|
15
|
+
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
16
|
+
|
|
17
|
+
/** What changed against what fleet.yaml says, as the PUT /api/settings/fleet/web body's `verified_authority` (null: nothing). */
|
|
18
|
+
export function verifiedAuthorityBody(original, f) {
|
|
19
|
+
const out = {};
|
|
20
|
+
if (f.enabled !== original.enabled) out.enabled = f.enabled ? null : false; // §0.1: on is the default
|
|
21
|
+
if (f.loopbackOnly !== original.loopbackOnly) out.loopback_only = f.loopbackOnly ? true : null;
|
|
22
|
+
if (f.hours !== original.hours) out.max_session_hours = f.hours === null ? null : f.hours;
|
|
23
|
+
return Object.keys(out).length ? out : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function WebAuthorityCard({ ctx, staged, disabled = false }) {
|
|
27
|
+
const [overview, setOverview] = useState(null), [failed, setFailed] = useState(false);
|
|
28
|
+
const [original, setOriginal] = useState(null), [f, setF] = useState(null), [msg, setMsg] = useState(null);
|
|
29
|
+
const load = async () => {
|
|
30
|
+
// What changes here (a revoke, the switch) changes this browser's own line: the chat reads it again when it shows.
|
|
31
|
+
resetWebAuthorization();
|
|
32
|
+
let r = null;
|
|
33
|
+
try { r = await api("/api/settings/web-authority"); } catch { r = null; }
|
|
34
|
+
// Anything but the overview's shape (an older server, an error page) is "could not be loaded", never a crash.
|
|
35
|
+
const o = r && r.ok ? r.body : null;
|
|
36
|
+
if (!o || typeof o !== "object" || !o.state || !o.hours || !Array.isArray(o.lanHosts) || !Array.isArray(o.authorized)) { setFailed(true); return; }
|
|
37
|
+
const c = o.configured || {};
|
|
38
|
+
const orig = { enabled: c.enabled !== false, loopbackOnly: c.loopback_only === true, hours: typeof c.max_session_hours === "number" ? c.max_session_hours : null };
|
|
39
|
+
setOverview(o); setFailed(false);
|
|
40
|
+
setOriginal((prev) => prev || orig);
|
|
41
|
+
setF((prev) => prev || { ...orig });
|
|
42
|
+
};
|
|
43
|
+
useEffect(() => { load(); }, []);
|
|
44
|
+
const card = (body) => html`<section class="card s-web-authority"><h3>${tn("vaTitle")}</h3><p class="note">${tn("vaHint")}</p>${body}</section>`;
|
|
45
|
+
if (failed) return card(html`<p class="note" role="status">${tn("vaLoadFailed")} <button type="button" class="btn btn-sm" onClick=${load}>${t("app.retry")}</button></p>`);
|
|
46
|
+
if (!overview || !f) return card(html`<p class="note">${t("app.loading")}</p>`);
|
|
47
|
+
|
|
48
|
+
const set = (patch) => setF((x) => ({ ...x, ...patch }));
|
|
49
|
+
const hoursText = f.hours === null ? "" : String(f.hours);
|
|
50
|
+
const hoursError = f.hours !== null && (!Number.isFinite(f.hours) || f.hours < overview.hours.min || f.hours > overview.hours.max) ? "vaHoursRange" : "";
|
|
51
|
+
const stuckOff = f.enabled && original.enabled && !overview.state.enabled;
|
|
52
|
+
const warnHosts = !f.loopbackOnly && overview.lanHosts.length > 0;
|
|
53
|
+
const when = (ms) => new Date(ms).toLocaleString();
|
|
54
|
+
|
|
55
|
+
const review = () => {
|
|
56
|
+
if (hoursError) return;
|
|
57
|
+
const body = verifiedAuthorityBody(original, f);
|
|
58
|
+
if (body) ctx.stage("verified_authority", { label: tn("vaTitle"), impact: "now", request: { method: "PUT", url: "/api/settings/fleet/web", body: { verified_authority: body } } });
|
|
59
|
+
else if (staged.has("verified_authority")) ctx.unstage("verified_authority");
|
|
60
|
+
setMsg(tn("readyApply"));
|
|
61
|
+
};
|
|
62
|
+
const revoke = async () => {
|
|
63
|
+
const yes = await confirmDialog({ title: tn("vaRevokeTitle"), message: tn("vaRevokeConfirm"), confirmLabel: tn("vaRevoke"), danger: true });
|
|
64
|
+
if (!yes) return;
|
|
65
|
+
let r = null;
|
|
66
|
+
try { r = await api("/api/settings/web-authority/revoke", { method: "POST", body: "{}" }); } catch { r = null; }
|
|
67
|
+
if (r && r.ok) { toast(tn(r.body && r.body.durable === false ? "vaRevokedNotDurable" : "vaRevoked")); load(); }
|
|
68
|
+
else toast(tn("vaRevokeFailed"), false);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return card(html`<fieldset class="s-va-fields" disabled=${disabled}>
|
|
72
|
+
${stuckOff ? html`<p class="note s-va-warn" role="status" id="vaStuckOff">${tn("vaStuckOff")}</p>` : null}
|
|
73
|
+
<label class="check"><input id="vaEnabled" type="checkbox" checked=${f.enabled} onChange=${(e) => set({ enabled: e.target.checked })} /><span>${tn("vaEnabled")}</span></label>
|
|
74
|
+
<p class="note">${tn("vaEnabledHint")}</p>
|
|
75
|
+
<label class="check"><input id="vaLoopbackOnly" type="checkbox" checked=${f.loopbackOnly} onChange=${(e) => set({ loopbackOnly: e.target.checked })} /><span>${tn("vaLoopbackOnly")}</span></label>
|
|
76
|
+
<p class="note">${tn("vaLoopbackOnlyHint")}</p>
|
|
77
|
+
${warnHosts ? html`<p class="note s-va-warn" role="status" id="vaLanWarning">${tn("vaLanWarning", overview.lanHosts.join(", "))}</p>` : null}
|
|
78
|
+
<div class="field"><label for="vaHours">${tn("vaHours", overview.hours.min, overview.hours.max)}</label>
|
|
79
|
+
<input id="vaHours" type="number" min=${overview.hours.min} max=${overview.hours.max} step="any" placeholder=${tn("defaultHours", overview.hours.default)}
|
|
80
|
+
class=${hoursError ? "invalid" : ""} value=${hoursText} onInput=${(e) => set({ hours: e.target.value.trim() === "" ? null : Number(e.target.value) })} />
|
|
81
|
+
<${Feedback} error=${hoursError} /></div>
|
|
82
|
+
<h4>${tn("vaSessionsTitle")}</h4>
|
|
83
|
+
${overview.authorized.length
|
|
84
|
+
? html`<ul class="s-va-sessions" id="vaSessions">${overview.authorized.map((a) => html`<li key=${a.handle} data-handle=${a.handle}>
|
|
85
|
+
<strong>${a.label}</strong> <span class="note">${a.account}</span><br /><span class="note">${tn("vaSessionUntil", when(a.authorizedAt), when(a.until))}</span></li>`)}</ul>`
|
|
86
|
+
: html`<p class="note" id="vaSessionsNone">${tn("vaSessionsNone")}</p>`}
|
|
87
|
+
</fieldset>
|
|
88
|
+
<div class="save-row"><button type="button" class="btn btn-primary" id="vaReview" disabled=${disabled || !!hoursError} onClick=${review}>${tn("vaReview")}</button>
|
|
89
|
+
<button type="button" class="btn btn-danger" id="vaRevoke" disabled=${disabled} onClick=${revoke}>${tn("vaRevoke")}</button>
|
|
90
|
+
${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>`);
|
|
91
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { html, useEffect, useRef, useState } from "./app-html.js";
|
|
5
5
|
import { Icon } from "./ui-icons.js";
|
|
6
6
|
import { t, lang } from "./app-i18n.js";
|
|
7
|
+
import { AuthorizeChip } from "./web-authorize.js";
|
|
7
8
|
|
|
8
9
|
async function getJson(url) {
|
|
9
10
|
try {
|
|
@@ -53,6 +54,7 @@ export function SessionMenu() {
|
|
|
53
54
|
${!me ? html`<h3>${t("app.signedOut")}</h3><a class="btn" href=${`/signin?next=${encodeURIComponent(location.pathname)}`}>${t("app.signIn")}</a>` : html`
|
|
54
55
|
<h3>${t("app.signedIn")} · ${me.label}</h3>
|
|
55
56
|
<p class="sub">${t("app.expires", clock(me.expiresAt), span(me.idleMs))}</p>
|
|
57
|
+
<${AuthorizeChip} />
|
|
56
58
|
${rows.length > 1 ? html`<h3>${t("app.devices")}</h3><ul class="devices">${rows.map(s => html`<li key=${s.handle}>
|
|
57
59
|
<span class="grow"><span class="name">${s.label}${s.current ? ` · ${t("app.thisDevice")}` : ""}</span><span class="when">${t("app.lastActive", ago(s.lastSeen))}</span></span>
|
|
58
60
|
${s.current ? null : html`<button type="button" class="btn btn-sm danger" onClick=${() => act("DELETE", `/auth/sessions/${encodeURIComponent(s.handle)}`, refresh)}>${t("app.signOut")}</button>`}</li>`)}</ul>` : null}
|
|
@@ -394,7 +394,7 @@ function PrefsPopover() {
|
|
|
394
394
|
* The header every panel shows: ☰ (phone, or a collapsed sidebar), its title and status, then its actions. `nav`
|
|
395
395
|
* (#1523 N2): an instance page's view switch — beside the title on a wide screen, a row of tabs under it on a phone.
|
|
396
396
|
*/
|
|
397
|
-
export function PanelHeader({ title, sub, children, headingRef, nav }) {
|
|
397
|
+
export function PanelHeader({ title, sub, children, headingRef, nav, notice }) {
|
|
398
398
|
const shell = useStore(shellStore);
|
|
399
399
|
const canCreate = useCanCreate() && !!newInstanceOpener;
|
|
400
400
|
return html`<header data-trust="header" class=${`panel-head${shell.collapsed ? " collapsed" : ""}`}>
|
|
@@ -404,6 +404,7 @@ export function PanelHeader({ title, sub, children, headingRef, nav }) {
|
|
|
404
404
|
<div class="panel-title"><h1 ref=${headingRef} tabindex="-1">${title}</h1>${sub ? html`<div class="panel-sub">${sub}</div>` : null}</div>
|
|
405
405
|
${nav || null}
|
|
406
406
|
<div class="panel-actions">${children}</div>
|
|
407
|
+
${notice || null}
|
|
407
408
|
</header>`;
|
|
408
409
|
}
|
|
409
410
|
|
|
@@ -10,8 +10,11 @@ import { t } from "./app-i18n.js";
|
|
|
10
10
|
import { toast } from "./ui-toast.js";
|
|
11
11
|
import { confirmDialog } from "./ui-confirm.js";
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* { rev, themes: { <name>: theme }, locked: [<name>], widgets: { <name>: { <slot>: widget } }, ambient: { <name>: { loop?, click? } } }
|
|
15
|
+
* as GET /ui/themes said (rev -1: not read yet).
|
|
16
|
+
*/
|
|
17
|
+
export const themesStore = createStore({ rev: -1, themes: {}, locked: [], widgets: {}, ambient: {}, shown: true });
|
|
15
18
|
const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
16
19
|
|
|
17
20
|
const COLOR = /^#[0-9a-f]{6}$/;
|
|
@@ -30,14 +33,15 @@ function labelOk(s) {
|
|
|
30
33
|
return n >= 1 && n <= 24;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
// Public sessions cannot write host prefs. Keep their explicit switch apart from boot-cached
|
|
36
|
+
// Public/guest sessions cannot write host prefs. Keep their explicit browser switch apart from boot-cached defaults.
|
|
37
|
+
// Keep the existing public key for compatibility; both read-only viewer surfaces share this device choice.
|
|
34
38
|
const PUBLIC_FX_SHOW = "agend_public_fx_show";
|
|
35
39
|
let publicFxChoice = (() => { try { const v = localStorage.getItem(PUBLIC_FX_SHOW); return v === "on" ? true : v === "off" ? false : null; } catch { return null; } })();
|
|
36
40
|
|
|
37
41
|
/** Whether this page shows agents' themes now: the viewer's switch (default on), and never in safe mode. */
|
|
38
42
|
export function themesShown() {
|
|
39
43
|
if (safeMode()) return false;
|
|
40
|
-
if (
|
|
44
|
+
if (localAppearanceViewer() && publicFxChoice !== null) return publicFxChoice;
|
|
41
45
|
try {
|
|
42
46
|
const f = prefInForce("fx.show"); // this page's choice, else the host's, else the default
|
|
43
47
|
if (f) return f.value !== false;
|
|
@@ -46,7 +50,7 @@ export function themesShown() {
|
|
|
46
50
|
}
|
|
47
51
|
/** The viewer's switch (Settings → Appearance): kept with the person's preferences, and in force on this page at once. */
|
|
48
52
|
export function setThemesShown(on) {
|
|
49
|
-
if (
|
|
53
|
+
if (localAppearanceViewer()) {
|
|
50
54
|
publicFxChoice = !!on;
|
|
51
55
|
try { localStorage.setItem(PUBLIC_FX_SHOW, on ? "on" : "off"); } catch { /* the in-memory choice still applies */ }
|
|
52
56
|
} else savePref("fx.show", on ? null : false);
|
|
@@ -122,7 +126,8 @@ async function read(rev) {
|
|
|
122
126
|
if (!body || typeof body !== "object") break;
|
|
123
127
|
themesStore.set({ rev: typeof body.rev === "number" ? body.rev : want, themes: body.themes && typeof body.themes === "object" ? body.themes : {},
|
|
124
128
|
locked: Array.isArray(body.locked) ? body.locked.filter((n) => typeof n === "string") : [],
|
|
125
|
-
widgets: body.widgets && typeof body.widgets === "object" && !Array.isArray(body.widgets) ? body.widgets : {}
|
|
129
|
+
widgets: body.widgets && typeof body.widgets === "object" && !Array.isArray(body.widgets) ? body.widgets : {},
|
|
130
|
+
ambient: body.ambient && typeof body.ambient === "object" && !Array.isArray(body.ambient) ? body.ambient : {} });
|
|
126
131
|
if (want === wanted) break;
|
|
127
132
|
}
|
|
128
133
|
} finally { reading = false; }
|
|
@@ -144,6 +149,7 @@ export function startThemes() {
|
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
const publicLink = () => typeof document !== "undefined" && !!document.body && document.body.dataset.publicLink === "1";
|
|
152
|
+
const localAppearanceViewer = () => publicLink() || typeof document !== "undefined" && document.body?.dataset.tier === "guest";
|
|
147
153
|
|
|
148
154
|
/** A person's Reset or Lock of an agent's theme (an admin session; never over the public link). */
|
|
149
155
|
async function themeAction(name, what, body) {
|
|
@@ -169,6 +175,18 @@ async function removeWidget(name, slot) {
|
|
|
169
175
|
else toast(t("chat.widgetRemoved", name));
|
|
170
176
|
}
|
|
171
177
|
|
|
178
|
+
/** #2117: a person's Clear of an agent's ambient, one kind ("loop" or "click"). */
|
|
179
|
+
export async function clearAmbient(name, which) {
|
|
180
|
+
let r;
|
|
181
|
+
try {
|
|
182
|
+
const res = await fetch(`/ui/themes/${encodeURIComponent(name)}/ambient`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ which }) });
|
|
183
|
+
r = await res.json().catch(() => ({}));
|
|
184
|
+
if (!res.ok) r = { error: r && r.error ? r.error : `HTTP ${res.status}` };
|
|
185
|
+
} catch (err) { r = { error: err && err.message ? err.message : "network" }; }
|
|
186
|
+
if (r && r.error) toast(t("chat.themeFailed", r.error), false);
|
|
187
|
+
else toast(t("chat.ambientCleared", name));
|
|
188
|
+
}
|
|
189
|
+
|
|
172
190
|
/** The chat's and Details' ⋯ entries for an agent's theme: Lock/Unlock, Reset… and its widgets' Remove — none over the public link. */
|
|
173
191
|
export function themeMenuItems(name) {
|
|
174
192
|
if (publicLink()) return [];
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -569,6 +569,20 @@ html.lb-open .scroller { overflow: hidden; }
|
|
|
569
569
|
.th-bg { position: absolute; inset: 0; width: 100%; height: 100%; opacity: var(--th-bg-opacity, 1); pointer-events: none; z-index: 0; user-select: none; }
|
|
570
570
|
.th-bg-cover { object-fit: cover; }
|
|
571
571
|
.th-bg-contain { object-fit: contain; }
|
|
572
|
+
/* #2117: an agent's ambient effect (chat-ambient.js). The loop sits behind the thread (z 0, under the scroller and the
|
|
573
|
+
dock at z 1); a burst follows the dock in the tree at z 1, so it shows over the messages and under the header (z 2).
|
|
574
|
+
Neither ever takes a pointer, focus or selection; positions and sizes are set through the CSSOM. */
|
|
575
|
+
.chat-main.has-amb { position: relative; }
|
|
576
|
+
.chat-main.has-amb > .amb-layer, .chat-main.has-amb > .amb-burst { position: absolute; inset: 0; overflow: hidden; pointer-events: none; user-select: none; contain: strict; }
|
|
577
|
+
.chat-main.has-amb > .amb-layer { z-index: 0; }
|
|
578
|
+
.chat-main.has-amb > .amb-burst { z-index: 1; }
|
|
579
|
+
.amb-p { position: absolute; left: 0; top: 0; line-height: 1; color: var(--accent); pointer-events: none; will-change: transform, opacity; }
|
|
580
|
+
img.amb-p { object-fit: contain; }
|
|
581
|
+
.s-mod-glyph { width: 24px; height: 24px; object-fit: contain; }
|
|
582
|
+
/* #2117: the one-time "reduced motion; effects are on" note, a strip above the thread. */
|
|
583
|
+
.chat-main > .amb-note { position: relative; z-index: 1; display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; margin: var(--s-2) var(--s-3) 0; padding: var(--s-1) var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface); color: var(--text-2); font-size: 13px; }
|
|
584
|
+
.chat-main > .amb-note .amb-note-off { border: 0; background: none; padding: 0; color: var(--accent); text-decoration: underline; cursor: pointer; font: inherit; min-height: 32px; }
|
|
585
|
+
.chat-main > .amb-note .amb-note-x { margin-left: auto; min-width: 32px; min-height: 32px; border: 0; background: none; color: var(--text-2); cursor: pointer; }
|
|
572
586
|
.pv-divider { flex: 0 0 8px; cursor: col-resize; background: var(--bg); border-left: 1px solid var(--border); touch-action: none; position: relative; }
|
|
573
587
|
.pv-divider::after { content: ""; position: absolute; top: 50%; left: 2px; width: 3px; height: 32px; margin-top: -16px; border-radius: 2px; background: var(--border-strong); }
|
|
574
588
|
.pv-divider:hover::after, .pv-divider:focus-visible::after, .chat-split.resizing .pv-divider::after { background: var(--accent); }
|
|
@@ -677,6 +691,15 @@ html.lb-open .scroller { overflow: hidden; }
|
|
|
677
691
|
.s-guests .guest-actions { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-top: var(--s-1); }
|
|
678
692
|
.s-guests .guest-narrow-edit { margin-top: var(--s-2); }
|
|
679
693
|
.s-guests .guest-away { color: var(--text-3); }
|
|
694
|
+
/* #1939 S2b: this browser's authorization, under the composer and in the session menu (one compact line). */
|
|
695
|
+
.webauth-chip { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2); margin: var(--s-1) 0 0; font-size: var(--fs-xs); }
|
|
696
|
+
.webauth-chip .webauth-ok { color: var(--ok); }
|
|
697
|
+
.webauth-chip .btn-sm { min-height: 24px; }
|
|
698
|
+
/* #1939 S2a: Settings → Web → Web authority. */
|
|
699
|
+
.s-web-authority .s-va-sessions { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
|
|
700
|
+
.s-web-authority .s-va-sessions li { padding: var(--s-2) var(--s-3); border: 1px solid var(--border); border-radius: var(--r-sm); overflow-wrap: anywhere; }
|
|
701
|
+
.s-web-authority .s-va-warn { background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn-line); border-radius: var(--r-sm); padding: var(--s-1) var(--s-2); }
|
|
702
|
+
.s-web-authority .save-row { flex-wrap: wrap; }
|
|
680
703
|
/* #1920: a guest's note under the composer — on a phone too, where the key hints are hidden. */
|
|
681
704
|
.guest-note { margin: 2px 0 0; font-size: var(--fs-xs); color: var(--text-3); text-align: center; }
|
|
682
705
|
.pending-files { display: flex; flex-wrap: wrap; gap: var(--s-2); padding: var(--s-2) var(--s-2) 0; }
|
|
@@ -1448,15 +1471,21 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1448
1471
|
|
|
1449
1472
|
.chat-card-toggle { display: flex; align-items: center; gap: var(--s-2); width: 100%; min-height: 32px; padding: 0 var(--s-4); border: 0; background: none; color: var(--text-2); font: inherit; font-size: var(--fs-xs); text-align: left; cursor: pointer; }
|
|
1450
1473
|
.chat-card-peek { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-3); }
|
|
1474
|
+
/* A phone: the portrait stays on the text's row, smaller (the user's ask: never a portrait alone on a line with the text
|
|
1475
|
+
under it); the S / M / L control takes the line below. The chat header's card is trimmed further: a smaller portrait
|
|
1476
|
+
and banner, and its tagline and bio cut to a few lines (Details and View show them whole). */
|
|
1451
1477
|
@media (max-width: 479px) {
|
|
1452
|
-
.id-card.card-s { --id-h:
|
|
1453
|
-
.id-card.card-m { --id-h:
|
|
1454
|
-
.id-card.card-l { --id-h:
|
|
1455
|
-
.id-card.card-c { --id-bmin:
|
|
1456
|
-
.id-card.card-c.bs-s { --id-bmin: 80px; --id-bmax:
|
|
1457
|
-
.id-card.card-c.bs-l { --id-bmin:
|
|
1458
|
-
.id-row { flex-wrap: wrap; }
|
|
1459
|
-
.
|
|
1478
|
+
.id-card.card-s { --id-h: 96px; --id-bmin: 80px; --id-bmax: 120px; }
|
|
1479
|
+
.id-card.card-m { --id-h: 128px; --id-bmin: 100px; --id-bmax: 150px; }
|
|
1480
|
+
.id-card.card-l { --id-h: 168px; --id-bmin: 120px; --id-bmax: 200px; }
|
|
1481
|
+
.id-card.card-c { --id-h: 64px; --id-bmin: 80px; --id-bmax: 110px; }
|
|
1482
|
+
.id-card.card-c.bs-s { --id-bmin: 80px; --id-bmax: 100px; }
|
|
1483
|
+
.id-card.card-c.bs-l { --id-bmin: 96px; --id-bmax: 130px; }
|
|
1484
|
+
.id-row { flex-wrap: wrap; gap: var(--s-2) var(--s-3); padding: var(--s-2) var(--s-3); }
|
|
1485
|
+
.card-size { flex-basis: 100%; justify-content: flex-end; }
|
|
1486
|
+
.id-card.card-c .id-tagline, .id-card.card-c .id-bio { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
|
|
1487
|
+
.id-card.card-c .id-tagline { -webkit-line-clamp: 2; line-clamp: 2; }
|
|
1488
|
+
.id-card.card-c .id-bio { -webkit-line-clamp: 3; line-clamp: 3; }
|
|
1460
1489
|
}
|
|
1461
1490
|
/* #1629 r4 §1: the card in View (above the profile row) and its part of Edit profile. */
|
|
1462
1491
|
.v-id { flex-shrink: 0; padding: var(--s-2) var(--s-4) 0; background: var(--bg); }
|
|
@@ -1514,3 +1543,8 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1514
1543
|
.effect-trial-head > span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
1515
1544
|
.effect-trial-body { flex: 1; min-height: 0; display: flex; padding: var(--s-3); }
|
|
1516
1545
|
.effect-trial-stage { position: relative; z-index: 0; flex: 1; min-width: 0; min-height: 0; contain: paint; overflow: clip; padding: 0; border: 0; outline: none; box-shadow: none; }
|
|
1546
|
+
|
|
1547
|
+
/* #2115: diagnostics remain inside the stock protected panel header, never in an effect plane. */
|
|
1548
|
+
.panel-head:has(> .chat-fx-notice) { flex-wrap: wrap; }
|
|
1549
|
+
.chat-fx-notice { order: 10; flex: 1 1 100%; min-width: 0; max-width: 100%; font-size: var(--fs-sm); overflow-wrap: anywhere; }
|
|
1550
|
+
.chat-fx-notice > span { display: block; }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** #1629: app-owned pool; Chat contributes a lease, never another registry/loader. */
|
|
2
|
+
import { effectPlaybackReason, EFFECT_BLOCK_REASONS } from "./effect-policy.js";
|
|
2
3
|
import { createEffectTrialSource } from "./effect-trial-source.js";
|
|
3
4
|
import { createOwnerEffectSource } from "./effect-owner-source.js";
|
|
4
5
|
import { createEffectRuntimeLoader } from "./effect-runtime-loader.js";
|
|
@@ -6,7 +7,7 @@ import { createEffectRegistry } from "./effect-registry.js";
|
|
|
6
7
|
import { createEffectSafety } from "./effect-safety.js";
|
|
7
8
|
import { stockEffectTrust } from "./effect-stock-trust.js";
|
|
8
9
|
import { createEffectProfileCollector } from "./effect-profiles.js";
|
|
9
|
-
import {
|
|
10
|
+
import { effectPlaybackIdentity, createEffectWriter } from "./effect-chat-runtime.js";
|
|
10
11
|
|
|
11
12
|
const episodes = new WeakSet();
|
|
12
13
|
|
|
@@ -14,7 +15,8 @@ const episodes = new WeakSet();
|
|
|
14
15
|
export function createAppEffectRuntime({ window: host, current, observe, onChange = () => {}, onDispose = () => {}, resolveOwnerTarget = null, onOwnerUnavailable = () => {}, factories = {} }) {
|
|
15
16
|
let dead = false, error = null, loader = null, registry = null, writer = null, source = null, personal = null, trial = null, pending = null, lastMetadata = null, serial = 0, revision = 0, stamp = null, syncing = false, queued = false;
|
|
16
17
|
const bindings = new Map(), profiles = new Map();
|
|
17
|
-
|
|
18
|
+
let lastContext = null, playbackUnavailable = null;
|
|
19
|
+
const identity = effectPlaybackIdentity(host), auth = host?.AgendAuth;
|
|
18
20
|
const safe = fn => { try { return fn(); } catch { return null; } };
|
|
19
21
|
const alive = () => { if (dead) return false; const value = safe(() => current() === true && !auth.sessionEnded()); return !dead && value === true; };
|
|
20
22
|
const sourceAlive = s => {
|
|
@@ -25,15 +27,49 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
25
27
|
const placementIdentity = p => JSON.stringify([p.by, p.manifest.layer, p.params]);
|
|
26
28
|
const clock = () => host.performance.now();
|
|
27
29
|
let offAuth = null;
|
|
28
|
-
const snapshot = s =>
|
|
30
|
+
const snapshot = s => {
|
|
31
|
+
const effects = s?.metadata?.effects ?? [], known = registry?.snapshot?.() ?? [];
|
|
32
|
+
const rows = effects.map(p => {
|
|
33
|
+
const b = s.bindings.get(p.id), row = known.find(r => b && r.key === b.key && r.id === p.id);
|
|
34
|
+
if (row && !s.error && !playbackUnavailable) return row.reason == null && row.phase == null ? { ...row, reason: "calibrating" } : row;
|
|
35
|
+
return { id: p.id, key: b?.key ?? null, verdict: "off", phase: null, quality: null,
|
|
36
|
+
reason: s.error ?? playbackUnavailable ?? effectPlaybackReason(lastContext) ?? (!p.manifest.regions.includes("chat") ? "region_unavailable" : "runtime_unavailable") };
|
|
37
|
+
});
|
|
38
|
+
return { error: error ?? s?.error ?? null, rows, effects, loading: !!s && !s.dead && s.metadata === null && !s.error };
|
|
39
|
+
};
|
|
29
40
|
function tell() {
|
|
30
41
|
const s = source, value = snapshot(s); if (dead) return;
|
|
31
|
-
if (sourceAlive(s))
|
|
42
|
+
if (sourceAlive(s)) {
|
|
43
|
+
const next = JSON.stringify(value);
|
|
44
|
+
if (next !== s.lastSnapshot) { s.lastSnapshot = next; safe(() => s.onChange(value)); }
|
|
45
|
+
}
|
|
32
46
|
if (!dead) safe(() => onChange(snapshot(source)));
|
|
33
47
|
}
|
|
48
|
+
function reportPlayback(s) {
|
|
49
|
+
if (!sourceAlive(s) || identity.scope !== "owner") return;
|
|
50
|
+
const now = safe(clock);
|
|
51
|
+
if (!Number.isFinite(now) || now < 0 || s.reportClock !== null && now < s.reportClock) { s.reporter.dispose(); return; }
|
|
52
|
+
s.reportClock = now;
|
|
53
|
+
const value = snapshot(s);
|
|
54
|
+
for (const id of s.reported.keys()) if (!value.effects.some(p => p.id === id)) s.reported.delete(id);
|
|
55
|
+
for (const row of value.rows) {
|
|
56
|
+
if (!sourceAlive(s)) return;
|
|
57
|
+
const status = row.phase === "running" && row.reason == null ? "playing" : "blocked";
|
|
58
|
+
const reason = status === "playing" ? null : EFFECT_BLOCK_REASONS.includes(row.reason) ? row.reason : "unknown";
|
|
59
|
+
const device = lastContext?.constrained === true ? "constrained" : "standard";
|
|
60
|
+
const next = JSON.stringify([status, reason, device]), last = s.reported.get(row.id);
|
|
61
|
+
if (last && (now - last.at < 10000 || next === last.value && now - last.at < 60000)) continue;
|
|
62
|
+
s.reported.set(row.id, { at: now, value: next });
|
|
63
|
+
void s.reporter.post(`/ui/effects/profile/${row.id}?instance=${encodeURIComponent(s.name)}&ownerEpoch=${identity.epoch}`,
|
|
64
|
+
{ v: 1, kind: "playback", device, status, reason });
|
|
65
|
+
}
|
|
66
|
+
// No server polling or unbounded retries: at most one observation per effect / 10s, a 60s heartbeat.
|
|
67
|
+
if (sourceAlive(s)) s.reportTimer = host.setTimeout(() => reportPlayback(s), 10000);
|
|
68
|
+
}
|
|
34
69
|
function discard(s) {
|
|
35
70
|
if (!s || s.dead) return;
|
|
36
71
|
s.dead = true; s.generation++; s.metadata = null;
|
|
72
|
+
safe(() => host.clearTimeout(s.reportTimer)); safe(() => s.reporter?.dispose());
|
|
37
73
|
if (source === s) source = null;
|
|
38
74
|
safe(() => trial?.stopFor(s));
|
|
39
75
|
for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear();
|
|
@@ -63,7 +99,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
63
99
|
writer = createEffectWriter(host, alive);
|
|
64
100
|
loader = make("loader", createEffectRuntimeLoader)({ window: host, ownerEpoch: identity.epoch, current: alive, onRevoke: () => fail("runtime_unavailable") });
|
|
65
101
|
if (!loader.ok || typeof loader.placementsSettled !== "function" || typeof loader.resourceSettled !== "function") throw Error("runtime_unavailable");
|
|
66
|
-
registry = make("registry", createEffectRegistry)({ window: host, origin: host.
|
|
102
|
+
registry = make("registry", createEffectRegistry)({ window: host, origin: host.location?.origin, boot: host.document.body.dataset.effectBoot,
|
|
67
103
|
load: async (descriptor, signal) => {
|
|
68
104
|
const b = bindingFor(descriptor.key, descriptor.id);
|
|
69
105
|
if (!bindingAlive(b) || descriptor.id !== b.id || signal.aborted) return null;
|
|
@@ -78,7 +114,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
78
114
|
stockSnapshot: ({ frameRoot }) => make("stock", stockEffectTrust)(host, { frameRoot, ownerCss: observe().ownerCss }),
|
|
79
115
|
onChange() { try { tell(); emit(); } catch { fail("runtime_unavailable"); } },
|
|
80
116
|
onMount(owner) {
|
|
81
|
-
const b = bindingFor(owner.key, owner.id); if (!bindingAlive(b) || b.id !== owner.id || !owner.current()) return;
|
|
117
|
+
const b = bindingFor(owner.key, owner.id); if (identity.scope !== "owner" || !bindingAlive(b) || b.id !== owner.id || !owner.current()) return;
|
|
82
118
|
const collector = make("collector", createEffectProfileCollector)({ key: owner.key, id: owner.id, device: owner.device, clock });
|
|
83
119
|
const began = safe(clock);
|
|
84
120
|
if (collector.ok && Number.isFinite(began) && began >= 0 && bindingAlive(b) && owner.current()) profiles.set(owner, { binding: b, collector, began, attempted: null }); else collector.dispose?.();
|
|
@@ -95,17 +131,23 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
95
131
|
void writer.post(`/ui/effects/profile/${owner.id}?${query}ownerEpoch=${identity.epoch}`, report.profile);
|
|
96
132
|
}, clock,
|
|
97
133
|
});
|
|
98
|
-
if (!registry.ok)
|
|
99
|
-
|
|
134
|
+
if (!registry.ok) {
|
|
135
|
+
// Keep authenticated placement metadata available for explanations. This inert adapter cannot create,
|
|
136
|
+
// load, resume, send to, or authorize a frame; a failed paint subsystem is never permission.
|
|
137
|
+
playbackUnavailable = "unsupported_host";
|
|
138
|
+
safe(() => registry?.dispose?.());
|
|
139
|
+
registry = { ok: true, frameRoot: null, update() {}, snapshot: () => [], publish() {}, resume: () => false, dispose() {} };
|
|
140
|
+
}
|
|
141
|
+
if (identity.scope === "owner") trial = make("trialSource", createEffectTrialSource)({ window: host, current: () => {
|
|
100
142
|
if (!alive()) return false; const v = observe(), c = v.context;
|
|
101
|
-
return alive() && c.enabled === true && c.show === true && c.
|
|
143
|
+
return alive() && c.enabled === true && c.show === true && c.transportReady === true
|
|
102
144
|
&& c.safe === false && c.publicLink === false && c.reducedMotion === false && c.saveData === false && c.hidden === false;
|
|
103
145
|
}, ownerCurrent: (s, name) => sourceAlive(s) && s.name === name,
|
|
104
146
|
stock: () => registry.frameRoot ? make("stock", stockEffectTrust)(host, { frameRoot: registry.frameRoot, ownerCss: observe().ownerCss }) : null,
|
|
105
147
|
readResource: (spec, signal) => typeof loader.trialResourceSettled === "function" ? loader.trialResourceSettled(spec, signal) : Promise.resolve(null),
|
|
106
148
|
onChange() { revision++; stamp = null; sync(); tell(); },
|
|
107
149
|
});
|
|
108
|
-
if (typeof resolveOwnerTarget === "function") {
|
|
150
|
+
if (identity.scope === "owner" && typeof resolveOwnerTarget === "function") {
|
|
109
151
|
if (typeof loader.ownerSettled !== "function") throw Error("runtime_unavailable");
|
|
110
152
|
personal = createOwnerEffectSource({ current: alive,
|
|
111
153
|
readMetadata: signal => loader.ownerSettled(signal), readResource: (spec, signal) => loader.resourceSettled(spec, signal), resolveTarget: resolveOwnerTarget,
|
|
@@ -139,7 +181,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
139
181
|
}
|
|
140
182
|
function refuseSource(s) {
|
|
141
183
|
if (!sourceAlive(s)) return;
|
|
142
|
-
s.error = "resource_unavailable"; s.
|
|
184
|
+
s.error = "resource_unavailable"; s.generation++;
|
|
143
185
|
for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear(); revision++; stamp = null; sync(); tell();
|
|
144
186
|
}
|
|
145
187
|
function queueSync() {
|
|
@@ -156,6 +198,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
156
198
|
if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
|
|
157
199
|
const entries = [...(personal?.descriptors() ?? []), ...(trial?.descriptors() ?? []), ...descriptors(s)];
|
|
158
200
|
if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
|
|
201
|
+
lastContext = { ...value.context };
|
|
159
202
|
const next = JSON.stringify([revision, value.context, entries.map(d => [d.key, d.paused])]);
|
|
160
203
|
if (stamp !== next) {
|
|
161
204
|
if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
|
|
@@ -164,12 +207,15 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
164
207
|
if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
|
|
165
208
|
emit();
|
|
166
209
|
const c = value.context;
|
|
167
|
-
|
|
168
|
-
|
|
210
|
+
// Metadata describes placements, not executable resources. Its existing session/epoch/current and
|
|
211
|
+
// shared settled-lane fences apply even when this viewer has declined playback. No code is fetched here.
|
|
212
|
+
if (!pending) {
|
|
213
|
+
const ownerNeeds = identity.scope === "owner" && !c.publicLink && c.enabled && c.show && c.transportReady && !c.safe && !c.reducedMotion && !c.saveData && !c.hidden && personal?.needsMetadata() === true, chatNeeds = sourceAlive(s) && !s.error && !s.metadata;
|
|
169
214
|
if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
|
|
170
215
|
if (ownerNeeds && (!chatNeeds || lastMetadata !== "owner")) void readPersonal();
|
|
171
216
|
else if (chatNeeds) void read(s);
|
|
172
217
|
}
|
|
218
|
+
tell();
|
|
173
219
|
} catch { fail("runtime_unavailable"); }
|
|
174
220
|
finally { syncing = false; }
|
|
175
221
|
}
|
|
@@ -216,14 +262,17 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
216
262
|
for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear();
|
|
217
263
|
if (pending?.source === s) safe(() => pending.controller.abort()); revision++; stamp = null; sync(); tell();
|
|
218
264
|
}
|
|
219
|
-
function publishReply(s, event) { if (sourceAlive(s) && !s.error && event.instance === s.name) registry.publish("reply", {}, s.name); }
|
|
265
|
+
function publishReply(s, event) { if (host.document.body.dataset.webTransport !== "poll" && sourceAlive(s) && !s.error && event.instance === s.name) registry.publish("reply", {}, s.name); }
|
|
220
266
|
function attachChat(options) {
|
|
221
267
|
discard(source); const expected = revision; sync();
|
|
222
268
|
if (!alive() || revision !== expected || source !== null) return null;
|
|
223
269
|
const { name, element, current: ownCurrent, observe: ownObserve, onChange: ownChange = () => {} } = options ?? {};
|
|
224
270
|
if (!alive() || typeof name !== "string" || name.length < 1 || name.length > 128 || !element || element.isConnected !== true || typeof ownCurrent !== "function" || typeof ownObserve !== "function" || safe(() => ownCurrent() === true) !== true || revision !== expected || source !== null) return null;
|
|
225
271
|
const s = { name, element, current: ownCurrent, observe: ownObserve, onChange: ownChange, serial: ++serial, generation: 0, dead: false, error: null, metadata: null, bindings: new Map(), paused: new Map() };
|
|
226
|
-
source = s;
|
|
272
|
+
source = s;
|
|
273
|
+
s.reporter = createEffectWriter(host, () => sourceAlive(s)); s.reported = new Map(); s.reportClock = null;
|
|
274
|
+
s.reportTimer = host.setTimeout(() => reportPlayback(s), 10000);
|
|
275
|
+
revision++; stamp = null;
|
|
227
276
|
const placement = id => sourceAlive(s) && !s.error ? s.metadata?.effects.find(p => p.id === id) : null;
|
|
228
277
|
const changed = () => changedSource(s);
|
|
229
278
|
const api = { ok: true, sync,
|
|
@@ -231,14 +280,14 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
231
280
|
reply(event) { publishReply(s, event); },
|
|
232
281
|
pause(id) { const p = placement(id); if (!p) return false; s.paused.set(id, placementIdentity(p)); revision++; sync(); return true; },
|
|
233
282
|
resume(id) { const p = placement(id); if (!p) return false; s.paused.delete(id); revision++; sync(); const b = s.bindings.get(id); return bindingAlive(b) && registry.resume(b.key); },
|
|
234
|
-
async remove(id) { const p = placement(id); if (!p) return false; const generation = s.generation; const ok = await writer.post("/ui/effects/remove", { instance: s.name, id }); if (!sourceAlive(s) || generation !== s.generation) return false; if (ok) changed(); return ok; },
|
|
283
|
+
async remove(id) { const p = placement(id); if (identity.scope !== "owner" || !p) return false; const generation = s.generation; const ok = await writer.post("/ui/effects/remove", { instance: s.name, id }); if (!sourceAlive(s) || generation !== s.generation) return false; if (ok) changed(); return ok; },
|
|
235
284
|
snapshot: () => sourceAlive(s) ? snapshot(s) : { error: null, rows: [], effects: [], loading: false },
|
|
236
285
|
dispose() { if (source !== s) return; discard(s); sync(); },
|
|
237
286
|
};
|
|
238
287
|
sync(); tell(); return sourceAlive(s) ? api : null;
|
|
239
288
|
}
|
|
240
|
-
return { ok: true, sync, attachChat, changedOwner, ownerSnapshot,
|
|
241
|
-
attachTrial(options) { const s = source; return sourceAlive(s) ? trial.attach(options, s) : null; },
|
|
289
|
+
return { ok: true, playbackAvailable: playbackUnavailable === null, sync, attachChat, changedOwner, ownerSnapshot,
|
|
290
|
+
attachTrial(options) { const s = source; return trial && !playbackUnavailable && sourceAlive(s) ? trial.attach(options, s) : null; },
|
|
242
291
|
stopTrial() { trial?.stop(); },
|
|
243
292
|
trialSnapshot() { return { rows: registry?.snapshot().filter(row => trial?.binding(row.key, row.id)) ?? [] }; },
|
|
244
293
|
cursorSelection: () => personal?.cursorSelection() ?? null, cursorImageSettled,
|
|
@@ -46,10 +46,23 @@ export function createEffectWriter(host, current) {
|
|
|
46
46
|
export function chatEffectIdentity(host) {
|
|
47
47
|
try {
|
|
48
48
|
const b = host.document.body.dataset, x = JSON.parse(host.document.documentElement.getAttribute("data-prefs") || "null");
|
|
49
|
-
if (b.mode !== "full" || b.publicLink === "1" || b.webTransport === "poll" || !["person", "operator"].includes(x?.ownerScope) || x.unreadable === true || typeof x.ownerEpoch !== "string" || !/^[a-f0-9]{16}$/.test(x.ownerEpoch)) return null;
|
|
49
|
+
if (b.mode !== "full" || b.tier === "guest" || b.publicLink === "1" || b.webTransport === "poll" || !["person", "operator"].includes(x?.ownerScope) || x.unreadable === true || typeof x.ownerEpoch !== "string" || !/^[a-f0-9]{16}$/.test(x.ownerEpoch)) return null;
|
|
50
50
|
return Object.freeze({ epoch: x.ownerEpoch, scope: x.ownerScope });
|
|
51
51
|
} catch { return null; }
|
|
52
52
|
}
|
|
53
|
+
/** Chat-only public/guest capability stays separate from personal preferences and management. */
|
|
54
|
+
export function effectPlaybackIdentity(host) {
|
|
55
|
+
try {
|
|
56
|
+
const b = host.document.body.dataset;
|
|
57
|
+
if (b.mode !== "full" || typeof b.effectEpoch !== "string" || !/^[a-f0-9]{16}$/.test(b.effectEpoch)) return null;
|
|
58
|
+
if (b.effectScope === "owner") {
|
|
59
|
+
const own = chatEffectIdentity(host);
|
|
60
|
+
return own?.epoch === b.effectEpoch ? Object.freeze({ epoch: b.effectEpoch, scope: "owner" }) : null;
|
|
61
|
+
}
|
|
62
|
+
if (b.effectScope === "chat" && (b.publicLink === "1" && b.webTransport === "poll" || b.tier === "guest")) return Object.freeze({ epoch: b.effectEpoch, scope: "chat" });
|
|
63
|
+
} catch { /* no session capability */ }
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
53
66
|
export function chatEffectState(state, name) {
|
|
54
67
|
const inst = state.instances?.find(i => i.name === name);
|
|
55
68
|
if (!inst) return null;
|
|
@@ -81,7 +94,7 @@ export function createChatEffectRuntime({ window: host, name, element, current,
|
|
|
81
94
|
writer = createEffectWriter(host, alive);
|
|
82
95
|
loader = make("loader", createEffectRuntimeLoader)({ window: host, ownerEpoch: identity.epoch, current: alive });
|
|
83
96
|
if (!loader.ok) throw Error("runtime_unavailable");
|
|
84
|
-
registry = make("registry", createEffectRegistry)({ window: host, origin: host.
|
|
97
|
+
registry = make("registry", createEffectRegistry)({ window: host, origin: host.location?.origin, boot: host.document.body.dataset.effectBoot,
|
|
85
98
|
load: async (d, signal) => {
|
|
86
99
|
const mine = generation, value = await loader.resource({ id: d.id, instance: name }, signal);
|
|
87
100
|
if (!alive() || mine !== generation || signal.aborted) return null;
|
|
@@ -125,7 +138,7 @@ export function createChatEffectRuntime({ window: host, name, element, current,
|
|
|
125
138
|
const stamp = JSON.stringify(ctx);
|
|
126
139
|
if (stamp !== lastContext) { lastContext = stamp; registry.update(descriptors(), ctx); }
|
|
127
140
|
emit();
|
|
128
|
-
if (!metadata && !active && ctx.enabled && ctx.show && ctx.
|
|
141
|
+
if (!metadata && !active && ctx.enabled && ctx.show && ctx.transportReady && !ctx.safe && !ctx.publicLink && !ctx.reducedMotion && !ctx.saveData && !ctx.hidden) void read();
|
|
129
142
|
}
|
|
130
143
|
async function read() {
|
|
131
144
|
if (!alive() || error || active) return;
|
|
@@ -39,12 +39,26 @@ export function effectTransportReady(host, origin, boot) {
|
|
|
39
39
|
} catch { return false; }
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** #2118: same URL origin, opaque realm. This helper does not alter product admission or HTML preview consent. */
|
|
43
|
+
export function effectBootstrapReady(host, origin, boot) {
|
|
44
|
+
try {
|
|
45
|
+
const target = new URL(origin), page = new URL(host.location.origin);
|
|
46
|
+
return origin === target.origin && target.origin === page.origin && ["http:", "https:"].includes(target.protocol)
|
|
47
|
+
&& typeof boot === "string" && /^[0-9a-f]{32}$/.test(boot);
|
|
48
|
+
} catch { return false; }
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
/**
|
|
43
52
|
* All options come from the host, never from an agent event or manifest origin. The registry supplies an admitted
|
|
44
53
|
* root and geometry in a later integration. This primitive alone does not implement caps, clipping or profiling.
|
|
45
54
|
*/
|
|
46
|
-
export function createEffectFrame(
|
|
47
|
-
|
|
55
|
+
export function createEffectFrame(options = {}) { return ownedEffectFrame(options, false); }
|
|
56
|
+
|
|
57
|
+
/** Product transport: the same-listener trusted document still has an opaque sandboxed realm. */
|
|
58
|
+
export function createOpaqueEffectFrame(options = {}) { return ownedEffectFrame(options, true); }
|
|
59
|
+
|
|
60
|
+
function ownedEffectFrame({ window: host, root, origin, boot, render, clock = () => performance.now(), onReady = () => {}, onRemove = () => {} } = {}, sameListener) {
|
|
61
|
+
if (!(sameListener ? effectBootstrapReady : effectTransportReady)(host, origin, boot)) return { ok: false, reason: sameListener ? "bootstrap_unknown" : "preview_unknown" };
|
|
48
62
|
const payload = snapshotRender(render);
|
|
49
63
|
if (!payload || !host || !root || root.ownerDocument !== host.document || typeof root.append !== "function" || typeof host.MessageChannel !== "function" || typeof clock !== "function" || typeof onReady !== "function" || typeof onRemove !== "function") return { ok: false, reason: "unsupported" };
|
|
50
64
|
let mounted;
|
|
@@ -122,7 +136,7 @@ export function createEffectFrame({ window: host, root, origin, boot, render, cl
|
|
|
122
136
|
host.addEventListener("message", hello);
|
|
123
137
|
frame.addEventListener("load", load);
|
|
124
138
|
timer = host.setTimeout(deadline, 10000);
|
|
125
|
-
try { frame.src =
|
|
139
|
+
try { frame.src = origin + (sameListener ? "/effect-bootstrap" : "/effect"); root.append(frame); } catch { remove("mount_failed"); }
|
|
126
140
|
return { ok: true, element: frame, state, remove,
|
|
127
141
|
send(event) {
|
|
128
142
|
if (!ready || !current() || !maySendEffectEvent(event, { instance: payload.instance, wants: payload.wants })) return false;
|
|
@@ -58,6 +58,19 @@ export function constrainedDevice(signals) {
|
|
|
58
58
|
return !!signals && (finite(signals.memory) && signals.memory > 0 && signals.memory <= 4 || finite(signals.cores) && signals.cores > 0 && signals.cores <= 4 || signals.coarse === true && finite(signals.width) && signals.width > 0 && signals.width <= 900);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export const EFFECT_BLOCK_REASONS = Object.freeze([
|
|
62
|
+
"transport_unavailable", "preview_unavailable", "preview_not_consented", "unsupported_host", "safe_mode", "reduced_motion", "save_data", "hidden", "disabled", "public_link",
|
|
63
|
+
"baseline_unsupported", "baseline_unavailable", "baseline_timeout", "calibrating", "clock_unavailable", "gates_unknown", "owner_css", "stock_overlay", "modal",
|
|
64
|
+
"safety_off", "safety_unknown", "interrupted", "paused", "host_budget", "frame_limit", "front_limit", "offscreen", "region_unavailable", "resource_unavailable", "resource_timeout", "runtime_unavailable", "unknown",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
/** Diagnostic names only; the admission policy below still validates every required flag. */
|
|
68
|
+
export function effectPlaybackReason(c) {
|
|
69
|
+
if (!c || ["safe", "publicLink", "reducedMotion", "saveData", "hidden", "enabled", "show", "transportReady"].some(k => typeof c[k] !== "boolean")) return "gates_unknown";
|
|
70
|
+
return c.safe ? "safe_mode" : c.reducedMotion ? "reduced_motion" : c.saveData ? "save_data"
|
|
71
|
+
: c.hidden ? "hidden" : !c.enabled ? "unsupported_host" : !c.show ? "disabled" : !c.transportReady ? "transport_unavailable" : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
const OWNER_ORDER = Object.freeze({ page: 0, header: 1, sidebar: 2, box: 3 });
|
|
62
75
|
/** Recomputed from host descriptors; queued/off/unknown descriptors never get a frame reservation. */
|
|
63
76
|
export function allocateEffectFrames(descriptors, context) {
|
|
@@ -65,15 +78,15 @@ export function allocateEffectFrames(descriptors, context) {
|
|
|
65
78
|
const keys = new Set(), duplicate = new Set();
|
|
66
79
|
for (const d of descriptors) if (d && typeof d.key === "string") { if (keys.has(d.key)) duplicate.add(d.key); keys.add(d.key); }
|
|
67
80
|
const result = descriptors.map(d => ({ key: d?.key, verdict: "off", reason: "unknown", queuePos: null }));
|
|
68
|
-
const gates = ["safe", "publicLink", "reducedMotion", "saveData", "hidden", "enabled", "show", "
|
|
81
|
+
const gates = ["safe", "publicLink", "reducedMotion", "saveData", "hidden", "enabled", "show", "transportReady", "constrained", "modal"];
|
|
69
82
|
if (gates.some(k => typeof context[k] !== "boolean") || context.openInstance !== null && !instanceName(context.openInstance)) return result;
|
|
70
83
|
let global = null;
|
|
71
84
|
if (context.safe === true) global = "safe_mode";
|
|
72
|
-
else if (context.publicLink === true) global = "public_link";
|
|
73
85
|
else if (context.reducedMotion === true) global = "reduced_motion";
|
|
74
86
|
else if (context.saveData === true) global = "save_data";
|
|
75
87
|
else if (context.hidden === true) global = "hidden";
|
|
76
|
-
else if (context.enabled !== true
|
|
88
|
+
else if (context.enabled !== true) global = "unsupported_host";
|
|
89
|
+
else if (context.show !== true) global = "disabled";
|
|
77
90
|
|
|
78
91
|
const candidates = [];
|
|
79
92
|
for (let i = 0; i < descriptors.length; i++) {
|
|
@@ -87,7 +100,8 @@ export function allocateEffectFrames(descriptors, context) {
|
|
|
87
100
|
if (!owner && d.region !== "chat") { r.reason = "scope"; continue; }
|
|
88
101
|
if (d.region === "chat" && (!instanceName(d.instance) || d.instance !== context.openInstance)) { r.reason = "closed_chat"; continue; }
|
|
89
102
|
if (global) { r.reason = global; continue; }
|
|
90
|
-
if (
|
|
103
|
+
if (context.publicLink && (d.region !== "chat" || trial)) { r.reason = "public_link"; continue; }
|
|
104
|
+
if (!context.transportReady) { r.reason = "transport_unavailable"; continue; }
|
|
91
105
|
if (d.paused === true) { r.reason = "paused"; continue; }
|
|
92
106
|
if (d.visible !== true) { r.reason = "offscreen"; continue; }
|
|
93
107
|
if (d.layer === "front" && context.modal === true && !trial) { r.reason = "modal"; continue; }
|