@songsid/agend 2.2.1 → 2.3.0-beta.10

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 (437) hide show
  1. package/dist/agent-cli-instructions.md +5 -2
  2. package/dist/agent-cli.js +284 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.d.ts +2 -0
  5. package/dist/agent-endpoint.js +29 -0
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-file-read.d.ts +38 -0
  8. package/dist/agent-file-read.js +317 -0
  9. package/dist/agent-file-read.js.map +1 -0
  10. package/dist/agent-whats-new.d.ts +11 -0
  11. package/dist/agent-whats-new.js +29 -2
  12. package/dist/agent-whats-new.js.map +1 -1
  13. package/dist/atomic-write.js +9 -1
  14. package/dist/atomic-write.js.map +1 -1
  15. package/dist/auth-api.d.ts +30 -1
  16. package/dist/auth-api.js +61 -12
  17. package/dist/auth-api.js.map +1 -1
  18. package/dist/authority/authority-state.d.ts +38 -0
  19. package/dist/authority/authority-state.js +114 -0
  20. package/dist/authority/authority-state.js.map +1 -0
  21. package/dist/authority/session-authorizations.d.ts +43 -0
  22. package/dist/authority/session-authorizations.js +102 -0
  23. package/dist/authority/session-authorizations.js.map +1 -0
  24. package/dist/authority/web-authority.d.ts +120 -0
  25. package/dist/authority/web-authority.js +157 -0
  26. package/dist/authority/web-authority.js.map +1 -0
  27. package/dist/backend/antigravity.js +3 -3
  28. package/dist/backend/antigravity.js.map +1 -1
  29. package/dist/backend/claude-code.d.ts +2 -2
  30. package/dist/backend/claude-code.js +10 -5
  31. package/dist/backend/claude-code.js.map +1 -1
  32. package/dist/backend/launch-effort.js +1 -1
  33. package/dist/backend/launch-effort.js.map +1 -1
  34. package/dist/backend/muse.js +10 -2
  35. package/dist/backend/muse.js.map +1 -1
  36. package/dist/backend/types.d.ts +14 -0
  37. package/dist/backend/types.js +25 -0
  38. package/dist/backend/types.js.map +1 -1
  39. package/dist/backend-availability.d.ts +120 -0
  40. package/dist/backend-availability.js +396 -0
  41. package/dist/backend-availability.js.map +1 -0
  42. package/dist/backend-command.d.ts +21 -3
  43. package/dist/backend-command.js +23 -4
  44. package/dist/backend-command.js.map +1 -1
  45. package/dist/bundled-skills.d.ts +14 -0
  46. package/dist/bundled-skills.js +29 -0
  47. package/dist/bundled-skills.js.map +1 -0
  48. package/dist/cache-analysis.d.ts +47 -1
  49. package/dist/cache-analysis.js +68 -8
  50. package/dist/cache-analysis.js.map +1 -1
  51. package/dist/cache-ledger.d.ts +16 -3
  52. package/dist/cache-ledger.js +33 -13
  53. package/dist/cache-ledger.js.map +1 -1
  54. package/dist/cache-report-tool.d.ts +17 -0
  55. package/dist/cache-report-tool.js +40 -0
  56. package/dist/cache-report-tool.js.map +1 -0
  57. package/dist/cache-service.d.ts +70 -3
  58. package/dist/cache-service.js +178 -17
  59. package/dist/cache-service.js.map +1 -1
  60. package/dist/channel/adapters/discord.d.ts +9 -0
  61. package/dist/channel/adapters/discord.js +52 -2
  62. package/dist/channel/adapters/discord.js.map +1 -1
  63. package/dist/channel/adapters/telegram.d.ts +8 -1
  64. package/dist/channel/adapters/telegram.js +74 -4
  65. package/dist/channel/adapters/telegram.js.map +1 -1
  66. package/dist/channel/mcp-tools.js +48 -2
  67. package/dist/channel/mcp-tools.js.map +1 -1
  68. package/dist/channel/types.d.ts +21 -0
  69. package/dist/classic-channel-manager.d.ts +10 -5
  70. package/dist/classic-channel-manager.js +12 -9
  71. package/dist/classic-channel-manager.js.map +1 -1
  72. package/dist/cli.js +130 -31
  73. package/dist/cli.js.map +1 -1
  74. package/dist/config-validator.js +119 -4
  75. package/dist/config-validator.js.map +1 -1
  76. package/dist/daemon.d.ts +5 -0
  77. package/dist/daemon.js +273 -22
  78. package/dist/daemon.js.map +1 -1
  79. package/dist/decision-list.d.ts +25 -0
  80. package/dist/decision-list.js +30 -0
  81. package/dist/decision-list.js.map +1 -0
  82. package/dist/delivery-outbox.d.ts +10 -0
  83. package/dist/delivery-outbox.js +12 -1
  84. package/dist/delivery-outbox.js.map +1 -1
  85. package/dist/fleet-context.d.ts +15 -0
  86. package/dist/fleet-manager.d.ts +506 -11
  87. package/dist/fleet-manager.js +1784 -130
  88. package/dist/fleet-manager.js.map +1 -1
  89. package/dist/fleet-traffic.d.ts +74 -0
  90. package/dist/fleet-traffic.js +196 -0
  91. package/dist/fleet-traffic.js.map +1 -0
  92. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
  93. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
  94. package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
  95. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
  96. package/dist/general-knowledge/skills/web-appearance/SKILL.md +492 -0
  97. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +476 -0
  98. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  99. package/dist/human-delivery-receipts.d.ts +24 -1
  100. package/dist/human-delivery-receipts.js +64 -11
  101. package/dist/human-delivery-receipts.js.map +1 -1
  102. package/dist/instance-avatar.js +3 -0
  103. package/dist/instance-avatar.js.map +1 -1
  104. package/dist/instance-config-impact.js +6 -1
  105. package/dist/instance-config-impact.js.map +1 -1
  106. package/dist/instance-lifecycle.js +9 -4
  107. package/dist/instance-lifecycle.js.map +1 -1
  108. package/dist/instance-names.d.ts +4 -0
  109. package/dist/instance-names.js +12 -0
  110. package/dist/instance-names.js.map +1 -0
  111. package/dist/instructions.d.ts +12 -1
  112. package/dist/instructions.js +13 -2
  113. package/dist/instructions.js.map +1 -1
  114. package/dist/locale.js +100 -8
  115. package/dist/locale.js.map +1 -1
  116. package/dist/ls-attach-resolve.d.ts +95 -0
  117. package/dist/ls-attach-resolve.js +174 -0
  118. package/dist/ls-attach-resolve.js.map +1 -0
  119. package/dist/memory-hogs.d.ts +27 -0
  120. package/dist/memory-hogs.js +124 -0
  121. package/dist/memory-hogs.js.map +1 -0
  122. package/dist/needs-you-hub.d.ts +4 -0
  123. package/dist/needs-you-hub.js +31 -6
  124. package/dist/needs-you-hub.js.map +1 -1
  125. package/dist/needs-you.d.ts +1 -13
  126. package/dist/needs-you.js +3 -37
  127. package/dist/needs-you.js.map +1 -1
  128. package/dist/outbound-handlers.d.ts +15 -0
  129. package/dist/outbound-handlers.js +152 -11
  130. package/dist/outbound-handlers.js.map +1 -1
  131. package/dist/outbound-schemas.d.ts +228 -4
  132. package/dist/outbound-schemas.js +147 -1
  133. package/dist/outbound-schemas.js.map +1 -1
  134. package/dist/public-web-gateway.js +21 -1
  135. package/dist/public-web-gateway.js.map +1 -1
  136. package/dist/public-web-link.d.ts +9 -0
  137. package/dist/public-web-link.js +8 -0
  138. package/dist/public-web-link.js.map +1 -1
  139. package/dist/quickstart-api.d.ts +10 -1
  140. package/dist/quickstart-api.js +12 -4
  141. package/dist/quickstart-api.js.map +1 -1
  142. package/dist/reply-buttons-controller.d.ts +4 -0
  143. package/dist/reply-buttons-controller.js +2 -2
  144. package/dist/reply-buttons-controller.js.map +1 -1
  145. package/dist/repo-worktree-guard.d.ts +36 -0
  146. package/dist/repo-worktree-guard.js +143 -0
  147. package/dist/repo-worktree-guard.js.map +1 -0
  148. package/dist/restart-progress.d.ts +65 -1
  149. package/dist/restart-progress.js +236 -10
  150. package/dist/restart-progress.js.map +1 -1
  151. package/dist/rollout-index.d.ts +5 -0
  152. package/dist/rollout-index.js +26 -5
  153. package/dist/rollout-index.js.map +1 -1
  154. package/dist/scheduler/scheduler.d.ts +70 -1
  155. package/dist/scheduler/scheduler.js +256 -17
  156. package/dist/scheduler/scheduler.js.map +1 -1
  157. package/dist/service-installer.d.ts +2 -1
  158. package/dist/service-installer.js +18 -3
  159. package/dist/service-installer.js.map +1 -1
  160. package/dist/settings-api.d.ts +19 -0
  161. package/dist/settings-api.js +210 -2
  162. package/dist/settings-api.js.map +1 -1
  163. package/dist/settings-change.js +112 -0
  164. package/dist/settings-change.js.map +1 -1
  165. package/dist/settings-effect.js +32 -2
  166. package/dist/settings-effect.js.map +1 -1
  167. package/dist/settings-http-confirmation.js +5 -0
  168. package/dist/settings-http-confirmation.js.map +1 -1
  169. package/dist/tmux-manager.d.ts +5 -0
  170. package/dist/tmux-manager.js +16 -0
  171. package/dist/tmux-manager.js.map +1 -1
  172. package/dist/tool-permissions.d.ts +4 -6
  173. package/dist/tool-permissions.js +100 -2
  174. package/dist/tool-permissions.js.map +1 -1
  175. package/dist/topic-commands.d.ts +17 -7
  176. package/dist/topic-commands.js +39 -21
  177. package/dist/topic-commands.js.map +1 -1
  178. package/dist/topic-link.d.ts +128 -0
  179. package/dist/topic-link.js +269 -0
  180. package/dist/topic-link.js.map +1 -0
  181. package/dist/types.d.ts +61 -1
  182. package/dist/ui/app-effects.js +233 -0
  183. package/dist/ui/chat-ambient.js +307 -0
  184. package/dist/ui/chat-effect-posters.js +42 -0
  185. package/dist/ui/chat-effects.js +160 -0
  186. package/dist/ui/chat-live-replies.js +58 -0
  187. package/dist/ui/chat-render.js +9 -6
  188. package/dist/ui/chat-store.js +9 -3
  189. package/dist/ui/chat-strings.js +70 -2
  190. package/dist/ui/chat-thread.js +191 -37
  191. package/dist/ui/chat-tour.js +1 -1
  192. package/dist/ui/effect-trial-controller.js +124 -0
  193. package/dist/ui/effect-trial-offer.js +31 -0
  194. package/dist/ui/effect-trial-panel.js +40 -0
  195. package/dist/ui/fleet-cache.js +23 -1
  196. package/dist/ui/fleet-canvas-layout.js +111 -0
  197. package/dist/ui/fleet-canvas.js +525 -0
  198. package/dist/ui/fleet-org.js +27 -12
  199. package/dist/ui/fleet-traffic.js +343 -0
  200. package/dist/ui/fleet-usage.js +401 -0
  201. package/dist/ui/image-lightbox.js +1 -1
  202. package/dist/ui/panel-chat.js +203 -57
  203. package/dist/ui/panel-details.js +20 -6
  204. package/dist/ui/panel-fleet.js +43 -18
  205. package/dist/ui/panel-log.js +6 -6
  206. package/dist/ui/panel-needs.js +3 -3
  207. package/dist/ui/panel-settings.js +34 -13
  208. package/dist/ui/preview-panel.js +25 -9
  209. package/dist/ui/preview.js +3 -2
  210. package/dist/ui/settings-appearance.js +349 -0
  211. package/dist/ui/settings-catalog.js +92 -0
  212. package/dist/ui/settings-dialogs.js +40 -12
  213. package/dist/ui/settings-effects.js +202 -0
  214. package/dist/ui/settings-guests.js +117 -0
  215. package/dist/ui/settings-learn.js +25 -0
  216. package/dist/ui/settings-model.js +15 -6
  217. package/dist/ui/settings-mods.js +271 -0
  218. package/dist/ui/settings-owner-effects.js +80 -0
  219. package/dist/ui/settings-strings.js +333 -10
  220. package/dist/ui/settings-web-authority.js +91 -0
  221. package/dist/ui/shared/agend-auth.js +28 -4
  222. package/dist/ui/shared/agent-widget.js +248 -0
  223. package/dist/ui/shared/app-i18n.js +6 -2
  224. package/dist/ui/shared/app-prefs.js +34 -13
  225. package/dist/ui/shared/app-route.js +24 -2
  226. package/dist/ui/shared/app-session.js +3 -1
  227. package/dist/ui/shared/app-shell.js +102 -23
  228. package/dist/ui/shared/app-store.js +28 -1
  229. package/dist/ui/shared/app-stream.js +14 -7
  230. package/dist/ui/shared/app-themes.js +206 -0
  231. package/dist/ui/shared/app.css +565 -4
  232. package/dist/ui/shared/app.js +23 -10
  233. package/dist/ui/shared/card-strings.js +32 -0
  234. package/dist/ui/shared/effect-app-runtime.js +297 -0
  235. package/dist/ui/shared/effect-baseline.js +64 -0
  236. package/dist/ui/shared/effect-chat-runtime.js +167 -0
  237. package/dist/ui/shared/effect-cursor.js +125 -0
  238. package/dist/ui/shared/effect-frame.js +146 -0
  239. package/dist/ui/shared/effect-geometry.js +137 -0
  240. package/dist/ui/shared/effect-host-style.js +10 -0
  241. package/dist/ui/shared/effect-owner-regions.js +74 -0
  242. package/dist/ui/shared/effect-owner-source.js +115 -0
  243. package/dist/ui/shared/effect-policy.js +151 -0
  244. package/dist/ui/shared/effect-poster-reader.js +98 -0
  245. package/dist/ui/shared/effect-posters.js +117 -0
  246. package/dist/ui/shared/effect-profiler.js +184 -0
  247. package/dist/ui/shared/effect-profiles.js +65 -0
  248. package/dist/ui/shared/effect-registry.js +535 -0
  249. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  250. package/dist/ui/shared/effect-safety.js +153 -0
  251. package/dist/ui/shared/effect-stock-snapshot.js +43 -0
  252. package/dist/ui/shared/effect-stock-trust.js +217 -0
  253. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  254. package/dist/ui/shared/effect-trial-source.js +103 -0
  255. package/dist/ui/shared/effect-trial-stage.js +170 -0
  256. package/dist/ui/shared/effect-trial.css +40 -0
  257. package/dist/ui/shared/fx-motion.js +67 -0
  258. package/dist/ui/shared/header-tools.js +1 -1
  259. package/dist/ui/shared/identity-card.js +273 -0
  260. package/dist/ui/shared/identity-color.js +261 -0
  261. package/dist/ui/shared/instance-nav.js +215 -41
  262. package/dist/ui/shared/owner-css.js +221 -0
  263. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  264. package/dist/ui/shared/panel-view.js +103 -10
  265. package/dist/ui/shared/signin.js +2 -0
  266. package/dist/ui/shared/tokens.css +9 -3
  267. package/dist/ui/shared/ui-dialog.js +1 -1
  268. package/dist/ui/shared/ui-icons.js +3 -0
  269. package/dist/ui/shared/ui-menu.js +1 -1
  270. package/dist/ui/shared/ui-toast.js +1 -1
  271. package/dist/ui/shared/usage-dialog.js +22 -2
  272. package/dist/ui/shared/view-strings.js +98 -2
  273. package/dist/ui/shared/web-authorize.js +151 -0
  274. package/dist/ui/signin.html +1 -0
  275. package/dist/ui/web-terminal/terminal.css +4 -4
  276. package/dist/usage/providers.d.ts +28 -0
  277. package/dist/usage/providers.js +126 -36
  278. package/dist/usage/providers.js.map +1 -1
  279. package/dist/usage-report.d.ts +158 -0
  280. package/dist/usage-report.js +226 -0
  281. package/dist/usage-report.js.map +1 -0
  282. package/dist/usage-rollup.d.ts +51 -0
  283. package/dist/usage-rollup.js +168 -0
  284. package/dist/usage-rollup.js.map +1 -0
  285. package/dist/view-api.d.ts +68 -0
  286. package/dist/view-api.js +249 -8
  287. package/dist/view-api.js.map +1 -1
  288. package/dist/web-ambient.d.ts +72 -0
  289. package/dist/web-ambient.js +161 -0
  290. package/dist/web-ambient.js.map +1 -0
  291. package/dist/web-api.d.ts +111 -5
  292. package/dist/web-api.js +1235 -58
  293. package/dist/web-api.js.map +1 -1
  294. package/dist/web-appearance-limits.d.ts +82 -0
  295. package/dist/web-appearance-limits.js +83 -0
  296. package/dist/web-appearance-limits.js.map +1 -0
  297. package/dist/web-appearance-policy.d.ts +64 -0
  298. package/dist/web-appearance-policy.js +87 -0
  299. package/dist/web-appearance-policy.js.map +1 -0
  300. package/dist/web-appearance-tools.d.ts +53 -0
  301. package/dist/web-appearance-tools.js +124 -0
  302. package/dist/web-appearance-tools.js.map +1 -0
  303. package/dist/web-appearance-view.d.ts +124 -0
  304. package/dist/web-appearance-view.js +278 -0
  305. package/dist/web-appearance-view.js.map +1 -0
  306. package/dist/web-appearance.d.ts +93 -0
  307. package/dist/web-appearance.js +373 -0
  308. package/dist/web-appearance.js.map +1 -0
  309. package/dist/web-asset-parts.d.ts +93 -0
  310. package/dist/web-asset-parts.js +133 -0
  311. package/dist/web-asset-parts.js.map +1 -0
  312. package/dist/web-auth.js +20 -2
  313. package/dist/web-auth.js.map +1 -1
  314. package/dist/web-card.d.ts +110 -0
  315. package/dist/web-card.js +266 -0
  316. package/dist/web-card.js.map +1 -0
  317. package/dist/web-channel-echo.d.ts +7 -1
  318. package/dist/web-channel-echo.js +6 -2
  319. package/dist/web-channel-echo.js.map +1 -1
  320. package/dist/web-chat-history.d.ts +59 -0
  321. package/dist/web-chat-history.js +102 -4
  322. package/dist/web-chat-history.js.map +1 -1
  323. package/dist/web-commands.d.ts +1 -0
  324. package/dist/web-effect-bootstrap.d.ts +12 -0
  325. package/dist/web-effect-bootstrap.js +49 -0
  326. package/dist/web-effect-bootstrap.js.map +1 -0
  327. package/dist/web-effect-bundle.d.ts +28 -0
  328. package/dist/web-effect-bundle.js +171 -0
  329. package/dist/web-effect-bundle.js.map +1 -0
  330. package/dist/web-effect-canonical.d.ts +8 -0
  331. package/dist/web-effect-canonical.js +27 -0
  332. package/dist/web-effect-canonical.js.map +1 -0
  333. package/dist/web-effect-library.d.ts +114 -0
  334. package/dist/web-effect-library.js +530 -0
  335. package/dist/web-effect-library.js.map +1 -0
  336. package/dist/web-effect-playback.d.ts +30 -0
  337. package/dist/web-effect-playback.js +71 -0
  338. package/dist/web-effect-playback.js.map +1 -0
  339. package/dist/web-effect-poster.d.ts +8 -0
  340. package/dist/web-effect-poster.js +52 -0
  341. package/dist/web-effect-poster.js.map +1 -0
  342. package/dist/web-effect-profiles.d.ts +118 -0
  343. package/dist/web-effect-profiles.js +180 -0
  344. package/dist/web-effect-profiles.js.map +1 -0
  345. package/dist/web-effect-service.d.ts +93 -0
  346. package/dist/web-effect-service.js +335 -0
  347. package/dist/web-effect-service.js.map +1 -0
  348. package/dist/web-effect-tools.d.ts +48 -0
  349. package/dist/web-effect-tools.js +129 -0
  350. package/dist/web-effect-tools.js.map +1 -0
  351. package/dist/web-effect-trials.d.ts +86 -0
  352. package/dist/web-effect-trials.js +204 -0
  353. package/dist/web-effect-trials.js.map +1 -0
  354. package/dist/web-effect-upload.d.ts +27 -0
  355. package/dist/web-effect-upload.js +185 -0
  356. package/dist/web-effect-upload.js.map +1 -0
  357. package/dist/web-effect-worker.d.ts +8 -0
  358. package/dist/web-effect-worker.js +170 -0
  359. package/dist/web-effect-worker.js.map +1 -0
  360. package/dist/web-effect.d.ts +103 -0
  361. package/dist/web-effect.js +153 -0
  362. package/dist/web-effect.js.map +1 -0
  363. package/dist/web-guest-access.d.ts +125 -0
  364. package/dist/web-guest-access.js +234 -0
  365. package/dist/web-guest-access.js.map +1 -0
  366. package/dist/web-guest-rooms.d.ts +82 -0
  367. package/dist/web-guest-rooms.js +135 -0
  368. package/dist/web-guest-rooms.js.map +1 -0
  369. package/dist/web-identity-color.d.ts +87 -0
  370. package/dist/web-identity-color.js +209 -0
  371. package/dist/web-identity-color.js.map +1 -0
  372. package/dist/web-identity-palette.d.ts +113 -0
  373. package/dist/web-identity-palette.js +84 -0
  374. package/dist/web-identity-palette.js.map +1 -0
  375. package/dist/web-image-meta.d.ts +25 -0
  376. package/dist/web-image-meta.js +200 -0
  377. package/dist/web-image-meta.js.map +1 -0
  378. package/dist/web-login.d.ts +47 -0
  379. package/dist/web-login.js +115 -0
  380. package/dist/web-login.js.map +1 -1
  381. package/dist/web-mod-agent.d.ts +50 -0
  382. package/dist/web-mod-agent.js +207 -0
  383. package/dist/web-mod-agent.js.map +1 -0
  384. package/dist/web-mod-bundle.d.ts +157 -0
  385. package/dist/web-mod-bundle.js +529 -0
  386. package/dist/web-mod-bundle.js.map +1 -0
  387. package/dist/web-mod-export.d.ts +68 -0
  388. package/dist/web-mod-export.js +100 -0
  389. package/dist/web-mod-export.js.map +1 -0
  390. package/dist/web-mod-store.d.ts +289 -0
  391. package/dist/web-mod-store.js +638 -0
  392. package/dist/web-mod-store.js.map +1 -0
  393. package/dist/web-org.d.ts +3 -3
  394. package/dist/web-org.js +11 -17
  395. package/dist/web-org.js.map +1 -1
  396. package/dist/web-owner-css-store.d.ts +180 -0
  397. package/dist/web-owner-css-store.js +384 -0
  398. package/dist/web-owner-css-store.js.map +1 -0
  399. package/dist/web-owner-css.d.ts +58 -0
  400. package/dist/web-owner-css.js +552 -0
  401. package/dist/web-owner-css.js.map +1 -0
  402. package/dist/web-prefs.d.ts +62 -0
  403. package/dist/web-prefs.js +232 -36
  404. package/dist/web-prefs.js.map +1 -1
  405. package/dist/web-preview.d.ts +15 -3
  406. package/dist/web-preview.js +28 -9
  407. package/dist/web-preview.js.map +1 -1
  408. package/dist/web-returning-device.d.ts +41 -0
  409. package/dist/web-returning-device.js +57 -5
  410. package/dist/web-returning-device.js.map +1 -1
  411. package/dist/web-session-authorization.d.ts +132 -0
  412. package/dist/web-session-authorization.js +193 -0
  413. package/dist/web-session-authorization.js.map +1 -0
  414. package/dist/web-session.d.ts +70 -10
  415. package/dist/web-session.js +137 -22
  416. package/dist/web-session.js.map +1 -1
  417. package/dist/web-settings-patch.d.ts +67 -2
  418. package/dist/web-settings-patch.js +196 -1
  419. package/dist/web-settings-patch.js.map +1 -1
  420. package/dist/web-settings-tools.d.ts +89 -0
  421. package/dist/web-settings-tools.js +270 -0
  422. package/dist/web-settings-tools.js.map +1 -0
  423. package/dist/web-shell-routes.d.ts +2 -2
  424. package/dist/web-shell-routes.js +2 -2
  425. package/dist/web-shell-routes.js.map +1 -1
  426. package/dist/web-theme-store.d.ts +257 -0
  427. package/dist/web-theme-store.js +627 -0
  428. package/dist/web-theme-store.js.map +1 -0
  429. package/dist/web-theme.d.ts +123 -0
  430. package/dist/web-theme.js +416 -0
  431. package/dist/web-theme.js.map +1 -0
  432. package/dist/web-widget.d.ts +72 -0
  433. package/dist/web-widget.js +245 -0
  434. package/dist/web-widget.js.map +1 -0
  435. package/dist/workflow-templates/default.md +1 -1
  436. package/package.json +1 -1
  437. package/templates/systemd.service.ejs +8 -0
