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

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