@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
@@ -0,0 +1,117 @@
1
+ // #1629: host-authoritative personal choices. No local cache, migration, frame, code fetch or automatic resend.
2
+ import { createStore, appStore } from "./app-store.js";
3
+ import { OWNER_EFFECT_PREF_KEYS } from "./app-prefs.js";
4
+ import { checkEffectOwnerSlots } from "./effect-runtime-loader.js";
5
+ const KEYS = OWNER_EFFECT_PREF_KEYS, ID = /^[a-f0-9]{32}$/, EPOCH = /^[a-f0-9]{16}$/;
6
+ const plain = x => !!x && typeof x === "object" && Object.getPrototypeOf(x) === Object.prototype;
7
+ const own = (x, k) => Object.hasOwn(x, k);
8
+ const unknownSlots = () => Object.fromEntries(KEYS.map(k => [k, { status: "unknown", id: null }]));
9
+ const publicPage = () => typeof document === "undefined" || document.body?.dataset.publicLink === "1" || document.body?.dataset.mode === "view-only";
10
+ function bootScope() {
11
+ try {
12
+ const x = JSON.parse(document.documentElement.getAttribute("data-prefs") || "null");
13
+ return plain(x) && ["person", "operator"].includes(x.ownerScope) && typeof x.ownerEpoch === "string" && EPOCH.test(x.ownerEpoch)
14
+ ? Object.freeze({ scope: x.ownerScope, epoch: x.ownerEpoch }) : null;
15
+ } catch { return null; }
16
+ }
17
+ const owner = bootScope();
18
+ export const ownerEffectsStore = createStore({ scope: owner?.scope ?? "none", status: "unknown", revoked: false, busy: false, slots: unknownSlots(), error: null });
19
+ let watchers = 0, generation = 0, active = null, queuedRead = false, unsubscribe = null, rev = -1, seen = null, failedAt = -Infinity;
20
+ const clock = () => { try { const n = performance.now(); return Number.isFinite(n) && n >= 0 ? n : null; } catch { return null; } };
21
+ const eligible = () => !!owner && watchers > 0 && !publicPage() && !ownerEffectsStore.get().revoked;
22
+ const current = op => eligible() && op.generation === generation && active === op;
23
+ function revoke() {
24
+ queuedRead = false;
25
+ ownerEffectsStore.set({ status: "unknown", revoked: true, slots: unknownSlots(), error: "sign_in_changed" });
26
+ }
27
+ function unknown(error) { const n = clock(); failedAt = n === null ? Infinity : n; ownerEffectsStore.set({ status: "unknown", slots: unknownSlots(), error }); }
28
+ function claim() {
29
+ if (!eligible() || active) return null;
30
+ const controller = new AbortController(), op = { controller, generation };
31
+ active = op; ownerEffectsStore.set({ busy: true, error: null }); return op;
32
+ }
33
+ async function call(op, path, init = {}) {
34
+ const started = clock();
35
+ if (started === null || op.controller.signal.aborted) return { ok: false, status: 0, body: null };
36
+ const timer = setTimeout(() => op.controller.abort(), 10000);
37
+ try {
38
+ const res = await fetch(path, { credentials: "same-origin", ...init, signal: op.controller.signal });
39
+ let body = null; try { body = await res.json(); } catch { /* unknown */ }
40
+ const ended = clock();
41
+ if (op.controller.signal.aborted || ended === null || ended < started || ended - started >= 10000) return { ok: false, status: 0, body: null };
42
+ return { ok: res.ok, status: res.status, body };
43
+ } catch { return { ok: false, status: 0, body: null }; }
44
+ finally { clearTimeout(timer); }
45
+ }
46
+ function finish(op) {
47
+ if (active !== op) return;
48
+ active = null; ownerEffectsStore.set({ busy: false });
49
+ const again = queuedRead; queuedRead = false;
50
+ const n = clock();
51
+ if (again && eligible() && n !== null && n - failedAt >= 5000) refreshOwnerEffects();
52
+ }
53
+ /** An owner response is closed and bounded. Unavailable means a stored id, not an empty slot or private manifest. */
54
+ export function checkOwnerSlots(body) { return checkEffectOwnerSlots(body); }
55
+
56
+ async function read(op) {
57
+ const r = await call(op, `/ui/effects/owner?ownerEpoch=${owner.epoch}`);
58
+ if (!current(op)) return false;
59
+ if ([401, 403, 409].includes(r.status)) { revoke(); return false; }
60
+ const data = r.ok ? checkOwnerSlots(r.body) : null;
61
+ if (!data) { unknown("unreadable"); return false; }
62
+ if (data.rev < rev) { unknown("stale"); return false; }
63
+ rev = data.rev; failedAt = -Infinity;
64
+ ownerEffectsStore.set({ status: "known", slots: data.slots, error: null }); return true;
65
+ }
66
+ export async function refreshOwnerEffects() {
67
+ if (!eligible()) return false;
68
+ if (active) { queuedRead = true; return false; }
69
+ const op = claim(); if (!op) return false;
70
+ try { return await read(op); } finally { finish(op); }
71
+ }
72
+ /** One physical operation survives until settlement, including after abort. At most one coalesced read follows. */
73
+ export function watchOwnerEffects() {
74
+ watchers++;
75
+ if (watchers === 1 && eligible()) {
76
+ const check = () => {
77
+ const next = appStore.get().prefsRev, n = clock();
78
+ if (next !== seen || ownerEffectsStore.get().status === "unknown" && n !== null && n - failedAt >= 5000) { seen = next; refreshOwnerEffects(); }
79
+ };
80
+ unsubscribe = appStore.subscribe(check); check();
81
+ }
82
+ let ended = false;
83
+ return () => {
84
+ if (ended) return; ended = true;
85
+ if (--watchers > 0) return;
86
+ generation++; queuedRead = false; unsubscribe?.(); unsubscribe = null; active?.controller.abort();
87
+ ownerEffectsStore.set({ status: "unknown", slots: unknownSlots(), error: null });
88
+ };
89
+ }
90
+ export async function saveOwnerEffects(set) {
91
+ if (!plain(set) || !Object.keys(set).length || Object.keys(set).some(k => !KEYS.includes(k) || set[k] !== null && (typeof set[k] !== "string" || !ID.test(set[k])))) return { ok: false, error: "invalid" };
92
+ // Snapshot both data and immutable boot context before the first wait. No optimistic runtime selection.
93
+ const values = { ...set }, op = claim(); if (!op) return { ok: false, error: "busy_or_unavailable" };
94
+ let saved = false;
95
+ try {
96
+ const r = await call(op, "/ui/prefs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ set: values, ownerEpoch: owner.epoch }) });
97
+ if (!current(op)) return { ok: false, error: "superseded" };
98
+ if ([401, 403, 409].includes(r.status)) { revoke(); return { ok: false, error: "sign_in_changed" }; }
99
+ if (r.ok && plain(r.body) && (r.body.ownerEpoch !== owner.epoch || r.body.ownerScope !== owner.scope)) { revoke(); return { ok: false, error: "sign_in_changed" }; }
100
+ if (!r.ok || !plain(r.body) || !plain(r.body.values) || !Number.isSafeInteger(r.body.rev) || r.body.rev < 0 || r.body.rev < rev || r.body.unreadable) { unknown("not_saved"); return { ok: false, error: "not_saved" }; }
101
+ if (!Object.entries(values).every(([k, v]) => v === null ? !own(r.body.values, k) : r.body.values[k] === v)) { unknown("not_saved"); return { ok: false, error: "not_saved" }; }
102
+ saved = true;
103
+ const refreshed = await read(op); return { ok: refreshed, saved, error: refreshed ? null : "refresh_failed" };
104
+ } finally { finish(op); }
105
+ }
106
+ /** Runtime adapter asks only for the exact, currently known selection; a cursor can never request Worker source. */
107
+ export function ownerEffectUrl(key, id, kind = "resource") {
108
+ const row = ownerEffectsStore.get().slots[key];
109
+ if (!eligible() || ownerEffectsStore.get().status !== "known" || !KEYS.includes(key) || !row || row.status !== "available" || row.id !== id || !["resource", "poster", "cursor"].includes(kind) || kind === "resource" && key === "fx.cursor") return null;
110
+ const path = kind === "resource" ? `/ui/effects/resource/${id}` : `/ui/effects/image/${id}/${kind}.png`;
111
+ return `${path}?slot=${encodeURIComponent(key)}&ownerEpoch=${owner.epoch}`;
112
+ }
113
+ export function markOwnerEffectUnavailable(key, id) {
114
+ const s = ownerEffectsStore.get(), row = s.slots[key];
115
+ if (!eligible() || s.status !== "known" || !KEYS.includes(key) || row?.id !== id) return;
116
+ ownerEffectsStore.set({ slots: { ...s.slots, [key]: { status: "unavailable", id } } });
117
+ }
@@ -27,6 +27,9 @@ import { Dialog } from "./ui-dialog.js";
27
27
  import { Empty, ErrorState, Skeleton } from "./ui-states.js";
28
28
  import { Icon } from "./ui-icons.js";
29
29
  import "./view-strings.js";
30
+ import { checkCard, fitPicture, IdentityCard, useCard } from "./identity-card.js";
31
+ import { confirmDialog } from "./ui-confirm.js";
32
+ import { openColorDialog } from "./identity-color.js";
30
33
 
31
34
  /** view.* text with named values: tn("paneFailed", { name, status }). */
32
35
  function tn(key, values = {}) {
@@ -180,15 +183,15 @@ export function ViewPanel({ route, navKey }) {
180
183
  <button type="button" class="icon-btn" onClick=${() => setDialog("help")} aria-label=${tn("helpButton")} title=${tn("helpButton")}><${Icon} name="info" /></button>`;
181
184
 
182
185
  let body;
183
- if (v.error && !v.roster.length) body = html`<div class="panel-body center"><${ErrorState} message=${tn("rosterFailedShort")} onRetry=${() => navigate(location.pathname, { replace: true })} /></div>`;
184
- else if (!v.loaded) body = html`<div class="panel-body"><${Skeleton} lines=${6} /></div>`;
185
- else if (!v.roster.length) body = html`<div class="panel-body center"><${Empty} icon="view" title=${tn("noInstances")} /></div>`;
186
- else if (!name) body = html`<div class="panel-body"><${Skeleton} lines=${6} /></div>`;
187
- else if (!it) body = html`<div class="panel-body center"><${Empty} icon="alert" title=${tn("notFound", { name })} hint=${tn("notFoundHint")} /></div>`;
186
+ if (v.error && !v.roster.length) body = html`<div data-trust="panel-content" class="panel-body center"><${ErrorState} message=${tn("rosterFailedShort")} onRetry=${() => navigate(location.pathname, { replace: true })} /></div>`;
187
+ else if (!v.loaded) body = html`<div data-trust="panel-content" class="panel-body"><${Skeleton} lines=${6} /></div>`;
188
+ else if (!v.roster.length) body = html`<div data-trust="panel-content" class="panel-body center"><${Empty} icon="view" title=${tn("noInstances")} /></div>`;
189
+ else if (!name) body = html`<div data-trust="panel-content" class="panel-body"><${Skeleton} lines=${6} /></div>`;
190
+ else if (!it) body = html`<div data-trust="panel-content" class="panel-body center"><${Empty} icon="alert" title=${tn("notFound", { name })} hint=${tn("notFoundHint")} /></div>`;
188
191
  else body = html`<${Terminal} key=${name} name=${name} lease=${lease} density=${density} />
189
192
  <${Card} it=${it} viewOnly=${viewOnly} onEdit=${() => setDialog("edit")} />`;
190
193
 
191
- return html`<div class="panel p-view">
194
+ return html`<div data-effect-panel="view" class="panel p-view">
192
195
  <${PanelHeader} title=${it ? (it.display_name || it.instance_name) : tn("title")}
193
196
  sub=${it ? html`<span class="status"><span class=${`dot ${it.status === "running" ? "ok" : it.status === "crashed" ? "bad" : "off"}`} aria-hidden="true"></span>${tn(STATUS_KEYS[it.status] || "statusUnknown")}</span>` : null}
194
197
  nav=${it && !viewOnly ? html`<${InstanceSwitch} name=${name} current="view" />` : null}>${actions}</${PanelHeader}>
@@ -266,7 +269,7 @@ function Terminal({ name, lease, density }) {
266
269
  lease.hold(() => ro.disconnect());
267
270
  }
268
271
  }, [lease, name]);
269
- return html`<div class="v-term" ref=${term}>${problem ? html`<p class="v-term-note" role="status">${problem}</p>` : null}<pre ref=${pre} class="v-pre"></pre></div>`;
272
+ return html`<div data-trust="panel-content" class="v-term" ref=${term}>${problem ? html`<p class="v-term-note" role="status">${problem}</p>` : null}<pre ref=${pre} class="v-pre"></pre></div>`;
270
273
  }
271
274
 
272
275
  // The advance of one monospace cell as a fraction of font-size, measured once on this page.
@@ -296,7 +299,9 @@ function Card({ it, viewOnly, onEdit }) {
296
299
  const initial = (it.display_name || it.instance_name || "?").trim().charAt(0) || "?";
297
300
  const ctx = it.context_pct != null ? ` · ctx ${Math.round(it.context_pct)}%` : "";
298
301
  const toggle = () => { const next = !open; setOpen(next); store("agend_view_card_expanded", next ? "1" : "0"); };
299
- return html`<section class=${`v-card${open ? " open" : ""}`} aria-label=${it.display_name || it.instance_name}>
302
+ // #1629 r4 §1: the identity card, above the profile row while it is expanded.
303
+ const card = useCard(it.instance_name, it.card_v);
304
+ return html`${open && card ? html`<div data-trust="panel-content" class="v-id"><${IdentityCard} name=${it.instance_name} card=${card} /></div>` : null}<section data-trust="panel-content" class=${`v-card${open ? " open" : ""}`} aria-label=${it.display_name || it.instance_name}>
300
305
  <div class="v-avatar">${it.has_avatar ? html`<img ref=${avatar} alt="" />` : html`<span class="v-initial" aria-hidden="true">${initial}</span>`}</div>
301
306
  <div class="v-card-body">
302
307
  <div class="v-card-line"><strong>${it.display_name || it.instance_name}</strong>${it.role ? html`<span class="v-role">${it.role}</span>` : null}</div>
@@ -320,6 +325,28 @@ function EditProfile({ it, onClose }) {
320
325
  // #1630 follow-up: the uploaded picture is removed on Save (Cancel keeps it); a newly picked file wins over removing.
321
326
  const [removePic, setRemovePic] = useState(false);
322
327
  const file = useRef(null);
328
+ // #1629 r4 §1 / r4.1: the identity card's own fields — this person's (theirs win over the agent's, field by field).
329
+ // Read when the dialog opens: what is drawn now, whose each field is, and the upload limits in force.
330
+ const [card, setCard] = useState(null);
331
+ const [cardText, setCardText] = useState({ tagline: "", bio: "" });
332
+ const [removeCard, setRemoveCard] = useState({ portrait: false, banner: false });
333
+ const [clearCard, setClearCard] = useState(false);
334
+ const pics = { portrait: useRef(null), banner: useRef(null) };
335
+ useEffect(() => {
336
+ let live = true;
337
+ // The card's fields stay disabled until this read is in (#1889 review: a draft typed meanwhile was overwritten);
338
+ // a failed read lets them be edited, starting empty.
339
+ const none = { mine: { tagline: null, bio: null, portrait: null, banner: null }, limits: null };
340
+ lease.fetch(`/api/card/${encodeURIComponent(it.instance_name)}`).then((r) => (r.ok ? r.json() : null)).then((body) => {
341
+ if (!live || !lease.current()) return;
342
+ if (!body) { setCard(none); return; }
343
+ const resolved = checkCard(it.instance_name, body);
344
+ const mine = (k) => (resolved && resolved[k] && resolved[k].by === "human" ? resolved[k] : null);
345
+ setCard({ mine: { tagline: mine("tagline"), bio: mine("bio"), portrait: mine("portrait"), banner: mine("banner") }, limits: body.limits || null });
346
+ setCardText({ tagline: mine("tagline") ? mine("tagline").text : "", bio: mine("bio") ? mine("bio").text : "" });
347
+ }).catch(() => { if (live && lease.current()) setCard(none); });
348
+ return () => { live = false; };
349
+ }, []);
323
350
  async function save() {
324
351
  if (busy) return;
325
352
  setBusy(true); setMsg(null);
@@ -329,7 +356,27 @@ function EditProfile({ it, onClose }) {
329
356
  const draft = { display_name: f.display, role: f.role, description: f.desc };
330
357
  const picked = file.current && file.current.files && file.current.files[0];
331
358
  const removing = removePic;
359
+ // The card: Clear card wins over everything else in it; otherwise only the fields that changed are sent.
360
+ const clearing = clearCard;
361
+ const before = card && card.mine ? { tagline: card.mine.tagline ? card.mine.tagline.text : "", bio: card.mine.bio ? card.mine.bio.text : "" } : { tagline: "", bio: "" };
362
+ if (!clearing) for (const k of ["tagline", "bio"]) if (cardText[k] !== before[k]) draft[`card_${k}`] = cardText[k];
363
+ const cardPics = {};
332
364
  try {
365
+ // A picture too large is fitted first (after asking) — before anything is written, so declining writes nothing.
366
+ if (!clearing) for (const slot of ["portrait", "banner"]) {
367
+ const chosen = pics[slot].current && pics[slot].current.files && pics[slot].current.files[0];
368
+ if (chosen) {
369
+ const limit = card && card.limits && card.limits[slot];
370
+ let blob = chosen;
371
+ if (limit) {
372
+ try { blob = await fitPicture(chosen, limit, (w, h, nw, nh) => confirmDialog({ message: tn("resizeAsk", { w, h, nw, nh }), confirmLabel: tn("resizeGo") })); }
373
+ catch { throw new Error(tn("pictureTooLarge")); }
374
+ }
375
+ if (!lease.current()) return;
376
+ if (!blob) { setBusy(false); return; }
377
+ cardPics[slot] = blob;
378
+ } else if (removeCard[slot]) cardPics[slot] = "remove";
379
+ }
333
380
  const r = await fetch(`/api/profile/${name}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(draft) });
334
381
  if (!lease.current()) return;
335
382
  if (r.status === 401) throw new Error(tn("signInToSave"));
@@ -342,6 +389,18 @@ function EditProfile({ it, onClose }) {
342
389
  if (!dr.ok) { const e = await dr.json().catch(() => ({})); throw new Error(tn("avatarRemoveFailed", { error: e.error || dr.status })); }
343
390
  }
344
391
  if (!lease.current()) return;
392
+ if (clearing) {
393
+ const cr = await fetch(`/api/card/${name}`, { method: "DELETE" });
394
+ if (!cr.ok) { const e = await cr.json().catch(() => ({})); throw new Error(tn("cardFailed", { error: e.error || cr.status })); }
395
+ }
396
+ for (const [slot, what] of Object.entries(cardPics)) {
397
+ if (!lease.current()) return;
398
+ const pr = what === "remove"
399
+ ? await fetch(`/api/card/${name}/${slot}`, { method: "DELETE" })
400
+ : await fetch(`/api/card/${name}/${slot}`, { method: "POST", headers: { "Content-Type": what.type || "application/octet-stream" }, body: what });
401
+ if (!pr.ok) { const e = await pr.json().catch(() => ({})); throw new Error(tn("cardFailed", { error: e.detail || e.error || pr.status })); }
402
+ }
403
+ if (!lease.current()) return;
345
404
  setBusy(false);
346
405
  onClose();
347
406
  } catch (e) { if (lease.current()) { setBusy(false); setMsg(e.message); } }
