@songsid/agend 2.3.0-beta.8 → 2.3.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/agent-cli-instructions.md +2 -1
  2. package/dist/agent-cli.js +3 -0
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-whats-new.js +2 -0
  5. package/dist/agent-whats-new.js.map +1 -1
  6. package/dist/channel/mcp-tools.js +2 -2
  7. package/dist/channel/mcp-tools.js.map +1 -1
  8. package/dist/decision-list.d.ts +25 -0
  9. package/dist/decision-list.js +30 -0
  10. package/dist/decision-list.js.map +1 -0
  11. package/dist/fleet-manager.d.ts +9 -2
  12. package/dist/fleet-manager.js +44 -12
  13. package/dist/fleet-manager.js.map +1 -1
  14. package/dist/fleet-traffic.d.ts +36 -0
  15. package/dist/fleet-traffic.js +100 -0
  16. package/dist/fleet-traffic.js.map +1 -1
  17. package/dist/general-knowledge/skills/web-appearance/SKILL.md +4 -0
  18. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +17 -5
  19. package/dist/outbound-schemas.d.ts +6 -4
  20. package/dist/outbound-schemas.js +6 -2
  21. package/dist/outbound-schemas.js.map +1 -1
  22. package/dist/tool-permissions.js +9 -42
  23. package/dist/tool-permissions.js.map +1 -1
  24. package/dist/ui/app-effects.js +4 -0
  25. package/dist/ui/chat-effects.js +5 -4
  26. package/dist/ui/chat-render.js +5 -2
  27. package/dist/ui/chat-strings.js +2 -2
  28. package/dist/ui/chat-thread.js +133 -16
  29. package/dist/ui/effect-trial-controller.js +13 -8
  30. package/dist/ui/fleet-canvas.js +54 -18
  31. package/dist/ui/fleet-org.js +3 -3
  32. package/dist/ui/fleet-traffic.js +156 -37
  33. package/dist/ui/panel-chat.js +94 -45
  34. package/dist/ui/shared/agend-auth.js +1 -1
  35. package/dist/ui/shared/app-stream.js +3 -2
  36. package/dist/ui/shared/app.css +22 -9
  37. package/dist/ui/shared/effect-policy.js +2 -2
  38. package/dist/ui/shared/effect-registry.js +16 -7
  39. package/dist/ui/shared/effect-stock-snapshot.js +9 -0
  40. package/dist/ui/shared/effect-stock-trust.js +35 -4
  41. package/dist/ui/shared/effect-trial-source.js +1 -1
  42. package/dist/ui/shared/effect-trial-stage.js +27 -21
  43. package/dist/ui/shared/effect-trial.css +1 -1
  44. package/dist/ui/shared/tokens.css +9 -3
  45. package/dist/ui/web-terminal/terminal.css +4 -4
  46. package/dist/web-ambient.js +9 -2
  47. package/dist/web-ambient.js.map +1 -1
  48. package/dist/web-api.d.ts +1 -0
  49. package/dist/web-api.js +1 -1
  50. package/dist/web-api.js.map +1 -1
  51. package/dist/workflow-templates/default.md +1 -1
  52. package/package.json +1 -1
@@ -7,7 +7,7 @@
7
7
  // The instance's own actions (details, start, stop, restart, delete) are in the header's ⋯ menu, and only the ones
8
8
  // that apply to its state are offered (#1408 rough edges 4 and 7).
