@songsid/agend 2.2.0 → 2.3.0-beta.2

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 (298) 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 +35 -0
  7. package/dist/agent-file-read.js +144 -0
  8. package/dist/agent-file-read.js.map +1 -0
  9. package/dist/agent-whats-new.js +8 -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/claude-code.js +5 -1
  18. package/dist/backend/claude-code.js.map +1 -1
  19. package/dist/backend/launch-effort.js +1 -1
  20. package/dist/backend/launch-effort.js.map +1 -1
  21. package/dist/backend/muse.js +10 -2
  22. package/dist/backend/muse.js.map +1 -1
  23. package/dist/backend/types.d.ts +8 -0
  24. package/dist/backend/types.js +10 -0
  25. package/dist/backend/types.js.map +1 -1
  26. package/dist/cache-analysis.d.ts +47 -1
  27. package/dist/cache-analysis.js +68 -8
  28. package/dist/cache-analysis.js.map +1 -1
  29. package/dist/cache-report-tool.d.ts +17 -0
  30. package/dist/cache-report-tool.js +40 -0
  31. package/dist/cache-report-tool.js.map +1 -0
  32. package/dist/cache-service.d.ts +15 -0
  33. package/dist/cache-service.js +26 -1
  34. package/dist/cache-service.js.map +1 -1
  35. package/dist/channel/adapters/discord.d.ts +2 -0
  36. package/dist/channel/adapters/discord.js +17 -2
  37. package/dist/channel/adapters/discord.js.map +1 -1
  38. package/dist/channel/adapters/telegram.d.ts +8 -1
  39. package/dist/channel/adapters/telegram.js +74 -4
  40. package/dist/channel/adapters/telegram.js.map +1 -1
  41. package/dist/channel/mcp-tools.js +36 -0
  42. package/dist/channel/mcp-tools.js.map +1 -1
  43. package/dist/channel/types.d.ts +15 -0
  44. package/dist/cli.js +7 -3
  45. package/dist/cli.js.map +1 -1
  46. package/dist/config-validator.js +22 -2
  47. package/dist/config-validator.js.map +1 -1
  48. package/dist/daemon.js +244 -20
  49. package/dist/daemon.js.map +1 -1
  50. package/dist/fleet-context.d.ts +5 -0
  51. package/dist/fleet-manager.d.ts +92 -0
  52. package/dist/fleet-manager.js +410 -29
  53. package/dist/fleet-manager.js.map +1 -1
  54. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +7 -0
  55. package/dist/general-knowledge/skills/web-appearance/SKILL.md +367 -0
  56. package/dist/instance-avatar.js +3 -0
  57. package/dist/instance-avatar.js.map +1 -1
  58. package/dist/instance-config-impact.js +3 -0
  59. package/dist/instance-config-impact.js.map +1 -1
  60. package/dist/instance-lifecycle.js +9 -4
  61. package/dist/instance-lifecycle.js.map +1 -1
  62. package/dist/needs-you-hub.d.ts +4 -0
  63. package/dist/needs-you-hub.js +31 -6
  64. package/dist/needs-you-hub.js.map +1 -1
  65. package/dist/needs-you.d.ts +1 -13
  66. package/dist/needs-you.js +3 -37
  67. package/dist/needs-you.js.map +1 -1
  68. package/dist/outbound-handlers.d.ts +7 -0
  69. package/dist/outbound-handlers.js +65 -10
  70. package/dist/outbound-handlers.js.map +1 -1
  71. package/dist/outbound-schemas.d.ts +74 -0
  72. package/dist/outbound-schemas.js +56 -0
  73. package/dist/outbound-schemas.js.map +1 -1
  74. package/dist/public-web-gateway.js +14 -0
  75. package/dist/public-web-gateway.js.map +1 -1
  76. package/dist/repo-worktree-guard.d.ts +36 -0
  77. package/dist/repo-worktree-guard.js +143 -0
  78. package/dist/repo-worktree-guard.js.map +1 -0
  79. package/dist/scheduler/scheduler.d.ts +70 -1
  80. package/dist/scheduler/scheduler.js +256 -17
  81. package/dist/scheduler/scheduler.js.map +1 -1
  82. package/dist/service-installer.d.ts +2 -1
  83. package/dist/service-installer.js +18 -3
  84. package/dist/service-installer.js.map +1 -1
  85. package/dist/settings-api.js +74 -1
  86. package/dist/settings-api.js.map +1 -1
  87. package/dist/settings-change.js +8 -0
  88. package/dist/settings-change.js.map +1 -1
  89. package/dist/settings-effect.js +16 -2
  90. package/dist/settings-effect.js.map +1 -1
  91. package/dist/settings-http-confirmation.js +5 -0
  92. package/dist/settings-http-confirmation.js.map +1 -1
  93. package/dist/tool-permissions.d.ts +5 -0
  94. package/dist/tool-permissions.js +61 -2
  95. package/dist/tool-permissions.js.map +1 -1
  96. package/dist/topic-commands.d.ts +10 -6
  97. package/dist/topic-commands.js +21 -17
  98. package/dist/topic-commands.js.map +1 -1
  99. package/dist/topic-link.d.ts +128 -0
  100. package/dist/topic-link.js +269 -0
  101. package/dist/topic-link.js.map +1 -0
  102. package/dist/types.d.ts +13 -0
  103. package/dist/ui/app-effects.js +187 -0
  104. package/dist/ui/chat-effect-posters.js +42 -0
  105. package/dist/ui/chat-effects.js +72 -0
  106. package/dist/ui/chat-live-replies.js +58 -0
  107. package/dist/ui/chat-render.js +4 -4
  108. package/dist/ui/chat-store.js +9 -3
  109. package/dist/ui/chat-strings.js +15 -0
  110. package/dist/ui/chat-thread.js +47 -21
  111. package/dist/ui/chat-tour.js +1 -1
  112. package/dist/ui/effect-trial-controller.js +119 -0
  113. package/dist/ui/effect-trial-offer.js +31 -0
  114. package/dist/ui/effect-trial-panel.js +37 -0
  115. package/dist/ui/fleet-cache.js +23 -1
  116. package/dist/ui/image-lightbox.js +1 -1
  117. package/dist/ui/panel-chat.js +85 -17
  118. package/dist/ui/panel-details.js +18 -5
  119. package/dist/ui/panel-fleet.js +3 -3
  120. package/dist/ui/panel-log.js +6 -6
  121. package/dist/ui/panel-needs.js +3 -3
  122. package/dist/ui/panel-settings.js +8 -5
  123. package/dist/ui/preview-panel.js +25 -9
  124. package/dist/ui/settings-appearance.js +325 -0
  125. package/dist/ui/settings-effects.js +201 -0
  126. package/dist/ui/settings-mods.js +257 -0
  127. package/dist/ui/settings-owner-effects.js +80 -0
  128. package/dist/ui/settings-strings.js +245 -0
  129. package/dist/ui/shared/agend-auth.js +27 -3
  130. package/dist/ui/shared/agent-widget.js +248 -0
  131. package/dist/ui/shared/app-i18n.js +2 -0
  132. package/dist/ui/shared/app-prefs.js +30 -10
  133. package/dist/ui/shared/app-route.js +23 -1
  134. package/dist/ui/shared/app-session.js +1 -1
  135. package/dist/ui/shared/app-shell.js +29 -16
  136. package/dist/ui/shared/app-store.js +2 -0
  137. package/dist/ui/shared/app-stream.js +12 -7
  138. package/dist/ui/shared/app-themes.js +188 -0
  139. package/dist/ui/shared/app.css +225 -3
  140. package/dist/ui/shared/app.js +6 -0
  141. package/dist/ui/shared/card-strings.js +28 -0
  142. package/dist/ui/shared/effect-app-runtime.js +248 -0
  143. package/dist/ui/shared/effect-baseline.js +64 -0
  144. package/dist/ui/shared/effect-chat-runtime.js +154 -0
  145. package/dist/ui/shared/effect-cursor.js +125 -0
  146. package/dist/ui/shared/effect-frame.js +127 -0
  147. package/dist/ui/shared/effect-geometry.js +122 -0
  148. package/dist/ui/shared/effect-host-style.js +10 -0
  149. package/dist/ui/shared/effect-owner-regions.js +74 -0
  150. package/dist/ui/shared/effect-owner-source.js +115 -0
  151. package/dist/ui/shared/effect-policy.js +137 -0
  152. package/dist/ui/shared/effect-poster-reader.js +98 -0
  153. package/dist/ui/shared/effect-posters.js +117 -0
  154. package/dist/ui/shared/effect-profiler.js +162 -0
  155. package/dist/ui/shared/effect-profiles.js +65 -0
  156. package/dist/ui/shared/effect-registry.js +496 -0
  157. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  158. package/dist/ui/shared/effect-safety.js +153 -0
  159. package/dist/ui/shared/effect-stock-snapshot.js +34 -0
  160. package/dist/ui/shared/effect-stock-trust.js +165 -0
  161. package/dist/ui/shared/effect-trial-source.js +103 -0
  162. package/dist/ui/shared/effect-trial-stage.js +147 -0
  163. package/dist/ui/shared/header-tools.js +1 -1
  164. package/dist/ui/shared/identity-card.js +202 -0
  165. package/dist/ui/shared/identity-color.js +261 -0
  166. package/dist/ui/shared/instance-nav.js +37 -19
  167. package/dist/ui/shared/owner-css.js +221 -0
  168. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  169. package/dist/ui/shared/panel-view.js +81 -8
  170. package/dist/ui/shared/signin.js +2 -0
  171. package/dist/ui/shared/ui-dialog.js +1 -1
  172. package/dist/ui/shared/ui-icons.js +2 -0
  173. package/dist/ui/shared/ui-menu.js +1 -1
  174. package/dist/ui/shared/ui-toast.js +1 -1
  175. package/dist/ui/shared/usage-dialog.js +14 -2
  176. package/dist/ui/shared/view-strings.js +36 -2
  177. package/dist/ui/signin.html +1 -0
  178. package/dist/usage/providers.d.ts +3 -0
  179. package/dist/usage/providers.js +4 -3
  180. package/dist/usage/providers.js.map +1 -1
  181. package/dist/view-api.d.ts +29 -0
  182. package/dist/view-api.js +188 -8
  183. package/dist/view-api.js.map +1 -1
  184. package/dist/web-api.d.ts +42 -0
  185. package/dist/web-api.js +898 -36
  186. package/dist/web-api.js.map +1 -1
  187. package/dist/web-appearance-limits.d.ts +82 -0
  188. package/dist/web-appearance-limits.js +83 -0
  189. package/dist/web-appearance-limits.js.map +1 -0
  190. package/dist/web-appearance-policy.d.ts +66 -0
  191. package/dist/web-appearance-policy.js +94 -0
  192. package/dist/web-appearance-policy.js.map +1 -0
  193. package/dist/web-appearance-tools.d.ts +50 -0
  194. package/dist/web-appearance-tools.js +105 -0
  195. package/dist/web-appearance-tools.js.map +1 -0
  196. package/dist/web-appearance-view.d.ts +99 -0
  197. package/dist/web-appearance-view.js +163 -0
  198. package/dist/web-appearance-view.js.map +1 -0
  199. package/dist/web-appearance.d.ts +79 -0
  200. package/dist/web-appearance.js +298 -0
  201. package/dist/web-appearance.js.map +1 -0
  202. package/dist/web-asset-parts.d.ts +93 -0
  203. package/dist/web-asset-parts.js +133 -0
  204. package/dist/web-asset-parts.js.map +1 -0
  205. package/dist/web-auth.js +17 -1
  206. package/dist/web-auth.js.map +1 -1
  207. package/dist/web-card.d.ts +96 -0
  208. package/dist/web-card.js +232 -0
  209. package/dist/web-card.js.map +1 -0
  210. package/dist/web-chat-history.d.ts +13 -0
  211. package/dist/web-chat-history.js +23 -2
  212. package/dist/web-chat-history.js.map +1 -1
  213. package/dist/web-effect-bundle.d.ts +28 -0
  214. package/dist/web-effect-bundle.js +126 -0
  215. package/dist/web-effect-bundle.js.map +1 -0
  216. package/dist/web-effect-canonical.d.ts +8 -0
  217. package/dist/web-effect-canonical.js +27 -0
  218. package/dist/web-effect-canonical.js.map +1 -0
  219. package/dist/web-effect-library.d.ts +114 -0
  220. package/dist/web-effect-library.js +530 -0
  221. package/dist/web-effect-library.js.map +1 -0
  222. package/dist/web-effect-poster.d.ts +8 -0
  223. package/dist/web-effect-poster.js +52 -0
  224. package/dist/web-effect-poster.js.map +1 -0
  225. package/dist/web-effect-profiles.d.ts +118 -0
  226. package/dist/web-effect-profiles.js +180 -0
  227. package/dist/web-effect-profiles.js.map +1 -0
  228. package/dist/web-effect-service.d.ts +86 -0
  229. package/dist/web-effect-service.js +314 -0
  230. package/dist/web-effect-service.js.map +1 -0
  231. package/dist/web-effect-tools.d.ts +47 -0
  232. package/dist/web-effect-tools.js +129 -0
  233. package/dist/web-effect-tools.js.map +1 -0
  234. package/dist/web-effect-trials.d.ts +86 -0
  235. package/dist/web-effect-trials.js +204 -0
  236. package/dist/web-effect-trials.js.map +1 -0
  237. package/dist/web-effect-upload.d.ts +27 -0
  238. package/dist/web-effect-upload.js +185 -0
  239. package/dist/web-effect-upload.js.map +1 -0
  240. package/dist/web-effect-worker.d.ts +8 -0
  241. package/dist/web-effect-worker.js +170 -0
  242. package/dist/web-effect-worker.js.map +1 -0
  243. package/dist/web-effect.d.ts +103 -0
  244. package/dist/web-effect.js +153 -0
  245. package/dist/web-effect.js.map +1 -0
  246. package/dist/web-identity-color.d.ts +87 -0
  247. package/dist/web-identity-color.js +209 -0
  248. package/dist/web-identity-color.js.map +1 -0
  249. package/dist/web-identity-palette.d.ts +113 -0
  250. package/dist/web-identity-palette.js +84 -0
  251. package/dist/web-identity-palette.js.map +1 -0
  252. package/dist/web-image-meta.d.ts +25 -0
  253. package/dist/web-image-meta.js +200 -0
  254. package/dist/web-image-meta.js.map +1 -0
  255. package/dist/web-mod-agent.d.ts +50 -0
  256. package/dist/web-mod-agent.js +205 -0
  257. package/dist/web-mod-agent.js.map +1 -0
  258. package/dist/web-mod-bundle.d.ts +149 -0
  259. package/dist/web-mod-bundle.js +470 -0
  260. package/dist/web-mod-bundle.js.map +1 -0
  261. package/dist/web-mod-export.d.ts +59 -0
  262. package/dist/web-mod-export.js +79 -0
  263. package/dist/web-mod-export.js.map +1 -0
  264. package/dist/web-mod-store.d.ts +283 -0
  265. package/dist/web-mod-store.js +625 -0
  266. package/dist/web-mod-store.js.map +1 -0
  267. package/dist/web-org.d.ts +3 -3
  268. package/dist/web-org.js +11 -17
  269. package/dist/web-org.js.map +1 -1
  270. package/dist/web-owner-css-store.d.ts +180 -0
  271. package/dist/web-owner-css-store.js +384 -0
  272. package/dist/web-owner-css-store.js.map +1 -0
  273. package/dist/web-owner-css.d.ts +58 -0
  274. package/dist/web-owner-css.js +552 -0
  275. package/dist/web-owner-css.js.map +1 -0
  276. package/dist/web-prefs.d.ts +48 -0
  277. package/dist/web-prefs.js +209 -33
  278. package/dist/web-prefs.js.map +1 -1
  279. package/dist/web-preview.d.ts +9 -1
  280. package/dist/web-preview.js +24 -7
  281. package/dist/web-preview.js.map +1 -1
  282. package/dist/web-settings-patch.d.ts +18 -0
  283. package/dist/web-settings-patch.js +51 -0
  284. package/dist/web-settings-patch.js.map +1 -1
  285. package/dist/web-shell-routes.d.ts +1 -1
  286. package/dist/web-shell-routes.js +1 -1
  287. package/dist/web-shell-routes.js.map +1 -1
  288. package/dist/web-theme-store.d.ts +218 -0
  289. package/dist/web-theme-store.js +516 -0
  290. package/dist/web-theme-store.js.map +1 -0
  291. package/dist/web-theme.d.ts +121 -0
  292. package/dist/web-theme.js +407 -0
  293. package/dist/web-theme.js.map +1 -0
  294. package/dist/web-widget.d.ts +72 -0
  295. package/dist/web-widget.js +245 -0
  296. package/dist/web-widget.js.map +1 -0
  297. package/package.json +1 -1
  298. package/templates/systemd.service.ejs +8 -0