@@ -356,6 +415,20 @@ function EditProfile({ it, onClose }) {
356
415
  <label class="field"><span>${tn("avatar")}</span><input ref=${file} type="file" accept="image/png,image/jpeg,image/gif,image/webp" /></label>
357
416
  ${it.has_avatar ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removePic ? "true" : "false"}
358
417
  onClick=${() => setRemovePic(!removePic)}>${tn(removePic ? "keepPicture" : "removePicture")}</button>${removePic ? html`<span class="muted">${tn("removePictureNote")}</span>` : null}</div>` : null}
418
+ <div class="field-row"><button type="button" class="btn btn-sm" onClick=${() => openColorDialog(it.instance_name, (appStore.get().instances || []).find((i) => i.name === it.instance_name))}>${t("color.menu")}</button></div>
419
+ <h3 class="form-sub">${tn("cardSection")}</h3>
420
+ <p class="note">${tn("cardLead")}</p>
421
+ <fieldset class="form" disabled=${clearCard || !card} aria-busy=${card ? "false" : "true"}>
422
+ <label class="field"><span>${tn("cardTagline")}</span><input value=${cardText.tagline} onInput=${(e) => setCardText({ ...cardText, tagline: e.target.value })} /></label>
423
+ <label class="field"><span>${tn("cardBio")}</span><textarea rows="4" value=${cardText.bio} onInput=${(e) => setCardText({ ...cardText, bio: e.target.value })}></textarea></label>
424
+ ${["portrait", "banner"].map((slot) => html`<div key=${slot} class="form">
425
+ <label class="field"><span>${tn(slot === "portrait" ? "cardPortrait" : "cardBanner")}</span><input ref=${pics[slot]} type="file" accept="image/png,image/jpeg,image/webp" /></label>
426
+ ${card && card.mine && card.mine[slot] ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removeCard[slot] ? "true" : "false"}
427
+ onClick=${() => setRemoveCard({ ...removeCard, [slot]: !removeCard[slot] })}>${tn(removeCard[slot] ? "cardKeep" : "cardRemove")}</button></div>` : null}
428
+ </div>`)}
429
+ </fieldset>
430
+ <div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${clearCard ? "true" : "false"} onClick=${() => setClearCard(!clearCard)}>${tn(clearCard ? "keepCard" : "clearCard")}</button>
431
+ ${clearCard ? html`<span class="muted">${tn("clearCardNote")}</span>` : null}</div>
359
432
  </fieldset></form>