@@ -0,0 +1,221 @@
1
+ // #1629 r4 §3: owner CSS on this page — the signed-in person's own stylesheet, linked last in <head> so it wins over
2
+ // app.css. It is a same-origin <link> (style-src 'self' admits it): no inline CSS, no nonce, no style attribute and no
3
+ // constructed stylesheet. Never linked:
4
+ // - on Settings (removed while it is open, put back on leaving: the way out is always rendered stock);
5
+ // - on the public link, for the anonymous View reader, and wherever the server named no version (data-owner-css);
6
+ // - on a device that turned it off (agend_owner_css_off), and for a load with ?safe=1 (safe mode).
7
+ // Sign-in, resend-code and setup are other pages: they never load this module.
8
+ import { navStore } from "./app-nav.js";
9
+ import { appStore, createStore, useStore } from "./app-store.js";
10
+ import { html, useEffect, useState } from "./app-html.js";
11
+ import { t } from "./app-i18n.js";
12
+
13
+ export const OWNER_CSS_OFF_KEY = "agend_owner_css_off";
14
+ const LINK_ID = "owner-css";
15
+
16
+ // Read when this module loads, so it holds for whatever asks first (themes start before installOwnerCss runs).
17
+ const readSafe = () => { try { return new URLSearchParams(location.search).get("safe") === "1"; } catch { return false; } };
18
+ let safe = readSafe();
19
+ let active = null;
20
+ let installed = false;
21
+ // B2: admission uses positive observed lifecycle, never active===null as proof of absence.
22
+ const lifeStore = createStore(Object.freeze({ state: "unknown", generation: 0, reason: "unverified" }));
23
+ export const ownerCssLifecycle = Object.freeze({ get: lifeStore.get, subscribe: lifeStore.subscribe });
24
+ let lifeGeneration = 0, linkGeneration = 0, ownedLink = null, linkState = "unknown", sourceState = "unknown";
25
+ function life(state, reason) { lifeStore.set(() => Object.freeze({ state, generation: ++lifeGeneration, reason })); }
26
+
27
+
28
+ /** This load is in safe mode (?safe=1): no owner CSS — and, as they come, no widgets or effects and default themes. */
29
+ export function safeMode() { return safe; }
30
+
31
+ /** This device turned owner CSS off (a per-device switch: not synced, so a broken phone layout is fixed on the phone). */
32
+ export function ownerCssOffHere() {
33
+ try { return localStorage.getItem(OWNER_CSS_OFF_KEY) === "1"; } catch { return false; }
34
+ }
35
+ export function setOwnerCssOffHere(off) {
36
+ try { if (off) localStorage.setItem(OWNER_CSS_OFF_KEY, "1"); else localStorage.removeItem(OWNER_CSS_OFF_KEY); } catch { /* this page only */ }
37
+ sync();
38
+ }
39
+
40
+ /** The person's version in force, as this page knows it (null: none). */
41
+ export function ownerCssActive() { return active; }
42
+
43
+ function eligiblePage() {
44
+ const b = document.body && document.body.dataset;
45
+ return !!b && b.mode === "full" && b.publicLink !== "1";
46
+ }
47
+ function wanted() {
48
+ if (!eligiblePage() || safe || !active || ownerCssOffHere()) return false;
49
+ const r = navStore.get().route;
50
+ return !(r && r.panel === "settings");
51
+ }
52
+
53
+ /** Revoke admission before a CSS/route mutation; a missing element alone is never an off verdict. */
54
+ function sync() {
55
+ life("unknown", "changing");
56
+ const el = document.getElementById(LINK_ID);
57
+ if (!wanted()) {
58
+ ++linkGeneration;
59
+ if (el) el.remove();
60
+ if (ownedLink && ownedLink !== el) ownedLink.remove();
61
+ ownedLink = null; linkState = "unknown";
62
+ const absent = document.querySelectorAll(`#${LINK_ID}`).length === 0;
63
+ const route = navStore.get().route;
64
+ const disabled = !eligiblePage() || safe || ownerCssOffHere() || route?.panel === "settings";
65
+ life(absent && (disabled || sourceState === "off") ? "off" : "unknown", absent ? disabled ? "disabled_here" : sourceState === "off" ? "host_off" : "unverified" : "removal_failed");
66
+ return;
67
+ }
68
+ const href = `/ui/owner-css.css?v=${encodeURIComponent(active)}`;
69
+ if (el && el !== ownedLink) { life("unknown", "unowned_link"); return; }
70
+ if (ownedLink && el === ownedLink && el.getAttribute("href") === href) { life(linkState, linkState === "on" ? "loaded" : linkState === "loading" ? "loading" : "load_failed"); return; }
71
+ // A new owned element fences late events from the previous version, including href changes.
72
+ const epoch = ++linkGeneration, sha = active;
73
+ if (ownedLink) ownedLink.remove();
74
+ const link = document.createElement("link"); ownedLink = link; linkState = "loading";
75
+ link.setAttribute("rel", "stylesheet"); link.setAttribute("id", LINK_ID); link.setAttribute("href", href);
76
+ const current = () => ownedLink === link && epoch === linkGeneration && active === sha && wanted()
77
+ && link.isConnected === true && document.getElementById(LINK_ID) === link && link.getAttribute("href") === href;
78
+ link.addEventListener("load", () => {
79
+ if (!current()) return;
80
+ let valid = false;
81
+ try { valid = link.disabled !== true && !!link.sheet && link.sheet.ownerNode === link && link.sheet.disabled !== true && link.sheet.href === new URL(href, location.href).href; } catch { /* unknown stylesheet */ }
82
+ linkState = valid ? "on" : "unknown"; life(linkState, valid ? "loaded" : "load_unverified");
83
+ });
84
+ link.addEventListener("error", () => { if (!current()) return; linkState = "unknown"; life("unknown", "load_failed"); });
85
+ life("loading", "loading");
86
+ document.head.appendChild(link); // last in head; no style text, inline attribute or constructed stylesheet
87
+ }
88
+
89
+ // #1883 r2: every change this page makes, and every read it starts, moves `gen`. A host answer is taken only if nothing
90
+ // newer happened after it was asked for — an older /state answer never overwrites a newer read, save, keep or revert.
91
+ let gen = 0;
92
+ const local = () => ++gen;
93
+
94
+ /** This page saved, restored or turned off a version: show it at once (null: none). */
95
+ export function setOwnerCssActive(sha) {
96
+ local();
97
+ showActive(sha, sha === null ? "off" : "on");
98
+ }
99
+ function showActive(sha, evidence = "unknown") {
100
+ active = typeof sha === "string" && /^[0-9a-f]{32}$/.test(sha) ? sha : null;
101
+ sourceState = evidence === "off" && sha === null ? "off" : evidence === "on" && active ? "on" : "unknown";
102
+ sync();
103
+ }
104
+
105
+ // ── Keep or revert (r4 §3): a saved version is on trial. "Keep" within 15 s keeps it; anything else reverts it — the
106
+ // page that saved it at 15 s, and the host at its own deadline (a closed tab, or CSS that hid this bar). The bar is the
107
+ // shell's, on every page and every device of the person's, so they can look at the result before answering.
108
+ // A trial has an id of its own (the same text saved twice is two trials): keep and revert name it, and the host acts
109
+ // only while that trial still is the one on trial — one kept, reverted or replaced meanwhile, anywhere, stays.
110
+ export const KEEP_SECONDS = 15;
111
+ /** The host's whole trial budget (20 s) and a second: the longest a page that did not save a trial waits to ask again. */
112
+ const HOST_TRIAL_SECONDS = 21;
113
+ /** { id, sha, previous, deadline (performance.now ms), local } while a version is on trial, else null. */
114
+ export const ownerCssTrial = createStore(null);
115
+ let trialTimer = null;
116
+ // set(fn) replaces the state; set(object) would merge into it (createStore), and null merges as nothing.
117
+ const dropTrial = () => { if (trialTimer) clearTimeout(trialTimer); trialTimer = null; ownerCssTrial.set(() => null); };
118
+
119
+ async function post(path, body) {
120
+ const res = await fetch(path, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
121
+ let json = null;
122
+ try { json = await res.json(); } catch { /* none */ }
123
+ return { ok: res.ok, status: res.status, body: json };
124
+ }
125
+
126
+ /**
127
+ * Trial `id` (of version `sha`) is in force: show it, and ask. `local`: this page saved it — at the end of `seconds` it
128
+ * reverts it (by its id). Otherwise (another device's trial, or this page loaded during one) `seconds` is the host's
129
+ * own remaining time (never the device's clock), and at its end the page asks the host again, which has settled it.
130
+ */
131
+ export function startOwnerCssTrial(id, sha, previous, seconds = KEEP_SECONDS, isLocal = true) {
132
+ local();
133
+ if (trialTimer) clearTimeout(trialTimer);
134
+ showActive(sha, "on");
135
+ const s = Math.max(0, Math.min(isLocal ? KEEP_SECONDS : HOST_TRIAL_SECONDS, Number.isFinite(seconds) ? seconds : 0));
136
+ const trial = { id, sha, previous: previous || null, deadline: performance.now() + s * 1000, local: isLocal };
137
+ ownerCssTrial.set(() => trial);
138
+ trialTimer = setTimeout(() => {
139
+ if (ownerCssTrial.get() !== trial) return;
140
+ if (isLocal) revertOwnerCss(); else { dropTrial(); reread(); }
141
+ }, s * 1000);
142
+ }
143
+ export async function keepOwnerCss() {
144
+ const trial = ownerCssTrial.get();
145
+ if (!trial) return false;
146
+ local();
147
+ dropTrial();
148
+ const r = await post("/ui/owner-css/keep", { trial: trial.id });
149
+ if (!r.ok) { await reread(); return false; } // too late: the host reverted it — show what is in force
150
+ return true;
151
+ }
152
+ export async function revertOwnerCss() {
153
+ const trial = ownerCssTrial.get();
154
+ if (!trial) return;
155
+ local();
156
+ dropTrial();
157
+ showActive(trial.previous); // at once; the host's answer below is what stays
158
+ await post("/ui/owner-css/revert", { trial: trial.id }).catch(() => null);
159
+ await reread();
160
+ }
161
+
162
+ /** The shell's bar: "Keep this CSS?", the seconds left, Keep and Revert. */
163
+ export function OwnerCssKeepBar() {
164
+ const trial = useStore(ownerCssTrial);
165
+ const [, tick] = useState(0);
166
+ useEffect(() => {
167
+ if (!trial) return undefined;
168
+ const id = setInterval(() => tick((n) => n + 1), 500);
169
+ return () => clearInterval(id);
170
+ }, [trial]);
171
+ if (!trial) return null;
172
+ const left = Math.max(0, Math.ceil((trial.deadline - performance.now()) / 1000));
173
+ return html`<div class="oc-keep" role="alertdialog" aria-live="assertive" aria-label=${t("app.ownerCssKeep")} data-trust="keep">
174
+ <span class="oc-keep-text"><span class="oc-keep-q">${t("app.ownerCssKeep")}</span><span class="oc-keep-left">${t("app.ownerCssKeepLeft", left)}</span></span>
175
+ <button type="button" class="btn btn-primary btn-sm" onClick=${() => { keepOwnerCss(); }}>${t("app.ownerCssKeepYes")}</button>
176
+ <button type="button" class="btn btn-sm" onClick=${() => { revertOwnerCss(); }}>${t("app.ownerCssKeepNo")}</button>
177
+ </div>`;
178
+ }
179
+
180
+ /** Ask the host which version is in force (another device changed it, or a trial ended), and whether one is on trial. */
181
+ async function reread() {
182
+ if (!eligiblePage()) return;
183
+ const mine = local();
184
+ sourceState = "unknown"; life("unknown", "refreshing");
185
+ try {
186
+ const res = await fetch("/ui/owner-css/state", { credentials: "same-origin" });
187
+ if (!res.ok) return;
188
+ const body = await res.json();
189
+ if (body && mine === gen) reconcile(body); // nothing newer happened since this was asked
190
+ } catch { /* the next change tries again */ }
191
+ }
192
+ /** Match the host: the version in force, and whether one is on trial (another device's included) — or none any more. */
193
+ function reconcile(state) {
194
+ const pending = state.pending && state.pending.sha === state.active && typeof state.pending.id === "string" ? state.pending : null;
195
+ const trial = ownerCssTrial.get();
196
+ if (!pending) { if (trial) dropTrial(); showActive(state.active, state.effectState); return; }
197
+ if (trial && trial.id === pending.id) return; // already asking about this one
198
+ // The host's own remaining time and a second (never this device's clock): it has settled the trial by then.
199
+ const remaining = typeof pending.remaining_ms === "number" && Number.isFinite(pending.remaining_ms) ? pending.remaining_ms : 0;
200
+ startOwnerCssTrial(pending.id, pending.sha, pending.previous, remaining / 1000 + 1, false);
201
+ }
202
+
203
+ /** Once per page load, before the first panel paints. */
204
+ export function installOwnerCss() {
205
+ if (installed) return;
206
+ installed = true;
207
+ safe = readSafe();
208
+ const b = document.body && document.body.dataset;
209
+ showActive(b?.ownerCss ?? null, b?.ownerCssState);
210
+ navStore.subscribe(sync);
211
+ // What the host says now: a trial in progress (this page reloaded during one, or another device's) shows the bar;
212
+ // one the host has settled since the page was served shows its outcome.
213
+ if (eligiblePage()) reread();
214
+ let rev = appStore.get().prefsRev;
215
+ appStore.subscribe((s) => {
216
+ if (s.prefsRev === rev) return;
217
+ const first = rev === null || rev === undefined;
218
+ rev = s.prefsRev;
219
+ if (!first) reread();
220
+ });
221
+ }
@@ -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
+ }
@@ -16,7 +16,7 @@ import { HeaderTools, VIEW_SCALE, textSizeStore } from "./header-tools.js";
16
16
  import { readStream } from "./read-stream.js";