9
9
  import { html, useCallback, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
10
- import { t } from "/assets/app-i18n.js";
10
+ import { lang, t } from "/assets/app-i18n.js";
11
11
  import { appStore, guestPage, useStore } from "/assets/app-store.js";
12
12
  import { AuthorizeChip } from "/assets/web-authorize.js";
13
13
  import { useLease } from "/assets/app-ctx.js";
@@ -317,66 +317,115 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
317
317
  </div>`;
318
318
  }
319
319
 
320
- /** The thread: Preact owns the elements, the keyed renderer owns what is inside the list (chat-thread.js). */
320
+ /**
321
+ * #2053: the last THREAD_KEEP chats' threads are kept when their view leaves (the chat view is keyed by instance, so
322
+ * each switch built the thread again: a revisit was no faster than a first visit). A kept thread is its list element
323
+ * and its renderer, and nothing more: it leaves as a chat always did (previews stop), with no subscription or timer
324
+ * left; shown again, one render draws only what changed meanwhile, where the reader was. A new language, another
325
+ * document, or more than THREAD_KEEP lets one go.
326
+ */
327
+ export const THREAD_KEEP = 4;
328
+ const keptThreads = new Map(); // name → { thread, list, sink, lang }
329
+ /** Tests only: let every kept thread go. */
330
+ export function forgetKeptThreads() { for (const e of keptThreads.values()) letGo(e, true); keptThreads.clear(); }
331
+ function letGo(e, own) { e.thread.dispose(own); e.list.remove(); }
332
+
333
+ /** A chat's thread: its own list element and renderer, and a sink for the view that shows it. */
334
+ function threadEntry(name, doc) {
335
+ const list = doc.createElement("div");
336
+ list.className = "thread";
337
+ list.setAttribute("role", "log");
338
+ list.setAttribute("aria-live", "off");
339
+ list.setAttribute("aria-label", t("chat.conversationWith", name));
340
+ const e = { list, sink: null, lang: lang(), thread: null };
341
+ const thread = createThread(list, doc.createElement("div"), {
342
+ t, tf: t, isUser: (x) => store.isUser(x, name), onJump: (j) => e.sink?.setJump(j), onEmpty: (v) => e.sink?.setEmpty(v),
343
+ setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
344
+ effectTrialStatus: x => getAppEffects()?.trialStatus(x) ?? "localAdmin",
345
+ effectTrialFailure: x => getAppEffects()?.trialFailure(x),
346
+ fixEffectTrial: (x, reason) => {
347
+ if (reason === "reducedMotion") setFollowsReducedMotion(false);
348
+ else if (reason === "busy") getAppEffects()?.stopTrial();
349
+ else if (reason === "transport") location.reload();
350
+ else if (reason === "localAdmin") location.href = "/signin?next=" + encodeURIComponent(location.pathname + location.search);
351
+ else navigate("/settings/appearance");
352
+ },
353
+ canTryEffect: x => getAppEffects()?.canTry(x) === true,
354
+ startEffectTrial: x => { const seq = navStore.get().seq; void getAppEffects()?.startTrial(x).then(ok => { if (!ok && e.sink && navStore.get().seq === seq && document.hasFocus?.() === true && document.visibilityState === "visible") toast(t("chat.effectTrialUnavailable")); }); },
355
+ clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
356
+ replyButtonBusy: (id) => store.state.rbBusy.has(id),
357
+ pollMine: (id) => store.state.pollMine[id], // #1823: this device's own vote
358
+ serverNow: () => Date.now() + store.state.clockSkew, // #1823: the deadline is the server's
359
+ panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims }, // #1481, #1704
360
+ openImage: openLightbox, // an image, full size, in the page
361
+ emojis: (id) => !!emojiIndex && emojiIndex.known(id), // #1630: the fleet's server emoji
362
+ mentions: (kind, id) => (mentionIndex ? mentionIndex.name(kind, id) : null), // #1682: mention names
363
+ });
364
+ e.thread = thread;
365
+ return e;
366
+ }
367
+ /** While a thread is shown: what it follows. Returns what lets it all go. */
368
+ function follow(name, thread) {
369
+ // #1630: the server-emoji list is read once a message shows a tag; when it arrives, those messages are drawn again.
370
+ // #1682: the same for mention names — asked for the ids the messages carry; when names arrive, drawn again.
371
+ const wantEmojis = (msgs) => { if (emojiIndex && emojiIndex.wants(msgs)) emojiIndex.ensure(); if (mentionIndex) mentionIndex.ensure(msgs); };
372
+ const offs = [];
373
+ if (emojiIndex) offs.push(emojiIndex.subscribe(() => thread.render(store.state.msgs[name] || [])));
374
+ if (mentionIndex) offs.push(mentionIndex.subscribe(() => thread.render(store.state.msgs[name] || [])));
375
+ wantEmojis(store.state.msgs[name]);
376
+ offs.push(store.subscribe((instance, kind) => {
377
+ if (instance !== name) return;
378
+ if (kind === "msgs") { thread.render(store.state.msgs[name] || []); wantEmojis(store.state.msgs[name]); }
379
+ else if (kind === "sent") thread.jumpLatest();
380
+ }));
381
+ offs.push(P() ? P().onChange(() => thread.refreshCards()) : () => {});
382
+ offs.push(getAppEffects()?.subscribeTrial(() => thread.render(store.state.msgs[name] || [])) ?? (() => {}));
383
+ offs.push(panelStore.subscribe(() => thread.refreshCards())); // a card shows whether the panel has its block
384
+ return () => { for (const off of offs) off(); };
385
+ }
386
+
387
+ /** The thread's view: Preact owns the scroller and the jump pill; the kept list (chat-thread.js) is put inside it. */
321
388
  function Thread({ name, th }) {
322
- const scroller = useRef(null), list = useRef(null);
389
+ const scroller = useRef(null), host = useRef(null);
323
390
  const [jump, setJump] = useState({ show: false, unseen: 0 });
324
391
  const [empty, setEmpty] = useState(false);
325
392
  useLayoutEffect(() => {
326
- let live = true;
327
- const thread = createThread(list.current, scroller.current, {
328
- t, tf: t, isUser: (x) => store.isUser(x, name), onJump: setJump, onEmpty: setEmpty,
329
- setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
330
- effectTrialStatus: x => getAppEffects()?.trialStatus(x) ?? "localAdmin",
331
- fixEffectTrial: (x, reason) => {
332
- if (reason === "reducedMotion") setFollowsReducedMotion(false);
333
- else if (reason === "busy") getAppEffects()?.stopTrial();
334
- else if (reason === "transport") location.reload();
335
- else if (reason === "localAdmin") location.href = "/signin?next=" + encodeURIComponent(location.pathname + location.search);
336
- else navigate("/settings/appearance");
337
- },
338
- canTryEffect: x => getAppEffects()?.canTry(x) === true,
339
- startEffectTrial: x => { const seq = navStore.get().seq; void getAppEffects()?.startTrial(x).then(ok => { if (!ok && live && navStore.get().seq === seq && document.hasFocus?.() === true && document.visibilityState === "visible") toast(t("chat.effectTrialUnavailable")); }); },
340
- clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
341
- replyButtonBusy: (id) => store.state.rbBusy.has(id),
342
- pollMine: (id) => store.state.pollMine[id], // #1823: this device's own vote
343
- serverNow: () => Date.now() + store.state.clockSkew, // #1823: the deadline is the server's
344
- panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims }, // #1481, #1704
345
- openImage: openLightbox, // an image, full size, in the page
346
- emojis: (id) => !!emojiIndex && emojiIndex.known(id), // #1630: the fleet's server emoji
347
- mentions: (kind, id) => (mentionIndex ? mentionIndex.name(kind, id) : null), // #1682: mention names
348
- });
393
+ const doc = host.current.ownerDocument;
394
+ let e = keptThreads.get(name);
395
+ // Kept only while it can be shown as it is: the same language, and this page's own document.
396
+ if (e && (e.lang !== lang() || e.list.ownerDocument !== doc)) { letGo(e, true); keptThreads.delete(name); e = undefined; }
397
+ const kept = !!e;
398
+ if (e) keptThreads.delete(name);
399
+ else e = threadEntry(name, doc);
400
+ keptThreads.set(name, e); // the most recent last
401
+ for (const [k, old] of keptThreads) {
402
+ if (keptThreads.size <= THREAD_KEEP) break;
403
+ if (k !== name) { letGo(old, true); keptThreads.delete(k); }
404
+ }
405
+ const thread = e.thread, sc = scroller.current, ls = e.list;
406
+ e.sink = { setJump, setEmpty };
407
+ host.current.append(ls);
408
+ thread.attach(sc);
349
409
  th.current = thread;
350
- // #1630: the server-emoji list is read once a message shows a tag; when it arrives, those messages are drawn again.
351
- // #1682: the same for mention names — asked for the ids the messages carry; when names arrive, drawn again.
352
- const wantEmojis = (msgs) => { if (emojiIndex && emojiIndex.wants(msgs)) emojiIndex.ensure(); if (mentionIndex) mentionIndex.ensure(msgs); };
353
- const offEmojis = emojiIndex ? emojiIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
354
- const offMentions = mentionIndex ? mentionIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
410
+ // Up to date, at the reader's place (the bottom unless they had scrolled up); a kept one's cards say what they can do.
355
411
  thread.render(store.state.msgs[name] || [], { restore: store.state.scrollMemo[name] ?? null });
356
- wantEmojis(store.state.msgs[name]);
357
- const off = store.subscribe((instance, kind) => {
358
- if (instance !== name) return;
359
- if (kind === "msgs") { thread.render(store.state.msgs[name] || []); wantEmojis(store.state.msgs[name]); }
360
- else if (kind === "sent") thread.jumpLatest();
361
- });
362
- const offPv = P() ? P().onChange(() => thread.refreshCards()) : () => {};
363
- const offTrials = getAppEffects()?.subscribeTrial(() => thread.render(store.state.msgs[name] || [])) ?? (() => {});
364
- const offPanel = panelStore.subscribe(() => thread.refreshCards()); // a card shows whether the panel has its block
412
+ if (kept) thread.refreshCards();
413
+ const unfollow = follow(name, thread);
365
414
  const onScroll = () => { store.state.scrollMemo[name] = thread.onScroll(); };
366
- const sc = scroller.current, ls = list.current;
367
415
  sc.addEventListener("scroll", onScroll, { passive: true });
368
416
  ls.addEventListener("click", thread.onClick);
369
417
  return () => {
370
- live = false;
371
- off(); offPv(); offPanel(); offTrials(); offEmojis(); offMentions();
418
+ unfollow();
372
419
  sc.removeEventListener("scroll", onScroll);
373
420
  ls.removeEventListener("click", thread.onClick);
374
- thread.dispose(); // the whole thread goes: every preview in it stops first
421
+ e.sink = null;
375
422
  th.current = null;
423
+ if (keptThreads.get(name) === e) { thread.park(); ls.remove(); } // kept for a later visit: the chat left
424
+ else letGo(e, false); // let go meanwhile (a new language)
376
425
  };
377
426
  }, [name]);
378
427
  return html`<div class="scroller" ref=${scroller}>
379
- <div class="thread" ref=${list} role="log" aria-live="off" aria-label=${t("chat.conversationWith", name)}></div>
428
+ <div class="thread-host" ref=${host}></div>
380
429
  ${empty ? html`<${Empty} icon="chat" title=${t("chat.noMessages")} />` : null}
381
430
  <div class="jump-dock">${jump.show ? html`<button data-trust="thread-control" type="button" class="jump-latest" onClick=${() => th.current && th.current.jumpLatest()}>
382
431
  <${Icon} name="down" size=${16} />${jump.unseen > 0 ? t("chat.newBelow", jump.unseen) : t("chat.jumpLatest")}</button>` : null}</div>
@@ -67,7 +67,7 @@
67
67
  bar.setAttribute("role", "alert");
68
68
  bar.setAttribute("data-trust", "session-ended");
69
69
  bar.className = "agend-session-ended";
70
- bar.style.cssText = "position:fixed;top:0;left:0;right:0;z-index:2147483647;padding:10px 16px;background:#f85149;color:#fff;font:14px system-ui,sans-serif;text-align:center";
70
+ bar.style.cssText = "position:fixed;top:0;left:0;right:0;z-index:2147483647;padding:10px 16px;background:#f85149;color:#fff;font:14px system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\",emoji;text-align:center";
71
71
  const zh = /^zh/i.test(navigator.language || "");
72
72
  bar.append(document.createTextNode(zh ? "登入已結束。" : "Your session has ended. "));
73
73
  const link = document.createElement("a");
@@ -30,8 +30,9 @@
30
30
  // - suspend()/resume(): the page is leaving (pagehide) / came back from the back-forward cache.
31
31
 
32
32
  // #1218: steps_tick — an instance's transcript moved while a Log watches it (SSE only: a poll page asks the Log itself).
33
- // #1918: traffic — a message between instances moved (SSE only: the org canvas reads /ui/traffic when it has no stream).
34
- const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "steps_tick", "clock", "traffic"];
33
+ // #1918: traffic — a message between instances moved; delegation — a task opened, or its report closed it (SSE only: the
34
+ // org canvas reads /ui/traffic when it has no stream).
35
+ const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "steps_tick", "clock", "traffic", "delegation"];
35
36
  const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