360
433
  ${msg ? html`<p class="err" role="alert">${msg}${msg === tn("signInToSave") ? html` <a class="link" href=${signInHref()}>${t("app.signIn")}</a>` : null}</p>` : null}
361
434
  </${Dialog}>`;
@@ -15,6 +15,7 @@
15
15
  label: "One-time code", submit: "Sign in", working: "Signing in…",
16
16
  hint: "No code? Send /web in General, then press a sign-in button to get one.",
17
17
  nobot: "No chat bot?", nobot_body: "Ask the operator to run agend web --code on the AgEnD computer.",
18
+ safe: "Page looks wrong after signing in?", safe_body: "Your custom CSS may be hiding something. Add ?safe=1 to the end of the address to load the page without it, then fix it or turn it off in Settings → Web appearance.",
18
19
  refused: "The code is invalid or no longer works. Send /web in General, then press a sign-in button for a new code.",
19
20
  unconfirmed: "Sign-in could not be confirmed. Get a new code and try again; if it keeps failing, contact the operator.",
20
21
  paused: "Too many wrong codes — sign-in is paused for a few minutes.",
@@ -28,6 +29,7 @@
28
29
  label: "一次性登入碼", submit: "登入", working: "登入中…",
29
30
  hint: "沒收到?在 General 傳 /web,再按登入按鈕取碼。",
30
31
  nobot: "沒有聊天 bot?", nobot_body: "請管理員在 AgEnD 電腦執行 agend web --code。",
32
+ safe: "登入後畫面不對勁?", safe_body: "可能是你的自訂 CSS 把東西藏起來了。在網址最後加上 ?safe=1,就會不套用它地載入;再到「設定 → 網頁外觀」修正或關閉。",
31
33
  refused: "登入碼無效或已失效。請在 General 傳 /web,再按登入按鈕取新碼。",
32
34
  unconfirmed: "登入暫時無法確認。請重新取碼再試;若仍失敗,請聯絡管理員。",
33
35
  paused: "錯誤次數過多,登入暫停幾分鐘。",
@@ -35,7 +35,7 @@ export function Dialog({ title, onClose, actions, children, busy = false, wide =
35
35
  d.addEventListener("click", onClick);
36
36
  return () => { d.removeEventListener("cancel", onCancel); d.removeEventListener("click", onClick); };
37
37
  }, [onClose]);
38
- return html`<dialog ref=${ref} class=${`dlg${wide ? " dlg-wide" : ""}`} aria-label=${label || title}>
38
+ return html`<dialog data-trust="dialog" ref=${ref} class=${`dlg${wide ? " dlg-wide" : ""}`} aria-label=${label || title}>
39
39
  <div class="dlg-box">
40
40
  <div class="dlg-head"><h2 class="dlg-title">${title}</h2>
41
41
  <button type="button" class="icon-btn dlg-x" aria-label=${t("app.close")} title=${t("app.close")} onClick=${() => { if (!busyRef.current) onClose(); }}><${Icon} name="close" /></button></div>
@@ -54,6 +54,8 @@ const P = {
54
54
  panel: () => html`<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M14 4v16"/>`,