17
17
  // The usage dialog's metric row lives with the dialog now (usage-dialog.js); re-exported for what imported it here.
18
18
  export { UsageMetric } from "./usage-dialog.js";
19
- import { backendLabel, STATUS_KEYS } from "./instance-nav.js";
19
+ import { backendLabel, modelText, STATUS_KEYS } from "./instance-nav.js";
20
20
  import { viewStore } from "./view-roster-store.js";
21
21
 
22
22
  /** #1580: the app's stream says the fleet is not answering (a restart): the View's polls skip their turn until it is. */
@@ -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 { BannerFocusPreview, checkCard, fitPicture, IdentityCard, useBannerPreview, 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,11 +299,13 @@ 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>
303
- <div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${it.model}` : ""}${ctx}</div>
308
+ <div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${modelText(it)}` : ""}${ctx}</div>
304
309
  ${open && it.description ? html`<p class="v-card-desc">${it.description}</p>` : null}
305
310
  </div>
306
311
  <button type="button" class="icon-btn" onClick=${toggle} aria-expanded=${open ? "true" : "false"} aria-label=${tn(open ? "collapseProfile" : "expandProfile")} title=${tn(open ? "collapseProfile" : "expandProfile")}>
@@ -320,6 +325,38 @@ 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
+ // #2086: the banner's focus — this person's (null: follow the agent's, else the centre), and the picture to preview:
336
+ // a newly chosen file, else the banner drawn now.
337
+ // `agent`: the agent's own focus (what "Follow the agent's" shows; else the centre).
338
+ const [focus, setFocus] = useState({ value: 50, mine: null, start: null, agent: null });
339
+ const [bannerPreview, preview] = useBannerPreview();
340
+ useEffect(() => {
341
+ let live = true;
342
+ // The card's fields stay disabled until this read is in (#1889 review: a draft typed meanwhile was overwritten);
343
+ // a failed read lets them be edited, starting empty.
344
+ const none = { mine: { tagline: null, bio: null, portrait: null, banner: null }, limits: null };
345
+ lease.fetch(`/api/card/${encodeURIComponent(it.instance_name)}`).then((r) => (r.ok ? r.json() : null)).then((body) => {
346
+ if (!live || !lease.current()) return;
347
+ if (!body) { setCard(none); return; }
348
+ const resolved = checkCard(it.instance_name, body);
349
+ const mine = (k) => (resolved && resolved[k] && resolved[k].by === "human" ? resolved[k] : null);
350
+ setCard({ mine: { tagline: mine("tagline"), bio: mine("bio"), portrait: mine("portrait"), banner: mine("banner") }, limits: body.limits || null });
351
+ const bf = resolved && resolved.banner_focus;
352
+ const agent = Number.isInteger(body.banner_focus_agent) && body.banner_focus_agent >= 0 && body.banner_focus_agent <= 100 ? body.banner_focus_agent : null;
353
+ const mineFocus = bf && bf.by === "human" ? bf.value : null;
354
+ setFocus({ value: mineFocus ?? agent ?? 50, mine: mineFocus, start: mineFocus, agent });
355
+ if (resolved && resolved.banner) preview.fromSrc(resolved.banner.src);
356
+ setCardText({ tagline: mine("tagline") ? mine("tagline").text : "", bio: mine("bio") ? mine("bio").text : "" });
357
+ }).catch(() => { if (live && lease.current()) setCard(none); });
358
+ return () => { live = false; };
359
+ }, []);
323
360
  async function save() {
324
361
  if (busy) return;
325
362
  setBusy(true); setMsg(null);
@@ -329,7 +366,28 @@ function EditProfile({ it, onClose }) {
329
366
  const draft = { display_name: f.display, role: f.role, description: f.desc };
330
367
  const picked = file.current && file.current.files && file.current.files[0];
331
368
  const removing = removePic;
369
+ // The card: Clear card wins over everything else in it; otherwise only the fields that changed are sent.
370
+ const clearing = clearCard;
371
+ const before = card && card.mine ? { tagline: card.mine.tagline ? card.mine.tagline.text : "", bio: card.mine.bio ? card.mine.bio.text : "" } : { tagline: "", bio: "" };
372
+ if (!clearing) for (const k of ["tagline", "bio"]) if (cardText[k] !== before[k]) draft[`card_${k}`] = cardText[k];
373
+ if (!clearing && focus.mine !== focus.start) draft.card_banner_focus = focus.mine; // #2086
374
+ const cardPics = {};
332
375
  try {
376
+ // A picture too large is fitted first (after asking) — before anything is written, so declining writes nothing.
377
+ if (!clearing) for (const slot of ["portrait", "banner"]) {
378
+ const chosen = pics[slot].current && pics[slot].current.files && pics[slot].current.files[0];
379
+ if (chosen) {
380
+ const limit = card && card.limits && card.limits[slot];
381
+ let blob = chosen;
382
+ if (limit) {
383
+ try { blob = await fitPicture(chosen, limit, (w, h, nw, nh) => confirmDialog({ message: tn("resizeAsk", { w, h, nw, nh }), confirmLabel: tn("resizeGo") })); }
384
+ catch { throw new Error(tn("pictureTooLarge")); }
385
+ }
386
+ if (!lease.current()) return;
387
+ if (!blob) { setBusy(false); return; }
388
+ cardPics[slot] = blob;
389
+ } else if (removeCard[slot]) cardPics[slot] = "remove";
390
+ }
333
391
  const r = await fetch(`/api/profile/${name}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(draft) });
334
392
  if (!lease.current()) return;
335
393
  if (r.status === 401) throw new Error(tn("signInToSave"));
@@ -342,6 +400,18 @@ function EditProfile({ it, onClose }) {
342
400
  if (!dr.ok) { const e = await dr.json().catch(() => ({})); throw new Error(tn("avatarRemoveFailed", { error: e.error || dr.status })); }
343
401
  }
344
402
  if (!lease.current()) return;
403
+ if (clearing) {
404
+ const cr = await fetch(`/api/card/${name}`, { method: "DELETE" });
405
+ if (!cr.ok) { const e = await cr.json().catch(() => ({})); throw new Error(tn("cardFailed", { error: e.error || cr.status })); }
406
+ }
407
+ for (const [slot, what] of Object.entries(cardPics)) {
408
+ if (!lease.current()) return;
409
+ const pr = what === "remove"
410
+ ? await fetch(`/api/card/${name}/${slot}`, { method: "DELETE" })
411
+ : await fetch(`/api/card/${name}/${slot}`, { method: "POST", headers: { "Content-Type": what.type || "application/octet-stream" }, body: what });
412
+ if (!pr.ok) { const e = await pr.json().catch(() => ({})); throw new Error(tn("cardFailed", { error: e.detail || e.error || pr.status })); }
413
+ }
414
+ if (!lease.current()) return;
345
415
  setBusy(false);
346
416
  onClose();
347
417
  } catch (e) { if (lease.current()) { setBusy(false); setMsg(e.message); } }
@@ -356,6 +426,29 @@ function EditProfile({ it, onClose }) {
356
426
  <label class="field"><span>${tn("avatar")}</span><input ref=${file} type="file" accept="image/png,image/jpeg,image/gif,image/webp" /></label>
357
427
  ${it.has_avatar ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removePic ? "true" : "false"}
358
428
  onClick=${() => setRemovePic(!removePic)}>${tn(removePic ? "keepPicture" : "removePicture")}</button>${removePic ? html`<span class="muted">${tn("removePictureNote")}</span>` : null}</div>` : null}
429
+ <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>
430
+ <h3 class="form-sub">${tn("cardSection")}</h3>
431
+ <p class="note">${tn("cardLead")}</p>
432
+ <fieldset class="form" disabled=${clearCard || !card} aria-busy=${card ? "false" : "true"}>
433
+ <label class="field"><span>${tn("cardTagline")}</span><input value=${cardText.tagline} onInput=${(e) => setCardText({ ...cardText, tagline: e.target.value })} /></label>
434
+ <label class="field"><span>${tn("cardBio")}</span><textarea rows="4" value=${cardText.bio} onInput=${(e) => setCardText({ ...cardText, bio: e.target.value })}></textarea></label>
435
+ ${["portrait", "banner"].map((slot) => html`<div key=${slot} class="form">
436
+ <label class="field"><span>${tn(slot === "portrait" ? "cardPortrait" : "cardBanner")}</span><input ref=${pics[slot]} type="file" accept="image/png,image/jpeg,image/webp"
437
+ onChange=${slot === "banner" ? (e) => { const fl = e.currentTarget.files && e.currentTarget.files[0]; if (fl) preview.fromFile(fl); } : undefined} /></label>
438
+ ${card && card.mine && card.mine[slot] ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removeCard[slot] ? "true" : "false"}
439
+ onClick=${() => setRemoveCard({ ...removeCard, [slot]: !removeCard[slot] })}>${tn(removeCard[slot] ? "cardKeep" : "cardRemove")}</button></div>` : null}
440
+ </div>`)}
441
+ ${bannerPreview ? html`<div class="field bf-field">
442
+ <span>${tn("cardBannerFocus")}</span>
443
+ <${BannerFocusPreview} src=${bannerPreview.src} focus=${focus.value} />
444
+ <input type="range" min="0" max="100" step="1" value=${focus.value} aria-label=${tn("cardBannerFocus")}
445
+ onInput=${(e) => { const v = Number(e.currentTarget.value); setFocus({ ...focus, value: v, mine: v }); }} />
446
+ <span class="note">${tn("cardBannerFocusNote")}</span>
447
+ ${focus.mine !== null ? html`<div class="field-row"><button type="button" class="btn btn-sm" onClick=${() => setFocus({ ...focus, value: focus.agent ?? 50, mine: null })}>${tn("cardFocusFollow")}</button></div>` : null}
448
+ </div>` : null}
449
+ </fieldset>
450
+ <div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${clearCard ? "true" : "false"} onClick=${() => setClearCard(!clearCard)}>${tn(clearCard ? "keepCard" : "clearCard")}</button>
451
+ ${clearCard ? html`<span class="muted">${tn("clearCardNote")}</span>` : null}</div>
359
452
  </fieldset></form>
360
453
  ${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
454
  </${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: "錯誤次數過多,登入暫停幾分鐘。",
@@ -9,7 +9,12 @@
9
9
  * surfaces it is used on, and focus rings and strong borders meet 3:1 (tests/web-tokens-1408.test.ts).
10
10
  *
11
11
  * Inter is bundled (/assets/inter.woff2, SIL OFL 1.1 — see fonts/README.md): the CSP allows fonts from 'self' only,
12
- * and nothing is loaded from another site. Chinese falls back to the system's CJK fonts, in the order below. */
12
+ * and nothing is loaded from another site. Chinese falls back to the system's CJK fonts, in the order below.
13
+ *
14
+ * Emoji (#1918 follow-up, the user's tofu boxes): every stack ends with the platforms' colour emoji fonts and the
15
+ * `emoji` generic (--font-emoji), so an emoji the text fonts lack is drawn by Windows' Segoe UI Emoji, macOS's Apple
16
+ * Color Emoji or Linux's Noto Color Emoji rather than as a box — for the stock stacks and each theme font
17
+ * (app.css: .th-font-system, .th-font-mono). A browser tries the families in order for each character. */
13
18
 
14
19
  @font-face {
15
20
  font-family: "Inter";
@@ -22,8 +27,9 @@
22
27
 
23
28
  :root {
24
29
  /* ── Type ── */
25
- --font-ui: Inter, "PingFang TC", "PingFang SC", "Noto Sans TC", "Noto Sans SC", "Noto Sans CJK TC", "Noto Sans CJK SC", "Microsoft JhengHei", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
26
- --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
30
+ --font-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji;
31
+ --font-ui: Inter, "PingFang TC", "PingFang SC", "Noto Sans TC", "Noto Sans SC", "Noto Sans CJK TC", "Noto Sans CJK SC", "Microsoft JhengHei", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif, var(--font-emoji);
32
+ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, var(--font-emoji);
27
33
  --fs-xs: 12px; --lh-xs: 16px;
28
34
  --fs-sm: 13px; --lh-sm: 18px;
29
35
  --fs-md: 15px; --lh-md: 24px; /* message text */
@@ -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>
@@ -37,6 +37,7 @@ const P = {
37
37
  type: () => html`<path d="M4 7V5h16v2M9 19h6M12 5v14"/>`,
38
38
  chart: () => html`<path d="M3 20h18M6 16v-4M11 16V8M16 16v-6"/>`,
39
39
  up: () => html`<path d="M12 19V5M5 12l7-7 7 7"/>`,
40
+ sort: () => html`<path d="M7 4v16M3 16l4 4 4-4M17 20V4M13 8l4-4 4 4"/>`, // #2179: the sidebar's sort
40
41
  star: () => html`<path d="M12 2.5l2.94 5.96 6.56.95-4.75 4.63 1.12 6.54L12 17.5l-5.87 3.08 1.12-6.54L2.5 9.41l6.56-.95L12 2.5z"/>`,
41
42
  eye: () => html`<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>`,
42
43
  check: () => html`<path d="M20 6L9 17l-5-5"/>`,
@@ -54,6 +55,8 @@ const P = {
54
55
  panel: () => html`<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M14 4v16"/>`,
55
56
  back: () => html`<path d="M19 12H5M12 19l-7-7 7-7"/>`,
56
57
  forward: () => html`<path d="M5 12h14M12 5l7 7-7 7"/>`,
58
+ // #1629: Settings → Appearance.
59
+ 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
60
  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
61
  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
62
  };
@@ -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
  }