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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/agent-cli-instructions.md +1 -1
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +4 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-file-read.js +68 -8
  7. package/dist/agent-file-read.js.map +1 -1
  8. package/dist/agent-whats-new.js +4 -0
  9. package/dist/agent-whats-new.js.map +1 -1
  10. package/dist/auth-api.js +3 -1
  11. package/dist/auth-api.js.map +1 -1
  12. package/dist/authority/authority-state.d.ts +38 -0
  13. package/dist/authority/authority-state.js +114 -0
  14. package/dist/authority/authority-state.js.map +1 -0
  15. package/dist/authority/session-authorizations.d.ts +43 -0
  16. package/dist/authority/session-authorizations.js +102 -0
  17. package/dist/authority/session-authorizations.js.map +1 -0
  18. package/dist/authority/web-authority.d.ts +31 -11
  19. package/dist/authority/web-authority.js +39 -12
  20. package/dist/authority/web-authority.js.map +1 -1
  21. package/dist/cache-ledger.d.ts +16 -3
  22. package/dist/cache-ledger.js +33 -13
  23. package/dist/cache-ledger.js.map +1 -1
  24. package/dist/cache-service.d.ts +55 -3
  25. package/dist/cache-service.js +152 -16
  26. package/dist/cache-service.js.map +1 -1
  27. package/dist/channel/mcp-tools.js +13 -7
  28. package/dist/channel/mcp-tools.js.map +1 -1
  29. package/dist/classic-channel-manager.d.ts +2 -2
  30. package/dist/classic-channel-manager.js +2 -6
  31. package/dist/classic-channel-manager.js.map +1 -1
  32. package/dist/config-validator.js +10 -4
  33. package/dist/config-validator.js.map +1 -1
  34. package/dist/daemon.d.ts +2 -0
  35. package/dist/daemon.js +31 -9
  36. package/dist/daemon.js.map +1 -1
  37. package/dist/delivery-outbox.d.ts +8 -0
  38. package/dist/delivery-outbox.js +7 -1
  39. package/dist/delivery-outbox.js.map +1 -1
  40. package/dist/fleet-manager.d.ts +109 -2
  41. package/dist/fleet-manager.js +280 -12
  42. package/dist/fleet-manager.js.map +1 -1
  43. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
  44. package/dist/general-knowledge/skills/multi-channel/SKILL.md +2 -1
  45. package/dist/general-knowledge/skills/web-appearance/SKILL.md +89 -30
  46. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +62 -8
  47. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  48. package/dist/human-delivery-receipts.d.ts +14 -0
  49. package/dist/human-delivery-receipts.js +42 -6
  50. package/dist/human-delivery-receipts.js.map +1 -1
  51. package/dist/instance-config-impact.js +1 -0
  52. package/dist/instance-config-impact.js.map +1 -1
  53. package/dist/instance-names.d.ts +4 -0
  54. package/dist/instance-names.js +12 -0
  55. package/dist/instance-names.js.map +1 -0
  56. package/dist/instructions.d.ts +8 -1
  57. package/dist/instructions.js +9 -2
  58. package/dist/instructions.js.map +1 -1
  59. package/dist/locale.js +52 -0
  60. package/dist/locale.js.map +1 -1
  61. package/dist/outbound-handlers.d.ts +6 -0
  62. package/dist/outbound-handlers.js +49 -2
  63. package/dist/outbound-handlers.js.map +1 -1
  64. package/dist/outbound-schemas.d.ts +57 -1
  65. package/dist/outbound-schemas.js +34 -2
  66. package/dist/outbound-schemas.js.map +1 -1
  67. package/dist/public-web-gateway.js +8 -2
  68. package/dist/public-web-gateway.js.map +1 -1
  69. package/dist/rollout-index.d.ts +5 -0
  70. package/dist/rollout-index.js +26 -5
  71. package/dist/rollout-index.js.map +1 -1
  72. package/dist/settings-api.d.ts +6 -0
  73. package/dist/settings-api.js +54 -1
  74. package/dist/settings-api.js.map +1 -1
  75. package/dist/settings-change.js +31 -0
  76. package/dist/settings-change.js.map +1 -1
  77. package/dist/settings-effect.js +10 -1
  78. package/dist/settings-effect.js.map +1 -1
  79. package/dist/tool-permissions.d.ts +0 -7
  80. package/dist/tool-permissions.js +73 -5
  81. package/dist/tool-permissions.js.map +1 -1
  82. package/dist/topic-commands.d.ts +2 -1
  83. package/dist/topic-commands.js +2 -4
  84. package/dist/topic-commands.js.map +1 -1
  85. package/dist/types.d.ts +13 -2
  86. package/dist/ui/app-effects.js +46 -28
  87. package/dist/ui/chat-ambient.js +307 -0
  88. package/dist/ui/chat-effect-posters.js +8 -8
  89. package/dist/ui/chat-effects.js +51 -22
  90. package/dist/ui/chat-strings.js +21 -6
  91. package/dist/ui/chat-thread.js +1 -1
  92. package/dist/ui/fleet-usage.js +401 -0
  93. package/dist/ui/panel-chat.js +22 -9
  94. package/dist/ui/panel-fleet.js +6 -5
  95. package/dist/ui/panel-settings.js +10 -2
  96. package/dist/ui/settings-appearance.js +8 -0
  97. package/dist/ui/settings-mods.js +19 -6
  98. package/dist/ui/settings-owner-effects.js +3 -3
  99. package/dist/ui/settings-strings.js +30 -0
  100. package/dist/ui/settings-web-authority.js +91 -0
  101. package/dist/ui/shared/app-route.js +1 -1
  102. package/dist/ui/shared/app-session.js +2 -0
  103. package/dist/ui/shared/app-shell.js +2 -1
  104. package/dist/ui/shared/app-themes.js +24 -6
  105. package/dist/ui/shared/app.css +95 -0
  106. package/dist/ui/shared/effect-app-runtime.js +68 -19
  107. package/dist/ui/shared/effect-chat-runtime.js +16 -3
  108. package/dist/ui/shared/effect-frame.js +17 -3
  109. package/dist/ui/shared/effect-policy.js +18 -5
  110. package/dist/ui/shared/effect-profiler.js +31 -9
  111. package/dist/ui/shared/effect-registry.js +17 -41
  112. package/dist/ui/shared/effect-stock-trust.js +4 -4
  113. package/dist/ui/shared/effect-trial-stage.js +18 -7
  114. package/dist/ui/shared/fx-motion.js +67 -0
  115. package/dist/ui/shared/usage-dialog.js +8 -0
  116. package/dist/ui/shared/view-strings.js +4 -0
  117. package/dist/ui/shared/web-authorize.js +151 -0
  118. package/dist/usage-report.d.ts +158 -0
  119. package/dist/usage-report.js +226 -0
  120. package/dist/usage-report.js.map +1 -0
  121. package/dist/usage-rollup.d.ts +51 -0
  122. package/dist/usage-rollup.js +168 -0
  123. package/dist/usage-rollup.js.map +1 -0
  124. package/dist/web-ambient.d.ts +72 -0
  125. package/dist/web-ambient.js +154 -0
  126. package/dist/web-ambient.js.map +1 -0
  127. package/dist/web-api.d.ts +33 -0
  128. package/dist/web-api.js +148 -18
  129. package/dist/web-api.js.map +1 -1
  130. package/dist/web-appearance-tools.d.ts +3 -0
  131. package/dist/web-appearance-tools.js +18 -3
  132. package/dist/web-appearance-tools.js.map +1 -1
  133. package/dist/web-appearance-view.d.ts +14 -2
  134. package/dist/web-appearance-view.js +32 -5
  135. package/dist/web-appearance-view.js.map +1 -1
  136. package/dist/web-appearance.d.ts +15 -1
  137. package/dist/web-appearance.js +77 -1
  138. package/dist/web-appearance.js.map +1 -1
  139. package/dist/web-asset-parts.d.ts +2 -2
  140. package/dist/web-asset-parts.js +3 -3
  141. package/dist/web-asset-parts.js.map +1 -1
  142. package/dist/web-auth.js +3 -1
  143. package/dist/web-auth.js.map +1 -1
  144. package/dist/web-effect-bootstrap.d.ts +12 -0
  145. package/dist/web-effect-bootstrap.js +49 -0
  146. package/dist/web-effect-bootstrap.js.map +1 -0
  147. package/dist/web-effect-bundle.js +46 -1
  148. package/dist/web-effect-bundle.js.map +1 -1
  149. package/dist/web-effect-playback.d.ts +30 -0
  150. package/dist/web-effect-playback.js +71 -0
  151. package/dist/web-effect-playback.js.map +1 -0
  152. package/dist/web-effect-profiles.d.ts +4 -4
  153. package/dist/web-effect-service.d.ts +11 -4
  154. package/dist/web-effect-service.js +25 -4
  155. package/dist/web-effect-service.js.map +1 -1
  156. package/dist/web-effect-tools.d.ts +1 -0
  157. package/dist/web-effect-tools.js +2 -1
  158. package/dist/web-effect-tools.js.map +1 -1
  159. package/dist/web-effect-worker.d.ts +1 -1
  160. package/dist/web-effect-worker.js +2 -2
  161. package/dist/web-effect-worker.js.map +1 -1
  162. package/dist/web-guest-access.js +3 -1
  163. package/dist/web-guest-access.js.map +1 -1
  164. package/dist/web-mod-agent.js +5 -1
  165. package/dist/web-mod-agent.js.map +1 -1
  166. package/dist/web-mod-bundle.d.ts +9 -1
  167. package/dist/web-mod-bundle.js +62 -3
  168. package/dist/web-mod-bundle.js.map +1 -1
  169. package/dist/web-mod-export.d.ts +9 -0
  170. package/dist/web-mod-export.js +21 -0
  171. package/dist/web-mod-export.js.map +1 -1
  172. package/dist/web-mod-store.d.ts +2 -1
  173. package/dist/web-mod-store.js +13 -6
  174. package/dist/web-mod-store.js.map +1 -1
  175. package/dist/web-session-authorization.d.ts +132 -0
  176. package/dist/web-session-authorization.js +193 -0
  177. package/dist/web-session-authorization.js.map +1 -0
  178. package/dist/web-session.d.ts +5 -0
  179. package/dist/web-session.js +16 -0
  180. package/dist/web-session.js.map +1 -1
  181. package/dist/web-settings-patch.d.ts +26 -2
  182. package/dist/web-settings-patch.js +50 -1
  183. package/dist/web-settings-patch.js.map +1 -1
  184. package/dist/web-settings-tools.d.ts +8 -0
  185. package/dist/web-settings-tools.js +14 -1
  186. package/dist/web-settings-tools.js.map +1 -1
  187. package/dist/web-shell-routes.d.ts +1 -1
  188. package/dist/web-shell-routes.js +1 -1
  189. package/dist/web-shell-routes.js.map +1 -1
  190. package/dist/web-theme-store.d.ts +39 -0
  191. package/dist/web-theme-store.js +122 -11
  192. package/dist/web-theme-store.js.map +1 -1
  193. package/package.json +1 -1