55
55
  back: () => html`<path d="M19 12H5M12 19l-7-7 7-7"/>`,
56
56
  forward: () => html`<path d="M5 12h14M12 5l7 7-7 7"/>`,
57
+ // #1629: Settings → Appearance.
58
+ palette: () => html`<path d="M12 3a9 9 0 1 0 0 18c1.1 0 1.7-.8 1.7-1.7 0-.5-.2-.9-.5-1.2-.3-.3-.5-.7-.5-1.2 0-.9.8-1.7 1.7-1.7H16a5 5 0 0 0 5-5c0-4-4-7.2-9-7.2z"/><circle cx="7.5" cy="10.5" r="1"/><circle cx="10.5" cy="7" r="1"/><circle cx="15" cy="7.5" r="1"/>`,
57
59
  book: () => html`<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20V3H6.5A2.5 2.5 0 0 0 4 5.5z"/><path d="M4 19.5A2.5 2.5 0 0 0 6.5 22H20v-5"/>`,
58
60
  external: () => html`<path d="M14 4h6v6M20 4l-9 9"/><path d="M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/>`,
59
61
  };
@@ -31,7 +31,7 @@ export function Menu({ items, label, icon = "more", cls = "" }) {
31
31
  return html`<div class=${`menu ${cls}`}>
32
32
  <button ref=${btn} type="button" class="icon-btn" aria-haspopup="menu" aria-expanded=${open ? "true" : "false"}
33
33
  aria-label=${label || t("app.more")} title=${label || t("app.more")} onClick=${() => setOpen(!open)}><${Icon} name=${icon} /></button>
34
- ${open ? html`<div ref=${list} class="menu-list" role="menu" aria-label=${label || t("app.more")} onKeyDown=${onKey}>
34
+ ${open ? html`<div data-trust="menu" ref=${list} class="menu-list" role="menu" aria-label=${label || t("app.more")} onKeyDown=${onKey}>
35
35
  ${shown.map(item => html`<button key=${item.key} type="button" role="menuitem" class=${`menu-item${item.danger ? " danger" : ""}`}
36
36
  disabled=${!!item.disabled} onClick=${() => choose(item)}>${item.icon ? html`<${Icon} name=${item.icon} size=${16} />` : null}<span>${item.label}</span></button>`)}
37
37
  </div>` : null}
@@ -14,5 +14,5 @@ export function toast(text, ok = true) {
14
14
  export function Toasts() {
15
15
  const [list, setList] = useState(toasts.get());
16
16
  useEffect(() => toasts.subscribe(setList), []);
17
- return html`<div class="toasts" role="status" aria-live="polite">${list.map(x => html`<div key=${x.id} class=${`toast ${x.ok ? "ok" : "err"}`}>${x.text}</div>`)}</div>`;
17
+ return html`<div data-trust="toast" class="toasts" role="status" aria-live="polite">${list.map(x => html`<div key=${x.id} class=${`toast ${x.ok ? "ok" : "err"}`}>${x.text}</div>`)}</div>`;
18
18
  }
@@ -66,7 +66,17 @@ export function UsageMetric({ m }) {
66
66
  const sub = [note, expiryText(m.expiresAt)].filter(Boolean).join(" · ");
67
67
  return html`<div class="u-metric"><div class="u-row"><span class="u-label">${label}</span><span class="u-val">${val}</span></div>${sub ? html`<div class="u-sub">${sub}</div>` : null}</div>`;
68
68
  }
69
- /** "Updated 3 min ago" (#1585): how old the numbers are — the panel may show the last snapshot while a fresh one comes. */
69
+ /**
70
+ * #2014: a row's title. A credential profile's row (`profile`; its API `name` is "Claude (<profile>)") shows the
71
+ * provider name on the title line beside the plan badge, and the profile on its own full-width muted line under it;
72
+ * the full name is the tooltip, and the dialog shows the profile unclipped. Any other row: its name.
73
+ */
74
+ export function providerTitle(p) {
75
+ const name = String((p && p.name) || ""), profile = p && typeof p.profile === "string" && p.profile ? p.profile : "";
76
+ const suffix = ` (${profile})`;
77
+ return profile && name.endsWith(suffix) ? { base: name.slice(0, -suffix.length), profile } : { base: name, profile };
78
+ }
79
+
70
80
  /**
71
81
  * #1749: a plan as its badge reads — one short line. "Name [Tier] (N×)" (Claude's "Team Premium (6.25x)", "Max (20x)")
72
82
  * becomes "Name ×N"; anything else (Pro, Codex's and muse's labels) as it is. The full name stays on the badge's
@@ -76,6 +86,7 @@ export function planBadge(plan) {
76
86
  const m = /^(\S+)(?: [^()]+)? \((\d+(?:\.\d+)?)x\)$/i.exec(String(plan || "").trim());
77
87
  return m ? `${m[1]} ×${m[2]}` : String(plan || "");
78
88
  }
89
+ /** "Updated 3 min ago" (#1585): how old the numbers are — the panel may show the last snapshot while a fresh one comes. */
79
90
  export function updatedAgo(fetchedAt, now = Date.now()) {
80
91
  const ms = now - new Date(fetchedAt).getTime();
81
92
  if (!Number.isFinite(ms) || ms < 60_000) return tn("usageUpdatedJustNow");
@@ -134,11 +145,12 @@ export function UsageBody({ owner }) {
134
145
  const providers = ordered(data.providers);
135
146
  content = html`${!providers.length ? html`<p class="note">${tn("usage.empty_active")}</p>` : null}
136
147
  ${providers.map((p, i) => html`<section key=${key(p)} class="u-provider">
137
- <div class="u-head"><strong title=${p.name}>${p.name}</strong>${p.plan ? html`<span class="pill u-plan" title=${p.plan}><span aria-hidden="true">${planBadge(p.plan)}</span><span class="sr-only">${p.plan}</span></span>` : null}
148
+ <div class="u-head"><strong title=${p.name}>${providerTitle(p).base}</strong>${p.plan ? html`<span class="pill u-plan" title=${p.plan}><span aria-hidden="true">${planBadge(p.plan)}</span><span class="sr-only">${p.plan}</span></span>` : null}
138
149
  <span class="u-order">
139
150
  <button type="button" class="icon-btn" disabled=${i === 0} aria-label=${tn("usageMoveUp")} title=${tn("usageMoveUp")} onClick=${() => move(key(p), -1, data.providers)}><${Icon} name="up" size=${14} /></button>
140
151
  <button type="button" class="icon-btn" disabled=${i === providers.length - 1} aria-label=${tn("usageMoveDown")} title=${tn("usageMoveDown")} onClick=${() => move(key(p), 1, data.providers)}><${Icon} name="down" size=${14} /></button>
141
152
  </span></div>
153
+ ${providerTitle(p).profile ? html`<div class="u-profile" title=${p.name}>${providerTitle(p).profile}</div>` : null}
142
154
  ${p.stale ? html`<p class="note">${usageText(p.hint || "", p.hintI18n)}</p>` : null}
143
155
  ${p.status === "error" && !p.transient ? html`<p class="u-err">${usageText(p.error || tn("usage.error_fallback"), p.errorI18n)}</p>`
144
156
  : p.status === "error" && p.transient ? html`<p class="note">${usageText(p.error || tn("usage.error_fallback"), p.errorI18n)}</p>`
@@ -156,6 +156,21 @@ register("view", {
156
156
  "removePicture": "Remove picture",
157
157
  "keepPicture": "Keep picture",
158
158
  "removePictureNote": "Removed when you save. The bot's avatar, the persona emoji or the first letter shows instead.",
159
+ "cardSection": "Identity card",
160
+ "cardLead": "Yours wins over the agent's, field by field. Leave a field empty to show the agent's.",
161
+ "cardTagline": "Tagline (one line, up to 60 characters)",
162
+ "cardBio": "Bio (up to 280 characters, 3 line breaks)",
163
+ "cardPortrait": "Portrait",
164
+ "cardBanner": "Banner",
165
+ "cardRemove": "Remove yours",
166
+ "cardKeep": "Keep yours",
167
+ "clearCard": "Clear card",
168
+ "keepCard": "Keep card",
169
+ "clearCardNote": "Removed when you save: your tagline, bio, portrait and banner. The agent's own card shows instead.",
170
+ "resizeAsk": "This picture is {w}×{h}; it will be resized to {nw}×{nh}.",
171
+ "resizeGo": "Resize",
172
+ "cardFailed": "Card not saved: {error}",
173
+ "pictureTooLarge": "This picture can't be made small enough to upload.",
159
174
  "saved": "Saved",
160
175
  "saveFailed": "Save failed: {error}",
161
176
  "signInToEdit": "Sign in to edit",
@@ -182,7 +197,9 @@ register("view", {
182
197
  "favUp": "Move {name} up",
183
198
  "favDown": "Move {name} down",
184
199
  "favFull": "Favorites are full ({max})",
185
- "favFiltered": "Clear the filter to reorder favorites"
200
+ "favFiltered": "Clear the filter to reorder favorites",
201
+ "pinnedCurrent": "Current",
202
+ "pinnedLabel": "The instance you have open"
186
203
  },
187
204
  "zh-TW": {
188
205
  "editProfile": "編輯個人資料",
@@ -336,6 +353,21 @@ register("view", {
336
353
  "removePicture": "移除圖片",
337
354
  "keepPicture": "保留圖片",
338
355
  "removePictureNote": "儲存後移除,改顯示 Bot 的頭像、persona emoji 或名稱第一個字。",
356
+ "cardSection": "身分卡",
357
+ "cardLead": "你設定的欄位優先於 agent 的,逐欄比較。欄位留空就顯示 agent 的。",
358
+ "cardTagline": "標語(一行,最多 60 個字)",
359
+ "cardBio": "簡介(最多 280 個字、3 次換行)",
360
+ "cardPortrait": "肖像",
361
+ "cardBanner": "橫幅",
362
+ "cardRemove": "移除你的",
363
+ "cardKeep": "保留你的",
364
+ "clearCard": "清除卡片",
365
+ "keepCard": "保留卡片",
366
+ "clearCardNote": "儲存後移除你的標語、簡介、肖像與橫幅,改顯示 agent 自己的卡片。",
367
+ "resizeAsk": "這張圖片是 {w}×{h},會縮小為 {nw}×{nh}。",
368
+ "resizeGo": "縮小",
369
+ "cardFailed": "卡片未儲存:{error}",
370
+ "pictureTooLarge": "這張圖片無法縮到可上傳的大小。",
339
371
  "saved": "已儲存",
340
372
  "saveFailed": "儲存失敗:{error}",
341
373
  "signInToEdit": "登入後才能編輯",
@@ -362,6 +394,8 @@ register("view", {
362
394
  "favUp": "把 {name} 上移",
363
395
  "favDown": "把 {name} 下移",
364
396
  "favFull": "最愛已滿({max} 個)",
365
- "favFiltered": "清除篩選後才能調整最愛順序"
397
+ "favFiltered": "清除篩選後才能調整最愛順序",
398
+ "pinnedCurrent": "目前",
399
+ "pinnedLabel": "你目前開著的 instance"
366
400
  }
367
401
  });
@@ -55,6 +55,7 @@
55
55
  <div id="msg" role="alert" aria-live="polite"></div>
56
56
  <div class="hint" data-i18n="hint">No code? Send /web in General, then press a sign-in button to get one.</div>
57
57
  <details class="help"><summary data-i18n="nobot">No chat bot?</summary><p data-i18n="nobot_body">Ask the operator to run <code>agend web --code</code> on the AgEnD computer.</p></details>
58
+ <details class="help"><summary data-i18n="safe">Page looks wrong after signing in?</summary><p data-i18n="safe_body">Your custom CSS may be hiding something. Add ?safe=1 to the end of the address to load the page without it, then fix it or turn it off in Settings → Web appearance.</p></details>
58
59
  </form>
59
60
  <noscript>This page needs JavaScript to sign you in.</noscript>
60
61
  </main>
@@ -34,6 +34,8 @@ export interface UsageMetric {
34
34
  export interface ProviderUsage {
35
35
  id: string;
36
36
  name: string;
37
+ /** #2014: the credential profile this row reads (its `name` ends in ` (<profile>)`); the page shows it on its own line. */
38
+ profile?: string;
37
39
  status: "ok" | "error" | "no-credentials";
38
40
  /** A non-metered subscription whose entitlement is unlimited (not 0/100%). */
39
41
  unlimited?: boolean;
@@ -221,6 +223,7 @@ export declare function fetchKiroUsage(storeHome?: string): Promise<Omit<Provide
221
223
  type UsageProvider = {
222
224
  id: string;
223
225
  name: string;
226
+ profile?: string;
224
227
  fetch: (storeHome?: string, scope?: ClaudeUsageScope) => Promise<Omit<ProviderUsage, "id" | "name">>;
225
228
  };
226
229
  type UsageFleetConfig = Parameters<typeof listConfiguredProfiles>[0];
@@ -1458,6 +1458,7 @@ export function providersForConfig(config, base = DEFAULT_PROVIDERS) {
1458
1458
  rows.push(...profiles.map(profile => ({
1459
1459
  id: `${provider.id}:${profile}`,
1460
1460
  name: `${provider.name} (${profile})`,
1461
+ profile,
1461
1462
  fetch: () => provider.fetch(credentialProfileStoreHome(getAgendHome(), backendName, profile), { profile }),
1462
1463
  })));
1463
1464
  return rows;
@@ -1644,16 +1645,16 @@ export async function fetchAllUsage(config) {
1644
1645
  // Fail soft: this row says it could not be read, the rest still answer,
1645
1646
  // and the snapshot returns in bounded time.
1646
1647
  return {
1647
- id: p.id, name: p.name, status: "error",
1648
+ id: p.id, name: p.name, ...(p.profile ? { profile: p.profile } : {}), status: "error",
1648
1649
  error: `Could not reach ${p.name}.`,
1649
1650
  errorI18n: i18n("usage.error.unreachable", p.name),
1650
1651
  metrics: [],
1651
1652
  };
1652
1653
  }
1653
- return { id: p.id, name: p.name, ...outcome };
1654
+ return { id: p.id, name: p.name, ...(p.profile ? { profile: p.profile } : {}), ...outcome };
1654
1655
  }
1655
1656
  catch (err) {
1656
- return { id: p.id, name: p.name, status: "error", error: String(err?.message ?? err), metrics: [] };
1657
+ return { id: p.id, name: p.name, ...(p.profile ? { profile: p.profile } : {}), status: "error", error: String(err?.message ?? err), metrics: [] };
1657
1658
  }
1658
1659
  }));
1659
1660
  return {