@songsid/agend 2.2.0 → 2.3.0-beta.1

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 (271) hide show
  1. package/dist/agent-cli.js +248 -2
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-endpoint.d.ts +2 -0
  4. package/dist/agent-endpoint.js +25 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-file-read.d.ts +30 -0
  7. package/dist/agent-file-read.js +129 -0
  8. package/dist/agent-file-read.js.map +1 -0
  9. package/dist/agent-whats-new.js +13 -0
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/atomic-write.js +9 -1
  12. package/dist/atomic-write.js.map +1 -1
  13. package/dist/auth-api.js +17 -0
  14. package/dist/auth-api.js.map +1 -1
  15. package/dist/backend/antigravity.js +3 -3
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/launch-effort.js +1 -1
  18. package/dist/backend/launch-effort.js.map +1 -1
  19. package/dist/backend/muse.js +10 -2
  20. package/dist/backend/muse.js.map +1 -1
  21. package/dist/channel/adapters/discord.d.ts +2 -0
  22. package/dist/channel/adapters/discord.js +17 -2
  23. package/dist/channel/adapters/discord.js.map +1 -1
  24. package/dist/channel/adapters/telegram.d.ts +8 -1
  25. package/dist/channel/adapters/telegram.js +74 -4
  26. package/dist/channel/adapters/telegram.js.map +1 -1
  27. package/dist/channel/mcp-tools.js +34 -0
  28. package/dist/channel/mcp-tools.js.map +1 -1
  29. package/dist/channel/types.d.ts +15 -0
  30. package/dist/config-validator.js +17 -1
  31. package/dist/config-validator.js.map +1 -1
  32. package/dist/daemon.js +244 -20
  33. package/dist/daemon.js.map +1 -1
  34. package/dist/fleet-context.d.ts +5 -0
  35. package/dist/fleet-manager.d.ts +89 -0
  36. package/dist/fleet-manager.js +401 -25
  37. package/dist/fleet-manager.js.map +1 -1
  38. package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
  39. package/dist/instance-avatar.js +3 -0
  40. package/dist/instance-avatar.js.map +1 -1
  41. package/dist/instance-config-impact.js +3 -0
  42. package/dist/instance-config-impact.js.map +1 -1
  43. package/dist/instance-lifecycle.js +9 -4
  44. package/dist/instance-lifecycle.js.map +1 -1
  45. package/dist/needs-you-hub.d.ts +4 -0
  46. package/dist/needs-you-hub.js +31 -6
  47. package/dist/needs-you-hub.js.map +1 -1
  48. package/dist/needs-you.d.ts +1 -13
  49. package/dist/needs-you.js +3 -37
  50. package/dist/needs-you.js.map +1 -1
  51. package/dist/outbound-handlers.d.ts +5 -0
  52. package/dist/outbound-handlers.js +30 -9
  53. package/dist/outbound-handlers.js.map +1 -1
  54. package/dist/outbound-schemas.d.ts +64 -0
  55. package/dist/outbound-schemas.js +50 -0
  56. package/dist/outbound-schemas.js.map +1 -1
  57. package/dist/public-web-gateway.js +14 -0
  58. package/dist/public-web-gateway.js.map +1 -1
  59. package/dist/repo-worktree-guard.d.ts +36 -0
  60. package/dist/repo-worktree-guard.js +143 -0
  61. package/dist/repo-worktree-guard.js.map +1 -0
  62. package/dist/scheduler/scheduler.d.ts +58 -1
  63. package/dist/scheduler/scheduler.js +176 -10
  64. package/dist/scheduler/scheduler.js.map +1 -1
  65. package/dist/settings-api.js +74 -1
  66. package/dist/settings-api.js.map +1 -1
  67. package/dist/settings-change.js +8 -0
  68. package/dist/settings-change.js.map +1 -1
  69. package/dist/settings-effect.js +16 -2
  70. package/dist/settings-effect.js.map +1 -1
  71. package/dist/settings-http-confirmation.js +5 -0
  72. package/dist/settings-http-confirmation.js.map +1 -1
  73. package/dist/tool-permissions.js +50 -0
  74. package/dist/tool-permissions.js.map +1 -1
  75. package/dist/topic-commands.d.ts +10 -6
  76. package/dist/topic-commands.js +21 -17
  77. package/dist/topic-commands.js.map +1 -1
  78. package/dist/topic-link.d.ts +128 -0
  79. package/dist/topic-link.js +269 -0
  80. package/dist/topic-link.js.map +1 -0
  81. package/dist/types.d.ts +13 -0
  82. package/dist/ui/app-effects.js +187 -0
  83. package/dist/ui/chat-effect-posters.js +42 -0
  84. package/dist/ui/chat-effects.js +72 -0
  85. package/dist/ui/chat-live-replies.js +58 -0
  86. package/dist/ui/chat-render.js +4 -4
  87. package/dist/ui/chat-store.js +9 -3
  88. package/dist/ui/chat-strings.js +15 -0
  89. package/dist/ui/chat-thread.js +47 -21
  90. package/dist/ui/chat-tour.js +1 -1
  91. package/dist/ui/effect-trial-controller.js +119 -0
  92. package/dist/ui/effect-trial-offer.js +31 -0
  93. package/dist/ui/effect-trial-panel.js +37 -0
  94. package/dist/ui/image-lightbox.js +1 -1
  95. package/dist/ui/panel-chat.js +85 -17
  96. package/dist/ui/panel-details.js +18 -5
  97. package/dist/ui/panel-fleet.js +3 -3
  98. package/dist/ui/panel-log.js +6 -6
  99. package/dist/ui/panel-needs.js +3 -3
  100. package/dist/ui/panel-settings.js +8 -5
  101. package/dist/ui/preview-panel.js +25 -9
  102. package/dist/ui/settings-appearance.js +325 -0
  103. package/dist/ui/settings-effects.js +201 -0
  104. package/dist/ui/settings-mods.js +257 -0
  105. package/dist/ui/settings-owner-effects.js +80 -0
  106. package/dist/ui/settings-strings.js +245 -0
  107. package/dist/ui/shared/agend-auth.js +27 -3
  108. package/dist/ui/shared/agent-widget.js +248 -0
  109. package/dist/ui/shared/app-i18n.js +2 -0
  110. package/dist/ui/shared/app-prefs.js +30 -10
  111. package/dist/ui/shared/app-route.js +23 -1
  112. package/dist/ui/shared/app-session.js +1 -1
  113. package/dist/ui/shared/app-shell.js +29 -16
  114. package/dist/ui/shared/app-store.js +2 -0
  115. package/dist/ui/shared/app-stream.js +12 -7
  116. package/dist/ui/shared/app-themes.js +188 -0
  117. package/dist/ui/shared/app.css +214 -1
  118. package/dist/ui/shared/app.js +6 -0
  119. package/dist/ui/shared/card-strings.js +28 -0
  120. package/dist/ui/shared/effect-app-runtime.js +248 -0
  121. package/dist/ui/shared/effect-baseline.js +64 -0
  122. package/dist/ui/shared/effect-chat-runtime.js +154 -0
  123. package/dist/ui/shared/effect-cursor.js +125 -0
  124. package/dist/ui/shared/effect-frame.js +127 -0
  125. package/dist/ui/shared/effect-geometry.js +122 -0
  126. package/dist/ui/shared/effect-host-style.js +10 -0
  127. package/dist/ui/shared/effect-owner-regions.js +74 -0
  128. package/dist/ui/shared/effect-owner-source.js +115 -0
  129. package/dist/ui/shared/effect-policy.js +137 -0
  130. package/dist/ui/shared/effect-poster-reader.js +98 -0
  131. package/dist/ui/shared/effect-posters.js +117 -0
  132. package/dist/ui/shared/effect-profiler.js +162 -0
  133. package/dist/ui/shared/effect-profiles.js +65 -0
  134. package/dist/ui/shared/effect-registry.js +496 -0
  135. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  136. package/dist/ui/shared/effect-safety.js +153 -0
  137. package/dist/ui/shared/effect-stock-snapshot.js +34 -0
  138. package/dist/ui/shared/effect-stock-trust.js +165 -0
  139. package/dist/ui/shared/effect-trial-source.js +103 -0
  140. package/dist/ui/shared/effect-trial-stage.js +147 -0
  141. package/dist/ui/shared/header-tools.js +1 -1
  142. package/dist/ui/shared/identity-card.js +202 -0
  143. package/dist/ui/shared/identity-color.js +261 -0
  144. package/dist/ui/shared/instance-nav.js +22 -14
  145. package/dist/ui/shared/owner-css.js +221 -0
  146. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  147. package/dist/ui/shared/panel-view.js +81 -8
  148. package/dist/ui/shared/signin.js +2 -0
  149. package/dist/ui/shared/ui-dialog.js +1 -1
  150. package/dist/ui/shared/ui-icons.js +2 -0
  151. package/dist/ui/shared/ui-menu.js +1 -1
  152. package/dist/ui/shared/ui-toast.js +1 -1
  153. package/dist/ui/shared/view-strings.js +30 -0
  154. package/dist/ui/signin.html +1 -0
  155. package/dist/view-api.d.ts +29 -0
  156. package/dist/view-api.js +188 -8
  157. package/dist/view-api.js.map +1 -1
  158. package/dist/web-api.d.ts +42 -0
  159. package/dist/web-api.js +896 -36
  160. package/dist/web-api.js.map +1 -1
  161. package/dist/web-appearance-limits.d.ts +82 -0
  162. package/dist/web-appearance-limits.js +83 -0
  163. package/dist/web-appearance-limits.js.map +1 -0
  164. package/dist/web-appearance-policy.d.ts +66 -0
  165. package/dist/web-appearance-policy.js +94 -0
  166. package/dist/web-appearance-policy.js.map +1 -0
  167. package/dist/web-appearance-tools.d.ts +50 -0
  168. package/dist/web-appearance-tools.js +105 -0
  169. package/dist/web-appearance-tools.js.map +1 -0
  170. package/dist/web-appearance-view.d.ts +99 -0
  171. package/dist/web-appearance-view.js +163 -0
  172. package/dist/web-appearance-view.js.map +1 -0
  173. package/dist/web-appearance.d.ts +79 -0
  174. package/dist/web-appearance.js +298 -0
  175. package/dist/web-appearance.js.map +1 -0
  176. package/dist/web-asset-parts.d.ts +98 -0
  177. package/dist/web-asset-parts.js +133 -0
  178. package/dist/web-asset-parts.js.map +1 -0
  179. package/dist/web-auth.js +17 -1
  180. package/dist/web-auth.js.map +1 -1
  181. package/dist/web-card.d.ts +96 -0
  182. package/dist/web-card.js +232 -0
  183. package/dist/web-card.js.map +1 -0
  184. package/dist/web-chat-history.d.ts +13 -0
  185. package/dist/web-chat-history.js +23 -2
  186. package/dist/web-chat-history.js.map +1 -1
  187. package/dist/web-effect-bundle.d.ts +28 -0
  188. package/dist/web-effect-bundle.js +126 -0
  189. package/dist/web-effect-bundle.js.map +1 -0
  190. package/dist/web-effect-canonical.d.ts +8 -0
  191. package/dist/web-effect-canonical.js +27 -0
  192. package/dist/web-effect-canonical.js.map +1 -0
  193. package/dist/web-effect-library.d.ts +114 -0
  194. package/dist/web-effect-library.js +530 -0
  195. package/dist/web-effect-library.js.map +1 -0
  196. package/dist/web-effect-poster.d.ts +8 -0
  197. package/dist/web-effect-poster.js +52 -0
  198. package/dist/web-effect-poster.js.map +1 -0
  199. package/dist/web-effect-profiles.d.ts +118 -0
  200. package/dist/web-effect-profiles.js +180 -0
  201. package/dist/web-effect-profiles.js.map +1 -0
  202. package/dist/web-effect-service.d.ts +86 -0
  203. package/dist/web-effect-service.js +312 -0
  204. package/dist/web-effect-service.js.map +1 -0
  205. package/dist/web-effect-tools.d.ts +47 -0
  206. package/dist/web-effect-tools.js +126 -0
  207. package/dist/web-effect-tools.js.map +1 -0
  208. package/dist/web-effect-trials.d.ts +86 -0
  209. package/dist/web-effect-trials.js +204 -0
  210. package/dist/web-effect-trials.js.map +1 -0
  211. package/dist/web-effect-upload.d.ts +29 -0
  212. package/dist/web-effect-upload.js +190 -0
  213. package/dist/web-effect-upload.js.map +1 -0
  214. package/dist/web-effect-worker.d.ts +8 -0
  215. package/dist/web-effect-worker.js +170 -0
  216. package/dist/web-effect-worker.js.map +1 -0
  217. package/dist/web-effect.d.ts +89 -0
  218. package/dist/web-effect.js +132 -0
  219. package/dist/web-effect.js.map +1 -0
  220. package/dist/web-identity-color.d.ts +87 -0
  221. package/dist/web-identity-color.js +209 -0
  222. package/dist/web-identity-color.js.map +1 -0
  223. package/dist/web-identity-palette.d.ts +113 -0
  224. package/dist/web-identity-palette.js +84 -0
  225. package/dist/web-identity-palette.js.map +1 -0
  226. package/dist/web-image-meta.d.ts +25 -0
  227. package/dist/web-image-meta.js +200 -0
  228. package/dist/web-image-meta.js.map +1 -0
  229. package/dist/web-mod-agent.d.ts +50 -0
  230. package/dist/web-mod-agent.js +203 -0
  231. package/dist/web-mod-agent.js.map +1 -0
  232. package/dist/web-mod-bundle.d.ts +144 -0
  233. package/dist/web-mod-bundle.js +464 -0
  234. package/dist/web-mod-bundle.js.map +1 -0
  235. package/dist/web-mod-export.d.ts +59 -0
  236. package/dist/web-mod-export.js +79 -0
  237. package/dist/web-mod-export.js.map +1 -0
  238. package/dist/web-mod-store.d.ts +283 -0
  239. package/dist/web-mod-store.js +624 -0
  240. package/dist/web-mod-store.js.map +1 -0
  241. package/dist/web-org.d.ts +3 -3
  242. package/dist/web-org.js +11 -17
  243. package/dist/web-org.js.map +1 -1
  244. package/dist/web-owner-css-store.d.ts +180 -0
  245. package/dist/web-owner-css-store.js +384 -0
  246. package/dist/web-owner-css-store.js.map +1 -0
  247. package/dist/web-owner-css.d.ts +58 -0
  248. package/dist/web-owner-css.js +552 -0
  249. package/dist/web-owner-css.js.map +1 -0
  250. package/dist/web-prefs.d.ts +48 -0
  251. package/dist/web-prefs.js +209 -33
  252. package/dist/web-prefs.js.map +1 -1
  253. package/dist/web-preview.d.ts +9 -1
  254. package/dist/web-preview.js +24 -7
  255. package/dist/web-preview.js.map +1 -1
  256. package/dist/web-settings-patch.d.ts +18 -0
  257. package/dist/web-settings-patch.js +51 -0
  258. package/dist/web-settings-patch.js.map +1 -1
  259. package/dist/web-shell-routes.d.ts +1 -1
  260. package/dist/web-shell-routes.js +1 -1
  261. package/dist/web-shell-routes.js.map +1 -1
  262. package/dist/web-theme-store.d.ts +218 -0
  263. package/dist/web-theme-store.js +516 -0
  264. package/dist/web-theme-store.js.map +1 -0
  265. package/dist/web-theme.d.ts +121 -0
  266. package/dist/web-theme.js +407 -0
  267. package/dist/web-theme.js.map +1 -0
  268. package/dist/web-widget.d.ts +72 -0
  269. package/dist/web-widget.js +245 -0
  270. package/dist/web-widget.js.map +1 -0
  271. package/package.json +1 -1