36
37
  const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
37
38
  const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
@@ -453,6 +453,9 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
453
453
  .msg.status-line .st-more { border: 0; background: none; color: var(--text-2); cursor: pointer; font-size: var(--fs-xs); padding: var(--s-1) 0 0 var(--s-3); }
454
454
  .msg.status-line .st-more:hover { color: var(--text); }
455
455
  .msg-tools { display: flex; gap: var(--s-1); margin-top: var(--s-1); opacity: 0; transition: opacity var(--t-fast); }
456
+ /* #2147: bounded stock toolbar paint, including fixed descendants. Padding preserves focus outlines. */
457
+ .shell .thread .msg-tools { contain: paint; overflow: clip; overflow-clip-margin: 0px; padding: 4px; margin: 0 -4px -4px; }
458
+ .shell .thread .codeblock > .cb-head { contain: paint; overflow: clip; overflow-clip-margin: 0px; }
456
459
  .msg:hover .msg-tools, .msg:focus-within .msg-tools { opacity: 1; }
457
460
  @media (hover: none) { .msg-tools { opacity: 1; } }
458
461
  /* Delivery ticks: always visible, in colour, with their label on hover and for screen readers (rough edge 10). */
@@ -559,7 +562,7 @@ html.lb-open .scroller { overflow: hidden; }
559
562
  these tokens. The surface paints its own background (the page's body is not its). */
560
563
  .p-chat.th { background: var(--bg); color: var(--text); }
561
564
  .p-chat.th .msg.user .body { border-radius: var(--bubble-radius, var(--r-xl)); }
562
- .p-chat.th-font-system .thread, .p-chat.th-font-system .composer textarea { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
565
+ .p-chat.th-font-system .thread, .p-chat.th-font-system .composer textarea { font-family: system-ui, -apple-system, "Segoe UI", sans-serif, var(--font-emoji); }
563
566
  .p-chat.th-font-mono .thread, .p-chat.th-font-mono .composer textarea { font-family: var(--font-mono); }
564
567
  .p-chat.th-bubble-thin .msg.user .body { box-shadow: inset 0 0 0 1px var(--border); }
565
568
  .p-chat.th-bubble-accent .msg.user .body { box-shadow: inset 0 0 0 1px var(--accent); }
@@ -1217,7 +1220,7 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1217
1220
  .oc-team-name { fill: var(--text); font-size: 13px; font-weight: 600; }
1218
1221
  .oc-team-meta { fill: var(--text-3); font-size: 11px; }
1219
1222
  .oc-team-sum { fill: var(--text-2); font-size: 11.5px; }
1220
- .oc-team.dim { opacity: 0.55; }
1223
+ .oc-team.dim, .oc-team-head.dim { opacity: 0.55; }
1221
1224
  .oc-node.dim { opacity: 0.4; }
1222
1225
  .oc-node { cursor: pointer; }
1223
1226
  .oc-node[role="img"] { cursor: default; }
@@ -1263,7 +1266,11 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1263
1266
  .oc-flow.h-broadcast { stroke: var(--fx-broadcast); } .oc-flow.h-team { stroke: var(--fx-team); stroke-width: 3.5; }
1264
1267
  .oc-flow.ph-live { stroke-dasharray: 7 6; animation: oc-dash 0.9s linear infinite; }
1265
1268
  .oc-flow.ph-queued { stroke-dasharray: 2 5; opacity: 0.8; }
1266
- .oc-flow.ph-fading { stroke: var(--text-3); animation: oc-fade 3s ease-out forwards; }
1269
+ .oc-flow.ph-fading { stroke: var(--text-3); animation: oc-fade 12s ease-out forwards; }
1270
+ /* #1918 review: the work — a delegation in progress (a slow dash, kept until its report), its report coming back, none. */
1271
+ .oc-flow.ph-work { stroke-dasharray: 10 7; stroke-width: 3; animation: oc-dash 2.4s linear infinite; }
1272
+ .oc-flow.ph-return { stroke-width: 3; animation: oc-fade 2.4s ease-out forwards; }
1273
+ .oc-flow.ph-stale { stroke: var(--text-3); stroke-dasharray: 3 6; animation: oc-fade 12s ease-out forwards; }
1267
1274
  .oc-ah.h-task { fill: var(--fx-task); } .oc-ah.h-send { fill: var(--fx-send); } .oc-ah.h-report { fill: var(--fx-report); }
1268
1275
  .oc-ah.h-broadcast { fill: var(--fx-broadcast); } .oc-ah.h-team { fill: var(--fx-team); } .oc-ah.h-fading { fill: var(--text-3); }
1269
1276
  .oc-pulse.h-task { fill: var(--fx-task); } .oc-pulse.h-send { fill: var(--fx-send); } .oc-pulse.h-report { fill: var(--fx-report); }
@@ -1272,16 +1279,19 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1272
1279
  .oc-badge rect { fill: var(--surface-2); stroke: var(--border); } .oc-badge text { fill: var(--text-2); font-size: 10.5px; }
1273
1280
  @keyframes oc-dash { to { stroke-dashoffset: -26; } }
1274
1281
  @keyframes oc-fade { from { opacity: 0.8; } to { opacity: 0.15; } }
1275
- .oc-legend { position: absolute; left: var(--s-3); bottom: var(--s-3); max-width: calc(100% - 220px);
1276
- font-size: var(--fs-xs); color: var(--text-2); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); padding: 6px 10px; }
1277
- .oc-legend summary { cursor: pointer; font-weight: var(--fw-semibold); color: var(--text); }
1278
- .oc-legend[open] summary { margin-bottom: 4px; }
1282
+ /* #2052: the key is a strip under the toolbar (its toggle is in the toolbar), never over the canvas. */
1283
+ .oc-legend { margin: 0 0 var(--s-2); font-size: var(--fs-xs); color: var(--text-2); background: var(--surface); border: 1px solid var(--border);
1284
+ border-radius: var(--r-md); padding: 6px 10px; }
1285
+ .oc-key.on { border-color: var(--accent); }
1279
1286
  .oc-lg-rows { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-3); }
