@songsid/agend 2.2.1 → 2.2.5

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 (447) 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 +34 -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-client-retirement.d.ts +5 -0
  61. package/dist/channel/adapters/discord-client-retirement.js +57 -0
  62. package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
  63. package/dist/channel/adapters/discord.d.ts +11 -0
  64. package/dist/channel/adapters/discord.js +66 -6
  65. package/dist/channel/adapters/discord.js.map +1 -1
  66. package/dist/channel/adapters/telegram.d.ts +8 -1
  67. package/dist/channel/adapters/telegram.js +74 -4
  68. package/dist/channel/adapters/telegram.js.map +1 -1
  69. package/dist/channel/mcp-tools.js +48 -2
  70. package/dist/channel/mcp-tools.js.map +1 -1
  71. package/dist/channel/types.d.ts +21 -0
  72. package/dist/classic-channel-manager.d.ts +10 -5
  73. package/dist/classic-channel-manager.js +12 -9
  74. package/dist/classic-channel-manager.js.map +1 -1
  75. package/dist/cli.js +142 -110
  76. package/dist/cli.js.map +1 -1
  77. package/dist/config-validator.js +119 -4
  78. package/dist/config-validator.js.map +1 -1
  79. package/dist/daemon.d.ts +8 -0
  80. package/dist/daemon.js +298 -22
  81. package/dist/daemon.js.map +1 -1
  82. package/dist/decision-list.d.ts +25 -0
  83. package/dist/decision-list.js +30 -0
  84. package/dist/decision-list.js.map +1 -0
  85. package/dist/delivery-outbox.d.ts +10 -0
  86. package/dist/delivery-outbox.js +12 -1
  87. package/dist/delivery-outbox.js.map +1 -1
  88. package/dist/fleet-context.d.ts +15 -0
  89. package/dist/fleet-manager.d.ts +514 -12
  90. package/dist/fleet-manager.js +1885 -163
  91. package/dist/fleet-manager.js.map +1 -1
  92. package/dist/fleet-process-handlers.d.ts +18 -0
  93. package/dist/fleet-process-handlers.js +69 -0
  94. package/dist/fleet-process-handlers.js.map +1 -0
  95. package/dist/fleet-traffic.d.ts +74 -0
  96. package/dist/fleet-traffic.js +196 -0
  97. package/dist/fleet-traffic.js.map +1 -0
  98. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
  99. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
  100. package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
  101. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
  102. package/dist/general-knowledge/skills/web-appearance/SKILL.md +492 -0
  103. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +482 -0
  104. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  105. package/dist/human-delivery-receipts.d.ts +24 -1
  106. package/dist/human-delivery-receipts.js +64 -11
  107. package/dist/human-delivery-receipts.js.map +1 -1
  108. package/dist/instance-avatar.js +3 -0
  109. package/dist/instance-avatar.js.map +1 -1
  110. package/dist/instance-config-impact.js +6 -1
  111. package/dist/instance-config-impact.js.map +1 -1
  112. package/dist/instance-lifecycle.d.ts +2 -0
  113. package/dist/instance-lifecycle.js +20 -10
  114. package/dist/instance-lifecycle.js.map +1 -1
  115. package/dist/instance-names.d.ts +4 -0
  116. package/dist/instance-names.js +12 -0
  117. package/dist/instance-names.js.map +1 -0
  118. package/dist/instructions.d.ts +12 -1
  119. package/dist/instructions.js +13 -2
  120. package/dist/instructions.js.map +1 -1
  121. package/dist/locale.js +100 -8
  122. package/dist/locale.js.map +1 -1
  123. package/dist/ls-attach-resolve.d.ts +95 -0
  124. package/dist/ls-attach-resolve.js +174 -0
  125. package/dist/ls-attach-resolve.js.map +1 -0
  126. package/dist/memory-hogs.d.ts +27 -0
  127. package/dist/memory-hogs.js +124 -0
  128. package/dist/memory-hogs.js.map +1 -0
  129. package/dist/needs-you-hub.d.ts +4 -0
  130. package/dist/needs-you-hub.js +31 -6
  131. package/dist/needs-you-hub.js.map +1 -1
  132. package/dist/needs-you.d.ts +1 -13
  133. package/dist/needs-you.js +3 -37
  134. package/dist/needs-you.js.map +1 -1
  135. package/dist/outbound-handlers.d.ts +15 -0
  136. package/dist/outbound-handlers.js +152 -11
  137. package/dist/outbound-handlers.js.map +1 -1
  138. package/dist/outbound-schemas.d.ts +228 -4
  139. package/dist/outbound-schemas.js +147 -1
  140. package/dist/outbound-schemas.js.map +1 -1
  141. package/dist/public-web-gateway.js +21 -1
  142. package/dist/public-web-gateway.js.map +1 -1
  143. package/dist/public-web-link.d.ts +9 -0
  144. package/dist/public-web-link.js +8 -0
  145. package/dist/public-web-link.js.map +1 -1
  146. package/dist/quickstart-api.d.ts +10 -1
  147. package/dist/quickstart-api.js +12 -4
  148. package/dist/quickstart-api.js.map +1 -1
  149. package/dist/reply-buttons-controller.d.ts +4 -0
  150. package/dist/reply-buttons-controller.js +2 -2
  151. package/dist/reply-buttons-controller.js.map +1 -1
  152. package/dist/repo-worktree-guard.d.ts +36 -0
  153. package/dist/repo-worktree-guard.js +143 -0
  154. package/dist/repo-worktree-guard.js.map +1 -0
  155. package/dist/restart-progress.d.ts +65 -1
  156. package/dist/restart-progress.js +236 -10
  157. package/dist/restart-progress.js.map +1 -1
  158. package/dist/rollout-index.d.ts +5 -0
  159. package/dist/rollout-index.js +26 -5
  160. package/dist/rollout-index.js.map +1 -1
  161. package/dist/scheduler/scheduler.d.ts +70 -1
  162. package/dist/scheduler/scheduler.js +256 -17
  163. package/dist/scheduler/scheduler.js.map +1 -1
  164. package/dist/service-installer.d.ts +2 -1
  165. package/dist/service-installer.js +18 -3
  166. package/dist/service-installer.js.map +1 -1
  167. package/dist/settings-api.d.ts +23 -0
  168. package/dist/settings-api.js +233 -2
  169. package/dist/settings-api.js.map +1 -1
  170. package/dist/settings-change.js +112 -0
  171. package/dist/settings-change.js.map +1 -1
  172. package/dist/settings-effect.js +32 -2
  173. package/dist/settings-effect.js.map +1 -1
  174. package/dist/settings-http-confirmation.js +5 -0
  175. package/dist/settings-http-confirmation.js.map +1 -1
  176. package/dist/tmux-manager.d.ts +5 -0
  177. package/dist/tmux-manager.js +16 -0
  178. package/dist/tmux-manager.js.map +1 -1
  179. package/dist/tool-permissions.d.ts +4 -6
  180. package/dist/tool-permissions.js +100 -2
  181. package/dist/tool-permissions.js.map +1 -1
  182. package/dist/topic-commands.d.ts +17 -7
  183. package/dist/topic-commands.js +39 -21
  184. package/dist/topic-commands.js.map +1 -1
  185. package/dist/topic-link.d.ts +128 -0
  186. package/dist/topic-link.js +269 -0
  187. package/dist/topic-link.js.map +1 -0
  188. package/dist/types.d.ts +61 -1
  189. package/dist/ui/app-effects.js +245 -0
  190. package/dist/ui/chat-ambient.js +307 -0
  191. package/dist/ui/chat-effect-posters.js +42 -0
  192. package/dist/ui/chat-effects.js +160 -0
  193. package/dist/ui/chat-live-replies.js +58 -0
  194. package/dist/ui/chat-render.js +25 -7
  195. package/dist/ui/chat-store.js +41 -13
  196. package/dist/ui/chat-strings.js +86 -2
  197. package/dist/ui/chat-thread.js +204 -37
  198. package/dist/ui/chat-tour.js +1 -1
  199. package/dist/ui/effect-trial-controller.js +124 -0
  200. package/dist/ui/effect-trial-offer.js +31 -0
  201. package/dist/ui/effect-trial-panel.js +40 -0
  202. package/dist/ui/fleet-cache.js +23 -1
  203. package/dist/ui/fleet-canvas-layout.js +111 -0
  204. package/dist/ui/fleet-canvas.js +525 -0
  205. package/dist/ui/fleet-org.js +27 -12
  206. package/dist/ui/fleet-traffic.js +343 -0
  207. package/dist/ui/fleet-usage.js +401 -0
  208. package/dist/ui/image-lightbox.js +1 -1
  209. package/dist/ui/panel-chat.js +252 -58
  210. package/dist/ui/panel-details.js +20 -6
  211. package/dist/ui/panel-fleet.js +45 -19
  212. package/dist/ui/panel-log.js +6 -6
  213. package/dist/ui/panel-needs.js +3 -3
  214. package/dist/ui/panel-settings.js +58 -13
  215. package/dist/ui/preview-panel.js +25 -9
  216. package/dist/ui/preview.js +3 -2
  217. package/dist/ui/settings-appearance.js +349 -0
  218. package/dist/ui/settings-catalog.js +92 -0
  219. package/dist/ui/settings-dialogs.js +40 -12
  220. package/dist/ui/settings-effects.js +202 -0
  221. package/dist/ui/settings-guests.js +117 -0
  222. package/dist/ui/settings-learn.js +25 -0
  223. package/dist/ui/settings-model.js +15 -6
  224. package/dist/ui/settings-mods.js +271 -0
  225. package/dist/ui/settings-owner-effects.js +80 -0
  226. package/dist/ui/settings-strings.js +335 -10
  227. package/dist/ui/settings-web-authority.js +91 -0
  228. package/dist/ui/shared/agend-auth.js +28 -4
  229. package/dist/ui/shared/agent-widget.js +248 -0
  230. package/dist/ui/shared/app-i18n.js +6 -2
  231. package/dist/ui/shared/app-prefs.js +34 -13
  232. package/dist/ui/shared/app-route.js +42 -2
  233. package/dist/ui/shared/app-session.js +3 -1
  234. package/dist/ui/shared/app-shell.js +119 -41
  235. package/dist/ui/shared/app-store.js +28 -1
  236. package/dist/ui/shared/app-stream.js +14 -7
  237. package/dist/ui/shared/app-themes.js +206 -0
  238. package/dist/ui/shared/app.css +566 -5
  239. package/dist/ui/shared/app.js +23 -10
  240. package/dist/ui/shared/card-strings.js +32 -0
  241. package/dist/ui/shared/effect-app-runtime.js +297 -0
  242. package/dist/ui/shared/effect-baseline.js +64 -0
  243. package/dist/ui/shared/effect-chat-runtime.js +167 -0
  244. package/dist/ui/shared/effect-cursor.js +125 -0
  245. package/dist/ui/shared/effect-frame.js +146 -0
  246. package/dist/ui/shared/effect-geometry.js +137 -0
  247. package/dist/ui/shared/effect-host-style.js +10 -0
  248. package/dist/ui/shared/effect-owner-regions.js +74 -0
  249. package/dist/ui/shared/effect-owner-source.js +122 -0
  250. package/dist/ui/shared/effect-policy.js +151 -0
  251. package/dist/ui/shared/effect-poster-reader.js +98 -0
  252. package/dist/ui/shared/effect-posters.js +117 -0
  253. package/dist/ui/shared/effect-profiler.js +184 -0
  254. package/dist/ui/shared/effect-profiles.js +65 -0
  255. package/dist/ui/shared/effect-registry.js +543 -0
  256. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  257. package/dist/ui/shared/effect-safety.js +153 -0
  258. package/dist/ui/shared/effect-stock-snapshot.js +43 -0
  259. package/dist/ui/shared/effect-stock-trust.js +217 -0
  260. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  261. package/dist/ui/shared/effect-trial-source.js +103 -0
  262. package/dist/ui/shared/effect-trial-stage.js +170 -0
  263. package/dist/ui/shared/effect-trial.css +40 -0
  264. package/dist/ui/shared/fx-motion.js +67 -0
  265. package/dist/ui/shared/header-tools.js +1 -1
  266. package/dist/ui/shared/identity-card.js +273 -0
  267. package/dist/ui/shared/identity-color.js +261 -0
  268. package/dist/ui/shared/instance-nav.js +215 -41
  269. package/dist/ui/shared/owner-css.js +250 -0
  270. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  271. package/dist/ui/shared/panel-view.js +103 -10
  272. package/dist/ui/shared/signin.js +2 -0
  273. package/dist/ui/shared/tokens.css +9 -3
  274. package/dist/ui/shared/ui-dialog.js +1 -1
  275. package/dist/ui/shared/ui-icons.js +3 -0
  276. package/dist/ui/shared/ui-menu.js +1 -1
  277. package/dist/ui/shared/ui-toast.js +1 -1
  278. package/dist/ui/shared/usage-dialog.js +22 -2
  279. package/dist/ui/shared/view-strings.js +98 -2
  280. package/dist/ui/shared/web-authorize.js +151 -0
  281. package/dist/ui/signin.html +1 -0
  282. package/dist/ui/web-terminal/terminal.css +4 -4
  283. package/dist/usage/providers.d.ts +28 -0
  284. package/dist/usage/providers.js +126 -36
  285. package/dist/usage/providers.js.map +1 -1
  286. package/dist/usage-report.d.ts +158 -0
  287. package/dist/usage-report.js +226 -0
  288. package/dist/usage-report.js.map +1 -0
  289. package/dist/usage-rollup.d.ts +51 -0
  290. package/dist/usage-rollup.js +168 -0
  291. package/dist/usage-rollup.js.map +1 -0
  292. package/dist/view-api.d.ts +68 -0
  293. package/dist/view-api.js +249 -8
  294. package/dist/view-api.js.map +1 -1
  295. package/dist/web-ambient.d.ts +72 -0
  296. package/dist/web-ambient.js +161 -0
  297. package/dist/web-ambient.js.map +1 -0
  298. package/dist/web-api.d.ts +117 -7
  299. package/dist/web-api.js +1283 -70
  300. package/dist/web-api.js.map +1 -1
  301. package/dist/web-appearance-limits.d.ts +82 -0
  302. package/dist/web-appearance-limits.js +83 -0
  303. package/dist/web-appearance-limits.js.map +1 -0
  304. package/dist/web-appearance-policy.d.ts +64 -0
  305. package/dist/web-appearance-policy.js +87 -0
  306. package/dist/web-appearance-policy.js.map +1 -0
  307. package/dist/web-appearance-tools.d.ts +53 -0
  308. package/dist/web-appearance-tools.js +124 -0
  309. package/dist/web-appearance-tools.js.map +1 -0
  310. package/dist/web-appearance-view.d.ts +124 -0
  311. package/dist/web-appearance-view.js +278 -0
  312. package/dist/web-appearance-view.js.map +1 -0
  313. package/dist/web-appearance.d.ts +93 -0
  314. package/dist/web-appearance.js +373 -0
  315. package/dist/web-appearance.js.map +1 -0
  316. package/dist/web-asset-parts.d.ts +93 -0
  317. package/dist/web-asset-parts.js +133 -0
  318. package/dist/web-asset-parts.js.map +1 -0
  319. package/dist/web-auth.js +20 -2
  320. package/dist/web-auth.js.map +1 -1
  321. package/dist/web-card.d.ts +110 -0
  322. package/dist/web-card.js +266 -0
  323. package/dist/web-card.js.map +1 -0
  324. package/dist/web-channel-echo.d.ts +7 -1
  325. package/dist/web-channel-echo.js +6 -2
  326. package/dist/web-channel-echo.js.map +1 -1
  327. package/dist/web-chat-history.d.ts +63 -0
  328. package/dist/web-chat-history.js +109 -6
  329. package/dist/web-chat-history.js.map +1 -1
  330. package/dist/web-commands.d.ts +1 -0
  331. package/dist/web-effect-bootstrap.d.ts +12 -0
  332. package/dist/web-effect-bootstrap.js +49 -0
  333. package/dist/web-effect-bootstrap.js.map +1 -0
  334. package/dist/web-effect-bundle.d.ts +28 -0
  335. package/dist/web-effect-bundle.js +171 -0
  336. package/dist/web-effect-bundle.js.map +1 -0
  337. package/dist/web-effect-canonical.d.ts +8 -0
  338. package/dist/web-effect-canonical.js +27 -0
  339. package/dist/web-effect-canonical.js.map +1 -0
  340. package/dist/web-effect-library.d.ts +114 -0
  341. package/dist/web-effect-library.js +530 -0
  342. package/dist/web-effect-library.js.map +1 -0
  343. package/dist/web-effect-playback.d.ts +30 -0
  344. package/dist/web-effect-playback.js +71 -0
  345. package/dist/web-effect-playback.js.map +1 -0
  346. package/dist/web-effect-poster.d.ts +8 -0
  347. package/dist/web-effect-poster.js +52 -0
  348. package/dist/web-effect-poster.js.map +1 -0
  349. package/dist/web-effect-profiles.d.ts +118 -0
  350. package/dist/web-effect-profiles.js +180 -0
  351. package/dist/web-effect-profiles.js.map +1 -0
  352. package/dist/web-effect-service.d.ts +93 -0
  353. package/dist/web-effect-service.js +335 -0
  354. package/dist/web-effect-service.js.map +1 -0
  355. package/dist/web-effect-tools.d.ts +48 -0
  356. package/dist/web-effect-tools.js +129 -0
  357. package/dist/web-effect-tools.js.map +1 -0
  358. package/dist/web-effect-trials.d.ts +86 -0
  359. package/dist/web-effect-trials.js +204 -0
  360. package/dist/web-effect-trials.js.map +1 -0
  361. package/dist/web-effect-upload.d.ts +27 -0
  362. package/dist/web-effect-upload.js +185 -0
  363. package/dist/web-effect-upload.js.map +1 -0
  364. package/dist/web-effect-worker.d.ts +8 -0
  365. package/dist/web-effect-worker.js +170 -0
  366. package/dist/web-effect-worker.js.map +1 -0
  367. package/dist/web-effect.d.ts +103 -0
  368. package/dist/web-effect.js +153 -0
  369. package/dist/web-effect.js.map +1 -0
  370. package/dist/web-guest-access.d.ts +125 -0
  371. package/dist/web-guest-access.js +234 -0
  372. package/dist/web-guest-access.js.map +1 -0
  373. package/dist/web-guest-rooms.d.ts +82 -0
  374. package/dist/web-guest-rooms.js +135 -0
  375. package/dist/web-guest-rooms.js.map +1 -0
  376. package/dist/web-identity-color.d.ts +87 -0
  377. package/dist/web-identity-color.js +209 -0
  378. package/dist/web-identity-color.js.map +1 -0
  379. package/dist/web-identity-palette.d.ts +113 -0
  380. package/dist/web-identity-palette.js +84 -0
  381. package/dist/web-identity-palette.js.map +1 -0
  382. package/dist/web-image-meta.d.ts +38 -0
  383. package/dist/web-image-meta.js +262 -0
  384. package/dist/web-image-meta.js.map +1 -0
  385. package/dist/web-login.d.ts +47 -0
  386. package/dist/web-login.js +115 -0
  387. package/dist/web-login.js.map +1 -1
  388. package/dist/web-mod-agent.d.ts +50 -0
  389. package/dist/web-mod-agent.js +207 -0
  390. package/dist/web-mod-agent.js.map +1 -0
  391. package/dist/web-mod-bundle.d.ts +157 -0
  392. package/dist/web-mod-bundle.js +529 -0
  393. package/dist/web-mod-bundle.js.map +1 -0
  394. package/dist/web-mod-export.d.ts +68 -0
  395. package/dist/web-mod-export.js +100 -0
  396. package/dist/web-mod-export.js.map +1 -0
  397. package/dist/web-mod-store.d.ts +289 -0
  398. package/dist/web-mod-store.js +638 -0
  399. package/dist/web-mod-store.js.map +1 -0
  400. package/dist/web-org.d.ts +3 -3
  401. package/dist/web-org.js +11 -17
  402. package/dist/web-org.js.map +1 -1
  403. package/dist/web-owner-css-store.d.ts +222 -0
  404. package/dist/web-owner-css-store.js +440 -0
  405. package/dist/web-owner-css-store.js.map +1 -0
  406. package/dist/web-owner-css.d.ts +58 -0
  407. package/dist/web-owner-css.js +552 -0
  408. package/dist/web-owner-css.js.map +1 -0
  409. package/dist/web-prefs.d.ts +62 -0
  410. package/dist/web-prefs.js +232 -36
  411. package/dist/web-prefs.js.map +1 -1
  412. package/dist/web-preview.d.ts +15 -3
  413. package/dist/web-preview.js +28 -9
  414. package/dist/web-preview.js.map +1 -1
  415. package/dist/web-returning-device.d.ts +41 -0
  416. package/dist/web-returning-device.js +57 -5
  417. package/dist/web-returning-device.js.map +1 -1
  418. package/dist/web-session-authorization.d.ts +132 -0
  419. package/dist/web-session-authorization.js +193 -0
  420. package/dist/web-session-authorization.js.map +1 -0
  421. package/dist/web-session.d.ts +70 -10
  422. package/dist/web-session.js +137 -22
  423. package/dist/web-session.js.map +1 -1
  424. package/dist/web-settings-patch.d.ts +67 -2
  425. package/dist/web-settings-patch.js +196 -1
  426. package/dist/web-settings-patch.js.map +1 -1
  427. package/dist/web-settings-tools.d.ts +89 -0
  428. package/dist/web-settings-tools.js +273 -0
  429. package/dist/web-settings-tools.js.map +1 -0
  430. package/dist/web-shell-routes.d.ts +2 -2
  431. package/dist/web-shell-routes.js +2 -2
  432. package/dist/web-shell-routes.js.map +1 -1
  433. package/dist/web-theme-store.d.ts +257 -0
  434. package/dist/web-theme-store.js +627 -0
  435. package/dist/web-theme-store.js.map +1 -0
  436. package/dist/web-theme.d.ts +123 -0
  437. package/dist/web-theme.js +416 -0
  438. package/dist/web-theme.js.map +1 -0
  439. package/dist/web-upload.d.ts +13 -0
  440. package/dist/web-upload.js +39 -3
  441. package/dist/web-upload.js.map +1 -1
  442. package/dist/web-widget.d.ts +72 -0
  443. package/dist/web-widget.js +245 -0
  444. package/dist/web-widget.js.map +1 -0
  445. package/dist/workflow-templates/default.md +1 -1
  446. package/package.json +1 -1
  447. package/templates/systemd.service.ejs +8 -0