@@ -31,6 +31,11 @@ register("cache", {
31
31
  shared: "shares its working directory with {0}; their transcripts cannot be told apart",
32
32
  empty: "No instances", emptyHint: "Instances show here once the fleet has them.",
33
33
  b0: "≤5m", b1: "≤30m", b2: "≤1h", b3: "≤2h", b4: "≤6h", b5: "≤24h", b6: ">24h", tokensUnit: "input-token equivalents",
34
+ writeMix: "Cache writes 5 min / 1 h", effectiveTtl: "Behaves as", gapMiss: "{0} of {1} 5–60 min gaps rewrote it", gapMissNone: "no 5–60 min gaps yet",
35
+ effMeasured: "{0} (measured)", effLabel: "{0} (as written)", effUnproven: "not known yet",
36
+ fleetSwitch: "Switch to 1-hour cache",
37
+ switchTo1h: "Its 5-minute cache really expires: {0} of {1} gaps of 5–60 minutes rewrote it. Set prompt_cache_ttl: \"1h\" for this instance (backend_options.claude-code) rather than keep-warm.",
38
+ unproven: "Written with a 5-minute TTL, but only {0} gaps of 5–60 minutes so far — too few to tell how long it really lasts. The keep-warm figures assume 5 minutes.",
34
39
  },
35
40
  "zh-TW": {
36
41
  title: "快取", w24h: "24 小時", w7d: "7 天", w30d: "30 天", window: "期間",
@@ -53,6 +58,11 @@ register("cache", {
53
58
  shared: "與 {0} 共用工作目錄,無法分辨各自的 transcript",
54
59
  empty: "沒有 instance", emptyHint: "fleet 有 instance 後會顯示在這裡。",
55
60
  b0: "≤5分", b1: "≤30分", b2: "≤1時", b3: "≤2時", b4: "≤6時", b5: "≤24時", b6: ">24時", tokensUnit: "輸入 token 當量",
61
+ writeMix: "快取寫入 5 分 / 1 時", effectiveTtl: "實際存活", gapMiss: "{1} 個 5–60 分鐘空檔中有 {0} 個重寫", gapMissNone: "還沒有 5–60 分鐘的空檔",
62
+ effMeasured: "{0}(實測)", effLabel: "{0}(依寫入標記)", effUnproven: "尚無法判斷",
63
+ fleetSwitch: "建議改用 1 小時快取",
64
+ switchTo1h: "它的 5 分鐘快取確實會過期:{1} 個 5–60 分鐘空檔中有 {0} 個重寫。請為這個 instance 設定 prompt_cache_ttl: \"1h\"(backend_options.claude-code),而不是保溫。",
65
+ unproven: "以 5 分鐘 TTL 寫入,但目前只有 {0} 個 5–60 分鐘空檔,太少,無法判斷實際存活多久。保溫的數字以 5 分鐘估算。",
56
66
  },
57
67
  });
58
68
  const tc = (k, ...v) => t(`cache.${k}`, ...v);
@@ -79,6 +89,8 @@ const pct = (x) => `${x >= 0.1 ? Math.round(x * 100) : (x * 100).toFixed(1)}%`;
79
89
  export function reasonText(a) {
80
90
  const r = a.recommendation, ttl = ttlText(a.ttlSec);
81
91
  if (r.reason === "quiet") return tc("quiet");
92
+ if (r.reason === "switch_to_1h") return tc("switchTo1h", a.effectiveTtl.misses, a.effectiveTtl.sample);
93
+ if (r.reason === "insufficient_evidence") return tc("unproven", a.effectiveTtl.sample);
82
94
  if (r.reason === "never_expired") {
83
95
  if (a.sim.pastTtl) return tc("survived", a.sim.pastTtl, ttl);
84
96
  return a.sim.tailPings ? tc("onlyIdle", a.sim.tailPings.toLocaleString("en-US")) : tc("neverExpired", ttl);
@@ -107,6 +119,12 @@ function Histogram({ buckets, ttlSec }) {
107
119
  </svg>`;
108
120
  }
109
121
 
122
+ /** #2010: what the TTL behaves as — measured, as written, or not known yet. */
123
+ export function effectiveText(e) {
124
+ if (e.ttlSec === null) return tc("effUnproven");
125
+ return e.basis === "measured" ? tc("effMeasured", ttlText(e.ttlSec)) : tc("effLabel", ttlText(e.ttlSec));
126
+ }
127
+
110
128
  function Metric({ label, value, sub }) {
111
129
  return html`<div class="ch-metric"><dt>${label}</dt><dd>${value}${sub ? html`<span class="ch-sub">${sub}</span>` : null}</dd></div>`;
112
130
  }
@@ -130,8 +148,11 @@ function InstanceCard({ r }) {
130
148
  <${Metric} label=${tc("pings")} value=${(s.pings + s.tailPings).toLocaleString("en-US")} sub=${s.tailPings ? tc("pingsAfter", s.tailPings) : null} />
131
149
  <${Metric} label=${tc("pingCost")} value=${$(s.pingCost + s.tailCost)} />
132
150
  <${Metric} label=${tc("net")} value=${a.priced ? `${s.net >= 0 ? "+" : "−"}${money(Math.abs(s.net))}` : "—"} />
151
+ ${a.writeMix["5m"] + a.writeMix["1h"] > 0 ? html`<${Metric} label=${tc("writeMix")} value=${`${tokens(a.writeMix["5m"])} / ${tokens(a.writeMix["1h"])}`} />` : null}
152
+ ${r.backend === "claude-code" ? html`<${Metric} label=${tc("effectiveTtl")} value=${effectiveText(a.effectiveTtl)}
153
+ sub=${a.effectiveTtl.sample ? tc("gapMiss", a.effectiveTtl.misses, a.effectiveTtl.sample) : tc("gapMissNone")} />` : null}
133
154
  </dl>
134
- <${Histogram} buckets=${a.gapBuckets} ttlSec=${a.ttlSec} />
155
+ <${Histogram} buckets=${a.gapBuckets} ttlSec=${a.effectiveTtl.basis === "measured" && a.effectiveTtl.ttlSec !== null ? a.effectiveTtl.ttlSec : a.ttlSec} />
135
156
  </article>`;
136
157
  }
137
158
 
@@ -154,6 +175,7 @@ export function CacheReport({ report, win, setWin, refresh }) {
154
175
  <${Metric} label=${tc("fleetExpiry")} value=${money(f.expiryCost)} />
155
176
  <${Metric} label=${tc("fleetSaving")} value=${money(f.saving)} />
156
177
  <${Metric} label=${tc("fleetRecommended")} value=${tc("ofN", f.recommended, f.analysed)} />
178
+ ${f.switchTo1h ? html`<${Metric} label=${tc("fleetSwitch")} value=${tc("ofN", f.switchTo1h, f.analysed)} />` : null}
157
179
  </section>` : html`<${Empty} icon="chart" title=${tc("empty")} hint=${tc("emptyHint")} />`}
158
180
  <div class="ch-notes note">
159
181
  <p>${tc("pricesNote", report.pricesChecked)} ${tc("quotaNote")}</p>
@@ -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>
@@ -41,9 +41,10 @@ import { takeAgentRequest } from "./settings-request.js";
41
41
  import { setPreviewOptIn } from "./panel-chat.js";
42
42
  import { startTour } from "./chat-tour.js";
43
43
  import "./chat-strings.js";
44
+ import { AppearanceSection } from "./settings-appearance.js";
44
45
 
45
46
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
46
- const ICONS = { device: "user", agents: "bot", bots: "plug", classic: "room", general: "sliders", web: "globe", advanced: "code" };
47
+ const ICONS = { device: "user", appearance: "palette", agents: "bot", bots: "plug", classic: "room", general: "sliders", web: "globe", advanced: "code" };
47
48
  const RELEASES = "https://github.com/songsid/AgEnD/releases";
48
49
 
49
50
  /** GET under the panel's lease → { ok, status, body }. */
@@ -224,6 +225,8 @@ export function SettingsPanel({ route, navKey }) {
224
225
  }, [!!ctx, navKey]);
225
226
  // #1604: This device needs no configuration: shown at once, whatever the configuration read does.
226
227
  const sectionBody = section === "device" ? html`<${DeviceSection} />`
228
+ // #1629 r4 §3: Appearance is the person's own (no configuration read): shown at once, like Preferences.
229
+ : section === "appearance" ? html`<${AppearanceSection} />`
227
230
  : !data ? html`<${Skeleton} lines=${6} />`
228
231
  : data.error ? html`<${ErrorState} message=${tn("configLoadFailed")} onRetry=${reload} />`
229
232
  : section === "agents" ? html`<${Agents} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
@@ -236,14 +239,14 @@ export function SettingsPanel({ route, navKey }) {
236
239
  const open = dialog && dialog.key === navKey ? dialog : null;
237
240
  const close = () => setDialog(null);
238
241
  const searchable = section === "agents" || section === "bots" || section === "classic";
239
- return html`<div class="panel p-settings">
242
+ return html`<div data-effect-panel="settings" class="panel p-settings">
240
243
  <${PanelHeader} title=${tn("title")} sub=${data && data.version ? html`<a class="pill ver" href=${data.version.includes("-") ? `${RELEASES}/latest` : `${RELEASES}/tag/v${data.version}`} target="_blank" rel="noopener">v${data.version}</a>` : null}>
241
244
  <button type="button" class="btn btn-ghost btn-sm" disabled=${!ctx} onClick=${() => setDialog({ kind: "wizard", key: navKey })}><${Icon} name="wand" size=${16} /><span class="hide-narrow">${tn("wizardButton")}</span></button>
242
245
  <button type="button" class="icon-btn" aria-label=${tn("helpButton")} title=${tn("helpButton")} onClick=${() => setDialog({ kind: "help", key: navKey })}><${Icon} name="info" /></button>
243
246
  </${PanelHeader}>
244
- <nav class="seg" aria-label=${tn("title")}>${SETTINGS_SECTIONS.map((k) => html`<a key=${k} href=${settingsPath(k)} class=${`seg-item${k === section ? " active" : ""}`}
247
+ <nav data-trust="panel-navigation" class="seg" aria-label=${tn("title")}>${SETTINGS_SECTIONS.map((k) => html`<a key=${k} href=${settingsPath(k)} class=${`seg-item${k === section ? " active" : ""}`}
245
248
  aria-current=${k === section ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${tn(`sec_${k}`)}</span></a>`)}</nav>
246
- <div class="panel-body"><div class="col">
249
+ <div data-trust="panel-content" class="panel-body"><div class="col">
247
250
  ${searchable ? html`<div class="s-toolbar"><label class="s-search"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("searchAll")}</span>
248
251
  <input type="search" value=${search} placeholder=${tn("searchAll")} onInput=${(e) => setSearch(e.target.value)} /></label></div>` : null}
249
252
  ${op ? html`<${OperationCard} op=${op} schema=${data && data.schema} />` : null}
@@ -915,7 +918,7 @@ function Developer({ ctx }) {
915
918
  /** Staged, not applied: the count, what it will cost, Discard and Apply. */
916
919
  function PendingBar({ staged, busy, onApply, onDiscard }) {
917
920
  const kinds = [...new Set([...staged.values()].map((c) => c.impact))];
918
- return html`<div class="s-pending" role="region" aria-label=${tn("pendingRegion")}>
921
+ return html`<div data-trust="panel-controls" class="s-pending" role="region" aria-label=${tn("pendingRegion")}>
919
922
  <strong>${tn("changesPending", staged.size)}</strong>
920
923
  <span class="note">${kinds.map(impactText).join(" · ")}</span>
921
924
  <span class="grow"></span>
@@ -16,6 +16,7 @@ import { Icon } from "/assets/ui-icons.js";
16
16
  import "./chat-render.js";
17
17
  import "./preview.js";
18
18
  import { loadHtmlAttachment } from "./html-attachment.js";
19
+ import { EffectTrialPanel } from "./effect-trial-panel.js";
19
20
  import { chatTime, onDayChange } from "./chat-time.js";
20
21
 
21
22
  const R = () => globalThis.AgendChatRender;
@@ -43,14 +44,25 @@ export const panelClaims = () => claims;
43
44
  /** The frame's key in preview.js: its own, so the card that opened it keeps its own state. */
44
45
  export const frameKey = (key) => `panel:${key}`;
45
46
  /** Show `spec` in the panel; `run`: start it there (the card's "Open in panel" click). The block shown before stops. */
47
+ function retire(open) {
48
+ if (!open) return;
49
+ if (open.kind === "trial") { try { open.handle.stop(); } catch { /* host controller owns cleanup */ } }
50
+ else if (P()) P().stop(frameKey(open.key), "stopped", "");
51
+ }
46
52
  export function openPanel(spec, { run = false } = {}) {
47
- if (state.open && P()) P().stop(frameKey(state.open.key), "stopped", "");
48
- set({ open: { ...spec }, run: run ? ++runs : 0 });
53
+ const old = state.open;
54
+ set({ open: { ...spec, kind: "html" }, run: run ? ++runs : 0 });
55
+ retire(old); // State changes first: cleanup reentry cannot close a newer panel.
56
+ }
57
+ /** A checked controller handle enters the existing component slot; no HTML run token is issued. */
58
+ export function openTrialPanel(spec) {
59
+ const old = state.open;
60
+ set({ open: { ...spec, kind: "trial" }, run: 0 }); retire(old);
49
61
  }
50
- /** Close the panel: its frame stops first. */
62
+ /** Close publishes the empty slot before old-owner callbacks can reenter. */
51
63
  export function closePanel() {
52
- if (state.open && P()) P().stop(frameKey(state.open.key), "stopped", "");
53
- if (state.open) set({ open: null, run: 0 });
64
+ const old = state.open; if (!old) return;
65
+ set({ open: null, run: 0 }); retire(old);
54
66
  }
55
67
  /** The card key the panel shows, or null. */
56
68
  export const shownKey = () => (state.open ? state.open.key : null);
@@ -67,7 +79,7 @@ export function htmlTitle(code) {
67
79
  * without one, the newest block does. `msgs` in order; `keyOf` makes a message's key.
68
80
  */
69
81
  export function newerVersion(open, msgs, keyOf) {
70
- if (!open || !Array.isArray(msgs)) return null;
82
+ if (!open || open.kind === "trial" || !Array.isArray(msgs)) return null;
71
83
  const at = msgs.findIndex(x => keyOf(x) === open.msgKey);
72
84
  if (at < 0) return null;
73
85
  // An attached file: a later agent reply attaching a file of the same name is its newer version. Its HTML is read
@@ -132,7 +144,8 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
132
144
  const pending = useRef(0); // #1704: the card's click, waiting for this device's Allow (the st.run it came with)
133
145
  const ownAllow = useRef(0); // while this panel's own Allow commits its choice: the click it answers (not cancelled by it)
134
146
  const holder = useRef(null), divider = useRef(null), panel = useRef(null);
135
- const open = st.open && st.open.instance === name ? st.open : null;
147
+ const trial = st.open?.kind === "trial" && st.open.instance === name ? st.open : null;
148
+ const open = st.open?.kind !== "trial" && st.open?.instance === name ? st.open : null;
136
149
 
137
150
  useEffect(() => panelStore.subscribe(setSt), []);
138
151
  // This device's choice (here or in another tab) decides whether Preview is offered: re-read it when it changes.
@@ -231,6 +244,9 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
231
244
  // Esc in the panel closes it (and is not the chat's "stop the reply").
232
245
  const onPanelKey = (e) => { if (e.key === "Escape" && !e.isComposing) { e.preventDefault(); closePanel(); } };
233
246
 
247
+ if (trial) return html`<${EffectTrialPanel} binding=${trial.binding} title=${trial.handle.reply.offer.title}
248
+ current=${trial.handle.current} ownerCss=${trial.ownerCss} onStage=${trial.onStage}
249
+ onStop=${trial.handle.stop} onClose=${() => { if (panelStore.get().open === trial) closePanel(); }} />`;
234
250
  if (!open) return null;
235
251
  const going = run.state === "starting" || run.state === "running";
236
252
  const a = P().availability();
@@ -250,9 +266,9 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
250
266
  if (want && pending.current === want && panelStore.get().run === want) start();
251
267
  };
252
268
  const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
253
- return html`<div class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
269
+ return html`<div data-trust="preview" class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
254
270
  aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>
255
- <aside class="pv-panel" ref=${panel} aria-label=${t("chat.pvPanel")} onKeyDown=${onPanelKey}>
271
+ <aside data-trust="preview" class="pv-panel" ref=${panel} aria-label=${t("chat.pvPanel")} onKeyDown=${onPanelKey}>
256
272
  <div class="pv-panel-head">
257
273
  <button type="button" class="icon-btn only-narrow pv-panel-close" title=${t("chat.pvBack")} aria-label=${t("chat.pvBack")} onClick=${closePanel}><${Icon} name="back" size=${18} /></button>
258
274
  <div class="pv-panel-title"><strong>${t("chat.pvPanel")}</strong>