@@ -0,0 +1,119 @@
1
+ /** #1629: explicit-click control lane. Responses never grant authority; Start reauthorizes on the server. */
2
+ import { effectTrialMessage } from "./effect-trial-offer.js";
3
+ import { checkEffectTrialStart } from "/assets/effect-runtime-loader.js";
4
+ import { chatEffectIdentity } from "/assets/effect-chat-runtime.js";
5
+ const owners = new WeakSet(), safe = fn => { try { return fn(); } catch { return null; } };
6
+ const LIMIT = 128 * 1024;
7
+ export function createEffectTrialController({ window: host, current, allowed, onStart, onChange = () => {} }) {
8
+ let dead = false, active = null, physical = null, previous = null, broken = false, clockBusy = false;
9
+ const cleanup = [], releases = [], identity = safe(() => chatEffectIdentity(host));
10
+ const tell = () => { if (!dead) safe(onChange); };
11
+ function clock() {
12
+ if (broken || clockBusy) { broken = true; return null; }
13
+ clockBusy = true; const t = safe(() => host.performance.now()); clockBusy = false;
14
+ if (broken || typeof t !== "number" || !Number.isFinite(t) || t < 0 || t > Number.MAX_SAFE_INTEGER || previous !== null && t < previous) { broken = true; return null; }
15
+ return previous = t;
16
+ }
17
+ function eligible(op) {
18
+ if (dead || broken || !op || active !== op || op.dead || op.checking || host.AgendAuth?.sessionEnded?.() !== false) return false;
19
+ op.checking = true;
20
+ try {
21
+ const t = clock();
22
+ if (t === null || t < op.began || t >= op.until || safe(() => current(op.instance) === true && allowed() === true) !== true) return false;
23
+ const after = clock();
24
+ return !dead && !op.dead && active === op && after !== null && after >= op.began && after < op.until;
25
+ } finally { op.checking = false; }
26
+ }
27
+ function end(op, reason = "stopped") {
28
+ if (!op || op.dead) return false;
29
+ op.dead = true; if (active === op) active = null; safe(() => op.resolve?.(false));
30
+ safe(() => host.clearTimeout(op.timer)); safe(() => op.controller.abort());
31
+ if (op.reply && !op.stopQueued) { op.stopQueued = true; cleanup.push(op); }
32
+ // Physical paint is removed before callbacks, cleanup I/O or another explicit click.
33
+ safe(() => op.dispose?.()); op.dispose = null; tell(); drain(); return true;
34
+ }
35
+ function arm(op) {
36
+ safe(() => host.clearTimeout(op.timer));
37
+ if (!eligible(op)) { end(op, "trial_ended"); return; }
38
+ const t = clock(); if (t === null || t >= op.until) { end(op, "trial_expired"); return; }
39
+ op.timer = host.setTimeout(() => { if (!op.dead) arm(op); }, Math.max(1, Math.ceil(op.until - t)));
40
+ }
41
+ async function read(response) {
42
+ let reader = null;
43
+ try {
44
+ if (!response || response.redirected || response.status !== 200 || !/^application\/json(?:\s*;|$)/i.test(response.headers.get("content-type") || "")) return null;
45
+ const length = response.headers.get("content-length"); if (length !== null && (!/^\d+$/.test(length) || Number(length) > LIMIT)) return null;
46
+ reader = response.body?.getReader(); if (!reader) return null;
47
+ const chunks = []; let size = 0;
48
+ for (;;) { const r = await reader.read(); if (r.done) break; if (!(r.value instanceof Uint8Array) || (size += r.value.byteLength) > LIMIT) return null; chunks.push(r.value); }
49
+ const bytes = new Uint8Array(size); let offset = 0; for (const c of chunks) { bytes.set(c, offset); offset += c.byteLength; }
50
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
51
+ } catch { return null; }
52
+ finally { try { if (reader) await reader.cancel(); else await response?.body?.cancel(); } catch { /* retained until cancellation settles */ } }
53
+ }
54
+ function drain() {
55
+ if (physical || !cleanup.length) return;
56
+ const op = cleanup.shift(), token = {}; physical = token; const controller = new host.AbortController();
57
+ const timer = host.setTimeout(() => controller.abort(), 10000);
58
+ // Stop remains permitted after CSS/gate/session changes. It cannot mount or mutate appearance.
59
+ Promise.resolve().then(async () => {
60
+ const response = await host.fetch(`/ui/effect-trials/${op.reply.trial}/stop?ownerEpoch=${identity.epoch}`, {
61
+ method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" }, body: "{}", signal: controller.signal,
62
+ });
63
+ try { await response.body?.cancel(); } catch { /* no response fields are consumed */ }
64
+ }).catch(() => {}).finally(() => { safe(() => host.clearTimeout(timer)); if (physical === token) physical = null; tell(); drain(); });
65
+ }
66
+ function dispose() {
67
+ if (dead) return; dead = true; end(active); for (const off of releases.splice(0)) safe(off); tell();
68
+ }
69
+ const api = { ok: false, start: async () => false, stop: () => end(active), sync() { const op = active; if (op && !eligible(op)) end(op, "trial_ended"); },
70
+ snapshot: () => Object.freeze({ busy: !!physical || cleanup.length > 0, phase: active ? active.reply ? "running" : "starting" : "idle" }), dispose };
71
+ if (!identity || owners.has(host) || typeof current !== "function" || typeof allowed !== "function" || typeof onStart !== "function"
72
+ || typeof host.AgendAuth?.trialSession !== "function" || typeof host.AgendAuth?.onSessionEnd !== "function") return Object.freeze(api);
73
+ owners.add(host);
74
+ try {
75
+ const off = host.AgendAuth.onSessionEnd(dispose);
76
+ if (dead) { safe(off); return Object.freeze(api); }
77
+ releases.push(off);
78
+ for (const [target, type, fn] of [[host, "blur", () => end(active)], [host, "pagehide", dispose], [host.document, "visibilitychange", () => { if (host.document.visibilityState !== "visible") end(active); }]]) {
79
+ target.addEventListener(type, fn); releases.push(() => target.removeEventListener(type, fn));
80
+ }
81
+ } catch { dispose(); return Object.freeze(api); }
82
+ if (dead) return Object.freeze(api);
83
+ api.ok = true;
84
+ api.start = async message => {
85
+ if (dead || active || physical || cleanup.length) return false;
86
+ const offer = safe(() => effectTrialMessage(message)), instance = safe(() => message.instance), began = clock();
87
+ if (!offer || began === null || began + 10000 > Number.MAX_SAFE_INTEGER || safe(() => host.Date.now() < offer.expiresAt) !== true) return false;
88
+ const op = { instance, offer, began, until: began + 10000, controller: new host.AbortController(), timer: null, reply: null, dead: false, stopQueued: false, dispose: null };
89
+ active = op; const token = {}; physical = token;
90
+ const cancelled = new Promise(resolve => { op.resolve = resolve; });
91
+ const work = Promise.resolve().then(async () => {
92
+ if (!eligible(op)) return false;
93
+ arm(op); const session = await host.AgendAuth.trialSession();
94
+ if (!eligible(op) || !session || session.localAdmin !== true || session.epoch !== identity.epoch) return false;
95
+ const response = await host.fetch(`/ui/effect-trials/start?ownerEpoch=${identity.epoch}`, {
96
+ method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" },
97
+ body: JSON.stringify({ offer: offer.offer, instance }), signal: op.controller.signal,
98
+ });
99
+ const reply = checkEffectTrialStart(await read(response), offer.offer, offer.id);
100
+ if (!reply) return false;
101
+ op.reply = reply;
102
+ if (op.dead && !op.stopQueued) { op.stopQueued = true; cleanup.push(op); }
103
+ if (reply.offer.title !== offer.title || reply.offer.expiresAt !== offer.expiresAt) return false;
104
+ if (!eligible(op)) { if (!op.stopQueued) { op.stopQueued = true; cleanup.push(op); } return false; }
105
+ op.until = began + reply.remainingMs;
106
+ if (op.until > Number.MAX_SAFE_INTEGER || !eligible(op)) return false;
107
+ arm(op);
108
+ const handle = Object.freeze({ reply, startedAt: began, instance, current: () => eligible(op), stop: () => end(op), });
109
+ const disposer = onStart(handle);
110
+ if (typeof disposer !== "function") return false;
111
+ if (!eligible(op)) { safe(disposer); return false; }
112
+ op.dispose = disposer; tell(); return eligible(op);
113
+ }).catch(() => false).then(ok => { if (!ok) end(op); return ok; }).finally(() => { if (physical === token) physical = null; tell(); drain(); });
114
+ // The logical caller may stop immediately, while the physical request/cancel still owns the lane.
115
+ tell();
116
+ return Promise.race([work, cancelled]);
117
+ };
118
+ return Object.freeze(api);
119
+ }
@@ -0,0 +1,31 @@
1
+ /** #1629: closed host display metadata. Neither this object nor its token grants a trial read. */
2
+ const KEYS = ["v", "offer", "id", "title", "expiresAt"], ID = /^[0-9a-f]{32}$/;
3
+ const FORBIDDEN = /(?!\u200d)[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
4
+ const VISIBLE = /[^\p{M}\p{C}\p{Z}\u115f\u1160\u3164\uffa0\u2800]/u;
5
+ let graphemes = null;
6
+ export function checkEffectTrialOffer(value) {
7
+ try {
8
+ if (!value || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return null;
9
+ const keys = Reflect.ownKeys(value);
10
+ if (keys.length !== KEYS.length || keys.some(k => typeof k !== "string" || !KEYS.includes(k))) return null;
11
+ const x = Object.create(null);
12
+ for (const key of KEYS) {
13
+ const d = Object.getOwnPropertyDescriptor(value, key);
14
+ if (!d || !("value" in d) || !d.enumerable) return null;
15
+ x[key] = d.value;
16
+ }
17
+ if (x.v !== 1 || typeof x.offer !== "string" || !ID.test(x.offer) || typeof x.id !== "string" || !ID.test(x.id)
18
+ || typeof x.title !== "string" || x.title.length > 400 || x.title.normalize("NFC") !== x.title || x.title.trim() !== x.title
19
+ || FORBIDDEN.test(x.title) || !VISIBLE.test(x.title) || !Number.isSafeInteger(x.expiresAt) || x.expiresAt < 0) return null;
20
+ graphemes ??= new Intl.Segmenter(undefined, { granularity: "grapheme" });
21
+ let count = 0; for (const _part of graphemes.segment(x.title)) if (++count > 40) return null;
22
+ if (count < 1) return null;
23
+ return Object.freeze({ v: 1, offer: x.offer, id: x.id, title: x.title, expiresAt: x.expiresAt });
24
+ } catch { return null; }
25
+ }
26
+ /** Role and sender are host fields, never inferred from a message's text. Start must separately authorize. */
27
+ export function effectTrialMessage(message) {
28
+ if (!message || message.role !== "status" || message.sender !== "agend" || typeof message.instance !== "string"
29
+ || message.instance.length < 1 || message.instance.length > 128) return null;
30
+ return checkEffectTrialOffer(message.effectTrial);
31
+ }
@@ -0,0 +1,37 @@
1
+ /** #1629: fixed host producer. Creates no frame or request; the admitted app controller owns its lease. */
2
+ import { html, useLayoutEffect, useRef } from "/assets/app-html.js";
3
+ import { register, t } from "/assets/app-i18n.js";
4
+ import { createEffectTrialStage } from "/assets/effect-trial-stage.js";
5
+ register("effectTrial", {
6
+ en: { label: "Trial", stop: "Stop trial", close: "Close", note: "A bounded sample. Nothing is applied or saved." },
7
+ "zh-TW": { label: "試播", stop: "停止試播", close: "關閉", note: "僅在此區塊試播,不會套用或儲存。" },
8
+ });
9
+ /** The caller owns authenticated admission, lease/deadline and shared pool. No script/HTML is rendered here. */
10
+ export function EffectTrialPanel({ window: host = globalThis.window, binding, title, current, ownerCss, onStage, onStop, onClose }) {
11
+ const panel = useRef(null), stage = useRef(null), life = useRef(null), latest = useRef(null);
12
+ latest.current = { current, ownerCss, onStage, onStop, onClose };
13
+ const stop = close => {
14
+ const entry = life.current; if (!entry || (close ? entry.closed : entry.dead)) return;
15
+ if (close) entry.closed = true;
16
+ if (!entry.dead) { entry.dead = true; entry.api?.dispose(); }
17
+ try { (close ? entry.onClose : entry.onStop)?.("stopped"); } catch { /* lease already ended */ }
18
+ };
19
+ useLayoutEffect(() => {
20
+ const entry = { dead: false, closed: false, api: null, onStop, onClose }; life.current = entry;
21
+ const ended = reason => { if (entry.dead) return; entry.dead = true; try { entry.onStop?.(reason); } catch { /* never revive */ } };
22
+ try {
23
+ entry.api = createEffectTrialStage({ window: host, panel: panel.current, stage: stage.current, binding,
24
+ current: own => !entry.dead && life.current === entry && latest.current.current?.(own) === true,
25
+ ownerCss: () => latest.current.ownerCss?.(), onStop: ended });
26
+ if (!entry.api) ended("stage_unavailable");
27
+ else { latest.current.onStage?.(entry.api); }
28
+ } catch { entry.api?.dispose(); ended("stage_unavailable"); }
29
+ return () => { entry.api?.dispose(); entry.dead = true; if (life.current === entry) life.current = null; };
30
+ }, [host, binding]);
31
+ return html`<aside ref=${panel} data-trust="preview" class="pv-panel effect-trial-panel" aria-label=${t("effectTrial.label")}>
32
+ <header class="effect-trial-head"><strong>${t("effectTrial.label")}</strong><span>${typeof title === "string" ? title : ""}</span><button type="button" class="btn" onClick=${() => stop(true)}>${t("effectTrial.close")}</button></header>
33
+ <div class="effect-trial-controls"><button type="button" class="btn" onClick=${() => stop(false)}>${t("effectTrial.stop")}</button></div>
34
+ <div class="effect-trial-body"><div ref=${stage} class="effect-trial-stage" aria-hidden="true"></div></div>
35
+ <p class="effect-trial-note">${t("effectTrial.note")}</p>
36
+ </aside>`;
37
+ }
@@ -90,7 +90,7 @@ function ImageLightbox({ items, index, opener, onClose }) {
90
90
  return () => { d.removeEventListener("cancel", onCancel); d.removeEventListener("close", onNativeClose); d.removeEventListener("click", onClick); d.removeEventListener("keydown", onKey); };
91
91
  }, [onClose, many, items.length]);
92
92
  const label = item.name ? t("chat.lbLabel", item.name) : t("chat.lbLabelNoName");
93
- return html`<dialog ref=${ref} class="lb" role="dialog" aria-modal="true" aria-label=${label}>
93
+ return html`<dialog data-trust="lightbox" ref=${ref} class="lb" role="dialog" aria-modal="true" aria-label=${label}>
94
94
  <div class="lb-bar">
95
95
  <span class="lb-name" title=${item.name}>${item.name}</span>
96
96
  ${many ? html`<span class="lb-count">${t("chat.lbCount", at + 1, items.length)}</span>` : null}
@@ -12,10 +12,10 @@ import { appStore, useStore } from "/assets/app-store.js";
12
12
  import { useLease } from "/assets/app-ctx.js";
13
13
  import { namesOf, namesStore } from "/assets/instance-nav.js";
14
14
  import { PanelHeader, onPanelKey, setTitle, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
15
- import { detailsPath } from "/assets/app-route.js";
15
+ import { detailsPath, textLinkSegments } from "/assets/app-route.js";
16
16
  import { InstanceSwitch } from "/assets/instance-switch.js";
17
17
  import { HeaderTools } from "/assets/header-tools.js";
18
- import { navigate } from "/assets/app-nav.js";
18
+ import { navigate, navStore } from "/assets/app-nav.js";
19
19
  import { Menu } from "/assets/ui-menu.js";
20
20
  import { Dialog } from "/assets/ui-dialog.js";
21
21
  import { Empty, Skeleton } from "/assets/ui-states.js";
@@ -23,6 +23,26 @@ import { Icon } from "/assets/ui-icons.js";
23
23
  import { toast } from "/assets/ui-toast.js";
24
24
  import { confirmDialog } from "/assets/ui-confirm.js";
25
25
  import { savePref } from "/assets/app-prefs.js";
26
+ import { applyTheme, resolvedMode, themeFor, themeImageUrl, themeMenuItems, themesStore } from "/assets/app-themes.js";
27
+ import { colorMenuItem } from "/assets/identity-color.js";
28
+ import { IdentityCard, useCard } from "/assets/identity-card.js";
29
+ import { AgentWidget, instanceBinds, widgetFor } from "/assets/agent-widget.js";
30
+
31
+ // #1629 r4 §1: the identity card folded into the chat's header — folded by default; open or not is this device's choice.
32
+ const CHAT_CARD_KEY = "agend_chat_card";
33
+ const chatCardOpen = () => { try { return localStorage.getItem(CHAT_CARD_KEY) === "open"; } catch { return false; } };
34
+ function ChatCard({ name, card }) {
35
+ const [open, setOpen] = useState(chatCardOpen);
36
+ if (!card) return null;
37
+ const toggle = () => { const next = !open; setOpen(next); try { if (next) localStorage.setItem(CHAT_CARD_KEY, "open"); else localStorage.removeItem(CHAT_CARD_KEY); } catch { /* this page only */ } };
38
+ return html`<div data-trust="card" class=${`chat-card${open ? " open" : ""}`}>
39
+ <button type="button" class="chat-card-toggle" aria-expanded=${open ? "true" : "false"} onClick=${toggle}>
40
+ <${Icon} name="chevron" size=${14} cls=${open ? "caret" : "caret flipped"} /><span>${t(open ? "card.hide" : "card.show")}</span>
41
+ ${!open && card.tagline ? html`<span class="chat-card-peek">${card.tagline.text}</span>` : null}
42
+ </button>
43
+ ${open ? html`<${IdentityCard} name=${name} card=${card} compact=${true} />` : null}
44
+ </div>`;
45
+ }
26
46
  import "./chat-strings.js";
27
47
  import { confirmedWrite } from "./settings-confirm.js";
28
48
  import { createChatStore } from "./chat-store.js";
@@ -34,6 +54,9 @@ import { PreviewPanel, claimPanel, openPanel, panelClaims, panelStore, panelVers
34
54
  import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
35
55
  import { installTour, refreshTourSpot } from "./chat-tour.js";
36
56
  import { LightboxHost, openLightbox } from "./image-lightbox.js";
57
+ import { ChatEffectPosters } from "/ui/js/chat-effect-posters.js";
58
+ import { ChatEffects, chatEffectsMenuAvailable } from "./chat-effects.js";
59
+ import { startAppEffects, getAppEffects } from "./app-effects.js";
37
60
 
38
61
  const R = () => globalThis.AgendChatRender;
39
62
  const P = () => globalThis.AgendPreview;
@@ -93,6 +116,7 @@ export function boot({ stream, boot: bootData, deps = {} }) {
93
116
  }
94
117
  // #1604: the preview opt-in and Tour live in Settings → This device now (and on each preview card's menu).
95
118
  installTour();
119
+ startAppEffects({ store });
96
120
  return store;
97
121
  }
98
122
 
@@ -171,11 +195,40 @@ function NotFound({ name }) {
171
195
  action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} /></div></div>`;
172
196
  }
173
197
 
198
+ /**
199
+ * #1629: the mode the page is drawn in, followed live (the chosen theme, or the system's for "system"), so an agent's
200
+ * light and dark palettes swap with it.
201
+ */
202
+ function useResolvedMode(lease) {
203
+ const [mode, setMode] = useState(resolvedMode);
204
+ useEffect(() => {
205
+ const T = globalThis.AgendTheme;
206
+ const update = () => setMode(resolvedMode());
207
+ const off = T && typeof T.onChange === "function" ? T.onChange(update) : null;
208
+ let mq = null;
209
+ try { mq = globalThis.matchMedia ? globalThis.matchMedia("(prefers-color-scheme: light)") : null; } catch { mq = null; }
210
+ if (mq && typeof mq.addEventListener === "function") lease.on(mq, "change", update);
211
+ return () => { if (typeof off === "function") off(); };
212
+ }, [lease]);
213
+ return mode;
214
+ }
215
+
174
216
  function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
175
217
  const names = useStore(namesStore); // #1722: which name leads (the title)
218
+ // #1629: this agent's theme, on this surface only; redrawn when the themes, the viewer's switch or the mode move.
219
+ useStore(themesStore);
220
+ const mode = useResolvedMode(lease);
221
+ const theme = themeFor(name);
222
+ const themeKey = JSON.stringify(theme);
223
+ const card = useCard(name, inst.card_v);
176
224
  const [dialog, setDialog] = useState(null); // "delete" | null
177
225
  const [wrap, setWrap] = useState(codeWrap);
178
- const view = useRef(null), split = useRef(null), thread = useRef(null);
226
+ const view = useRef(null), split = useRef(null), thread = useRef(null), bg = useRef(null), effectSurface = useRef(null);
227
+ useLayoutEffect(() => { applyTheme(view.current, theme, mode); }, [themeKey, mode]);
228
+ useLayoutEffect(() => {
229
+ const img = bg.current;
230
+ if (img && img.style && theme && theme.background) img.style.setProperty("--th-bg-opacity", String(theme.background.opacity));
231
+ }, [themeKey]);
179
232
  useEffect(() => { wrapListeners.add(setWrap); return () => wrapListeners.delete(setWrap); }, []);
180
233
  // First visit to this instance's chat on this page: its history (a navigation — it counts as use). Back again: nothing.
181
234
  useEffect(() => { store.openHistory(name, lease); }, [name]);
@@ -217,6 +270,11 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
217
270
  running ? null : { key: "start", label: t("chat.start"), icon: "play", onSelect: () => action("start") },
218
271
  running ? { key: "restart", label: t("chat.restart"), icon: "restart", onSelect: () => action("restart") } : null,
219
272
  running ? { key: "stop", label: t("chat.stopInstance"), icon: "stop", onSelect: () => action("stop") } : null,
273
+ // #1958: its identity colour and where it shows (the public link too, as profile edits).
274
+ colorMenuItem(name, inst),
275
+ // #1629: a person's control over this agent's theme — not over the public link (it only views themes).
276
+ ...themeMenuItems(name),
277
+ chatEffectsMenuAvailable() ? { key: "effects", label: t("chatFx.title"), onSelect: () => setDialog("effects") } : null,
220
278
  { key: "delete", label: t("chat.delete"), icon: "trash", danger: true, onSelect: () => setDialog("delete") },
221
279
  ];
222
280
  const cls = statusClass(inst, exec, awaiting);
@@ -226,17 +284,20 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
226
284
  ${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", inst.model)} aria-label=${t("chat.chipModel", inst.model)} onClick=${() => pick("model")}>${inst.model}</button>` : null}
227
285
  ${inst.effort ? html`<button type="button" class="hd-chip" title=${t("chat.chipEffort", inst.effort)} aria-label=${t("chat.chipEffort", inst.effort)} onClick=${() => pick("effort")}>${inst.effort}</button>` : null}`;
228
286
  return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
229
- <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
287
+ <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${ChatEffectPosters} name=${name} lease=${lease} element=${effectSurface} /><${ChatEffects} name=${name} lease=${lease} element=${effectSurface} store=${store} open=${dialog === "effects"} onClose=${() => setDialog(null)} /><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
288
+ <${ChatCard} key=${name} name=${name} card=${card} />
289
+ <${AgentWidget} slot="chat_header" widget=${widgetFor(name, "chat_header")} binds=${instanceBinds(inst, exec, awaiting)} />
230
290
  <div class="chat-split" ref=${split}>
231
- <div class="chat-main">
291
+ <div ref=${effectSurface} class=${`chat-main${theme && theme.background ? " has-th-bg" : ""}`}>
292
+ ${theme && theme.background ? html`<img ref=${bg} class=${`th-bg th-bg-${theme.background.fit}`} src=${themeImageUrl(name, theme.background.image)} alt="" aria-hidden="true" decoding="async" />` : null}
232
293
  <${Thread} name=${name} th=${thread} />
233
- <${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} />
294
+ <${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} workingLabel=${theme ? theme.workingLabel : undefined} />
234
295
  </div>
235
296
  <${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
236
297
  keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${(commit) => setPreviewOptIn(true, commit)} />
237
298
  </div>
238
299
  <${LightboxHost} />
239
- <div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
300
+ <div data-trust="drop" class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
240
301
  ${dialog === "delete" ? html`<${DeleteDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
241
302
  </div>`;
242
303
  }
@@ -247,9 +308,12 @@ function Thread({ name, th }) {
247
308
  const [jump, setJump] = useState({ show: false, unseen: 0 });
248
309
  const [empty, setEmpty] = useState(false);
249
310
  useLayoutEffect(() => {
311
+ let live = true;
250
312
  const thread = createThread(list.current, scroller.current, {
251
313
  t, tf: t, isUser: (x) => store.isUser(x, name), onJump: setJump, onEmpty: setEmpty,
252
314
  setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
315
+ canTryEffect: x => getAppEffects()?.canTry(x) === true,
316
+ startEffectTrial: x => { const seq = navStore.get().seq; void getAppEffects()?.startTrial(x).then(ok => { if (!ok && live && navStore.get().seq === seq && document.hasFocus?.() === true && document.visibilityState === "visible") toast(t("chat.effectTrialUnavailable")); }); },
253
317
  clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
254
318
  replyButtonBusy: (id) => store.state.rbBusy.has(id),
255
319
  pollMine: (id) => store.state.pollMine[id], // #1823: this device's own vote
@@ -273,13 +337,15 @@ function Thread({ name, th }) {
273
337
  else if (kind === "sent") thread.jumpLatest();
274
338
  });
275
339
  const offPv = P() ? P().onChange(() => thread.refreshCards()) : () => {};
340
+ const offTrials = getAppEffects()?.subscribeTrial(() => thread.render(store.state.msgs[name] || [])) ?? (() => {});
276
341
  const offPanel = panelStore.subscribe(() => thread.refreshCards()); // a card shows whether the panel has its block
277
342
  const onScroll = () => { store.state.scrollMemo[name] = thread.onScroll(); };
278
343
  const sc = scroller.current, ls = list.current;
279
344
  sc.addEventListener("scroll", onScroll, { passive: true });
280
345
  ls.addEventListener("click", thread.onClick);
281
346
  return () => {
282
- off(); offPv(); offPanel(); offEmojis(); offMentions();
347
+ live = false;
348
+ off(); offPv(); offPanel(); offTrials(); offEmojis(); offMentions();
283
349
  sc.removeEventListener("scroll", onScroll);
284
350
  ls.removeEventListener("click", thread.onClick);
285
351
  thread.dispose(); // the whole thread goes: every preview in it stops first
@@ -289,18 +355,18 @@ function Thread({ name, th }) {
289
355
  return html`<div class="scroller" ref=${scroller}>
290
356
  <div class="thread" ref=${list} role="log" aria-live="off" aria-label=${t("chat.conversationWith", name)}></div>
291
357
  ${empty ? html`<${Empty} icon="chat" title=${t("chat.noMessages")} />` : null}
292
- ${jump.show ? html`<button type="button" class="jump-latest" onClick=${() => th.current && th.current.jumpLatest()}>
358
+ ${jump.show ? html`<button data-trust="thread-control" type="button" class="jump-latest" onClick=${() => th.current && th.current.jumpLatest()}>
293
359
  <${Icon} name="down" size=${16} />${jump.unseen > 0 ? t("chat.newBelow", jump.unseen) : t("chat.jumpLatest")}</button>` : null}
294
360
  </div>`;
295
361
  }
296
362
 
297
363
  /** Everything under the thread: prompts, the working line, a failed send, files waiting, and the composer. */
298
- function Dock({ name, inst, lease, exec, awaiting }) {
364
+ function Dock({ name, inst, lease, exec, awaiting, workingLabel }) {
299
365
  useChatTick(name, ["state"]);
300
366
  const s = store.state;
301
- return html`<div class="dock"><div class="dock-col">
367
+ return html`<div data-trust="composer" class="dock"><div class="dock-col">
302
368
  <${Prompts} name=${name} />
303
- <${WorkBar} name=${name} lease=${lease} exec=${exec} awaiting=${awaiting} />
369
+ <${WorkBar} name=${name} lease=${lease} exec=${exec} awaiting=${awaiting} workingLabel=${workingLabel} />
304
370
  <${CommandCard} name=${name} lease=${lease} />
305
371
  <${QuickActions} name=${name} inst=${inst} lease=${lease} />
306
372
  ${s.failedSends[name] ? html`<div class="failed-send" role="alert"><span>${t("chat.notSent")}</span><span class="txt">${s.failedSends[name]}</span>
@@ -347,14 +413,14 @@ function QuickActions({ name, inst, lease }) {
347
413
  function Prompts({ name }) {
348
414
  const list = store.promptsFor(name);
349
415
  if (!list.length) return null;
350
- return html`<div class="prompts" aria-live="polite">${list.map(p => html`<div key=${p.nonce} class=${`prompt${p.resolved ? " done" : ""}`} role="group" aria-label=${p.text}>
351
- <div class="txt">${p.resolved ? (p.outcome || t("chat.promptGone")) : p.text}</div>
416
+ return html`<div data-trust="permission" class="prompts" aria-live="polite">${list.map(p => html`<div key=${p.nonce} class=${`prompt${p.resolved ? " done" : ""}`} role="group" aria-label=${p.text}>
417
+ <div class="txt">${p.resolved ? (p.outcome || t("chat.promptGone")) : textLinkSegments(p.text, p.links).map(sg => sg.href ? html`<a data-trust="link" class="st-link" href=${sg.href}>${sg.text}</a>` : sg.text)}</div>
352
418
  ${p.resolved ? null : html`<div class="acts">${p.actions.map(a => html`<button key=${a.id} type="button" class="btn" disabled=${!!p.busy} onClick=${() => store.answerPrompt(p, a.id)}>${a.label}</button>`)}
353
419
  ${p.busy ? html`<span class="wait">${t("chat.promptAnswering")}</span>` : null}</div>`}</div>`)}</div>`;
354
420
  }
355
421
 
356
422
  /** "<name> is working… 0:42", "looks stuck", "Stopping…", or what it waits on at its terminal. Text only. */
357
- function WorkBar({ name, lease, exec, awaiting }) {
423
+ function WorkBar({ name, lease, exec, awaiting, workingLabel }) {
358
424
  const s = store.state;
359
425
  const busy = R().isBusy(exec) ? exec : "";
360
426
  const kind = awaiting != null ? "awaiting" : s.stopping[name] && busy ? "stopping" : busy;
@@ -365,7 +431,9 @@ function WorkBar({ name, lease, exec, awaiting }) {
365
431
  return () => lease.clear(h);
366
432
  }, [kind, lease]);
367
433
  if (!kind) return null;
368
- const label = t(kind === "stuck" ? "chat.stuck" : kind === "stopping" ? "chat.stopping" : kind === "awaiting" ? "chat.awaiting" : "chat.working", name);
434
+ // #1629: the agent's own working label (its theme), shown as text — only for "working", never for the other states.
435
+ const label = kind === "working" && workingLabel ? workingLabel
436
+ : t(kind === "stuck" ? "chat.stuck" : kind === "stopping" ? "chat.stopping" : kind === "awaiting" ? "chat.awaiting" : "chat.working", name);
369
437
  const since = s.workingSince[name];
370
438
  return html`<div class=${`work-bar ${kind}`}><span class="pulse" aria-hidden="true"></span><span class="lbl">${label}</span>
371
439
  ${kind === "awaiting" ? html`<span class="note" title=${t("chat.approxNote")}>${awaiting || t("chat.approxNote")}</span>`
@@ -428,7 +496,7 @@ function Composer({ name, lease, busy }) {
428
496
  };
429
497
  const optId = (c) => `cmd-${c.name}`;
430
498
  return html`<div class="composer-box">
431
- ${palette ? html`<ul class="cmd-palette" id="cmdPalette" role="listbox" aria-label=${t("chat.cmdCommands")}>
499
+ ${palette ? html`<ul data-trust="palette" class="cmd-palette" id="cmdPalette" role="listbox" aria-label=${t("chat.cmdCommands")}>
432
500
  ${palette.length ? palette.map((c) => html`<li key=${c.name} id=${optId(c)} role="option" aria-selected=${c === pick ? "true" : "false"}
433
501
  class=${c === pick ? "on" : ""} onMouseDown=${(e) => { e.preventDefault(); complete(c); }}>
434
502
  <span class="cmd-name mono">/${c.name}${c.arg ? html` <span class="cmd-arg">${c.arg}</span>` : null}</span><span class="cmd-desc">${t(`chat.cmd_${c.name}`)}</span></li>`)
@@ -16,6 +16,10 @@ import { settingsPath } from "/assets/app-route.js";
16
16
  import { InstanceSwitch } from "/assets/instance-switch.js";
17
17
  import { HeaderTools } from "/assets/header-tools.js";
18
18
  import { Menu } from "/assets/ui-menu.js";
19
+ import { themeMenuItems, themesStore } from "/assets/app-themes.js";
20
+ import { colorMenuItem } from "/assets/identity-color.js";
21
+ import { IdentityCard, useCard } from "/assets/identity-card.js";
22
+ import { AgentWidget, instanceBinds, widgetFor } from "/assets/agent-widget.js";
19
23
  import { Empty, ErrorState, Skeleton } from "/assets/ui-states.js";
20
24
  import { toast } from "/assets/ui-toast.js";
21
25
  import "./chat-strings.js";
@@ -27,10 +31,10 @@ export function DetailsPanel({ route, navKey }) {
27
31
  const name = route.instance;
28
32
  const inst = app.instances.find((i) => i.name === name) || null;
29
33
  useEffect(() => { setTitle(`${name} · ${t("app.details")}`); }, [name, navKey]);
30
- if (!app.ready) return html`<div class="panel p-details"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${6} /></div></div>`;
34
+ if (!app.ready) return html`<div data-effect-panel="details" class="panel p-details"><${PanelHeader} title=${name} /><div data-trust="panel-content" class="panel-body"><${Skeleton} lines=${6} /></div></div>`;
31
35
  if (!inst) {
32
- return html`<div class="panel p-details"><${PanelHeader} title=${name} />
33
- <div class="panel-body center"><${Empty} icon="alert" title=${t("chat.notFound", name)} hint=${t("chat.notFoundHint")}
36
+ return html`<div data-effect-panel="details" class="panel p-details"><${PanelHeader} title=${name} />
37
+ <div data-trust="panel-content" class="panel-body center"><${Empty} icon="alert" title=${t("chat.notFound", name)} hint=${t("chat.notFoundHint")}
34
38
  action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} /></div></div>`;
35
39
  }
36
40
  return html`<${DetailsView} key=${name} name=${name} inst=${inst} navKey=${navKey} exec=${app.exec[name]}
@@ -38,7 +42,10 @@ export function DetailsPanel({ route, navKey }) {
38
42
  }
39
43
 
40
44
  function DetailsView({ name, inst, navKey, exec, awaiting }) {
45
+ useStore(themesStore); // #1629: Lock / Unlock follows the themes as read
41
46
  const lease = useLease(`details:${name}`);
47
+ // #1629 r4 §1: the identity card at the top, read when the status frame's card_v moves.
48
+ const card = useCard(name, inst.card_v);
42
49
  const [d, setD] = useState(null);
43
50
  const [failed, setFailed] = useState(false);
44
51
  const [busy, setBusy] = useState("");
@@ -92,6 +99,8 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
92
99
  { key: "edit", label: t("chat.dEditInSettings"), icon: "settings", onSelect: editInSettings },
93
100
  st === "running" ? { key: "pause", label: t("chat.dPause"), icon: "pause", disabled: !!busy, onSelect: () => pauseWake("pause") } : null,
94
101
  st === "paused" ? { key: "wake", label: t("chat.dWake"), icon: "play", disabled: !!busy, onSelect: () => pauseWake("wake") } : null,
102
+ colorMenuItem(name, inst), // #1958
103
+ ...themeMenuItems(name), // #1629
95
104
  ] : [
96
105
  { key: "edit", label: t("chat.dEditInSettings"), icon: "settings", onSelect: editInSettings },
97
106
  st === "running" ? null : st === "paused" ? null : { key: "start", label: t("chat.start"), icon: "play", disabled: !!busy, onSelect: () => lifecycle("start") },
@@ -99,6 +108,9 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
99
108
  st === "running" ? { key: "stop", label: t("chat.stopInstance"), icon: "stop", disabled: !!busy, onSelect: () => lifecycle("stop") } : null,
100
109
  st === "running" ? { key: "pause", label: t("chat.dPause"), icon: "pause", disabled: !!busy, onSelect: () => pauseWake("pause") } : null,
101
110
  st === "paused" ? { key: "wake", label: t("chat.dWake"), icon: "play", disabled: !!busy, onSelect: () => pauseWake("wake") } : null,
111
+ colorMenuItem(name, inst), // #1958: its identity colour (the public link too)
112
+ // #1629: a person's Lock and Reset of this agent's own web theme (not over the public link).
113
+ ...themeMenuItems(name),
102
114
  ];
103
115
 
104
116
  const cls = statusClass(inst, exec, awaiting);
@@ -141,8 +153,9 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
141
153
  <span class="mono muted">${a.timestamp ? new Date(`${a.timestamp.replace(" ", "T")}Z`).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""}</span>
142
154
  <span class="ev">${a.event}</span> ${a.summary || ""}</div>`) : html`<p class="note">${t("chat.dNoActivity")}</p>`}</section>`;
143
155
  }
144
- return html`<div class="panel p-details">
156
+ return html`<div data-effect-panel="details" class="panel p-details">
145
157
  <${PanelHeader} title=${name} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="details" />`}><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
146
- <div class="panel-body"><div class="col" ref=${body}>${busy ? html`<p class="note" role="status">${t("chat.dWorking")}</p>` : null}${content}</div></div>
158
+ <div data-trust="panel-content" class="panel-body"><div class="col" ref=${body}>${busy ? html`<p class="note" role="status">${t("chat.dWorking")}</p>` : null}<${IdentityCard} name=${name} card=${card} />
159
+ <${AgentWidget} slot="details" widget=${widgetFor(name, "details")} binds=${instanceBinds(inst, exec, awaiting)} />${content}</div></div>
147
160
  </div>`;
148
161
  }
@@ -134,11 +134,11 @@ export function FleetPanel({ route, navKey }) {
134
134
  // #1685: host memory pressure slowing or holding new agents — shown here, where macOS's routine "elevated" is not a chat notice.
135
135
  const { hostMemory } = useStore(appStore);
136
136
  const sub = hostMemory ? html`<span class=${`status host-mem ${hostMemory}`} role="status"><span class=${`dot ${hostMemory === "critical" ? "bad" : "warn"}`} aria-hidden="true"></span>${t(`fleet.hostMemory_${hostMemory}`)}</span>` : null;
137
- return html`<div class="panel p-fleet">
137
+ return html`<div data-effect-panel="fleet" class="panel p-fleet">
138
138
  <${PanelHeader} title=${t("fleet.title")} sub=${sub}><${DocsLink} /><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
139
- <nav class="seg" aria-label=${t("fleet.title")}>${FLEET_TABS.map(k => html`<a key=${k} href=${fleetPath(k)} class=${`seg-item${k === tab ? " active" : ""}`}
139
+ <nav data-trust="panel-navigation" class="seg" aria-label=${t("fleet.title")}>${FLEET_TABS.map(k => html`<a key=${k} href=${fleetPath(k)} class=${`seg-item${k === tab ? " active" : ""}`}
140
140
  aria-current=${k === tab ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${t(`fleet.${k}`)}</span></a>`)}</nav>
141
- <div class="panel-body"><div class=${`col${tab === "org" || tab === "cache" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
141
+ <div data-trust="panel-content" class="panel-body"><div class=${`col${tab === "org" || tab === "cache" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
142
142
  </div>`;
143
143
  }
144
144
 
@@ -200,10 +200,10 @@ export function LogPanel({ route, navKey }) {
200
200
  const name = route.instance;
201
201
  const inst = app.instances.find((i) => i.name === name) || null;
202
202
  useEffect(() => { setTitle(`${name} · ${t("app.log")}`); }, [name, navKey]);
203
- if (!app.ready) return html`<div class="panel p-log"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${6} /></div></div>`;
203
+ if (!app.ready) return html`<div data-effect-panel="log" class="panel p-log"><${PanelHeader} title=${name} /><div data-trust="panel-content" class="panel-body"><${Skeleton} lines=${6} /></div></div>`;
204
204
  if (!inst) {
205
- return html`<div class="panel p-log"><${PanelHeader} title=${name} />
206
- <div class="panel-body center"><${Empty} icon="alert" title=${t("chat.notFound", name)} hint=${t("chat.notFoundHint")}
205
+ return html`<div data-effect-panel="log" class="panel p-log"><${PanelHeader} title=${name} />
206
+ <div data-trust="panel-content" class="panel-body center"><${Empty} icon="alert" title=${t("chat.notFound", name)} hint=${t("chat.notFoundHint")}
207
207
  action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} /></div></div>`;
208
208
  }
209
209
  return html`<${LogView} key=${name} name=${name} inst=${inst} navKey=${navKey} exec=${app.exec[name]}
@@ -372,13 +372,13 @@ function LogView({ name, inst, navKey, exec, awaiting }) {
372
372
  ${items.length ? html`<ol class="log-list" role="log" aria-label=${t("app.log")}>${items.map((x, i) => html`<${Row} key=${`${x.kind}:${x.id}`} x=${x} prev=${items[i - 1]} />`)}</ol>`
373
373
  : html`<p class="note log-empty">${tl("empty")}</p>`}`;
374
374
  }
375
- const tools = state.phase === "on" ? html`<div class="log-bar" role="group" aria-label=${tl("filter")}>
375
+ const tools = state.phase === "on" ? html`<div data-trust="panel-controls" class="log-bar" role="group" aria-label=${tl("filter")}>
376
376
  ${["all", "tools", "errors"].map((k) => html`<button key=${k} type="button" class=${`chip-btn${filter === k ? " on" : ""}`} aria-pressed=${filter === k ? "true" : "false"} onClick=${() => setFilter(k)}>${tl(k)}</button>`)}
377
377
  <span class="grow"></span><span class="log-live" title=${tl("live")}><span class="dot running" aria-hidden="true"></span>${tl("live")}</span></div>` : null;
378
- return html`<div class="panel p-log">
378
+ return html`<div data-effect-panel="log" class="panel p-log">
379
379
  <${PanelHeader} title=${name} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="log" />`}><${HeaderTools} navKey=${navKey} /><${HeaderSlot} /></${PanelHeader}>
380
380
  ${tools}
381
- <div class="panel-body log-body" ref=${scroller} onScroll=${onScroll}><div class="col log-col">${content}</div></div>
381
+ <div data-trust="panel-content" class="panel-body log-body" ref=${scroller} onScroll=${onScroll}><div class="col log-col">${content}</div></div>
382
382
  </div>`;
383
383
  }
384
384
 
@@ -123,9 +123,9 @@ export function NeedsPanel({ navKey }) {
123
123
  useEffect(() => { if (owner) return owner.subscribe(() => tick((n) => n + 1)); return undefined; }, [owner, lease]);
124
124
  const groups = groupNeeds(items);
125
125
  const nowMs = Date.now();
126
- return html`<div class="panel p-needs">
126
+ return html`<div data-effect-panel="needs" class="panel p-needs">
127
127
  <${PanelHeader} title=${tn("title")} sub=${items.length ? t("app.needsCount", items.length) : null} />
128
- <div class="panel-body"><div class="col">
128
+ <div data-trust="panel-content" class="panel-body"><div class="col">
129
129
  ${!items.length ? html`<${Empty} icon="check" title=${tn("empty")} hint=${tn(chatApp ? "emptyHint" : "emptyHintWeb")} />`
130
130
  : groups.map((g) => html`<section key=${g.instance} class="n-group" aria-label=${g.instance}>
131
131
  <h2 class="n-group-head"><a href=${chatPath(g.instance)} title=${g.instance}>${nameFor(g.instance)}</a></h2>
@@ -175,7 +175,7 @@ function NeedsItem({ item, nowMs, owner }) {
175
175
  setClaim("acks", item.id, res && res.ok ? "done" : null);
176
176
  toast((body && (body.message || body.error)) || t("app.failed"), !!(res && res.ok));
177
177
  };
178
- return html`<article class=${`n-item${BAD.has(item.reason) ? " bad" : ""}`}>
178
+ return html`<article data-trust="permission" class=${`n-item${BAD.has(item.reason) ? " bad" : ""}`}>
179
179
  <span class="n-icon"><${Icon} name=${ICON[item.type] || "alert"} size=${18} /></span>
180
180
  <div class="n-body">
181
181
  <div class="n-title"><strong>${reasonText(item)}</strong><span class="n-age">${age(item.since, nowMs)}</span></div>