@@ -1,7 +1,6 @@
1
1
  /** #1629 B2: one host-owned Canvas Worker pool. Product adapters are deliberately separate. */
2
- import { allocateEffectFrames, effectEvent } from "./effect-policy.js";
3
- import { captureEffectBaseline } from "./effect-baseline.js";
4
- import { createEffectFrame } from "./effect-frame.js";
2
+ import { allocateEffectFrames, effectEvent, effectPlaybackReason } from "./effect-policy.js";
3
+ import { createOpaqueEffectFrame } from "./effect-frame.js";
5
4
  import { createEffectGeometry, hostEffectRect } from "./effect-geometry.js";
6
5
  import { effectStockSnapshot, effectStockAllows } from "./effect-stock-snapshot.js";
7
6
  import { effectTrialStage, effectTrialStageIdentity, watchEffectTrialStage } from "./effect-trial-stage.js";
@@ -10,7 +9,7 @@ import { EffectBudget } from "./effect-profiler.js";
10
9
  const registries = new WeakMap();
11
10
  const plain = o => !!o && (Object.getPrototypeOf(o) === Object.prototype || Object.getPrototypeOf(o) === null);
12
11
  const finite = v => typeof v === "number" && Number.isFinite(v);
13
- const FLAGS = ["safe", "publicLink", "reducedMotion", "saveData", "hidden", "enabled", "show", "previewReady", "optIn", "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,7 +40,7 @@ function snapshotDescriptor(d) { try { return unsafeSnapshotDescriptor(d); } cat
41
40
  */
42
41
  export function createEffectRegistry({ window: host, origin, boot, load, safety, stockSnapshot, onChange = () => {}, onProfile = () => {}, onMount = () => {}, onRetire = () => {}, clock = () => host.performance.now() } = {}) {
43
42
  try { if (!host?.document || !host.document.body || registries.has(host) || typeof load !== "function" || typeof stockSnapshot !== "function" || typeof onChange !== "function" || typeof onProfile !== "function" || typeof onMount !== "function" || typeof onRetire !== "function" || typeof clock !== "function" || !safety || ["allowed", "begin", "finish", "pause", "resume"].some(k => typeof safety[k] !== "function") || ["requestAnimationFrame", "cancelAnimationFrame", "setTimeout", "clearTimeout", "MutationObserver", "ResizeObserver", "AbortController"].some(k => typeof host[k] !== "function")) return { ok: false, reason: "registry_unsupported" }; } catch { return { ok: false, reason: "registry_unsupported" }; }
44
- let dead = false, blocked = null, revision = 0, last = null, lastView = null, frameSerial = 0, context = null, descriptors = [], 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
45
  let readingStock = false;
47
46
  let root, geometry, observer, resize, longTasks = null, longTasksSupported = false;
@@ -105,12 +104,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
105
104
  return true;
106
105
  };
107
106
  // Display only: the closed global gate a row is waiting on, when the host knows it (else the caller's unknown).
108
- const gateReason = () => {
109
- if (!gatesKnown()) return null;
110
- const c = context;
111
- return c.safe ? "safe_mode" : c.publicLink ? "public_link" : c.reducedMotion ? "reduced_motion" : c.saveData ? "save_data"
112
- : !(c.enabled && c.show) ? "disabled" : !(c.previewReady && c.optIn) ? "preview_unavailable" : null;
113
- };
107
+ const gateReason = () => effectPlaybackReason(context);
114
108
  const clearMarker = (s, why) => {
115
109
  if (!s.token) return true;
116
110
  const token = s.token; s.token = null;
@@ -122,7 +116,9 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
122
116
  s.live = false;
123
117
  if (slots.get(s.d.key) === s) slots.delete(s.d.key);
124
118
  if (s.request) { s.request.controller.abort(); if (s.request.timer !== null) host.clearTimeout(s.request.timer); }
125
- if (!["disposed", "superseded", "hidden", "human_resume", "calibrating", "geometry_changed"].includes(why)) failures.set(s.d.key, { identity: signature(s.d), reason: why });
119
+ // A closed/unknown viewer gate revokes paint, not the effect identity. Fresh known gates
120
+ // may admit it again; resource failures and actual safety pauses retain their latches.
121
+ if (!["disposed", "superseded", "hidden", "human_resume", "geometry_changed", "gates_unknown"].includes(why)) failures.set(s.d.key, { identity: signature(s.d), reason: why });
126
122
  s.stageOff?.(); s.stageOff = null;
127
123
  s.frame?.remove(why); s.geometry?.remove();
128
124
  if (s.loadOff) s.loadOff();
@@ -141,17 +137,12 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
141
137
  retire(s, why);
142
138
  if (revision !== proposed || dead !== wasDead) return false;
143
139
  }
144
- if (calibration) { calibration.cancel(); calibration = null; }
145
140
  return true;
146
141
  }