@@ -0,0 +1,67 @@
1
+ // #2117 (the user's direction, hmsg-3c166c26): agents' effects play whatever the system's reduced-motion setting says,
2
+ // unless this device chose "Effects: follow the system's reduced-motion setting". One per-device switch (not synced:
3
+ // it is about this screen), shared by ambient effects and Canvas Worker effects; it also works on the public link.
4
+ export const FX_FOLLOW_REDUCED_MOTION_KEY = "agend_fx_follow_reduced_motion";
5
+ /** The one-time chat note "your system asks for reduced motion; effects are on" was shown on this device. */
6
+ export const FX_MOTION_NOTE_KEY = "agend_fx_motion_note_seen";
7
+ const listeners = new Set();
8
+ let pageChoice = null, noteShown = false;
9
+
10
+ /** Whether this device asked effects to follow the system's reduced-motion setting (default: no). */
11
+ export function followsReducedMotion() {
12
+ if (pageChoice !== null) return pageChoice;
13
+ try { return globalThis.localStorage.getItem(FX_FOLLOW_REDUCED_MOTION_KEY) === "1"; } catch { return false; }
14
+ }
15
+ export function setFollowsReducedMotion(on) {
16
+ pageChoice = !!on;
17
+ try { if (on) globalThis.localStorage.setItem(FX_FOLLOW_REDUCED_MOTION_KEY, "1"); else globalThis.localStorage.removeItem(FX_FOLLOW_REDUCED_MOTION_KEY); } catch { /* this page only */ }
18
+ for (const fn of [...listeners]) { try { fn(); } catch { /* a listener's own */ } }
19
+ }
20
+ /** `fn()` when the switch or the system's answer moves (this tab, another tab, the OS). Returns the unsubscribe. */
21
+ export function onMotionChange(fn) {
22
+ listeners.add(fn);
23
+ const onStorage = (e) => {
24
+ if (!e || e.key === FX_FOLLOW_REDUCED_MOTION_KEY || e.key === null) {
25
+ pageChoice = e?.key === FX_FOLLOW_REDUCED_MOTION_KEY ? e.newValue === "1" : null;
26
+ fn();
27
+ }
28
+ };
29
+ let q = null;
30
+ try { q = typeof globalThis.matchMedia === "function" ? globalThis.matchMedia("(prefers-reduced-motion: reduce)") : null; } catch { q = null; }
31
+ try { globalThis.addEventListener && globalThis.addEventListener("storage", onStorage); } catch { /* none */ }
32
+ try { if (q && q.addEventListener) q.addEventListener("change", fn); } catch { /* read again on the next render */ }
33
+ return () => {
34
+ listeners.delete(fn);
35
+ try { globalThis.removeEventListener && globalThis.removeEventListener("storage", onStorage); } catch { /* gone */ }
36
+ try { if (q && q.removeEventListener) q.removeEventListener("change", fn); } catch { /* gone */ }
37
+ };
38
+ }
39
+ /** Whether the system asks for reduced motion (false when it can't say: the note is only for a clear yes). */
40
+ export function systemReducesMotion(win = globalThis) {
41
+ try { return typeof win.matchMedia === "function" && win.matchMedia("(prefers-reduced-motion: reduce)").matches === true; } catch { return false; }
42
+ }
43
+ /**
44
+ * The one-time note (the user's direction): the system asks for reduced motion, this device does not follow it, and the
45
+ * note was not shown here before. Showing it marks it shown.
46
+ */
47
+ export function motionNoteDue(win = globalThis) {
48
+ if (noteShown || followsReducedMotion() || !systemReducesMotion(win)) return false;
49
+ try { return globalThis.localStorage.getItem(FX_MOTION_NOTE_KEY) !== "1"; } catch { return true; }
50
+ }
51
+ export function markMotionNoteShown() {
52
+ noteShown = true;
53
+ try { globalThis.localStorage.setItem(FX_MOTION_NOTE_KEY, "1"); } catch { /* shown again next time: harmless */ }
54
+ }
55
+
56
+ /**
57
+ * True when effects stay still here: this device follows the system's setting and the system asks for reduced motion
58
+ * (or cannot say — then still, as asked for). With the switch off (the default), never.
59
+ */
60
+ export function motionBlocked(win = globalThis) {
61
+ if (!followsReducedMotion()) return false;
62
+ try {
63
+ if (typeof win.matchMedia !== "function") return true;
64
+ const q = win.matchMedia("(prefers-reduced-motion: reduce)");
65
+ return !q || typeof q.matches !== "boolean" || q.matches;
66
+ } catch { return true; }
67
+ }
@@ -125,7 +125,7 @@ export function UsageRail({ route }) {
125
125
  const wide = useRailWidth();
126
126
  const { open } = useStore(usageRailStore);
127
127
  if (!wide || !open || !route || !RAIL_PANELS.includes(route.panel) || !usageOffered()) return null;
128
- return html`<aside class="u-rail" aria-label=${t("view.usage.title_plain")}>
128
+ return html`<aside data-trust="usage" class="u-rail" aria-label=${t("view.usage.title_plain")}>
129
129
  <div class="u-rail-head"><h2>${t("view.usage.title")}</h2>
130
130
  <button type="button" class="icon-btn" onClick=${() => setUsageRail(false)} aria-label=${t("app.usageHide")} title=${t("app.usageHide")}><${Icon} name="close" size=${16} /></button></div>
131
131
  <div class="u-rail-body"><${UsageBody} owner="usage-rail" /></div>
@@ -0,0 +1,273 @@
1
+ // #1629 r4 §1 / r4.1 Q3 (a): an instance's identity card — banner, portrait, tagline, bio — as the host resolves it
2
+ // (GET /api/card/<name>: the person's fields over the agent's). Shown on Details, in View, and folded into the agent's
3
+ // own chat header. Public (/assets/): View's anonymous reader sees cards too.
4
+ //
5
+ // Nothing the agent wrote is markup: the tagline and bio are text nodes (never linkified), the pictures are same-origin
6
+ // <img> elements whose URLs are checked here again, and sizes are class tokens only (card-s | card-m | card-l, heights in
7
+ // app.css) — no style attribute, no CSSOM size (#1300). #2086: the banner's focus is a position, not a size: one custom
8
+ // property set through the CSSOM (`--id-focus`), and the banner is never drawn wider than BANNER_MAX_ASPECT : 1. The size is a viewer's choice: every card's default
9
+ // (`card.size`) and the cards set apart from it (`card.sizeByInstance`, at most 200), synced like the other preferences.
10
+ import { html, useEffect, useState } from "./app-html.js";
11
+ import { t } from "./app-i18n.js";
12
+ import { createStore, useStore } from "./app-store.js";
13
+ import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
14
+ import "./card-strings.js";
15
+
16
+ export const CARD_SIZES = ["s", "m", "l"];
17
+ /**
18
+ * #2086: the widest a banner is ever drawn (width : height) — app.css caps its width at this many times its size's
19
+ * tallest height. So the band of the picture shown everywhere is (picture's aspect ÷ this) of its height, at the focus.
20
+ */
21
+ export const BANNER_MAX_ASPECT = 5;
22
+ /**
23
+ * #2086 (#2089 review): the narrowest a card draws its banner — 9 : 4 (app.css caps a banner's height at its box's width ÷ 2.25, on
24
+ * every layout) — which crops a wide picture's sides. Every card is between this and BANNER_MAX_ASPECT, so the area shown on every screen is
25
+ * the narrowest box's width (centred) × the widest box's height (at the focus).
26
+ */
27
+ export const BANNER_MIN_ASPECT = 9 / 4;
28
+ /** The share of a picture's height a box of `boxAspect` shows (1: all of it; a narrower box crops the sides instead). */
29
+ export const bannerShownHeight = (pictureAspect, boxAspect) => Math.min(1, pictureAspect / boxAspect);
30
+ /** The share of a picture's width a box of `boxAspect` shows, centred (1: all of it). */
31
+ export const bannerShownWidth = (pictureAspect, boxAspect) => Math.min(1, boxAspect / pictureAspect);
32
+ /** The area every screen shows, as shares of the picture: { left, top, width, height } at focus `f`. */
33
+ export function bannerSafeArea(pictureAspect, f) {
34
+ const width = bannerShownWidth(pictureAspect, BANNER_MIN_ASPECT), height = bannerShownHeight(pictureAspect, BANNER_MAX_ASPECT);
35
+ return { left: (1 - width) / 2, top: bannerBandTop(height, f), width, height };
36
+ }
37
+ /** That band's top, as a share of the picture's height, at focus `f` (0–100) — object-position's own rule. */
38
+ export const bannerBandTop = (shown, f) => (1 - shown) * (f / 100);
39
+ export const MAX_CARD_SIZES = 200;
40
+ const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
41
+ const readLocal = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
42
+
43
+ /** Bumped whenever a size choice changes (here or on another device), so cards redraw. */
44
+ export const cardSizeStore = createStore({ n: 0 });
45
+ const bump = () => cardSizeStore.set((s) => ({ n: s.n + 1 }));
46
+ onPrefsChanged((changed) => { if (changed.has("card.size") || changed.has("card.sizeByInstance")) bump(); });
47
+
48
+ /** Every card's size unless set apart: the person's choice, else M. */
49
+ export function cardSizeDefault() {
50
+ const f = prefInForce("card.size");
51
+ const v = f ? f.value : readLocal("agend_card_size");
52
+ return CARD_SIZES.includes(v) ? v : "m";
53
+ }
54
+ function overrides() {
55
+ const f = prefInForce("card.sizeByInstance");
56
+ let v = f ? f.value : undefined;
57
+ if (!f) { try { v = JSON.parse(readLocal("agend_card_sizes") || "null"); } catch { v = null; } }
58
+ return v && typeof v === "object" && !Array.isArray(v) ? v : {};
59
+ }
60
+ /** The size `name`'s card is drawn at: its own, else the default. */
61
+ export function cardSizeFor(name) {
62
+ const o = overrides();
63
+ return own(o, name) && CARD_SIZES.includes(o[name]) ? o[name] : cardSizeDefault();
64
+ }
65
+ /**
66
+ * The size for one card (its own control). Choosing the default size removes its entry, so it follows the default
67
+ * again; a new or changed entry goes last, and past 200 the oldest go.
68
+ */
69
+ export function setCardSize(name, size) {
70
+ if (!CARD_SIZES.includes(size)) return;
71
+ const next = Object.create(null);
72
+ for (const [k, v] of Object.entries(overrides())) if (k !== name) Object.defineProperty(next, k, { value: v, enumerable: true, writable: true, configurable: true });
73
+ if (size !== cardSizeDefault()) Object.defineProperty(next, name, { value: size, enumerable: true, writable: true, configurable: true });
74
+ const keys = Object.keys(next);
75
+ for (const k of keys.slice(0, Math.max(0, keys.length - MAX_CARD_SIZES))) delete next[k];
76
+ const plain = {};
77
+ for (const k of Object.keys(next)) Object.defineProperty(plain, k, { value: next[k], enumerable: true, writable: true, configurable: true });
78
+ savePref("card.sizeByInstance", Object.keys(plain).length ? plain : null);
79
+ bump();
80
+ }
81
+ /** Every card's default size (Settings → Appearance). M is the default, so choosing it clears the choice. */
82
+ export function setCardSizeDefault(size) {
83
+ if (!CARD_SIZES.includes(size)) return;
84
+ savePref("card.size", size === "m" ? null : size);
85
+ bump();
86
+ }
87
+
88
+ // ── The card's data: read once per version (the status frame's / View roster's card_v) ──
89
+ /** { <name>: { v, epoch, card } } — card null until read; a failed read keeps the last one. */
90
+ export const cardsStore = createStore({});
91
+ const SRC = /^\/api\/card\/[^/?#\\]+\/(portrait|banner)\?v=[0-9a-f]{32}$/;
92
+ const ID = /^[0-9a-f]{32}$/;
93
+ /** What the host said, checked again: same-origin picture paths of this card, text as text. */
94
+ export function checkCard(name, body) {
95
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
96
+ const out = {};
97
+ const base = `/api/card/${encodeURIComponent(name)}/`;
98
+ for (const slot of ["portrait", "banner"]) {
99
+ const p = body[slot];
100
+ if (p && typeof p === "object" && typeof p.src === "string" && SRC.test(p.src) && p.src.startsWith(base + slot + "?") && ID.test(p.id) && (p.by === "human" || p.by === "agent")) out[slot] = { src: p.src, by: p.by };
101
+ }
102
+ // The host's bounds (web-theme.ts TAGLINE_MAX_UNITS / BIO_MAX_UNITS): anything it stores is shown.
103
+ const MAX = { tagline: 600, bio: 2800 };
104
+ for (const k of ["tagline", "bio"]) {
105
+ const v = body[k];
106
+ if (v && typeof v === "object" && typeof v.text === "string" && v.text.length && v.text.length <= MAX[k] && (v.by === "human" || v.by === "agent")) out[k] = { text: v.text, by: v.by };
107
+ }
108
+ // #2086: the banner's focus, a whole number 0 (top) … 100 (bottom).
109
+ const f = body.banner_focus;
110
+ if (f && typeof f === "object" && Number.isInteger(f.value) && f.value >= 0 && f.value <= 100 && (f.by === "human" || f.by === "agent")) out.banner_focus = { value: f.value, by: f.by };
111
+ return Object.keys(out).length ? out : null;
112
+ }
113
+ // #1889 review: per instance, an epoch that moves whenever the version this page wants changes (a clear included).
114
+ // GET /api/card is not versioned — its body is whatever the host had then — so a read belongs to the epoch that started
115
+ // it: only a read of the current epoch is shared or applied, and an older read never touches its replacement.
116
+ const want = new Map(); // name → { epoch, v }
117
+ const inflight = new Map(); // name → { epoch, promise }
118
+ /** Read `name`'s card if `v` is newer than what this page has (no version: no card). */
119
+ export async function loadCard(name, v, fetcher = (u) => fetch(u, { credentials: "same-origin" })) {
120
+ if (!name) return;
121
+ const target = v || null;
122
+ const cur = want.get(name);
123
+ const epoch = cur && cur.v === target ? cur.epoch : (cur ? cur.epoch : 0) + 1;
124
+ if (!cur || cur.v !== target) want.set(name, { epoch, v: target });
125
+ const have = own(cardsStore.get(), name) ? cardsStore.get()[name] : null;
126
+ if (!target) {
127
+ if (have) cardsStore.set((s) => { const n = { ...s }; delete n[name]; return n; });
128
+ return;
129
+ }
130
+ if (have && have.v === target && have.epoch === epoch) return; // read in this epoch already
131
+ const out = inflight.get(name);
132
+ if (out && out.epoch === epoch) return out.promise; // this epoch's read is on its way
133
+ const current = () => want.get(name)?.epoch === epoch;
134
+ const entry = { epoch, promise: null };
135
+ entry.promise = (async () => {
136
+ try {
137
+ const r = await fetcher(`/api/card/${encodeURIComponent(name)}`);
138
+ if (!r.ok || !current()) return;
139
+ const card = checkCard(name, await r.json());
140
+ if (!current()) return; // superseded or cleared while it was read
141
+ cardsStore.set((s) => ({ ...s, [name]: { v: target, epoch, card } }));
142
+ } catch { /* the next version tries again */ } finally { if (inflight.get(name) === entry) inflight.delete(name); }
143
+ })();
144
+ inflight.set(name, entry);
145
+ return entry.promise;
146
+ }
147
+ /** `name`'s card for version `v` (null while none or not read yet). */
148
+ export function useCard(name, v) {
149
+ const cards = useStore(cardsStore);
150
+ useEffect(() => { loadCard(name, v); }, [name, v]);
151
+ const c = name && own(cards, name) ? cards[name] : null;
152
+ return c && c.v === v ? c.card : c ? c.card : null;
153
+ }
154
+
155
+ /** The S / M / L control (a segmented one): sets this card's size. */
156
+ function SizeControl({ name, size }) {
157
+ return html`<div class="card-size" role="group" aria-label=${t("card.size")}>
158
+ ${CARD_SIZES.map((s) => html`<button key=${s} type="button" class=${`seg-item${s === size ? " active" : ""}`} aria-pressed=${s === size ? "true" : "false"}
159
+ title=${t(`card.size${s.toUpperCase()}Name`)} onClick=${() => setCardSize(name, s)}>${t(`card.size${s.toUpperCase()}`)}</button>`)}
160
+ </div>`;
161
+ }
162
+
163
+ /**
164
+ * The card. `compact`: the chat header's own small size (no size control; the banner still follows the size choice,
165
+ * #2038). Nothing when the card is empty. The
166
+ * tagline and bio say whose they are (title), since a person's field wins over the agent's.
167
+ */
168
+ export function IdentityCard({ name, card, compact = false, sizeControl = !compact }) {
169
+ useStore(cardSizeStore);
170
+ const [broken, setBroken] = useState({});
171
+ if (!card) return null;
172
+ const size = compact ? "c" : cardSizeFor(name);
173
+ // #2038: the chat header's card is its own small size, but its banner follows the person's size choice too.
174
+ const bannerSize = compact ? ` bs-${cardSizeFor(name)}` : "";
175
+ const whose = (f) => t(f.by === "human" ? "card.byHuman" : "card.byAgent");
176
+ const pic = (slot, cls, alt) => card[slot] && broken[slot] !== card[slot].src
177
+ ? html`<img class=${cls} src=${card[slot].src} alt=${alt} title=${whose(card[slot])} decoding="async" onError=${() => setBroken({ ...broken, [slot]: card[slot].src })} />` : null;
178
+ // #2086: the banner at its focus; on a screen wider than BANNER_MAX_ASPECT it is centred over a blurred copy of itself.
179
+ const focus = card.banner_focus ? card.banner_focus.value : 50;
180
+ const atFocus = (el) => { if (el && el.style) el.style.setProperty("--id-focus", `${focus}%`); };
181
+ const banner = pic("banner", "id-banner", "");
182
+ return html`<section class=${`id-card card-${size}${bannerSize}`} aria-label=${t("card.label", name)}>
183
+ ${banner ? html`<div class="id-banner-box" ref=${atFocus}><img class="id-banner-fill" src=${card.banner.src} alt="" aria-hidden="true" decoding="async" />${banner}</div>` : null}
184
+ <div class="id-row">
185
+ ${pic("portrait", "id-portrait", t("card.portraitAlt", name))}
186
+ <div class="id-text">
187
+ ${card.tagline ? html`<p class="id-tagline" title=${whose(card.tagline)}>${card.tagline.text}</p>` : null}
188
+ ${card.bio ? html`<p class="id-bio" title=${whose(card.bio)}>${card.bio.text}</p>` : null}
189
+ </div>
190
+ ${sizeControl ? html`<${SizeControl} name=${name} size=${size} />` : null}
191
+ </div>
192
+ </section>`;
193
+ }
194
+
195
+ // ── #2086: the banner's focus, set in View's Edit profile — the whole picture, what each screen shows of it ──
196
+ /**
197
+ * #2089 review: the picture the focus preview shows — a file just chosen (a blob URL this owns: released when another is
198
+ * chosen and when the dialog closes) or the banner drawn now (a server URL, never released).
199
+ */
200
+ export function useBannerPreview() {
201
+ const [preview, setPreview] = useState(null);
202
+ useEffect(() => () => { if (preview && preview.blob) { try { URL.revokeObjectURL(preview.blob); } catch { /* already gone */ } } }, [preview]);
203
+ return [preview, {
204
+ fromFile(file) { let url = null; try { url = URL.createObjectURL(file); } catch { return; } setPreview({ src: url, blob: url }); },
205
+ fromSrc(src) { setPreview({ src }); },
206
+ }];
207
+ }
208
+ /** A phone draws an M card's banner at about 3 : 1 (app.css, ≤479 px) — a guide for that size, not every phone card. */
209
+ export const BANNER_PHONE_ASPECT = 3;
210
+ /**
211
+ * The banner whole, with the band every screen shows at this focus (dashed; outside it shaded) and the band a phone
212
+ * shows (solid) — measured from the picture itself once it has loaded. Positions go through the CSSOM (#1300).
213
+ */
214
+ export function BannerFocusPreview({ src, focus }) {
215
+ const [size, setSize] = useState(null);
216
+ if (!src) return null;
217
+ const ar = size ? size.w / size.h : null;
218
+ const put = (el, a) => { for (const k of ["left", "top", "width", "height"]) el.style.setProperty(k, `${(a[k] * 100).toFixed(2)}%`); };
219
+ // Every screen: the safe area (both crops). The phone guide: an M card on a phone, as drawn there (both crops too).
220
+ const safe = (el) => { if (el && el.style && ar) put(el, bannerSafeArea(ar, focus)); };
221
+ const phone = (el) => {
222
+ if (!el || !el.style || !ar) return;
223
+ const width = bannerShownWidth(ar, BANNER_PHONE_ASPECT), height = bannerShownHeight(ar, BANNER_PHONE_ASPECT);
224
+ put(el, { left: (1 - width) / 2, top: bannerBandTop(height, focus), width, height });
225
+ };
226
+ return html`<div class="bf-wrap">
227
+ <img class="bf-pic" src=${src} alt="" onLoad=${(e) => { const i = e.currentTarget; if (i.naturalWidth && i.naturalHeight) setSize({ w: i.naturalWidth, h: i.naturalHeight }); }} />
228
+ ${ar ? html`<div class="bf-band bf-phone" ref=${phone}><span>${t("card.bannerPhone")}</span></div>
229
+ <div class="bf-band bf-safe" ref=${safe}><span>${t("card.bannerSafe")}</span></div>` : null}
230
+ </div>`;
231
+ }
232
+
233
+ // ── A person's picture for the card (View's Edit profile), fitted before it is sent (r4.1 Q3 (b)) ──
234
+ /**
235
+ * `file` fitted to `limit` ({ max_width, max_height, max_bytes }): as it is when it already fits; otherwise, once the
236
+ * person agrees (`ask(w, h, nw, nh)`), drawn smaller and re-encoded as WebP (which also drops its metadata). The server
237
+ * checks whatever is sent like any upload — this is a convenience, never the check. Returns the Blob to send, null when
238
+ * the person declined, or throws when the picture cannot be read or made small enough.
239
+ */
240
+ export async function fitPicture(file, limit, ask, io = browserImageIo) {
241
+ const okType = /^image\/(png|jpeg|webp)$/.test(file.type);
242
+ const bitmap = await io.decode(file);
243
+ const w = bitmap.width, h = bitmap.height;
244
+ if (okType && w <= limit.max_width && h <= limit.max_height && file.size <= limit.max_bytes) return file;
245
+ let scale = Math.min(1, limit.max_width / w, limit.max_height / h);
246
+ let nw = Math.max(1, Math.floor(w * scale)), nh = Math.max(1, Math.floor(h * scale));
247
+ if (!(await ask(w, h, nw, nh))) return null;
248
+ for (let tries = 0; tries < 6; tries++) {
249
+ for (const quality of [0.9, 0.8, 0.7]) {
250
+ const out = await io.encode(bitmap, nw, nh, quality);
251
+ if (out && out.size <= limit.max_bytes) return out;
252
+ }
253
+ scale *= 0.8;
254
+ nw = Math.max(1, Math.floor(w * scale)); nh = Math.max(1, Math.floor(h * scale));
255
+ }
256
+ throw new Error("too_large");
257
+ }
258
+ /** The browser's decoder and encoder: createImageBitmap (no URL, so img-src stays 'self') and a canvas to WebP (or JPEG). */
259
+ export const browserImageIo = {
260
+ decode: (file) => createImageBitmap(file),
261
+ encode: (bitmap, w, h, quality) => new Promise((resolve) => {
262
+ const canvas = document.createElement("canvas");
263
+ canvas.width = w; canvas.height = h;
264
+ const ctx = canvas.getContext("2d");
265
+ if (!ctx) { resolve(null); return; }
266
+ ctx.drawImage(bitmap, 0, 0, w, h);
267
+ // WebP where the browser encodes it; JPEG where it does not (it answers PNG then) — either drops the metadata.
268
+ canvas.toBlob((b) => {
269
+ if (b && b.type === "image/webp") { resolve(b); return; }
270
+ canvas.toBlob((j) => resolve(j && j.type === "image/jpeg" ? j : null), "image/jpeg", quality);
271
+ }, "image/webp", quality);
272
+ }),
273
+ };
@@ -0,0 +1,261 @@
1
+ // #1958: an instance's identity colour on the page — the cue people scan the sidebar by — and where it shows (the
2
+ // name, a bar at the row's left edge, the avatar's ring). The server sends each instance's tiers in the status frame
3
+ // (`color`, `color_show`); this module resolves them as src/web-identity-color.ts does (a test pins the two equal),
4
+ // paints the sidebar row through the CSSOM (no style attribute, #1300), and holds the Color dialog.
5
+ // colour: a person's pick > the agent's (themes shown) > its own bot's role colour > automatic (a few soft colours)
6
+ // places: a person's choice > the agent's (themes shown) > this host's default (color.show) > all three (#2056)
7
+ // Settings → Web appearance can turn colours off for the host (color.off): nothing is painted, every pick is kept.
8
+ import { html, useEffect, useState } from "./app-html.js";
9
+ import { createStore, useStore } from "./app-store.js";
10
+ import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
11
+ import { register, t } from "./app-i18n.js";
12
+ import { themesShown, themesStore } from "./app-themes.js";
13
+ import { Dialog } from "./ui-dialog.js";
14
+ import { toast } from "./ui-toast.js";
15
+
16
+ // ── The palettes (src/web-identity-palette.ts: the same values, pinned by a test) ──
17
+
18
+ export const IDENTITY_PALETTE = [
19
+ { id: "red", light: "#cf222e", dark: "#ff7b72" }, { id: "orange", light: "#bc4c00", dark: "#ffa657" },
20
+ { id: "amber", light: "#8a5d00", dark: "#e3b341" }, { id: "lime", light: "#4d7c0f", dark: "#a3e635" },
21
+ { id: "green", light: "#1a7f37", dark: "#56d364" }, { id: "teal", light: "#0f766e", dark: "#2dd4bf" },
22
+ { id: "cyan", light: "#0e7490", dark: "#22d3ee" }, { id: "blue", light: "#0a62c2", dark: "#7ab7ff" },
23
+ { id: "indigo", light: "#4f46e5", dark: "#a5b4fc" }, { id: "violet", light: "#8250df", dark: "#d2a8ff" },
24
+ { id: "pink", light: "#bf3989", dark: "#f778ba" }, { id: "slate", light: "#57606a", dark: "#b1bac4" },
25
+ ];
26
+ export const AUTO_PALETTE = [
27
+ { id: "sand", light: "#8a6d3b", dark: "#d4b98a" }, { id: "clay", light: "#9c5a4a", dark: "#d9a294" },
28
+ { id: "rose", light: "#9b4f6a", dark: "#d99ab0" }, { id: "plum", light: "#7a5a8c", dark: "#c3a6d6" },
29
+ { id: "dusk", light: "#5a628f", dark: "#a9b0db" }, { id: "sea", light: "#3f7286", dark: "#92c2d4" },
30
+ { id: "moss", light: "#5b7340", dark: "#a9c48d" }, { id: "slate", light: "#5f6b78", dark: "#aeb8c4" },
31
+ ];
32
+ export const PLACEMENTS = ["name", "bar", "ring"];
33
+ export const DEFAULT_PLACEMENTS = ["name", "bar", "ring"]; // #2056: all three where nobody chose (was ring + bar)
34
+ const HEX = /^#[0-9a-f]{6}$/;
35
+ const presetOf = (c) => (typeof c === "string" && c.startsWith("preset:") ? IDENTITY_PALETTE.find((p) => p.id === c.slice(7)) : undefined);
36
+ const autoOf = (c) => (typeof c === "string" && c.startsWith("auto:") ? AUTO_PALETTE.find((p) => p.id === c.slice(5)) : undefined);
37
+ /** A colour as the server sends it (preset:<id>, auto:<id>, #rrggbb), checked; null for anything else. */
38
+ export function colorOk(c) {
39
+ if (typeof c !== "string") return null;
40
+ if (presetOf(c) || autoOf(c) || HEX.test(c)) return c;
41
+ return null;
42
+ }
43
+ /** A place list, in PLACEMENTS order; null for anything that is not one. */
44
+ export function placesOk(v) {
45
+ if (!Array.isArray(v) || v.length > 3 || !v.every((p) => PLACEMENTS.includes(p)) || new Set(v).size !== v.length) return null;
46
+ return PLACEMENTS.filter((p) => v.includes(p));
47
+ }
48
+ /** The colour drawn for `c` on a page in `mode`. */
49
+ export function colorValue(c, mode) {
50
+ const p = presetOf(c) || autoOf(c);
51
+ return p ? p[mode] : c;
52
+ }
53
+
54
+ // ── Contrast: name text reads at 4.5:1 on the page's and the sidebar's background (tokens.css) ──
55
+
56
+ const BG = { light: ["#ffffff", "#f9f9f9"], dark: ["#212121", "#171717"] };
57
+ const TEXT = { light: "#0d0d0d", dark: "#ececec" };
58
+ function luminance(hex) {
59
+ const c = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((x) => (x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4));
60
+ return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
61
+ }
62
+ export function contrast(a, b) {
63
+ const [l1, l2] = [luminance(a), luminance(b)].sort((x, y) => y - x);
64
+ return (l1 + 0.05) / (l2 + 0.05);
65
+ }
66
+ /** The shade of `hex` name text uses: itself if readable, else the nearest step toward black (light) or white (dark). */
67
+ export function readableShade(hex, mode) {
68
+ if (!HEX.test(hex)) return TEXT[mode];
69
+ const toward = mode === "light" ? 0 : 255;
70
+ const ch = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
71
+ for (let step = 0; step <= 20; step++) {
72
+ const shade = `#${ch.map((c) => Math.round(c + (toward - c) * (step / 20)).toString(16).padStart(2, "0")).join("")}`;
73
+ if (BG[mode].every((bg) => contrast(shade, bg) >= 4.5)) return shade;
74
+ }
75
+ return TEXT[mode];
76
+ }
77
+
78
+ // ── Resolution ──
79
+
80
+ /** The colour in force and its source, from the frame's tiers. */
81
+ export function resolveColor(tiers, showThemes) {
82
+ const t2 = tiers && typeof tiers === "object" ? tiers : {};
83
+ const person = colorOk(t2.person);
84
+ if (person) return { color: person, source: "person" };
85
+ const agent = showThemes ? colorOk(t2.agent) : null;
86
+ if (agent && !autoOf(agent)) return { color: agent, source: "agent" };
87
+ const role = typeof t2.role === "string" && HEX.test(t2.role) ? t2.role : null;
88
+ if (role) return { color: role, source: "role" };
89
+ return { color: autoOf(t2.auto) ? t2.auto : "auto:sand", source: "auto" };
90
+ }
91
+ /** Where it shows, and whose choice that is. */
92
+ export function resolvePlaces(tiers, showThemes, hostDefault) {
93
+ const t2 = tiers && typeof tiers === "object" ? tiers : {};
94
+ const person = placesOk(t2.person);
95
+ if (person) return { show: person, source: "person" };
96
+ const agent = showThemes ? placesOk(t2.agent) : null;
97
+ if (agent) return { show: agent, source: "agent" };
98
+ return { show: placesOk(hostDefault) || [...DEFAULT_PLACEMENTS], source: "host" };
99
+ }
100
+
101
+ // ── This host's settings (web prefs color.show / color.off) ──
102
+
103
+ export const colorPrefsStore = createStore({ n: 0 });
104
+ const bump = () => colorPrefsStore.set((s) => ({ n: s.n + 1 }));
105
+ onPrefsChanged((changed) => { if (changed.has("color.show") || changed.has("color.off")) bump(); });
106
+ const readLocal = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
107
+ /** Where colours show by default on this host (Settings → Web appearance). */
108
+ export function defaultPlaces() {
109
+ const f = prefInForce("color.show");
110
+ let v = f ? f.value : undefined;
111
+ if (!f) { try { v = JSON.parse(readLocal("agend_color_show") || "null"); } catch { v = null; } }
112
+ return placesOk(v) || [...DEFAULT_PLACEMENTS];
113
+ }
114
+ export function colorsOff() {
115
+ const f = prefInForce("color.off");
116
+ return f ? f.value === true : readLocal("agend_color_off") === "on";
117
+ }
118
+ /** The default places (null: back to all three, #2056). */
119
+ export function setDefaultPlaces(list) {
120
+ const v = list === null ? null : placesOk(list);
121
+ const same = v && v.length === DEFAULT_PLACEMENTS.length && v.every((p, i) => p === DEFAULT_PLACEMENTS[i]);
122
+ savePref("color.show", v === null || same ? null : v);
123
+ bump();
124
+ }
125
+ export function setColorsOff(off) {
126
+ savePref("color.off", off ? true : null);
127
+ bump();
128
+ }
129
+
130
+ /** What one instance's row shows: its colour per mode, name shades, places, and whether anything is painted at all. */
131
+ export function identityOf(it, opts = {}) {
132
+ const showThemes = opts.showThemes ?? themesShown();
133
+ const c = resolveColor(it && it.color, showThemes);
134
+ const p = resolvePlaces(it && it.color_show, showThemes, opts.hostDefault ?? defaultPlaces());
135
+ const light = colorValue(c.color, "light"), dark = colorValue(c.color, "dark");
136
+ return {
137
+ color: c.color, source: c.source, show: p.show, showSource: p.source, off: opts.off ?? colorsOff(),
138
+ light, dark, nameLight: readableShade(light, "light"), nameDark: readableShade(dark, "dark"),
139
+ };
140
+ }
141
+ /** The row's classes for its identity colour ("" when colours are off or shown nowhere). */
142
+ export function identityClasses(id) {
143
+ if (!id || id.off || !id.show.length) return "";
144
+ return ["idc", ...id.show.map((p) => `id-${p}`)].join(" ");
145
+ }
146
+ /** Paint an element (a callback ref): the colour's values for both modes as custom properties; the stylesheet picks. */
147
+ export function paintIdentity(el, id) {
148
+ if (!el || !el.style || typeof el.style.setProperty !== "function") return;
149
+ const on = id && !id.off && id.show.length;
150
+ for (const [k, v] of [["--id-light", id && id.light], ["--id-dark", id && id.dark], ["--id-name-light", id && id.nameLight], ["--id-name-dark", id && id.nameDark]]) {
151
+ if (on && v) el.style.setProperty(k, v); else el.style.removeProperty(k);
152
+ }
153
+ }
154
+ /** Redraw when this host's colour settings or the viewer's themes switch change. */
155
+ export function useIdentityColors() {
156
+ useStore(colorPrefsStore);
157
+ useStore(themesStore);
158
+ }
159
+
160
+ // ── The Color dialog (Chat ⋯, Details ⋯, View's Edit profile) ──
161
+
162
+ export const colorDialogStore = createStore({ name: null, it: null });
163
+ export function openColorDialog(name, it) { colorDialogStore.set({ name, it: it || null }); }
164
+ const closeColorDialog = () => colorDialogStore.set({ name: null, it: null });
165
+
166
+ /** The ⋯ entry: on the public link too (decision 3: colours change wherever the profile does). */
167
+ export function colorMenuItem(name, it) {
168
+ return { key: "identity-color", label: t("color.menu"), icon: "palette", onSelect: () => openColorDialog(name, it) };
169
+ }
170
+
171
+ export function ColorDialogHost() {
172
+ const s = useStore(colorDialogStore);
173
+ return s.name ? html`<${ColorDialog} key=${s.name} name=${s.name} it=${s.it} onClose=${closeColorDialog} />` : null;
174
+ }
175
+
176
+ function ColorDialog({ name, it, onClose }) {
177
+ useIdentityColors();
178
+ const [loaded, setLoaded] = useState(null); // the person's choice as stored: { color, color_show }
179
+ const [color, setColor] = useState(null); // the colour picked here (null: none — the next tier)
180
+ const [hex, setHex] = useState("");
181
+ const [places, setPlaces] = useState(null); // null: the default places
182
+ const [busy, setBusy] = useState(false);
183
+ const [error, setError] = useState("");
184
+ useEffect(() => {
185
+ let live = true;
186
+ fetch(`/api/profile/${encodeURIComponent(name)}`).then((r) => (r.ok ? r.json() : null)).then((row) => {
187
+ if (!live) return;
188
+ const c = row ? colorOk(row.color) : null, s = row ? placesOk(row.color_show) : null;
189
+ setLoaded({ color: c, show: s }); setColor(c); setHex(c && HEX.test(c) ? c : ""); setPlaces(s);
190
+ }).catch(() => { if (live) setLoaded({ color: null, show: null }); });
191
+ return () => { live = false; };
192
+ }, [name]);
193
+ // The preview: the instance as it would be drawn with the choice here.
194
+ const preview = identityOf({ ...(it || {}), color: { ...((it && it.color) || {}), person: color || undefined }, color_show: { ...((it && it.color_show) || {}), person: places || undefined } }, { off: false });
195
+ const pick = (c) => { setColor(c); setHex(c && HEX.test(c) ? c : ""); setError(""); };
196
+ const togglePlace = (p) => {
197
+ const base = places || preview.show;
198
+ setPlaces(base.includes(p) ? base.filter((x) => x !== p) : PLACEMENTS.filter((x) => x === p || base.includes(x)));
199
+ };
200
+ async function save(body) {
201
+ setBusy(true); setError("");
202
+ try {
203
+ const res = await fetch(`/api/profile/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
204
+ const r = await res.json().catch(() => ({}));
205
+ if (!res.ok) { setError(t("color.failed", (r && (r.detail || r.error)) || `HTTP ${res.status}`)); setBusy(false); return; }
206
+ toast(t("color.saved", name));
207
+ onClose();
208
+ } catch (err) { setError(t("color.failed", (err && err.message) || "network")); setBusy(false); }
209
+ }
210
+ const onHex = (v) => { setHex(v); const c = /^#[0-9a-fA-F]{6}$/.test(v) ? v.toLowerCase() : null; if (c) { setColor(c); setError(""); } };
211
+ const off = colorsOff();
212
+ return html`<${Dialog} title=${t("color.title", name)} onClose=${onClose} busy=${busy}
213
+ actions=${html`<button type="button" class="btn" disabled=${busy || !loaded} onClick=${() => save({ color: null, color_show: null })}>${t("color.reset")}</button>
214
+ <span class="dlg-spacer"></span>
215
+ <button type="button" class="btn" disabled=${busy} onClick=${onClose}>${t("color.cancel")}</button>
216
+ <button type="button" class="btn btn-primary" disabled=${busy || !loaded} onClick=${() => save({ color, color_show: places })}>${t("color.save")}</button>`}>
217
+ ${off ? html`<p class="note" role="status">${t("color.offNote")}</p>` : null}
218
+ <p class="sub">${t(`color.src_${preview.source}`)}</p>
219
+ <fieldset class="idc-swatches"><legend>${t("color.colour")}</legend>
220
+ ${IDENTITY_PALETTE.map((p) => html`<button type="button" key=${p.id} class=${`idc-swatch${color === `preset:${p.id}` ? " on" : ""}`}
221
+ aria-pressed=${color === `preset:${p.id}` ? "true" : "false"} aria-label=${t(`color.p_${p.id}`)} title=${t(`color.p_${p.id}`)}
222
+ ref=${(el) => { if (el && el.style) { el.style.setProperty("--sw-light", p.light); el.style.setProperty("--sw-dark", p.dark); } }}
223
+ onClick=${() => pick(`preset:${p.id}`)}></button>`)}
224
+ <label class="idc-custom">${t("color.custom")}
225
+ <input type="color" value=${HEX.test(hex) ? hex : "#808080"} onInput=${(e) => onHex(e.currentTarget.value)} aria-label=${t("color.custom")} />
226
+ <input type="text" class="idc-hex" inputmode="text" spellcheck=${false} maxlength="7" placeholder="#rrggbb" value=${hex} onInput=${(e) => onHex(e.currentTarget.value.trim())} aria-label=${t("color.hex")} /></label>
227
+ <button type="button" class="btn btn-sm" disabled=${!color} onClick=${() => pick(null)}>${t("color.noPick")}</button>
228
+ </fieldset>
229
+ <fieldset class="idc-places"><legend>${t("color.places")}</legend>
230
+ ${PLACEMENTS.map((p) => html`<label key=${p} class="check"><input type="checkbox" checked=${preview.show.includes(p)} onChange=${() => togglePlace(p)} /> ${t(`color.place_${p}`)}</label>`)}
231
+ <label class="check"><input type="checkbox" checked=${places === null} onChange=${() => setPlaces(places === null ? [...preview.show] : null)} /> ${t("color.useDefault")}</label>
232
+ </fieldset>
233
+ <div class="idc-preview" aria-hidden="true"><span class=${`inst v-inst ${identityClasses(preview)}`} ref=${(el) => paintIdentity(el, preview)}>
234
+ <span class="av"><span class="av-text">${(name || "?").slice(0, 1).toUpperCase()}</span></span>
235
+ <span class="inst-text"><span class="inst-name">${name}</span></span></span></div>
236
+ ${error ? html`<p class="err" role="alert">${error}</p>` : null}
237
+ </${Dialog}>`;
238
+ }
239
+
240
+ register("color", {
241
+ en: {
242
+ menu: "Colour…", title: "Colour for {0}", colour: "Colour", custom: "Custom", hex: "Hex colour", noPick: "Not chosen",
243
+ places: "Where it shows", place_name: "Name", place_bar: "Left bar", place_ring: "Ring", useDefault: "Use this host's default places",
244
+ src_person: "Shown now: picked by a person.", src_agent: "Shown now: the agent's own colour.", src_role: "Shown now: the colour of its bot's Discord role.",
245
+ src_auto: "Shown now: an automatic colour.", save: "Save", cancel: "Cancel", reset: "Reset",
246
+ saved: "Colour saved for {0}", failed: "Couldn't save the colour: {0}",
247
+ offNote: "Colours are turned off on this host (Settings → Web appearance). What you pick here is kept for when they're on.",
248
+ p_red: "Red", p_orange: "Orange", p_amber: "Amber", p_lime: "Lime", p_green: "Green", p_teal: "Teal", p_cyan: "Cyan", p_blue: "Blue",
249
+ p_indigo: "Indigo", p_violet: "Violet", p_pink: "Pink", p_slate: "Slate",
250
+ },
251
+ "zh-TW": {
252
+ menu: "顏色…", title: "{0} 的顏色", colour: "顏色", custom: "自訂", hex: "色碼", noPick: "不指定",
253
+ places: "顯示位置", place_name: "名稱", place_bar: "左側色條", place_ring: "頭像外框", useDefault: "使用這台主機的預設位置",
254
+ src_person: "目前顯示:有人挑選的顏色。", src_agent: "目前顯示:agent 自己的顏色。", src_role: "目前顯示:它的 bot 在 Discord 身分組的顏色。",
255
+ src_auto: "目前顯示:自動配色。", save: "儲存", cancel: "取消", reset: "重設",
256
+ saved: "已儲存 {0} 的顏色", failed: "無法儲存顏色:{0}",
257
+ offNote: "這台主機已關閉顏色(設定 → 網頁外觀)。在這裡挑的顏色會保留,重新開啟後就會出現。",
258
+ p_red: "紅", p_orange: "橙", p_amber: "琥珀", p_lime: "萊姆", p_green: "綠", p_teal: "青綠", p_cyan: "青", p_blue: "藍",
259
+ p_indigo: "靛", p_violet: "紫", p_pink: "粉紅", p_slate: "石板灰",
260
+ },
261
+ });