1280
1287
  .oc-lg { display: inline-flex; align-items: center; gap: 6px; }
1281
1288
  .oc-sw { width: 18px; height: 3px; border-radius: 2px; display: inline-block; background: var(--text-3); }
1282
1289
  .oc-sw.h-task { background: var(--fx-task); } .oc-sw.h-send { background: var(--fx-send); } .oc-sw.h-report { background: var(--fx-report); } .oc-sw.h-broadcast { background: var(--fx-broadcast); }
1283
1290
  .oc-sw.ph-queued { background: repeating-linear-gradient(90deg, var(--text-2) 0 3px, transparent 3px 7px); }
1284
1291
  .oc-sw.ph-fading { opacity: 0.5; }
1292
+ .oc-sw.ph-work { background: repeating-linear-gradient(90deg, var(--fx-task) 0 6px, transparent 6px 9px); height: 4px; }
1293
+ .oc-sw.ph-return { height: 4px; }
1294
+ .oc-sw.ph-stale { background: repeating-linear-gradient(90deg, var(--text-3) 0 2px, transparent 2px 6px); }
1285
1295
  .oc-live { gap: 6px; }
1286
1296
  .oc-live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-3); }
1287
1297
  .oc-live.on .oc-live-dot { background: var(--ok); }
@@ -1291,14 +1301,16 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1291
1301
  @media (prefers-reduced-motion: reduce) {
1292
1302
  .oc-flow.ph-live { animation: none; stroke-dasharray: none; }
1293
1303
  .oc-flow.ph-fading { animation: none; opacity: 0.4; }
1304
+ .oc-flow.ph-work { animation: none; }
1305
+ .oc-flow.ph-return, .oc-flow.ph-stale { animation: none; opacity: 0.5; }
1294
1306
  .oc-pulse { display: none; }
1295
1307
  }
1296
1308
  @media (max-width: 640px) {
1297
1309
  .oc-stage { height: calc(100dvh - 248px); min-height: 340px; }
1298
1310
  .oc-find { flex: 1 1 150px; }
1299
- .oc-tools { margin-left: 0; width: 100%; flex-wrap: nowrap; }
1311
+ /* #2052: Find, Key, Live and Canvas/List are wider than a phone together: the row wraps (nothing is cut off). */
1312
+ .oc-tools { margin-left: 0; width: 100%; flex-wrap: wrap; row-gap: var(--s-2); }
1300
1313
  .oc-mini { display: none; }
1301
- .oc-legend { max-width: calc(100% - 24px); gap: 2px var(--s-2); }
1302
1314
  }
1303
1315
  /* A phone: Fleet's five tabs fit when only the open one shows its label (the others keep theirs for screen readers). */
1304
1316
  @media (max-width: 479px) {
@@ -1440,6 +1452,7 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1440
1452
  }
1441
1453
  /* #1630: a Discord server emoji in a chat message, the size of the text around it. */
1442
1454
  .md img.emoji { display: inline-block; width: 1.375em; height: 1.375em; object-fit: contain; vertical-align: -0.3em; margin: 0 0.05em; }
1455
+ .md .emoji-text { color: var(--text-2); }
1443
1456
  /* #1682: a mention, as Discord draws one — accent text on a tint of it; the id (or the Telegram handle) on hover. */
1444
1457
  .md .mention { display: inline; padding: 0 0.2em; border-radius: var(--r-sm); color: var(--accent); background: var(--accent-dim);
1445
1458
  font-weight: var(--fw-medium); white-space: nowrap; }