147
142
  function gatesKnown() { return context && FLAGS.every(k => typeof context[k] === "boolean") && (context.openInstance === null || typeof context.openInstance === "string" && context.openInstance.length >= 1 && context.openInstance.length <= 128); }
148
- function consent(d) {
149
- const trial = d?.trialStage && effectTrialStageIdentity(d.trialStage);
150
- return trial ? context.trialReady === true : context.previewReady === true && context.optIn === true;
151
- }
152
143
  function eligible(d = null) {
153
- return gatesKnown() && !context.safe && !context.publicLink && !context.reducedMotion && !context.saveData && context.enabled && context.show
154
- && (d ? consent(d) : context.previewReady && context.optIn || descriptors.some(x => x.trialStage && consent(x)));
144
+ return gatesKnown() && !context.safe && !context.reducedMotion && context.enabled && context.show && context.transportReady
145
+ && (!d || !context.publicLink || d.region === "chat" && !d.trialStage);
155
146
  }
156
147
  const globalAllowed = (d = null) => eligible(d) && host.document.visibilityState === "visible";
157
148
  const signature = d => d.identity;
@@ -228,10 +219,9 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
228
219
  if (!chosen.has(s.d.key) || !d || signature(d) !== signature(s.d) || d.element !== s.d.element || s.device !== device()) retire(s, "superseded");
229
220
  if (dead || proposal !== revision) return;
230
221
  }