@@ -77,9 +77,9 @@ export function allocateEffectFrames(descriptors, context) {
77
77
  if (!Array.isArray(descriptors) || descriptors.length > 128 || !plain(context)) return [];
78
78
  const keys = new Set(), duplicate = new Set();
79
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); }
80
- const result = descriptors.map(d => ({ key: d?.key, verdict: "off", reason: "unknown", queuePos: null }));
80
+ const result = descriptors.map(d => ({ key: d?.key, verdict: "off", reason: "descriptor_invalid", queuePos: null }));
81
81
  const gates = ["safe", "publicLink", "reducedMotion", "hidden", "enabled", "show", "transportReady", "constrained", "modal"];
82
- if (gates.some(k => typeof context[k] !== "boolean") || context.openInstance !== null && !instanceName(context.openInstance)) return result;
82
+ if (gates.some(k => typeof context[k] !== "boolean") || context.openInstance !== null && !instanceName(context.openInstance)) return result.map(r => ({ ...r, reason: "gates_unknown" }));
83
83
  let global = null;
84
84
  if (context.safe === true) global = "safe_mode";
85
85
  else if (context.reducedMotion === true) global = "reduced_motion";
@@ -2,7 +2,7 @@
2
2
  import { allocateEffectFrames, effectEvent, effectPlaybackReason } from "./effect-policy.js";
3
3
  import { createOpaqueEffectFrame } from "./effect-frame.js";
4
4
  import { createEffectGeometry, hostEffectRect } from "./effect-geometry.js";
5
- import { effectStockSnapshot, effectStockAllows } from "./effect-stock-snapshot.js";
5
+ import { effectStockSnapshot, effectStockAllows, effectStockRefusal } from "./effect-stock-snapshot.js";
6
6
  import { effectTrialStage, effectTrialStageIdentity, watchEffectTrialStage } from "./effect-trial-stage.js";
7
7
  import { EffectBudget } from "./effect-profiler.js";
8
8
 
@@ -42,7 +42,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
42
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" }; }
43
43
  let dead = false, blocked = null, revision = 0, last = null, lastView = null, frameSerial = 0, context = null, descriptors = [], raf = null, timer = null;
44
44
  const slots = new Map(), loads = new Set(), paused = new Map(), offs = [], observed = new Set(), failures = new Map(), results = new Map();
45
- let readingStock = false;
45
+ let readingStock = false, stockFailure = "stock_evidence_invalid", warned = null;
46
46
  let root, geometry, observer, resize, longTasks = null, longTasksSupported = false;