231
- if (chosen.size === 0) { if (calibration) { calibration.cancel(); calibration = null; } baseline = null; baselineDevice = null; tell(); return; }
222
+ if (chosen.size === 0) { tell(); return; }
232
223
  if (hidden) {
233
224
  hideGeometry();
234
- if (calibration) { calibration.cancel(); calibration = null; }
235
225
  for (const s of [...slots.values()]) {
236
226
  if (!s.frame) retire(s, "hidden");
237
227
  else { if (s.hiddenSince === null) { s.hiddenSince = last; s.healthySince = null; } if (s.budget.gates({ hidden: true, reducedMotion: false, saveData: false, show: true }).remove) retire(s, "hidden"); }
@@ -245,20 +235,6 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
245
235
  if (dead || proposal !== revision) return;
246
236
  }
247
237
  try { observe(); } catch { stopAll("trust_geometry_unknown"); tell(); return; }
248
- if (baseline === null || baselineDevice !== device()) {
249
- if (calibration && baselineDevice === device()) { tell(); return; }
250
- if (!stopAll("calibrating") || dead || proposal !== revision) return;
251
- const epoch = ++serial;
252
- const job = captureEffectBaseline({ window: host, empty: () => !dead && slots.size === 0 && host.document.querySelectorAll("iframe.agend-effect-frame").length === 0 && globalAllowed() && !context.hidden && !!coverage(), clock: nativeClock });
253
- calibration = job; baselineDevice = device();
254
- job.promise.then(r => {
255
- if (dead || calibration !== job || epoch !== serial) return;
256
- calibration = null;
257
- if (!r.ok) { blocked = r.reason; tell(); return; }
258
- baseline = r.baselineFps; reconcile();
259
- });
260
- tell(); return;
261
- }
262
238
  const max = context.constrained ? 1 : 3;
263
239
  for (const d of descriptors) {
264
240
  if (!chosen.has(d.key) || slots.has(d.key)) continue;
@@ -328,8 +304,8 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
328
304
  if (d.trialStage && !local || !current(s)) { if (current(s)) retire(s, "admission_revoked"); return; }
329
305
  if (local) fresh = local.rect;
330
306
  s.rect = fresh;
331
- s.budget = new EffectBudget({ device: s.device, baselineFps: baseline, clock: nativeClock, longTasksSupported });
332
- const frame = createEffectFrame({ window: host, root: local?.root ?? root, origin, boot, clock: nativeClock,
307
+ s.budget = new EffectBudget({ device: s.device, clock: nativeClock, longTasksSupported });
308
+ const frame = createOpaqueEffectFrame({ window: host, root: local?.root ?? root, origin, boot, clock: nativeClock,
333
309
  render: { code: resource.code, params: resource.params, assets: resource.assets, wants: resource.wants, instance: d.instance, width: fresh.width, height: fresh.height, dpr: dpr(s.device) },
334
310
  onReady() { if (!current(s)) return; if (!globalAllowed(d) || context.hidden || !coverage(d)) { retire(s, "hidden"); tell(); return; } const n = time(); if (n === null) { blocked = "clock_unavailable"; stopAll(blocked); tell(); return; } s.ready = true; s.readyAt = n; s.healthySince = n; publishEnv(s); schedule(); tell(); },
335
311
  onRemove(why) { if (current(s)) { retire(s, why); tell(); } },
@@ -510,7 +486,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
510
486
  descriptors = ds.map(snapshotDescriptor).filter(Boolean);
511
487
  for (const [key, identity] of paused) if (!descriptors.some(d => d.key === key && signature(d) === identity)) paused.delete(key);
512
488
  for (const [key, f] of failures) if (!descriptors.some(d => d.key === key && signature(d) === f.identity)) failures.delete(key);
513
- context = Object.fromEntries([...FLAGS, "trialReady", "openInstance"].map(k => [k, ctx[k]]));
489
+ context = Object.fromEntries([...FLAGS, "openInstance"].map(k => [k, ctx[k]]));
514
490
  reconcile(); return view();
515
491
  },
516
492
  snapshot: view,
@@ -518,11 +494,11 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
518
494
  revalidateSafety,
519
495
  /** Called only by the host's explicit human Resume UI. A descriptor update never clears this latch. */
520
496
  resume(key) {
521
- if (dead || reentrantStock() || blocked && !blocked.startsWith("baseline_") || typeof key !== "string") return false;
497
+ if (dead || reentrantStock() || blocked || typeof key !== "string") return false;
522
498
  const proposed = revision, d = descriptors.find(d => d.key === key); if (!d || d.trialStage && !coverage(d) || revision !== proposed || receipt(safety.resume, d.id, device()) !== true || revision !== proposed) return false;
523
499
  revision++;
524
500
  if (!stopAll("human_resume")) return false;
525
- if (blocked && !blocked.startsWith("baseline_")) return false; blocked = null; paused.delete(key); failures.delete(key); baseline = null; serial++; reconcile(); return true;
501
+ if (blocked) return false; paused.delete(key); failures.delete(key); reconcile(); return true;
526
502
  },
527
503
  /** Host-only content-free signal API. Raw pointer publishing is refused; native pointer metadata is coalesced here. */
528
504
  publish(type, fields, instance = null) {
@@ -538,7 +514,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
538
514
  return sent;
539
515
  },
540
516
  dispose() {
541
- if (dead) return; dead = true; revision++; serial++; stopAll("disposed");
517
+ if (dead) return; dead = true; revision++; stopAll("disposed");
542
518
  if (raf !== null) host.cancelAnimationFrame(raf); if (timer !== null) host.clearTimeout(timer);
543
519
  observer.disconnect(); resize.disconnect(); longTasks?.disconnect(); for (const off of offs) off();
544
520
  geometry.dispose(); root.remove(); observed.clear(); if (loads.size === 0 && registries.get(host) === api) registries.delete(host);
@@ -11,7 +11,7 @@ const SPECS = [
11
11
  ["confirmation", ".shell > .pending-stack, .shell .p-settings > .panel-body > .col > .pending-stack.inline"], ["preview", ".shell .chat-split > .pv-panel, .shell .chat-split > .pv-divider"],
12
12
  ["preview-menu", ".shell .thread details.pv-menu"], ["palette", ".shell .dock .cmd-palette"],
13
13
  ["drop", ".shell .p-chat > .drop-overlay"],
14
- ["thread-control", ".shell .p-chat .jump-latest, .shell .thread .msg-tools, .shell .thread .st-more, .shell .thread .cb-head, .shell .thread .cb-more, .shell .thread .tc-head, .shell .thread .tc-more, .shell .thread .pv-head, .shell .thread .pv-note, .shell .thread .pv-banner, .shell .thread .effect-trial-offer, .shell .thread .click-chip"],
14
+ ["thread-control", ".shell .chat-main > .amb-note, .shell .p-chat .jump-latest, .shell .thread .msg-tools, .shell .thread .st-more, .shell .thread .cb-head, .shell .thread .cb-more, .shell .thread .tc-head, .shell .thread .tc-more, .shell .thread .pv-head, .shell .thread .pv-note, .shell .thread .pv-banner, .shell .thread .effect-trial-offer, .shell .thread .click-chip"],
15
15
  ["link", ".shell .thread .md a, .shell .thread .st-text > a.st-link, .shell .dock .prompts .txt > a.st-link"], ["attachment", ".shell .thread .atts"], ["media", ".shell .thread .att-media"],
16
16
  ["panel-navigation", ".shell .p-fleet > .seg, .shell .p-settings > .seg"],
17
17
  ["panel-content", ".shell .p-fleet > .panel-body, .shell .p-needs > .panel-body, .shell .p-settings > .panel-body, .shell .p-log > .panel-body, .shell .p-details > .panel-body, .shell .p-view > .panel-body, .shell .p-view > .v-term, .shell .p-view > .v-id, .shell .p-view > .v-card"],
@@ -20,7 +20,7 @@ const SPECS = [
20
20
  ];
21
21
  export const STOCK_TRUST_INVENTORY = Object.freeze(SPECS.map(([kind, selector]) => Object.freeze({ kind, selector })));
22
22
  const STRUCTURAL = ["html", "body", "#app", "#app > .shell", ".shell > .main", ".main > .p-chat", ".p-chat > .chat-split", ".chat-split > .chat-main"];
23
- const CONTENT = [".main > .conn", ".p-chat > .panel-head", ".p-chat > .chat-card", ".p-chat > .aw-chat_header", ".chat-main > .scroller", ".chat-main > .dock", ".sidebar > .side-head", ".sidebar > .side-nav", ".sidebar > .side-section", ".sidebar > .side-widget", ".sidebar > .side-foot", ".u-rail > .u-rail-head", ".u-rail > .u-rail-body"];
23
+ const CONTENT = [".main > .conn", ".p-chat > .panel-head", ".p-chat > .chat-card", ".p-chat > .aw-chat_header", ".chat-main > .amb-note", ".chat-main > .scroller", ".chat-main > .dock", ".sidebar > .side-head", ".sidebar > .side-nav", ".sidebar > .side-section", ".sidebar > .side-widget", ".sidebar > .side-foot", ".u-rail > .u-rail-head", ".u-rail > .u-rail-body"];
24
24
  // Closed producer/topology inventory. Bare Outlet loading/error panels never become an effect surface.
25
25
  const PANELS = Object.freeze({
26
26
  fleet: { required: [".panel-head", ".seg", ".panel-body"], optional: [] },
@@ -178,9 +178,9 @@ export function stockEffectTrust(host, { frameRoot = null, ownerCss = "unknown"
178
178
  if (doc.querySelector(".agend-session-ended")) return fail("session_ended");
179
179
  let barrier = [...doc.querySelectorAll(BARRIERS)].some(el => visible(host, el));
180
180
  if (host.innerWidth < 900 && [...doc.querySelectorAll(".pv-panel")].some(el => visible(host, el))) barrier = true;
181
- const active = doc.activeElement;
182
181
  if (doc.fullscreenElement || doc.pictureInPictureElement) barrier = true;
183
- if (active && active !== doc.body && active !== doc.documentElement && (active.matches("input,select,textarea,[contenteditable]") || active.isContentEditable)) barrier = true;
182
+ // Focus alone does not change protected paint bounds. Composer/controls stay clipped; editable focus
183
+ // separately suppresses pointer delivery in the registry, and keyboard events never enter a Worker.
184
184
  return { ok: true, rects, front: !barrier, reason: barrier ? "stock_overlay" : "stock", agentFront: !barrier && ownerCss === "off", agentReason: barrier ? "stock_overlay" : ownerCss === "off" ? "stock" : "owner_css", settingsHref: "/settings/appearance" };
185
185
  } catch { return fail("stock_trust_unknown"); }
186
186
  }
@@ -120,21 +120,32 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
120
120
  function bind(frame, { layer } = {}, stock) {
121
121
  if (frames.size || !["front", "behind"].includes(layer) || frame?.ownerDocument !== host.document || frame.parentElement !== stage || frame.tagName !== "IFRAME") { frame?.remove?.(); return { ok: false }; }
122
122
  // The new frame is temporarily known to this owner before the topology check; it remains hidden until sync.
123
- const token = `agend-effect-trial-${identity.serial}`; let rule, removed = false;
123
+ const token = `agend-effect-trial-${identity.serial}`; let rule, removed = false, shown = false;
124
124
  const remove = () => {
125
125
  if (removed) return; removed = true; frame.remove(); frames.delete(handle);
126
126
  try { frame.classList.remove(token); const i = [...sheet.cssRules].indexOf(rule); if (i >= 0) sheet.deleteRule(i); } catch { /* physically removed first */ }
127
127
  };
128
- const hide = () => { try { rule.style.setProperty("visibility", "hidden", "important"); if (frame.isConnected && host.getComputedStyle(frame).visibility !== "hidden") remove(); } catch { remove(); } };
128
+ const hide = () => {
129
+ shown = false;
130
+ try { rule.style.setProperty("visibility", "hidden", "important"); if (frame.isConnected && host.getComputedStyle(frame).visibility !== "hidden") { remove(); return false; } return !removed; } catch { remove(); return false; }
131
+ };
129
132
  const handle = { ok: true, element: frame, remove, hide,
130
133
  sync(proof) {
131
- hide(); const r = check(proof); if (!r || removed) { invalidate("stage_unavailable"); return false; }
134
+ const r = check(proof); if (!r || removed) { invalidate("stage_unavailable"); return false; }
132
135
  try {
136
+ const matches = visibility => {
137
+ const s = host.getComputedStyle(frame), actual = rect(frame);
138
+ return !!actual && s.position === "absolute" && s.visibility === visibility && s.pointerEvents === "none" && s.zIndex === "0" && s.opacity === (layer === "front" ? "0.85" : "1") && neutral(s)
139
+ && ["x", "y", "width", "height"].every(k => Math.abs(actual[k] - r[k]) < 0.02) && actual.x >= r.x && actual.y >= r.y && actual.x + actual.width <= r.x + r.width && actual.y + actual.height <= r.y + r.height
140
+ && frame.isConnected === true && frame.ownerDocument === host.document && frame.parentElement === stage && !dead && !removed && owners.get(stage) === receipt;
141
+ };
142
+ // Reprove the current stage, clipping and actual frame every tick. Hiding/revealing an
143
+ // unchanged iframe each rAF can starve Firefox's OffscreenCanvas presentation.
144
+ if (shown && matches("visible")) return true;
145
+ if (!hide()) throw Error("local hiding lost");
133
146
  for (const [k, v] of Object.entries({ left: "0px", top: "0px", width: `${r.width}px`, height: `${r.height}px` })) rule.style.setProperty(k, v, "important");
134
- const s = host.getComputedStyle(frame), actual = rect(frame);
135
- if (!actual || s.position !== "absolute" || s.visibility !== "hidden" || s.pointerEvents !== "none" || s.zIndex !== "0" || s.opacity !== (layer === "front" ? "0.85" : "1") || !neutral(s)
136
- || !["x", "y", "width", "height"].every(k => Math.abs(actual[k] - r[k]) < 0.02) || actual.x < r.x || actual.y < r.y || actual.x + actual.width > r.x + r.width || actual.y + actual.height > r.y + r.height) throw Error("local geometry lost");
137
- rule.style.setProperty("visibility", "visible", "important"); return true;
147
+ if (!matches("hidden")) throw Error("local geometry lost");
148
+ rule.style.setProperty("visibility", "visible", "important"); shown = true; return true;
138
149
  } catch { invalidate("stage_unavailable"); return false; }
139
150
  },
140
151
  };
@@ -0,0 +1,67 @@
1
+ // #2117 (the user's direction, hmsg-3c166c26): agents' effects play whatever the system's reduced-motion setting says,
2
+ // unless this device chose "Effects: follow the system's reduced-motion setting". One per-device switch (not synced:
3
+ // it is about this screen), shared by ambient effects and Canvas Worker effects; it also works on the public link.
4
+ export const FX_FOLLOW_REDUCED_MOTION_KEY = "agend_fx_follow_reduced_motion";
5
+ /** The one-time chat note "your system asks for reduced motion; effects are on" was shown on this device. */
6
+ export const FX_MOTION_NOTE_KEY = "agend_fx_motion_note_seen";
7
+ const listeners = new Set();
8
+ let pageChoice = null, noteShown = false;
9
+
10
+ /** Whether this device asked effects to follow the system's reduced-motion setting (default: no). */
11
+ export function followsReducedMotion() {
12
+ if (pageChoice !== null) return pageChoice;
13
+ try { return globalThis.localStorage.getItem(FX_FOLLOW_REDUCED_MOTION_KEY) === "1"; } catch { return false; }
14
+ }
15
+ export function setFollowsReducedMotion(on) {
16
+ pageChoice = !!on;
17
+ try { if (on) globalThis.localStorage.setItem(FX_FOLLOW_REDUCED_MOTION_KEY, "1"); else globalThis.localStorage.removeItem(FX_FOLLOW_REDUCED_MOTION_KEY); } catch { /* this page only */ }
18
+ for (const fn of [...listeners]) { try { fn(); } catch { /* a listener's own */ } }
19
+ }
20
+ /** `fn()` when the switch or the system's answer moves (this tab, another tab, the OS). Returns the unsubscribe. */
21
+ export function onMotionChange(fn) {
22
+ listeners.add(fn);
23
+ const onStorage = (e) => {
24
+ if (!e || e.key === FX_FOLLOW_REDUCED_MOTION_KEY || e.key === null) {
25
+ pageChoice = e?.key === FX_FOLLOW_REDUCED_MOTION_KEY ? e.newValue === "1" : null;
26
+ fn();
27
+ }
28
+ };
29
+ let q = null;
30
+ try { q = typeof globalThis.matchMedia === "function" ? globalThis.matchMedia("(prefers-reduced-motion: reduce)") : null; } catch { q = null; }
31
+ try { globalThis.addEventListener && globalThis.addEventListener("storage", onStorage); } catch { /* none */ }
32
+ try { if (q && q.addEventListener) q.addEventListener("change", fn); } catch { /* read again on the next render */ }
33
+ return () => {
34
+ listeners.delete(fn);
35
+ try { globalThis.removeEventListener && globalThis.removeEventListener("storage", onStorage); } catch { /* gone */ }
36
+ try { if (q && q.removeEventListener) q.removeEventListener("change", fn); } catch { /* gone */ }
37
+ };
38
+ }
39
+ /** Whether the system asks for reduced motion (false when it can't say: the note is only for a clear yes). */
40
+ export function systemReducesMotion(win = globalThis) {
41
+ try { return typeof win.matchMedia === "function" && win.matchMedia("(prefers-reduced-motion: reduce)").matches === true; } catch { return false; }
42
+ }
43
+ /**
44
+ * The one-time note (the user's direction): the system asks for reduced motion, this device does not follow it, and the
45
+ * note was not shown here before. Showing it marks it shown.
46
+ */
47
+ export function motionNoteDue(win = globalThis) {
48
+ if (noteShown || followsReducedMotion() || !systemReducesMotion(win)) return false;
49
+ try { return globalThis.localStorage.getItem(FX_MOTION_NOTE_KEY) !== "1"; } catch { return true; }
50
+ }
51
+ export function markMotionNoteShown() {
52
+ noteShown = true;
53
+ try { globalThis.localStorage.setItem(FX_MOTION_NOTE_KEY, "1"); } catch { /* shown again next time: harmless */ }
54
+ }
55
+
56
+ /**
57
+ * True when effects stay still here: this device follows the system's setting and the system asks for reduced motion
58
+ * (or cannot say — then still, as asked for). With the switch off (the default), never.
59
+ */
60
+ export function motionBlocked(win = globalThis) {
61
+ if (!followsReducedMotion()) return false;
62
+ try {
63
+ if (typeof win.matchMedia !== "function") return true;
64
+ const q = win.matchMedia("(prefers-reduced-motion: reduce)");
65
+ return !q || typeof q.matches !== "boolean" || q.matches;
66
+ } catch { return true; }
67
+ }
@@ -4,6 +4,7 @@
4
4
  import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "./app-html.js";
5
5
  import { t } from "./app-i18n.js";
6
6
  import { useLease } from "./app-ctx.js";
7
+ import { guestPage } from "./app-store.js";
7
8
  import { readStream } from "./read-stream.js";
8
9
  import { Dialog } from "./ui-dialog.js";
9
10
  import { ErrorState, Skeleton } from "./ui-states.js";
@@ -159,11 +160,18 @@ export function UsageBody({ owner }) {
159
160
  ${!p.stale && p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : !(p.metrics || []).length ? html`<p class="note">${tn("usage.no_data")}</p>` : null}`}
160
161
  </section>`)}
161
162
  <div class="u-foot"><span class="note" title=${new Date(data.fetchedAt).toLocaleString()}>${updatedAgo(data.fetchedAt)}${data.refreshing ? html` · <span class="u-refreshing">${tn("usageRefreshing")}</span>` : null}</span>
163
+ ${byAgent() ? html`<a class="btn btn-ghost btn-sm u-by-agent" href="/ui/fleet/usage" title=${tn("usageByAgentTitle")}>${tn("usageByAgent")}</a>` : null}
162
164
  <button type="button" class="btn btn-sm" onClick=${() => load(true)}>${tn("usageRefresh")}</button></div>`;
163
165
  }
164
166
  return content;
165
167
  }
166
168
 
169
+ /**
170
+ * #2122: the plans above are per login; each agent's own tokens and API-equivalent are Fleet → Usage — linked from the
171
+ * app (not View's anonymous reader, not a guest, who has no Fleet).
172
+ */
173
+ const byAgent = () => !guestPage() && typeof location !== "undefined" && location.pathname.startsWith("/ui");
174
+
167
175
  /** `owner`: the navigation that opened it — its reads and the minute's timer live and end with it. */
168
176
  export function UsageDialog({ onClose, owner = "" }) {
169
177
  return html`<${Dialog} title=${tn("usage.title")} onClose=${onClose} wide=${true}><${UsageBody} owner=${`usage-dialog:${owner}`} /></${Dialog}>`;
@@ -40,6 +40,8 @@ register("view", {
40
40
  "usageMinutes": "{n} min",
41
41
  "usageHours": "{n} h",
42
42
  "usageRefresh": "Refresh",
43
+ "usageByAgent": "By agent →",
44
+ "usageByAgentTitle": "Each agent's tokens and API-equivalent (Fleet → Usage)",
43
45
  "usageHigh": "high",
44
46
  "usageNear": "near limit",
45
47
  "usageMoveUp": "Move up",
@@ -248,6 +250,8 @@ register("view", {
248
250
  "usageMinutes": "{n} 分鐘",
249
251
  "usageHours": "{n} 小時",
250
252
  "usageRefresh": "重新整理",
253
+ "usageByAgent": "依 agent →",
254
+ "usageByAgentTitle": "每個 agent 的 token 與 API 等值(Fleet → 用量)",
251
255
  "usageHigh": "偏高",
252
256
  "usageNear": "接近上限",
253
257
  "usageMoveUp": "上移",
@@ -0,0 +1,151 @@
1
+ // #1939 S2b (design §0.1): "Authorize this browser". The app shows this browser's state — verified (until when), a
2
+ // Confirm card waiting in the account's Discord or Telegram DM (with its countdown), or why it can't be authorized here —
3
+ // and asks for the card. The card itself is the gate: nothing here can authorize anything.
4
+ import { html, useEffect, useRef, useState } from "./app-html.js";
5
+ import { register, t } from "./app-i18n.js";
6
+ import { guestPage } from "./app-store.js";
7
+
8
+ register("webauth", {
9
+ en: {
10
+ verified: "Verified · speaks as you until {0}",
11
+ notVerified: "Not verified",
12
+ authorize: "Authorize this browser",
13
+ waiting: "Waiting for your Confirm in Discord or Telegram ({0})",
14
+ sent: "Sent — confirm it in your DM within 5 minutes.",
15
+ publicLink: "Not on the temporary public link: open the dashboard directly to authorize.",
16
+ reason_disabled: "Web authority is turned off.",
17
+ reason_loopback_only: "Only this computer can authorize, and this browser is not on it.",
18
+ reason_no_account: "This browser was not signed in as an account (agend web): sign in with /web to authorize.",
19
+ reason_not_admin: "Your account is not a fleet admin here.",
20
+ reason_not_admin_session: "Guests can't authorize.",
21
+ reason_token_rotated: "The dashboard's token changed: sign in again.",
22
+ reason_public_link: "Not on the temporary public link.",
23
+ reason_not_session: "Sign in to authorize.",
24
+ reason_other: "This browser can't be authorized here.",
25
+ refusal_pending: "A request is already waiting in your DM.",
26
+ refusal_rate_limited: "Three requests this hour — try again in {0} min.",
27
+ refusal_dm_unavailable: "Couldn't send you a DM. Open your DMs with the bot, then try again.",
28
+ refusal_other: "That didn't work: {0}",
29
+ settings: "Settings",
30
+ },
31
+ "zh-TW": {
32
+ verified: "已驗證 · 以你的身分發言到 {0}",
33
+ notVerified: "未驗證",
34
+ authorize: "授權這個瀏覽器",
35
+ waiting: "等你在 Discord 或 Telegram 按下確認({0})",
36
+ sent: "已送出——請在 5 分鐘內到私訊確認。",
37
+ publicLink: "臨時公開連結上無法授權:請直接開啟儀表板再授權。",
38
+ reason_disabled: "網頁授權已關閉。",
39
+ reason_loopback_only: "只有這台電腦可以授權,而這個瀏覽器不在這台電腦上。",
40
+ reason_no_account: "這個瀏覽器不是用帳號登入的(agend web):請用 /web 登入後再授權。",
41
+ reason_not_admin: "你的帳號在這裡不是 fleet 管理者。",
42
+ reason_not_admin_session: "訪客無法授權。",
43
+ reason_token_rotated: "儀表板的 token 已變更:請重新登入。",
44
+ reason_public_link: "臨時公開連結上無法授權。",
45
+ reason_not_session: "登入後才能授權。",
46
+ reason_other: "這個瀏覽器在這裡無法授權。",
47
+ refusal_pending: "已經有一個請求在你的私訊中等待。",
48
+ refusal_rate_limited: "這一小時已請求三次——請 {0} 分鐘後再試。",
49
+ refusal_dm_unavailable: "無法私訊給你。請開放與 bot 的私訊後再試一次。",
50
+ refusal_other: "沒有成功:{0}",
51
+ settings: "設定",
52
+ },
53
+ });
54
+ const tn = (k, ...v) => t(`webauth.${k}`, ...v);
55
+ const REASONS = ["disabled", "loopback_only", "no_account", "not_admin", "not_admin_session", "token_rotated", "public_link", "not_session"];
56
+ // Until when: the time, with the day when it isn't today (an authorization lasts up to 48 h).
57
+ const clock = (ms) => {
58
+ const d = new Date(ms), today = new Date().toDateString() === d.toDateString();
59
+ return today ? d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
60
+ : d.toLocaleString([], { weekday: "short", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
61
+ };
62
+ const mmss = (ms) => { const s = Math.max(0, Math.ceil(ms / 1000)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; };
63
+ const publicLink = () => globalThis.document?.body?.dataset?.publicLink === "1";
64
+
65
+ // One read per page load, shared by every line that shows it (the composer's, the session menu's): coming back to a chat
66
+ // reads nothing (#1374). It is read again only after an ask, and while a card is pending.
67
+ // An answer of any kind counts as this page's read (one that isn't a state hides the line); only a network failure lets a
68
+ // later mount try again. Signed out (401: the session ended, e.g. Not me), the line goes.
69
+ let shared = null, answered = false, inflight = null, signedOut = false;
70
+ async function readStatus() {
71
+ inflight ??= (async () => {
72
+ try {
73
+ const r = await fetch("/ui/authorization", { credentials: "same-origin" });
74
+ answered = true;
75
+ if (r.status === 401) { signedOut = true; shared = null; }
76
+ const body = r.ok ? await r.json().catch(() => null) : null;
77
+ if (body && typeof body.state === "string") shared = body;
78
+ } catch { /* the line stays as it was; a later mount may try again */ }
79
+ finally { inflight = null; }
80
+ return shared;
81
+ })();
82
+ return inflight;
83
+ }
84
+
85
+ /** This browser's authorization, kept fresh while a card is pending (every 3 s, until it is decided or expires). */
86
+ export function useWebAuthorization() {
87
+ const [st, setSt] = useState(shared), [notice, setNotice] = useState(null);
88
+ const [deadline, setDeadline] = useState(0), [now, setNow] = useState(() => Date.now());
89
+ const alive = useRef(true);
90
+ const load = async () => {
91
+ if (publicLink() || guestPage()) return;
92
+ const body = await readStatus();
93
+ if (!alive.current) return;
94
+ if (signedOut) { setSt(null); setNotice(null); return; }
95
+ if (!body) return;
96
+ setSt(body);
97
+ if (body.state !== "pending") setNotice((n) => (n === tn("sent") ? null : n)); // "Sent —" was about the card
98
+ if (body.state === "pending" && typeof body.expiresInMs === "number") setDeadline(Date.now() + body.expiresInMs);
99
+ };
100
+ useEffect(() => { alive.current = true; if (!answered) load(); return () => { alive.current = false; }; }, []);
101
+ useEffect(() => {
102
+ if (!st || st.state !== "pending") return undefined;
103
+ const tick = setInterval(() => setNow(Date.now()), 1000), poll = setInterval(load, 3000);
104
+ return () => { clearInterval(tick); clearInterval(poll); };
105
+ }, [st && st.state]);
106
+ // "Verified · until X" is read again at X (a page left open must not keep saying verified after it ends).
107
+ useEffect(() => {
108
+ if (!st || st.state !== "verified" || typeof st.until !== "number") return undefined;
109
+ const at = setTimeout(() => { resetWebAuthorization(); load(); }, Math.max(0, Math.min(st.until - Date.now() + 1000, 2 ** 31 - 1)));
110
+ return () => clearTimeout(at);
111
+ }, [st && st.state, st && st.until]);
112
+ const ask = async () => {
113
+ setNotice(null);
114
+ let r = null, body = null;
115
+ try { r = await fetch("/ui/authorization", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: "{}" }); body = await r.json(); } catch { body = null; }
116
+ if (r && r.ok && body && body.ok) { setNotice(tn("sent")); setSt(shared = { state: "pending", expiresInMs: body.expiresInMs }); setDeadline(Date.now() + body.expiresInMs); return; }
117
+ const refusal = body && body.refusal;
118
+ if (refusal === "rate_limited") setNotice(tn("refusal_rate_limited", Math.max(1, Math.ceil((body.retryAfterMs || 0) / 60_000))));
119
+ else if (refusal === "pending" || refusal === "dm_unavailable") setNotice(tn(`refusal_${refusal}`));
120
+ else if (REASONS.includes(refusal)) setNotice(tn(`reason_${refusal}`));
121
+ else setNotice(tn("refusal_other", String(refusal || (r && r.status) || "?")));
122
+ load();
123
+ };
124
+ return { st, notice, ask, left: deadline - now };
125
+ }
126
+
127
+ /**
128
+ * Forget the page's one read, so the next line that mounts reads again: Settings → Web calls it when it shows (a revoke
129
+ * or a changed switch there changes what the line should say). Tests use it too: each case is its own page load.
130
+ */
131
+ export function resetWebAuthorization() { shared = null; answered = false; inflight = null; signedOut = false; }
132
+
133
+ /** The line under the composer: verified, waiting, or "Not verified" with the button (or why not, with the fix). */
134
+ export function AuthorizeChip() {
135
+ const a = useWebAuthorization();
136
+ if (guestPage()) return null;
137
+ if (publicLink()) return html`<p class="webauth-chip note" data-state="public">${tn("publicLink")}</p>`;
138
+ if (!a.st) return null;
139
+ return html`<div class="webauth-chip" data-state=${a.st.state}>${body(a)}${a.notice ? html` <span class="note" role="status">${a.notice}</span>` : null}</div>`;
140
+ }
141
+
142
+ function body(a) {
143
+ const st = a.st;
144
+ if (st.state === "verified") return html`<span class="webauth-ok">✓ ${tn("verified", clock(st.until))}</span>`;
145
+ if (st.state === "pending") return html`<span class="note">${tn("waiting", mmss(a.left))}</span>`;
146
+ if (st.reason === "not_authorized" || st.reason === "authorization_expired")
147
+ return html`<span class="note">${tn("notVerified")} ·</span> <button type="button" class="btn btn-sm" onClick=${a.ask}>${tn("authorize")}</button>`;
148
+ const why = REASONS.includes(st.reason) ? tn(`reason_${st.reason}`) : tn("reason_other");
149
+ // The fix, where there is one the person can take here: the switch lives in Settings → Web.
150
+ return html`<span class="note">${tn("notVerified")}: ${why}</span>${st.reason === "disabled" || st.reason === "loopback_only" ? html` <a href="/settings/web">${tn("settings")}</a>` : null}`;
151
+ }