47
47
  const time = () => {
48
48
  let n; try { n = clock(); } catch { return null; }
@@ -68,10 +68,14 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
68
68
  const coverage = (d = null) => {
69
69
  if (dead || blocked || readingStock) return null;
70
70
  const before = revision; let proof = null; readingStock = true;
71
- try { proof = effectStockSnapshot(stockSnapshot(Object.freeze({ frameRoot: root }))); }
72
- catch { /* unknown is never permission */ }
71
+ let failure = "stock_provider_failed";
72
+ try { const raw = stockSnapshot(Object.freeze({ frameRoot: root })); proof = effectStockSnapshot(raw); failure = proof ? null : effectStockRefusal(raw); }
73
+ catch { /* unavailable stays denied, with a local diagnostic */ }
73
74
  finally { readingStock = false; }
74
75
  if (dead || blocked || before !== revision) return null;
76
+ stockFailure = failure;
77
+ if (failure !== warned) { warned = failure; if (failure) { try { host.console?.warn("[AgEnD effects] Playback blocked:", failure); } catch { /* display only */ } } }
78
+ if (dead || blocked || before !== revision) return null;
75
79
  return d === null || permits(d, proof) ? proof : null;
76
80
  };
77
81
  const position = (d, proof) => d.trialStage ? localStage(d, proof)?.rect ?? null : hostEffectRect(d.element);
@@ -150,7 +154,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
150
154
  const shown = why => (why === "gates_unknown" && !eligible() && gateReason()) || why;
151
155
  function view() {
152
156
  return descriptors.map(d => {
153
- const r = results.get(d.key) ?? { verdict: "off", reason: blocked ?? (!eligible() && gateReason()) ?? "unknown", queuePos: null }, s = slots.get(d.key);
157
+ const r = results.get(d.key) ?? { verdict: "off", reason: blocked ?? (eligible() ? stockFailure : gateReason()) ?? "runtime_unavailable", queuePos: null }, s = slots.get(d.key);
154
158
  // #2055 one display precedence: a global latch, then the sticky safety history, then this page's own pause/failure.
155
159
  const n = named.get(d.key), sticky = n && n.identity === signature(d) ? n.reason : null;
156
160
  return { key: d.key, id: d.id, ...r, ...(sticky && r.verdict === "queued" ? { verdict: "off", queuePos: null } : {}), phase: s?.frame ? s.ready ? "running" : "starting" : s ? "loading" : null,
@@ -173,7 +177,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
173
177
  };
174
178
  function observe() {
175
179
  const nodes = host.document.querySelectorAll("[data-trust]");
176
- if (nodes.length > 128) throw Error("trust coverage unavailable");
180
+ if (nodes.length > 4096) throw Error("trust coverage unavailable");
177
181
  const wanted = new Set([host.document.documentElement, ...nodes]);
178
182
  for (const d of descriptors) if (d.element?.ownerDocument === host.document && d.element.isConnected === true) wanted.add(d.element);
179
183
  for (const n of observed) if (!wanted.has(n)) { resize.unobserve(n); observed.delete(n); }
@@ -186,6 +190,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
186
190
  if (time() === null) blocked = "clock_unavailable";
187
191
  const hidden = context?.hidden === true || host.document.visibilityState !== "visible";
188
192
  const stock = eligible() ? coverage() : null;
193
+ if (dead || proposal !== revision) return;
189
194
  if (blocked || !eligible() || !stock) {
190
195
  if (!stopAll(blocked ?? "gates_unknown")) return;
191
196
  results.clear();
@@ -234,7 +239,11 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
234
239
  if (s.frame && s.hiddenSince !== null) { s.hiddenSince = null; s.healthySince = last; if (s.budget.gates({ hidden: false, reducedMotion: false, saveData: false, show: true }).remove) retire(s, "hidden"); }
235
240
  if (dead || proposal !== revision) return;
236
241
  }
237
- try { observe(); } catch { stopAll("trust_geometry_unknown"); tell(); return; }
242
+ try { observe(); } catch {
243
+ if (dead || proposal !== revision || !stopAll("trust_geometry_unknown")) return;
244
+ for (const d of descriptors) results.set(d.key, { verdict: "off", reason: "trust_geometry_unknown", queuePos: null });
245
+ tell(); return;
246
+ }
238
247
  const max = context.constrained ? 1 : 3;
239
248
  for (const d of descriptors) {
240
249
  if (!chosen.has(d.key) || slots.has(d.key)) continue;
@@ -32,3 +32,12 @@ export function effectStockAllows(snapshot, { layer, by } = {}) {
32
32
  const proof = effectStockSnapshot(snapshot);
33
33
  return !!proof && (layer === "behind" || layer === "front" && proof.front && (by === "human" || proof.agentFront));
34
34
  }
35
+
36
+ /** Display only. Refused provider metadata never becomes paint evidence or authority. */
37
+ export function effectStockRefusal(value) {
38
+ try {
39
+ if (!plain(value)) return "stock_evidence_invalid";
40
+ const d = Object.getOwnPropertyDescriptor(value, "reason");
41
+ return d && Object.hasOwn(d, "value") && typeof d.value === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(d.value) ? d.value : "stock_evidence_invalid";
42
+ } catch { return "stock_evidence_invalid"; }
43
+ }
@@ -115,13 +115,36 @@ function stockBannerFill(host, el) {
115
115
  if (b.display !== "block" || el.offsetParent !== box) return null;
116
116
  return rect(box);
117
117
  }
118
+ function zeroClipMargin(host, style) {
119
+ const margin = style.overflowClipMargin ?? style.getPropertyValue?.("overflow-clip-margin");
120
+ // WebKit versions without this property still implement paint containment. An absent
121
+ // computed value is usable only when the engine explicitly reports it unsupported;
122
+ // unknown capability or a supported but unreadable/expanded margin cannot prove a box.
123
+ return margin === "0px" || [undefined, ""].includes(margin)
124
+ && host.CSS?.supports?.("overflow-clip-margin", "1px") === false;
125
+ }
118
126
  function protectedPaint(host, root, budget) {
127
+ // These two stock toolbar boxes physically clip ALL descendants, including fixed-position
128
+ // children. Their entire box is a hole; descendant ink cannot escape it. Inline, fragmented,
129
+ // expanded-clip or CSS-overridden boxes take the ordinary bounded walk instead.
130
+ const s = host.getComputedStyle(root);
131
+ if (root.matches?.(".shell .thread .msg-tools, .shell .thread .codeblock > .cb-head") && s.contain === "paint"
132
+ && s.display === "flex" && s.overflowX === "clip" && s.overflowY === "clip" && zeroClipMargin(host, s)
133
+ && none(s.filter) && none(s.backdropFilter) && s.borderImageSource === "none" && [undefined, "", "visible"].includes(s.contentVisibility) && root.getClientRects().length === 1) {
134
+ if (++budget.n > 4096) { budget.exceeded = true; return null; }
135
+ const r = rect(root), pad = outset(s);
136
+ // Containment clips descendants, not the root's own ink. Do not infer a world-space
137
+ // outset from local CSS lengths under a potentially transformed content ancestor.
138
+ if (!r || pad !== 0) return null;
139
+ return r;
140
+ }
119
141
  const nodes = [root, ...root.querySelectorAll("*")]; budget.n += nodes.length;
120
- if (budget.n > 4096) return null;
142
+ if (budget.n > 4096) { budget.exceeded = true; return null; }
121
143
  let union = null;
122
144
  for (const el of nodes) {
123
145
  if (!visible(host, el)) continue;
124
146
  const style = host.getComputedStyle(el), pad = outset(style);
147
+ if (style.borderImageSource !== undefined && style.borderImageSource !== "none" || ![undefined, "", "visible"].includes(style.contentVisibility)) return null;
125
148
  const r = none(style.filter) ? rect(el) : stockBannerFill(host, el);
126
149
  if (!r || pad === null || !none(style.backdropFilter)) return null;
127
150
  const a = { x: r.x - pad, y: r.y - pad, width: r.width + pad * 2, height: r.height + pad * 2 };
@@ -165,15 +188,23 @@ export function stockEffectTrust(host, { frameRoot = null, ownerCss = "unknown"
165
188
  if (s.position !== "relative" || s.zIndex !== "1" || !neutral(s)) return fail("stock_order_unknown");
166
189
  }
167
190
  const marked = [...doc.querySelectorAll("[data-trust]")], expected = new Set(), rects = [], budget = { n: 0 };
168
- if (marked.length > 128) return fail("stock_trust_unknown");
191
+ if (marked.length > 4096) return fail("stock_marker_limit");
169
192
  for (const { kind, selector } of STOCK_TRUST_INVENTORY) for (const el of doc.querySelectorAll(selector)) {
170
193
  if (el.ownerDocument !== doc || el.isConnected !== true || el.getAttribute("data-trust") !== kind) return fail("stock_trust_unknown");
171
194
  expected.add(el);
172
195
  }
173
196
  if (marked.some(el => !expected.has(el)) || surface.kind === "chat" && !chat.querySelector('.dock[data-trust="composer"]') || !surface.panel.querySelector('.panel-head[data-trust="header"]')) return fail("stock_trust_unknown");
174
197
  for (const el of marked) {
175
- const bounds = protectedPaint(host, el, budget); if (!bounds) return fail("stock_paint_unknown");
176
- if (bounds.width && bounds.height) rects.push(bounds);
198
+ const bounds = protectedPaint(host, el, budget); if (!bounds) return fail(budget.exceeded ? "stock_paint_limit" : "stock_paint_unknown");
199
+ // Off-screen history remains inventoried and paint-checked; it need not consume a
200
+ // viewport hole. Include the clip algorithm's extra 8px protection margin.
201
+ if (bounds.width && bounds.height && (!finite(host.innerHeight) || bounds.x + bounds.width + 8 >= 0 && bounds.y + bounds.height + 8 >= 0 && bounds.x - 8 <= host.innerWidth && bounds.y - 8 <= host.innerHeight)) rects.push(bounds);
202
+ }
203
+ // Keep the existing 128-hole transport bound. Conservative union may hide extra
204
+ // effect pixels; it can never reveal protected pixels, even for a dense viewport.
205
+ if (rects.length > 128) {
206
+ const rest = rects.splice(127), x = Math.min(...rest.map(r => r.x)), y = Math.min(...rest.map(r => r.y));
207
+ rects.push({ x, y, width: Math.max(...rest.map(r => r.x + r.width)) - x, height: Math.max(...rest.map(r => r.y + r.height)) - y });
177
208
  }
178
209
  if (doc.querySelector(".agend-session-ended")) return fail("session_ended");
179
210
  let barrier = [...doc.querySelectorAll(BARRIERS)].some(el => visible(host, el));
@@ -64,7 +64,7 @@ export function createEffectTrialSource({ window: host, current, ownerCurrent, s
64
64
  const lease = { identity, reply, stage, owner, current: x.current, onStop: x.onStop, startedAt: x.startedAt, until, dead: false, checking: false, pending: new Set(), timer: null, off: null, binding: null };
65
65
  lease.binding = Object.freeze({ kind: "trial", key: identity.key, id: identity.id, source: lease, generation: identity.generation });
66
66
  active = lease; used.add(stage.receipt);
67
- lease.off = watchEffectTrialStage(stage.receipt, () => stop(lease, "stage_ended"));
67
+ lease.off = watchEffectTrialStage(stage.receipt, reason => stop(lease, typeof reason === "string" ? reason : "stage_ended"));
68
68
  if (typeof lease.off !== "function" || constrainEffectTrialStage(stage.receipt, () => checked(lease)) !== true || !checked(lease)) { stop(lease, "stage_unavailable"); return null; }
69
69
  try { arm(lease); } catch { stop(lease, "trial_unavailable"); return null; }
70
70
  if (!checked(lease)) return null;
@@ -1,7 +1,7 @@
1
1
  /** #1629: host-issued LOCAL trial paint receipt. No authority, loader, frame factory or pool is created here. */
2
2
  import { effectTrialShadow } from "./effect-trial-shadow.js";
3
3
  import { effectHostStyle } from "./effect-host-style.js";
4
- import { effectStockSnapshot } from "./effect-stock-snapshot.js";
4
+ import { effectStockSnapshot, effectStockRefusal } from "./effect-stock-snapshot.js";
5
5
  import { STOCK_EFFECT_BARRIERS } from "./effect-stock-trust.js";
6
6
  const receipts = new WeakMap(), owners = new WeakMap();
7
7
  let serial = 0;
@@ -64,7 +64,8 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
64
64
  if (!own || typeof current !== "function" || typeof onStop !== "function" || !host?.document || !panel || !stage || owners.has(stage)) return null;
65
65
  const shell = effectTrialShadow(host, panel), hostSheet = effectHostStyle(host);
66
66
  let sheet; try { sheet = shell?.sheet; if (!shell || shell.stage !== stage || !hostSheet || !sheet || host.CSS?.supports("contain", "paint") !== true || host.CSS.supports("overflow", "clip") !== true) return null; } catch { return null; }
67
- const receipt = Object.freeze(Object.create(null)), frames = new Set(), listeners = new Set(); let dead = false, constraint = null;
67
+ const receipt = Object.freeze(Object.create(null)), frames = new Set(), listeners = new Set(); let dead = false, constraint = null, failure = "stage_unavailable";
68
+ const reject = reason => { failure = reason; return null; };
68
69
  const identity = Object.freeze({ key: own.key, id: own.id, instance: own.instance, generation: own.generation, serial: ++serial });
69
70
  const invalidate = reason => {
70
71
  if (dead) return; dead = true; receipts.delete(receipt); if (owners.get(stage) === receipt) owners.delete(stage);
@@ -76,45 +77,49 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
76
77
  };
77
78
  const blur = event => { if (event.target === host) invalidate("blur"); };
78
79
  function check(stock) {
80
+ failure = "trial_layout_unverified";
79
81
  try {
80
82
  // This callback may reenter disposal or replace the owner. Recheck identity after it returns.
81
- if (dead || current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead) return null;
83
+ if (dead || current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead) return reject("trial_current_changed");
82
84
  const proof = effectStockSnapshot(stock), doc = host.document;
83
- if (effectHostStyle(host) !== hostSheet || effectTrialShadow(host, panel) !== shell) return null;
85
+ if (!proof) return reject(effectStockRefusal(stock));
86
+ if (effectHostStyle(host) !== hostSheet || effectTrialShadow(host, panel) !== shell) return reject("trial_style_unavailable");
84
87
  // The own panel MUST still suppress ordinary foreground effects. Never synthesize front:true.
85
- if (!proof || proof.front !== false || doc.visibilityState !== "visible" || doc.hasFocus?.() !== true || doc.fullscreenElement || doc.pictureInPictureElement) return null;
88
+ if (doc.hasFocus?.() !== true) return reject("trial_window_unfocused");
89
+ if (doc.visibilityState !== "visible") return reject("hidden");
90
+ if (proof.front !== false || doc.fullscreenElement || doc.pictureInPictureElement) return reject("trial_overlay_unverified");
86
91
  const split = doc.querySelector(".main > .p-chat > .chat-split");
87
92
  if (!split || doc.querySelectorAll(".main > .p-chat").length !== 1 || doc.querySelectorAll(".chat-split > .pv-panel").length !== 1
88
- || panel.parentElement !== split || panel.ownerDocument !== doc || panel.isConnected !== true || panel.className !== "pv-panel effect-trial-panel" || panel.getAttribute("data-trust") !== "preview") return null;
93
+ || panel.parentElement !== split || panel.ownerDocument !== doc || panel.isConnected !== true || panel.className !== "pv-panel effect-trial-panel" || panel.getAttribute("data-trust") !== "preview") return reject("trial_panel_unavailable");
89
94
  const children = [...shell.root.children], names = ["effect-trial-style", "effect-trial-head", "effect-trial-controls", "effect-trial-body", "effect-trial-note"];
90
95
  if (panel.children.length !== 0 || children.length !== names.length || children.some((el, i) => el.className !== names[i]) || children[3].children.length !== 1 || children[3].firstElementChild !== stage
91
- || stage.className !== "effect-trial-stage" || stage.ownerDocument !== doc || stage.isConnected !== true || stage.hasAttribute("tabindex") || stage.hasAttribute("contenteditable")) return null;
96
+ || stage.className !== "effect-trial-stage" || stage.ownerDocument !== doc || stage.isConnected !== true || stage.hasAttribute("tabindex") || stage.hasAttribute("contenteditable")) return reject("trial_shadow_unverified");
92
97
  for (const control of [shell.head, shell.controls, shell.close, shell.stop, shell.note]) {
93
98
  const style = host.getComputedStyle(control);
94
- if (style.display === "none" || style.visibility !== "visible" || style.opacity !== "1" || !rect(control)) return null;
99
+ if (style.display === "none" || style.visibility !== "visible" || style.opacity !== "1" || !rect(control)) return reject("trial_controls_unavailable");
95
100
  }
96
101
  if ([...doc.querySelectorAll(STOCK_EFFECT_BARRIERS)].some(el => el !== panel && visible(host, el))
97
- || [...doc.querySelectorAll(".pv-panel")].some(el => el !== panel && visible(host, el))) return null;
102
+ || [...doc.querySelectorAll(".pv-panel")].some(el => el !== panel && visible(host, el))) return reject("trial_other_overlay");
98
103
  const active = doc.activeElement;
99
- if (active && active !== doc.body && active !== doc.documentElement && (active.matches("input,select,textarea,[contenteditable]") || active.isContentEditable)) return null;
104
+ if (active && active !== doc.body && active !== doc.documentElement && (active.matches("input,select,textarea,[contenteditable]") || active.isContentEditable)) return reject("trial_editable_focus");
100
105
  let el = stage, depth = 0;
101
- while (el) { const s = host.getComputedStyle(el); if (++depth > 128 || el.ownerDocument !== doc || !neutral(s) || s.opacity !== "1" || s.visibility !== "visible" || s.display === "none" || s.contain !== (el === stage ? "paint" : "none")) return null; if (el === doc.documentElement) break; el = el.parentElement ?? (el.getRootNode?.() === shell.root ? panel : null); }
102
- if (el !== doc.documentElement) return null;
106
+ while (el) { const s = host.getComputedStyle(el); if (++depth > 128 || el.ownerDocument !== doc || !neutral(s) || s.opacity !== "1" || s.visibility !== "visible" || s.display === "none" || s.contain !== (el === stage ? "paint" : "none")) return reject("trial_ancestor_paint"); if (el === doc.documentElement) break; el = el.parentElement ?? (el.getRootNode?.() === shell.root ? panel : null); }
107
+ if (el !== doc.documentElement) return reject("trial_ancestor_paint");
103
108
  const s = host.getComputedStyle(stage), r = rect(stage), p = rect(panel);
104
109
  if (!r || !p || s.position !== "relative" || s.zIndex !== "0" || s.overflowX !== "clip" || s.overflowY !== "clip"
105
110
  || s.paddingLeft !== "0px" || s.paddingRight !== "0px" || s.paddingTop !== "0px" || s.paddingBottom !== "0px"
106
111
  || ["borderLeftWidth", "borderRightWidth", "borderTopWidth", "borderBottomWidth"].some(k => s[k] !== "0px") || !none(s.boxShadow) || s.outlineStyle !== "none"
107
112
  || r.x < p.x || r.y < p.y || r.x + r.width > p.x + p.width || r.y + r.height > p.y + p.height
108
- || r.x < 0 || r.y < 0 || r.x + r.width > host.innerWidth || r.y + r.height > host.innerHeight) return null;
109
- if ([...stage.children].some(child => ![...frames].some(f => f.element === child))) return null;
113
+ || r.x < 0 || r.y < 0 || r.x + r.width > host.innerWidth || r.y + r.height > host.innerHeight) return reject("trial_clip_unverified");
114
+ if ([...stage.children].some(child => ![...frames].some(f => f.element === child))) return reject("trial_frame_unknown");
110
115
  const width = Math.floor(r.width), height = Math.floor(r.height);
111
- if (width < 1 || height < 1 || width > 4096 || height > 4096 || width * height > 8388608 || dead || owners.get(stage) !== receipt) return null;
112
- if (current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead || stage.isConnected !== true || panel.parentElement !== split) return null;
116
+ if (width < 1 || height < 1 || width > 4096 || height > 4096 || width * height > 8388608 || dead || owners.get(stage) !== receipt) return reject("trial_size_limit");
117
+ if (current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead || stage.isConnected !== true || panel.parentElement !== split) return reject("trial_current_changed");
113
118
  return { x: r.x, y: r.y, width, height };
114
- } catch { return null; }
119
+ } catch { return reject("trial_check_failed"); }
115
120
  }
116
121
  function project(stock) {
117
- const r = check(stock); if (!r) { invalidate("stage_unavailable"); return null; }
122
+ const r = check(stock); if (!r) { invalidate(failure); return null; }
118
123
  return Object.freeze({ root: stage, rect: Object.freeze(r), identity, bind });
119
124
  }
120
125
  function bind(frame, { layer } = {}, stock) {
@@ -131,7 +136,7 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
131
136
  };
132
137
  const handle = { ok: true, element: frame, remove, hide,
133
138
  sync(proof) {
134
- const r = check(proof); if (!r || removed) { invalidate("stage_unavailable"); return false; }
139
+ const r = check(proof); if (!r || removed) { invalidate(failure); return false; }
135
140
  try {
136
141
  const matches = visibility => {
137
142
  const s = host.getComputedStyle(frame), actual = rect(frame);
@@ -146,7 +151,7 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
146
151
  for (const [k, v] of Object.entries({ left: "0px", top: "0px", width: `${r.width}px`, height: `${r.height}px` })) rule.style.setProperty(k, v, "important");
147
152
  if (!matches("hidden")) throw Error("local geometry lost");
148
153
  rule.style.setProperty("visibility", "visible", "important"); shown = true; return true;
149
- } catch { invalidate("stage_unavailable"); return false; }
154
+ } catch { invalidate("trial_frame_geometry"); return false; }
150
155
  },
151
156
  };
152
157
  frames.add(handle);
@@ -154,7 +159,8 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
154
159
  const i = sheet.insertRule(`.${token}{}`, sheet.cssRules.length); rule = sheet.cssRules[i];
155
160
  for (const [k, v] of Object.entries({ position: "absolute", visibility: "hidden", "pointer-events": "none", "z-index": "0", opacity: layer === "front" ? "0.85" : "1", border: "0px", margin: "0px", padding: "0px", "box-sizing": "border-box", "min-width": "0px", "min-height": "0px", "max-width": "none", "max-height": "none", outline: "none", "box-shadow": "none", "background-color": "transparent", animation: "none", transition: "none" })) rule.style.setProperty(k, v, "important");
156
161
  frame.classList.add(token);
157
- if (host.getComputedStyle(frame).visibility !== "hidden" || !check(stock)) throw Error("initial local hiding lost");
162
+ if (host.getComputedStyle(frame).visibility !== "hidden") throw Error("initial local hiding lost");
163
+ if (!check(stock)) { remove(); invalidate(failure); return { ok: false }; }
158
164
  } catch { remove(); invalidate("stage_unavailable"); return { ok: false }; }
159
165
  return handle;
160
166
  }
@@ -16,7 +16,7 @@
16
16
  overflow: clip !important;
17
17
  background: #ffffff !important;
18
18
  color: #1b1b1f !important;
19
- font: 400 16px/1.4 system-ui, sans-serif !important;
19
+ font: 400 16px/1.4 system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji !important;
20
20
  direction: ltr !important;
21
21
  unicode-bidi: isolate !important;
22
22
  text-align: start !important;