@songsid/agend 2.2.0-beta.9 → 2.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/README.md +40 -24
  2. package/README.zh-TW.md +40 -24
  3. package/dist/agent-cli.js +248 -2
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +2 -0
  6. package/dist/agent-endpoint.js +28 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-file-read.d.ts +30 -0
  9. package/dist/agent-file-read.js +129 -0
  10. package/dist/agent-file-read.js.map +1 -0
  11. package/dist/agent-whats-new.js +20 -1
  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 +3 -1
  16. package/dist/auth-api.js +32 -1
  17. package/dist/auth-api.js.map +1 -1
  18. package/dist/backend/antigravity.d.ts +33 -0
  19. package/dist/backend/antigravity.js +129 -21
  20. package/dist/backend/antigravity.js.map +1 -1
  21. package/dist/backend/claude-code.d.ts +3 -0
  22. package/dist/backend/claude-code.js +9 -0
  23. package/dist/backend/claude-code.js.map +1 -1
  24. package/dist/backend/codex-metadata.d.ts +33 -0
  25. package/dist/backend/codex-metadata.js +122 -18
  26. package/dist/backend/codex-metadata.js.map +1 -1
  27. package/dist/backend/codex.d.ts +3 -0
  28. package/dist/backend/codex.js +17 -1
  29. package/dist/backend/codex.js.map +1 -1
  30. package/dist/backend/effort-metadata.d.ts +37 -1
  31. package/dist/backend/effort-metadata.js +86 -15
  32. package/dist/backend/effort-metadata.js.map +1 -1
  33. package/dist/backend/grok.d.ts +3 -0
  34. package/dist/backend/grok.js +9 -0
  35. package/dist/backend/grok.js.map +1 -1
  36. package/dist/backend/kiro.d.ts +3 -1
  37. package/dist/backend/kiro.js +7 -3
  38. package/dist/backend/kiro.js.map +1 -1
  39. package/dist/backend/launch-effort.d.ts +61 -0
  40. package/dist/backend/launch-effort.js +170 -0
  41. package/dist/backend/launch-effort.js.map +1 -0
  42. package/dist/backend/muse.js +10 -2
  43. package/dist/backend/muse.js.map +1 -1
  44. package/dist/backend/types.d.ts +24 -0
  45. package/dist/backend/types.js +12 -1
  46. package/dist/backend/types.js.map +1 -1
  47. package/dist/backend-command.d.ts +91 -0
  48. package/dist/backend-command.js +122 -0
  49. package/dist/backend-command.js.map +1 -0
  50. package/dist/channel/adapters/discord.d.ts +14 -0
  51. package/dist/channel/adapters/discord.js +66 -4
  52. package/dist/channel/adapters/discord.js.map +1 -1
  53. package/dist/channel/adapters/telegram.d.ts +26 -1
  54. package/dist/channel/adapters/telegram.js +98 -7
  55. package/dist/channel/adapters/telegram.js.map +1 -1
  56. package/dist/channel/mcp-tools.js +36 -0
  57. package/dist/channel/mcp-tools.js.map +1 -1
  58. package/dist/channel/tool-router.d.ts +2 -0
  59. package/dist/channel/tool-router.js +1 -1
  60. package/dist/channel/tool-router.js.map +1 -1
  61. package/dist/channel/types.d.ts +31 -0
  62. package/dist/cli.js +124 -24
  63. package/dist/cli.js.map +1 -1
  64. package/dist/command-table.d.ts +2 -2
  65. package/dist/command-table.js +5 -2
  66. package/dist/command-table.js.map +1 -1
  67. package/dist/compact-watch.d.ts +86 -0
  68. package/dist/compact-watch.js +288 -0
  69. package/dist/compact-watch.js.map +1 -0
  70. package/dist/config-validator.js +55 -0
  71. package/dist/config-validator.js.map +1 -1
  72. package/dist/config.d.ts +1 -0
  73. package/dist/config.js +2 -1
  74. package/dist/config.js.map +1 -1
  75. package/dist/daemon.d.ts +9 -0
  76. package/dist/daemon.js +394 -29
  77. package/dist/daemon.js.map +1 -1
  78. package/dist/delivery-outbox.d.ts +8 -1
  79. package/dist/delivery-outbox.js +6 -2
  80. package/dist/delivery-outbox.js.map +1 -1
  81. package/dist/doctor.js +7 -0
  82. package/dist/doctor.js.map +1 -1
  83. package/dist/fleet-context.d.ts +12 -0
  84. package/dist/fleet-manager.d.ts +247 -9
  85. package/dist/fleet-manager.js +1227 -142
  86. package/dist/fleet-manager.js.map +1 -1
  87. package/dist/forged-envelope-scan.d.ts +4 -3
  88. package/dist/forged-envelope-scan.js +4 -3
  89. package/dist/forged-envelope-scan.js.map +1 -1
  90. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  91. package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
  92. package/dist/install-lock.d.ts +18 -1
  93. package/dist/install-lock.js +30 -3
  94. package/dist/install-lock.js.map +1 -1
  95. package/dist/instance-avatar.d.ts +5 -4
  96. package/dist/instance-avatar.js +15 -8
  97. package/dist/instance-avatar.js.map +1 -1
  98. package/dist/instance-config-impact.js +3 -0
  99. package/dist/instance-config-impact.js.map +1 -1
  100. package/dist/instance-lifecycle.js +9 -4
  101. package/dist/instance-lifecycle.js.map +1 -1
  102. package/dist/instructions.js +2 -2
  103. package/dist/instructions.js.map +1 -1
  104. package/dist/kiro-compact-pane.d.ts +38 -0
  105. package/dist/kiro-compact-pane.js +57 -0
  106. package/dist/kiro-compact-pane.js.map +1 -0
  107. package/dist/launchd-handoff.d.ts +43 -0
  108. package/dist/launchd-handoff.js +56 -0
  109. package/dist/launchd-handoff.js.map +1 -0
  110. package/dist/locale.js +202 -30
  111. package/dist/locale.js.map +1 -1
  112. package/dist/log-service.js +10 -2
  113. package/dist/log-service.js.map +1 -1
  114. package/dist/login-shell.d.ts +21 -0
  115. package/dist/login-shell.js +98 -0
  116. package/dist/login-shell.js.map +1 -0
  117. package/dist/needs-you-hub.d.ts +4 -0
  118. package/dist/needs-you-hub.js +31 -6
  119. package/dist/needs-you-hub.js.map +1 -1
  120. package/dist/needs-you.d.ts +1 -13
  121. package/dist/needs-you.js +3 -37
  122. package/dist/needs-you.js.map +1 -1
  123. package/dist/outbound-handlers.d.ts +32 -0
  124. package/dist/outbound-handlers.js +150 -45
  125. package/dist/outbound-handlers.js.map +1 -1
  126. package/dist/outbound-schemas.d.ts +76 -0
  127. package/dist/outbound-schemas.js +65 -4
  128. package/dist/outbound-schemas.js.map +1 -1
  129. package/dist/provider-probe.d.ts +29 -0
  130. package/dist/provider-probe.js +198 -2
  131. package/dist/provider-probe.js.map +1 -1
  132. package/dist/public-web-gateway.d.ts +2 -0
  133. package/dist/public-web-gateway.js +25 -1
  134. package/dist/public-web-gateway.js.map +1 -1
  135. package/dist/quickstart-api.d.ts +1 -1
  136. package/dist/quickstart-api.js +8 -6
  137. package/dist/quickstart-api.js.map +1 -1
  138. package/dist/quickstart.d.ts +12 -0
  139. package/dist/quickstart.js +36 -6
  140. package/dist/quickstart.js.map +1 -1
  141. package/dist/reply-buttons-controller.d.ts +86 -5
  142. package/dist/reply-buttons-controller.js +271 -7
  143. package/dist/reply-buttons-controller.js.map +1 -1
  144. package/dist/reply-buttons.d.ts +48 -3
  145. package/dist/reply-buttons.js +105 -10
  146. package/dist/reply-buttons.js.map +1 -1
  147. package/dist/reply-polls.d.ts +85 -0
  148. package/dist/reply-polls.js +154 -0
  149. package/dist/reply-polls.js.map +1 -0
  150. package/dist/repo-worktree-guard.d.ts +36 -0
  151. package/dist/repo-worktree-guard.js +143 -0
  152. package/dist/repo-worktree-guard.js.map +1 -0
  153. package/dist/restart-guard.d.ts +6 -3
  154. package/dist/restart-guard.js +6 -3
  155. package/dist/restart-guard.js.map +1 -1
  156. package/dist/restart-progress.d.ts +0 -2
  157. package/dist/restart-progress.js +0 -2
  158. package/dist/restart-progress.js.map +1 -1
  159. package/dist/scheduler/scheduler.d.ts +58 -1
  160. package/dist/scheduler/scheduler.js +176 -10
  161. package/dist/scheduler/scheduler.js.map +1 -1
  162. package/dist/sd-notify.d.ts +22 -1
  163. package/dist/sd-notify.js +63 -10
  164. package/dist/sd-notify.js.map +1 -1
  165. package/dist/service-activation.d.ts +15 -7
  166. package/dist/service-activation.js +42 -26
  167. package/dist/service-activation.js.map +1 -1
  168. package/dist/service-installer.d.ts +24 -0
  169. package/dist/service-installer.js +49 -1
  170. package/dist/service-installer.js.map +1 -1
  171. package/dist/service-plan.d.ts +11 -0
  172. package/dist/service-plan.js +23 -0
  173. package/dist/service-plan.js.map +1 -1
  174. package/dist/settings-api.js +74 -1
  175. package/dist/settings-api.js.map +1 -1
  176. package/dist/settings-change.js +8 -0
  177. package/dist/settings-change.js.map +1 -1
  178. package/dist/settings-effect.js +16 -2
  179. package/dist/settings-effect.js.map +1 -1
  180. package/dist/settings-http-confirmation.js +5 -0
  181. package/dist/settings-http-confirmation.js.map +1 -1
  182. package/dist/systemd-peer-bus.d.ts +95 -0
  183. package/dist/systemd-peer-bus.js +430 -0
  184. package/dist/systemd-peer-bus.js.map +1 -0
  185. package/dist/systemd-runtime.d.ts +27 -1
  186. package/dist/systemd-runtime.js +69 -7
  187. package/dist/systemd-runtime.js.map +1 -1
  188. package/dist/tool-permissions.js +53 -3
  189. package/dist/tool-permissions.js.map +1 -1
  190. package/dist/topic-commands.d.ts +72 -2
  191. package/dist/topic-commands.js +156 -38
  192. package/dist/topic-commands.js.map +1 -1
  193. package/dist/topic-link.d.ts +128 -0
  194. package/dist/topic-link.js +269 -0
  195. package/dist/topic-link.js.map +1 -0
  196. package/dist/transcript-jsonl.d.ts +7 -2
  197. package/dist/transcript-jsonl.js +13 -7
  198. package/dist/transcript-jsonl.js.map +1 -1
  199. package/dist/transcript-sources.js +6 -1
  200. package/dist/transcript-sources.js.map +1 -1
  201. package/dist/types.d.ts +21 -0
  202. package/dist/ui/app-effects.js +187 -0
  203. package/dist/ui/chat-commands.js +1 -0
  204. package/dist/ui/chat-effect-posters.js +42 -0
  205. package/dist/ui/chat-effects.js +72 -0
  206. package/dist/ui/chat-live-replies.js +58 -0
  207. package/dist/ui/chat-render.js +59 -14
  208. package/dist/ui/chat-store.js +78 -8
  209. package/dist/ui/chat-strings.js +43 -16
  210. package/dist/ui/chat-thread.js +175 -31
  211. package/dist/ui/chat-tour.js +18 -15
  212. package/dist/ui/effect-trial-controller.js +119 -0
  213. package/dist/ui/effect-trial-offer.js +31 -0
  214. package/dist/ui/effect-trial-panel.js +37 -0
  215. package/dist/ui/image-lightbox.js +1 -1
  216. package/dist/ui/panel-chat.js +101 -27
  217. package/dist/ui/panel-details.js +18 -5
  218. package/dist/ui/panel-fleet.js +18 -5
  219. package/dist/ui/panel-log.js +10 -10
  220. package/dist/ui/panel-needs.js +13 -10
  221. package/dist/ui/panel-settings.js +66 -12
  222. package/dist/ui/preview-panel.js +25 -9
  223. package/dist/ui/settings-appearance.js +325 -0
  224. package/dist/ui/settings-dialogs.js +11 -6
  225. package/dist/ui/settings-effects.js +201 -0
  226. package/dist/ui/settings-model.js +3 -1
  227. package/dist/ui/settings-mods.js +257 -0
  228. package/dist/ui/settings-owner-effects.js +80 -0
  229. package/dist/ui/settings-strings.js +269 -10
  230. package/dist/ui/settings-token.js +7 -0
  231. package/dist/ui/settings-wizard.js +5 -3
  232. package/dist/ui/shared/agend-auth.js +27 -3
  233. package/dist/ui/shared/agent-widget.js +248 -0
  234. package/dist/ui/shared/app-i18n.js +3 -1
  235. package/dist/ui/shared/app-prefs.js +30 -10
  236. package/dist/ui/shared/app-route.js +23 -1
  237. package/dist/ui/shared/app-session.js +1 -1
  238. package/dist/ui/shared/app-shell.js +29 -16
  239. package/dist/ui/shared/app-store.js +2 -0
  240. package/dist/ui/shared/app-stream.js +15 -7
  241. package/dist/ui/shared/app-themes.js +188 -0
  242. package/dist/ui/shared/app.css +266 -11
  243. package/dist/ui/shared/app.js +6 -0
  244. package/dist/ui/shared/card-strings.js +28 -0
  245. package/dist/ui/shared/effect-app-runtime.js +248 -0
  246. package/dist/ui/shared/effect-baseline.js +64 -0
  247. package/dist/ui/shared/effect-chat-runtime.js +154 -0
  248. package/dist/ui/shared/effect-cursor.js +125 -0
  249. package/dist/ui/shared/effect-frame.js +127 -0
  250. package/dist/ui/shared/effect-geometry.js +122 -0
  251. package/dist/ui/shared/effect-host-style.js +10 -0
  252. package/dist/ui/shared/effect-owner-regions.js +74 -0
  253. package/dist/ui/shared/effect-owner-source.js +115 -0
  254. package/dist/ui/shared/effect-policy.js +137 -0
  255. package/dist/ui/shared/effect-poster-reader.js +98 -0
  256. package/dist/ui/shared/effect-posters.js +117 -0
  257. package/dist/ui/shared/effect-profiler.js +162 -0
  258. package/dist/ui/shared/effect-profiles.js +65 -0
  259. package/dist/ui/shared/effect-registry.js +496 -0
  260. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  261. package/dist/ui/shared/effect-safety.js +153 -0
  262. package/dist/ui/shared/effect-stock-snapshot.js +34 -0
  263. package/dist/ui/shared/effect-stock-trust.js +165 -0
  264. package/dist/ui/shared/effect-trial-source.js +103 -0
  265. package/dist/ui/shared/effect-trial-stage.js +147 -0
  266. package/dist/ui/shared/header-tools.js +35 -15
  267. package/dist/ui/shared/identity-card.js +202 -0
  268. package/dist/ui/shared/identity-color.js +261 -0
  269. package/dist/ui/shared/instance-nav.js +22 -14
  270. package/dist/ui/shared/instance-switch.js +5 -2
  271. package/dist/ui/shared/owner-css.js +221 -0
  272. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  273. package/dist/ui/shared/panel-view.js +96 -12
  274. package/dist/ui/shared/signin.js +17 -9
  275. package/dist/ui/shared/ui-dialog.js +1 -1
  276. package/dist/ui/shared/ui-icons.js +3 -0
  277. package/dist/ui/shared/ui-menu.js +1 -1
  278. package/dist/ui/shared/ui-toast.js +1 -1
  279. package/dist/ui/shared/view-strings.js +40 -2
  280. package/dist/ui/signin.html +9 -3
  281. package/dist/update-check.d.ts +31 -0
  282. package/dist/update-check.js +47 -0
  283. package/dist/update-check.js.map +1 -1
  284. package/dist/update-launch.d.ts +48 -1
  285. package/dist/update-launch.js +159 -5
  286. package/dist/update-launch.js.map +1 -1
  287. package/dist/usage/providers.d.ts +28 -8
  288. package/dist/usage/providers.js +96 -41
  289. package/dist/usage/providers.js.map +1 -1
  290. package/dist/view-api.d.ts +29 -0
  291. package/dist/view-api.js +214 -17
  292. package/dist/view-api.js.map +1 -1
  293. package/dist/web-api.d.ts +86 -2
  294. package/dist/web-api.js +1160 -92
  295. package/dist/web-api.js.map +1 -1
  296. package/dist/web-appearance-limits.d.ts +82 -0
  297. package/dist/web-appearance-limits.js +83 -0
  298. package/dist/web-appearance-limits.js.map +1 -0
  299. package/dist/web-appearance-policy.d.ts +66 -0
  300. package/dist/web-appearance-policy.js +94 -0
  301. package/dist/web-appearance-policy.js.map +1 -0
  302. package/dist/web-appearance-tools.d.ts +50 -0
  303. package/dist/web-appearance-tools.js +105 -0
  304. package/dist/web-appearance-tools.js.map +1 -0
  305. package/dist/web-appearance-view.d.ts +99 -0
  306. package/dist/web-appearance-view.js +163 -0
  307. package/dist/web-appearance-view.js.map +1 -0
  308. package/dist/web-appearance.d.ts +79 -0
  309. package/dist/web-appearance.js +298 -0
  310. package/dist/web-appearance.js.map +1 -0
  311. package/dist/web-asset-parts.d.ts +98 -0
  312. package/dist/web-asset-parts.js +133 -0
  313. package/dist/web-asset-parts.js.map +1 -0
  314. package/dist/web-auth.js +17 -1
  315. package/dist/web-auth.js.map +1 -1
  316. package/dist/web-card.d.ts +96 -0
  317. package/dist/web-card.js +232 -0
  318. package/dist/web-card.js.map +1 -0
  319. package/dist/web-chat-history.d.ts +39 -1
  320. package/dist/web-chat-history.js +69 -9
  321. package/dist/web-chat-history.js.map +1 -1
  322. package/dist/web-commands.d.ts +39 -3
  323. package/dist/web-commands.js +22 -5
  324. package/dist/web-commands.js.map +1 -1
  325. package/dist/web-effect-bundle.d.ts +28 -0
  326. package/dist/web-effect-bundle.js +126 -0
  327. package/dist/web-effect-bundle.js.map +1 -0
  328. package/dist/web-effect-canonical.d.ts +8 -0
  329. package/dist/web-effect-canonical.js +27 -0
  330. package/dist/web-effect-canonical.js.map +1 -0
  331. package/dist/web-effect-library.d.ts +114 -0
  332. package/dist/web-effect-library.js +530 -0
  333. package/dist/web-effect-library.js.map +1 -0
  334. package/dist/web-effect-poster.d.ts +8 -0
  335. package/dist/web-effect-poster.js +52 -0
  336. package/dist/web-effect-poster.js.map +1 -0
  337. package/dist/web-effect-profiles.d.ts +118 -0
  338. package/dist/web-effect-profiles.js +180 -0
  339. package/dist/web-effect-profiles.js.map +1 -0
  340. package/dist/web-effect-service.d.ts +86 -0
  341. package/dist/web-effect-service.js +312 -0
  342. package/dist/web-effect-service.js.map +1 -0
  343. package/dist/web-effect-tools.d.ts +47 -0
  344. package/dist/web-effect-tools.js +126 -0
  345. package/dist/web-effect-tools.js.map +1 -0
  346. package/dist/web-effect-trials.d.ts +86 -0
  347. package/dist/web-effect-trials.js +204 -0
  348. package/dist/web-effect-trials.js.map +1 -0
  349. package/dist/web-effect-upload.d.ts +29 -0
  350. package/dist/web-effect-upload.js +190 -0
  351. package/dist/web-effect-upload.js.map +1 -0
  352. package/dist/web-effect-worker.d.ts +8 -0
  353. package/dist/web-effect-worker.js +170 -0
  354. package/dist/web-effect-worker.js.map +1 -0
  355. package/dist/web-effect.d.ts +89 -0
  356. package/dist/web-effect.js +132 -0
  357. package/dist/web-effect.js.map +1 -0
  358. package/dist/web-identity-color.d.ts +87 -0
  359. package/dist/web-identity-color.js +209 -0
  360. package/dist/web-identity-color.js.map +1 -0
  361. package/dist/web-identity-palette.d.ts +113 -0
  362. package/dist/web-identity-palette.js +84 -0
  363. package/dist/web-identity-palette.js.map +1 -0
  364. package/dist/web-image-meta.d.ts +25 -0
  365. package/dist/web-image-meta.js +200 -0
  366. package/dist/web-image-meta.js.map +1 -0
  367. package/dist/web-login.d.ts +14 -0
  368. package/dist/web-login.js +4 -2
  369. package/dist/web-login.js.map +1 -1
  370. package/dist/web-mod-agent.d.ts +50 -0
  371. package/dist/web-mod-agent.js +203 -0
  372. package/dist/web-mod-agent.js.map +1 -0
  373. package/dist/web-mod-bundle.d.ts +144 -0
  374. package/dist/web-mod-bundle.js +464 -0
  375. package/dist/web-mod-bundle.js.map +1 -0
  376. package/dist/web-mod-export.d.ts +59 -0
  377. package/dist/web-mod-export.js +79 -0
  378. package/dist/web-mod-export.js.map +1 -0
  379. package/dist/web-mod-store.d.ts +283 -0
  380. package/dist/web-mod-store.js +624 -0
  381. package/dist/web-mod-store.js.map +1 -0
  382. package/dist/web-org.d.ts +3 -3
  383. package/dist/web-org.js +11 -17
  384. package/dist/web-org.js.map +1 -1
  385. package/dist/web-owner-css-store.d.ts +180 -0
  386. package/dist/web-owner-css-store.js +384 -0
  387. package/dist/web-owner-css-store.js.map +1 -0
  388. package/dist/web-owner-css.d.ts +58 -0
  389. package/dist/web-owner-css.js +552 -0
  390. package/dist/web-owner-css.js.map +1 -0
  391. package/dist/web-prefs.d.ts +66 -0
  392. package/dist/web-prefs.js +243 -35
  393. package/dist/web-prefs.js.map +1 -1
  394. package/dist/web-preview.d.ts +9 -1
  395. package/dist/web-preview.js +24 -7
  396. package/dist/web-preview.js.map +1 -1
  397. package/dist/web-profile.d.ts +32 -0
  398. package/dist/web-profile.js +132 -0
  399. package/dist/web-profile.js.map +1 -0
  400. package/dist/web-session.d.ts +14 -0
  401. package/dist/web-session.js +10 -0
  402. package/dist/web-session.js.map +1 -1
  403. package/dist/web-settings-patch.d.ts +18 -0
  404. package/dist/web-settings-patch.js +51 -0
  405. package/dist/web-settings-patch.js.map +1 -1
  406. package/dist/web-shell-routes.d.ts +1 -1
  407. package/dist/web-shell-routes.js +1 -1
  408. package/dist/web-shell-routes.js.map +1 -1
  409. package/dist/web-theme-store.d.ts +218 -0
  410. package/dist/web-theme-store.js +516 -0
  411. package/dist/web-theme-store.js.map +1 -0
  412. package/dist/web-theme.d.ts +121 -0
  413. package/dist/web-theme.js +407 -0
  414. package/dist/web-theme.js.map +1 -0
  415. package/dist/web-upload.d.ts +119 -4
  416. package/dist/web-upload.js +252 -39
  417. package/dist/web-upload.js.map +1 -1
  418. package/dist/web-widget.d.ts +72 -0
  419. package/dist/web-widget.js +245 -0
  420. package/dist/web-widget.js.map +1 -0
  421. package/launcher/install-admission.cjs +61 -6
  422. package/package.json +3 -3
@@ -0,0 +1,248 @@
1
+ // #1629 r4-4 (option A): widgets — an agent's in its own chat header and Details page, a person's in their sidebar —
2
+ // drawn by the page's own components from a closed declarative spec and its values (src/web-widget.ts). Nothing from a
3
+ // mod becomes markup, script, a handler, a URL or a style: text is text nodes, a number fills a <progress> or an SVG
4
+ // polyline's points, and tones are fixed class tokens. Colours are the theme's tokens (and a person's own CSS). The box
5
+ // always carries the page's own ✦ chip, so a widget never reads as AgEnD's chrome, and nothing in it can be clicked.
6
+ //
7
+ // What the host sends is checked again here with the same rules; a widget that fails is not drawn, and a value that
8
+ // fails shows "—". A `bind` is a value the page already has (the instance's own state, context and cost; the fleet's
9
+ // counts in the person's sidebar only) — nothing new is fetched for it.
10
+ import { html } from "./app-html.js";
11
+ import { register, t } from "./app-i18n.js";
12
+ import { statusFacet } from "./instance-nav.js";
13
+ import { themesShown, themesStore } from "./app-themes.js";
14
+ import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
15
+ import { createStore } from "./app-store.js";
16
+ import { safeMode } from "./owner-css.js";
17
+
18
+ register("widget", {
19
+ en: {
20
+ label: "Widget: {0}",
21
+ chip: "A widget — drawn by AgEnD from its owner's data",
22
+ byHuman: "placed by a person",
23
+ none: "—",
24
+ },
25
+ "zh-TW": {
26
+ label: "小工具:{0}",
27
+ chip: "小工具——由 AgEnD 依擁有者的資料繪製",
28
+ byHuman: "由人放置",
29
+ none: "—",
30
+ },
31
+ });
32
+
33
+ export const WIDGET_ITEM_TYPES = ["stat", "progress", "text", "list", "badge", "spark"];
34
+ export const WIDGET_TONES = ["ok", "warn", "bad", "info", "muted"];
35
+ const INSTANCE_BINDS = ["state", "context_pct", "cost_usd"];
36
+ const FLEET_BINDS = ["fleet.idle", "fleet.working", "fleet.needs_you", "fleet.stopped"];
37
+ export const SLOT_RULES = {
38
+ chat_header: { maxItems: 4, types: ["stat", "progress", "text", "badge"], binds: INSTANCE_BINDS },
39
+ details: { maxItems: 8, types: WIDGET_ITEM_TYPES, binds: INSTANCE_BINDS },
40
+ sidebar: { maxItems: 8, types: WIDGET_ITEM_TYPES, binds: FLEET_BINDS },
41
+ };
42
+ const BINDS_FOR = {
43
+ stat: ["context_pct", "cost_usd", ...FLEET_BINDS], progress: ["context_pct"], text: ["state"], badge: ["state"], list: [], spark: [],
44
+ };
45
+ const ITEM_KEYS = {
46
+ stat: ["type", "label", "key", "bind", "unit"], progress: ["type", "label", "key", "bind", "max"], text: ["type", "label", "key", "bind"],
47
+ list: ["type", "label", "key"], badge: ["type", "label", "key", "bind", "tone", "tones"], spark: ["type", "label", "key"],
48
+ };
49
+ const LABEL_REQUIRED = ["stat", "progress", "spark"];
50
+ const KEY = /^[a-z][a-z0-9_]{0,31}$/;
51
+ const NUMBER_MAX = 1e15;
52
+ const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
53
+ const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
54
+
55
+ // One line of text, as the host's canonicalText: NFC, no control, format or separator character but the zero-width
56
+ // joiner, at least one character that draws something, 1..max graphemes.
57
+ const FORBIDDEN = /(?!‍)[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
58
+ const VISIBLE = /[^\p{M}\p{C}\p{Z}ᅟᅠㅤᅠ⠀]/u;
59
+ let graphemes = null;
60
+ export function lineOk(s, max) {
61
+ if (typeof s !== "string" || s.normalize("NFC") !== s || FORBIDDEN.test(s) || s.trim() !== s || !VISIBLE.test(s)) return false;
62
+ try { graphemes ??= new Intl.Segmenter(undefined, { granularity: "grapheme" }); } catch { return false; }
63
+ const n = [...graphemes.segment(s)].length;
64
+ return n >= 1 && n <= max;
65
+ }
66
+ const isNumber = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= NUMBER_MAX;
67
+
68
+ /** Whether `v` is a value an item of `type` can show (web-widget.ts valueFits). */
69
+ export function valueFits(type, v) {
70
+ switch (type) {
71
+ case "stat": return isNumber(v) || lineOk(v, 24);
72
+ case "progress": return isNumber(v);
73
+ case "text": return lineOk(v, 120);
74
+ case "badge": return lineOk(v, 24);
75
+ case "list": return Array.isArray(v) && v.length >= 1 && v.length <= 6 && v.every((s) => lineOk(s, 60));
76
+ case "spark": return Array.isArray(v) && v.length >= 2 && v.length <= 48 && v.every(isNumber);
77
+ default: return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * A widget `{ spec, data, by? }` for `slot`, checked again: `{ widget }`, or `{ problem }` (where, and what) when its
83
+ * spec fails — then the whole widget is not drawn. A data value that fails is left out (shown as "—").
84
+ */
85
+ export function explainWidget(slot, w) {
86
+ const no = (problem) => ({ problem });
87
+ const rules = own(SLOT_RULES, slot) ? SLOT_RULES[slot] : null;
88
+ if (!rules) return no("slot");
89
+ if (!isObj(w) || !isObj(w.spec)) return no("spec: an object { v: 1, title, items }");
90
+ const s = w.spec;
91
+ const extra = Object.keys(s).find((k) => !["v", "title", "items"].includes(k));
92
+ if (extra) return no(`spec.${extra}: unknown key`);
93
+ if (s.v !== 1) return no("spec.v: must be 1");
94
+ if (!lineOk(s.title, 24)) return no("spec.title: 1..24 characters on one line");
95
+ if (!Array.isArray(s.items) || s.items.length < 1 || s.items.length > rules.maxItems) return no(`spec.items: 1..${rules.maxItems} items`);
96
+ const items = [];
97
+ for (const [i, it] of s.items.entries()) {
98
+ const at = `spec.items.${i}`;
99
+ if (!isObj(it) || !rules.types.includes(it.type)) return no(`${at}.type: ${rules.types.join(", ")}`);
100
+ const unknown = Object.keys(it).find((k) => !ITEM_KEYS[it.type].includes(k));
101
+ if (unknown) return no(`${at}.${unknown}: unknown key`);
102
+ if ((it.label !== undefined || LABEL_REQUIRED.includes(it.type)) && !lineOk(it.label, 24)) return no(`${at}.label: 1..24 characters`);
103
+ if ((it.key === undefined) === (it.bind === undefined)) return no(`${at}: exactly one of key or bind`);
104
+ if (it.key !== undefined && !(typeof it.key === "string" && KEY.test(it.key))) return no(`${at}.key: a-z, 0-9 and _`);
105
+ if (it.bind !== undefined && !(BINDS_FOR[it.type].includes(it.bind) && rules.binds.includes(it.bind))) return no(`${at}.bind: ${BINDS_FOR[it.type].filter((b) => rules.binds.includes(b)).join(", ") || "none here"}`);
106
+ if (it.unit !== undefined && !lineOk(it.unit, 8)) return no(`${at}.unit: 1..8 characters`);
107
+ if (it.max !== undefined && !(isNumber(it.max) && it.max > 0)) return no(`${at}.max: a number > 0`);
108
+ if (it.tone !== undefined && !WIDGET_TONES.includes(it.tone)) return no(`${at}.tone: ${WIDGET_TONES.join(", ")}`);
109
+ let tones;
110
+ if (it.tones !== undefined) {
111
+ if (!isObj(it.tones)) return no(`${at}.tones: { value: tone }`);
112
+ tones = new Map();
113
+ for (const [k, v] of Object.entries(it.tones)) { if (!lineOk(k, 24) || !WIDGET_TONES.includes(v)) return no(`${at}.tones: { value: ${WIDGET_TONES.join(" | ")} }`); tones.set(k, v); }
114
+ if (tones.size < 1 || tones.size > 8) return no(`${at}.tones: 1..8 entries`);
115
+ }
116
+ items.push({ type: it.type, label: it.label, key: it.key, bind: it.bind, unit: it.unit, max: it.max, tone: it.tone, tones });
117
+ }
118
+ const data = new Map();
119
+ if (isObj(w.data)) items.forEach((it, i) => { if (it.key && own(w.data, it.key) && valueFits(it.type, w.data[it.key])) data.set(`${i}`, w.data[it.key]); });
120
+ return { widget: { title: s.title, items, data, by: w.by === "human" ? "human" : "agent" } };
121
+ }
122
+ /** explainWidget's widget, or null. */
123
+ export function checkWidget(slot, w) { return explainWidget(slot, w).widget || null; }
124
+
125
+ /**
126
+ * What the host would refuse in a person's `{ spec, data }` for `slot` (its whole check, web-widget.ts and web-prefs.ts),
127
+ * or null — so the editor can say why before anything is saved.
128
+ */
129
+ export function widgetProblem(slot, v) {
130
+ if (!isObj(v) || Object.keys(v).length !== 2 || !own(v, "spec") || !own(v, "data")) return "{ \"spec\": …, \"data\": … }";
131
+ const r = explainWidget(slot, v);
132
+ if (r.problem) return r.problem;
133
+ if (!isObj(v.data)) return "data: an object { key: value }";
134
+ for (const [k, val] of Object.entries(v.data)) {
135
+ const users = r.widget.items.filter((it) => it.key === k);
136
+ if (!users.length) return `data.${k}: no item shows this key`;
137
+ const bad = users.find((it) => !valueFits(it.type, val));
138
+ if (bad) return `data.${k}: not a value a ${bad.type} shows`;
139
+ }
140
+ const bytes = (x) => new TextEncoder().encode(JSON.stringify(x)).length;
141
+ if (bytes(v.spec) > 2048) return "spec: at most 2048 bytes";
142
+ if (bytes(v.data) > 4096) return "data: at most 4096 bytes";
143
+ return null;
144
+ }
145
+
146
+ /** The values the page has for one instance: its state word (a sidebar facet), context % and cost. */
147
+ export function instanceBinds(inst, exec, awaiting) {
148
+ if (!inst) return {};
149
+ return {
150
+ state: statusFacet(inst, exec, awaiting),
151
+ context_pct: isNumber(inst.context_pct) ? Math.round(Math.max(0, Math.min(100, inst.context_pct))) : undefined,
152
+ cost_usd: isNumber(inst.cost) ? inst.cost : undefined,
153
+ };
154
+ }
155
+ /** The fleet's counts, for the person's sidebar widget only: how many instances are idle, working, need you, stopped. */
156
+ export function fleetBinds(instances, exec, awaiting) {
157
+ const n = { "fleet.idle": 0, "fleet.working": 0, "fleet.needs_you": 0, "fleet.stopped": 0 };
158
+ for (const it of Array.isArray(instances) ? instances : []) {
159
+ const f = statusFacet(it, exec && exec[it.name], awaiting && own(awaiting, it.name) ? awaiting[it.name] : null);
160
+ n[f === "needs" ? "fleet.needs_you" : f === "working" ? "fleet.working" : f === "idle" ? "fleet.idle" : "fleet.stopped"] += 1;
161
+ }
162
+ return n;
163
+ }
164
+
165
+ let numbers = null;
166
+ function fmt(v) {
167
+ try { numbers ??= new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }); return numbers.format(v); } catch { return String(v); }
168
+ }
169
+ /** An item's value: its data, or its bind; undefined when there is none (shown as "—"). */
170
+ function valueOf(item, i, w, binds) {
171
+ if (item.key) return w.data.get(`${i}`);
172
+ const v = binds && own(binds, item.bind) ? binds[item.bind] : undefined;
173
+ return item.bind === "state" ? (lineOk(v, 24) ? v : undefined) : isNumber(v) ? v : undefined;
174
+ }
175
+ function shown(item, v) {
176
+ if (v === undefined) return t("widget.none");
177
+ if (item.bind === "cost_usd") return `$${v.toFixed(2)}`;
178
+ if (item.bind === "context_pct") return `${v}%`;
179
+ return typeof v === "number" ? fmt(v) : v;
180
+ }
181
+ /** The sparkline's points in a 100×24 box (numbers only; nothing else reaches the SVG). */
182
+ export function sparkPoints(values) {
183
+ const lo = Math.min(...values), hi = Math.max(...values), span = hi - lo || 1;
184
+ return values.map((v, i) => `${((i / (values.length - 1)) * 100).toFixed(2)},${(22 - ((v - lo) / span) * 20).toFixed(2)}`).join(" ");
185
+ }
186
+
187
+ function Item({ item, v }) {
188
+ const label = item.label ? html`<span class="aw-label">${item.label}</span>` : null;
189
+ switch (item.type) {
190
+ case "stat":
191
+ return html`<div class="aw-item aw-stat">${label}<span class="aw-value">${shown(item, v)}${v !== undefined && item.unit ? html`<span class="aw-unit">${item.unit}</span>` : null}</span></div>`;
192
+ case "progress": {
193
+ const max = item.max ?? 100;
194
+ const pct = v === undefined ? null : Math.round((Math.max(0, Math.min(max, v)) / max) * 100);
195
+ // No value: no bar at all (a <progress> without one is the browser's animated "busy" bar), only "—".
196
+ return html`<div class="aw-item aw-progress">${label}${pct === null ? null : html`<progress class="aw-bar" max=${max} value=${Math.max(0, Math.min(max, v))} aria-label=${item.label}></progress>`}<span class="aw-value">${pct === null ? t("widget.none") : `${pct}%`}</span></div>`;
197
+ }
198
+ case "text":
199
+ return html`<div class="aw-item aw-text">${label}<span class="aw-value">${shown(item, v)}</span></div>`;
200
+ case "badge": {
201
+ const tone = v !== undefined && item.tones && item.tones.has(v) ? item.tones.get(v) : item.tone || "muted";
202
+ return html`<div class="aw-item aw-badge-item">${label}<span class=${`aw-badge aw-tone-${WIDGET_TONES.includes(tone) ? tone : "muted"}`}>${shown(item, v)}</span></div>`;
203
+ }
204
+ case "list":
205
+ return html`<div class="aw-item aw-list">${label}${v === undefined ? html`<span class="aw-value">${t("widget.none")}</span>` : html`<ul>${v.map((line, i) => html`<li key=${i}>${line}</li>`)}</ul>`}</div>`;
206
+ case "spark":
207
+ return html`<div class="aw-item aw-spark-item">${label}${v === undefined ? html`<span class="aw-value">${t("widget.none")}</span>`
208
+ : html`<svg class="aw-spark" viewBox="0 0 100 24" preserveAspectRatio="none" aria-hidden="true"><polyline points=${sparkPoints(v)} /></svg><span class="aw-value">${fmt(v[v.length - 1])}</span>`}</div>`;
209
+ default: return null;
210
+ }
211
+ }
212
+
213
+ /** One widget, checked (checkWidget's result), in `slot`. `binds`: instanceBinds or fleetBinds. */
214
+ export function AgentWidget({ slot, widget, binds, tools = null }) {
215
+ if (!widget) return null;
216
+ return html`<section data-trust="widget" class=${`aw aw-${slot}`} aria-label=${t("widget.label", widget.title)} data-widget-slot=${slot}>
217
+ <div class="aw-head"><span class="aw-chip" title=${t("widget.chip")}>✦ ${widget.title}</span>
218
+ ${widget.by === "human" ? html`<span class="aw-by">${t("widget.byHuman")}</span>` : null}${tools}</div>
219
+ <div class="aw-items">${widget.items.map((item, i) => html`<${Item} key=${i} item=${item} v=${valueOf(item, i, widget, binds)} />`)}</div>
220
+ </section>`;
221
+ }
222
+
223
+ /** An agent's widget in `slot` ("chat_header" | "details"), checked; null when none, or agents' looks are not shown here. */
224
+ export function widgetFor(name, slot) {
225
+ const s = themesStore.get();
226
+ if (!s.shown || !themesShown() || !own(s.widgets, name)) return null;
227
+ const slots = s.widgets[name];
228
+ return isObj(slots) && own(slots, slot) ? checkWidget(slot, slots[slot]) : null;
229
+ }
230
+
231
+ const SIDEBAR_KEY = "widget.sidebar";
232
+ const publicLink = () => typeof document !== "undefined" && !!document.body && document.body.dataset.publicLink === "1";
233
+ /** The person's own sidebar widget (a synced preference), raw; null when none. Never on the public link or in safe mode. */
234
+ export function sidebarWidgetRaw() {
235
+ if (publicLink() || safeMode()) return null;
236
+ const f = prefInForce(SIDEBAR_KEY);
237
+ let v = f ? f.value : undefined;
238
+ if (!f) { try { v = JSON.parse(localStorage.getItem("agend_widget_sidebar") || "null"); } catch { v = null; } }
239
+ return isObj(v) ? v : null;
240
+ }
241
+ /** The person's sidebar widget, checked; null when none. */
242
+ export function sidebarWidget() { const v = sidebarWidgetRaw(); return v ? checkWidget("sidebar", v) : null; }
243
+ /** Bumped when the person's sidebar widget changes (here, or on another device), so the sidebar redraws. */
244
+ export const sidebarWidgetStore = createStore({ n: 0 });
245
+ const bump = () => sidebarWidgetStore.set((s) => ({ n: s.n + 1 }));
246
+ onPrefsChanged((changed) => { if (changed.has(SIDEBAR_KEY)) bump(); });
247
+ /** Set (or with null, remove) the person's sidebar widget on all their devices. */
248
+ export function setSidebarWidget(value) { savePref(SIDEBAR_KEY, value); bump(); }
@@ -68,6 +68,7 @@ register("app", {
68
68
  previews: "Allow HTML previews", tour: "Tour", tourTitle: "Show the web chat tour again",
69
69
  needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", log: "Log", instanceViews: "Views of {0}",
70
70
  textSize: "Text size: {0}", textSize_s: "S", textSize_m: "M", textSize_l: "L", textSize_fit: "Fit", usage: "Usage", usageHide: "Hide usage",
71
+ ownerCssKeep: "Keep this custom CSS?", ownerCssKeepLeft: "Reverts in {0} s", ownerCssKeepYes: "Keep", ownerCssKeepNo: "Revert",
71
72
  statusRunning: "running", statusStopped: "stopped", statusPaused: "paused", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
72
73
  connReconnecting: "Can't reach AgEnD — it may be restarting. Reconnecting…", connReconnectingIn: "Can't reach AgEnD — it may be restarting. Trying again in {0} s.",
73
74
  hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
@@ -98,7 +99,8 @@ register("app", {
98
99
  theme: "主題", themeSystem: "跟隨系統", themeLight: "淺色", themeDark: "深色", language: "語言",
99
100
  previews: "允許預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
100
101
  needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", log: "紀錄", instanceViews: "{0} 的檢視",
101
- textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "剛好填滿", usage: "用量", usageHide: "隱藏用量",
102
+ textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "填滿", usage: "用量", usageHide: "隱藏用量",
103
+ ownerCssKeep: "保留這份自訂 CSS?", ownerCssKeepLeft: "{0} 秒後還原", ownerCssKeepYes: "保留", ownerCssKeepNo: "還原",
102
104
  statusRunning: "執行中", statusStopped: "已停止", statusPaused: "已暫停", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
103
105
  connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
104
106
  hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
@@ -33,13 +33,32 @@ export const SYNCED = {
33
33
  "nav.favorites": { ls: "agend_favorites", ...json }, // #1664
34
34
  "preview.optIn": { ls: "agend_html_preview", toLocal: (v) => (v ? "on" : null), fromLocal: (s) => (s === "on" ? true : undefined) },
35
35
  "names.primary": { ls: "agend_names_primary", toLocal: (v) => (v === "instance" ? v : null), fromLocal: (s) => (s === "instance" ? s : undefined) }, // #1722
36
+ // #1629 r2 (d): "Show agents' themes and effects" — on unless turned off (only "off" is cached).
37
+ "fx.show": { ls: "agend_fx_show", toLocal: (v) => (v === false ? "off" : null), fromLocal: (s) => (s === "off" ? false : undefined) },
38
+ // #1629 r4.1 Q3 (a): the identity card's size — every card's default (M unless chosen), and the cards set apart.
39
+ "card.size": { ls: "agend_card_size", toLocal: (v) => (v === "s" || v === "l" ? v : null), fromLocal: (s) => (s === "s" || s === "l" ? s : undefined) },
40
+ "card.sizeByInstance": { ls: "agend_card_sizes", ...json },
41
+ // #1629 r4-4: the person's own sidebar widget ({ spec, data }), drawn by the page's own components.
42
+ "widget.sidebar": { ls: "agend_widget_sidebar", ...json },
43
+ // #1958: where identity colours show by default (ring + bar unless chosen), and the host's switch to turn them off.
44
+ "color.show": { ls: "agend_color_show", ...json },
45
+ "color.off": { ls: "agend_color_off", toLocal: (v) => (v === true ? "on" : null), fromLocal: (s) => (s === "on" ? true : undefined) },
36
46
  };
47
+ // #1629: fixed personal scope. A separate session-bound owner-effect store synchronizes these, never this host cache.
48
+ export const OWNER_EFFECT_PREF_KEYS = Object.freeze(["fx.sidebar", "fx.header", "fx.page", "fx.box", "fx.cursor"]);
49
+ for (const spec of Object.values(SYNCED)) spec.scope = "host";
50
+ for (const key of OWNER_EFFECT_PREF_KEYS) SYNCED[key] = Object.freeze({ scope: "person", ls: null });
51
+ for (const spec of Object.values(SYNCED)) Object.freeze(spec);
52
+ Object.freeze(SYNCED);
53
+ const HOST_KEYS = Object.keys(SYNCED).filter(key => SYNCED[key].scope === "host");
54
+ const hostKey = key => own(SYNCED, key) && SYNCED[key].scope === "host";
55
+ const hostValues = values => Object.fromEntries(HOST_KEYS.filter(key => own(values, key)).map(key => [key, values[key]]));
37
56
  const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
38
57
  const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
39
58
 
40
59
  function readLocal(key) { try { return localStorage.getItem(key); } catch { return null; } }
41
60
  function writeLocal(key, s) { try { if (s == null) localStorage.removeItem(key); else localStorage.setItem(key, s); } catch { /* this page only */ } }
42
- function cache(key, value) { const k = SYNCED[key]; writeLocal(k.ls, value == null ? null : k.toLocal(value)); }
61
+ function cache(key, value) { if (!hostKey(key)) return; const k = SYNCED[key]; writeLocal(k.ls, value == null ? null : k.toLocal(value)); }
43
62
  const inStep = () => readLocal(SYNCED_FLAG) === "1";
44
63
 
45
64
  function pageHost() {
@@ -51,7 +70,7 @@ function pageHost() {
51
70
 
52
71
  const initial = typeof document !== "undefined" ? pageHost() : null;
53
72
  let rev = initial ? initial.rev : null;
54
- let host = initial ? initial.values : null;
73
+ let host = initial ? hostValues(initial.values) : null;
55
74
  const pending = new Map(); // key → value chosen here, not sent yet
56
75
  const mine = new Map(); // key → value chosen here whose write is not over (pending or out)
57
76
  const inflight = new Map(); // key → writes sent and not answered yet
@@ -63,7 +82,7 @@ const listeners = new Set();
63
82
  // the host does not have: the default, on a device in step with the host; this device's own (to upload) otherwise.
64
83
  if (host) {
65
84
  const step = inStep();
66
- for (const key of Object.keys(SYNCED)) {
85
+ for (const key of HOST_KEYS) {
67
86
  if (own(host, key)) cache(key, host[key]);
68
87
  else if (step) cache(key, null);
69
88
  }
@@ -80,14 +99,14 @@ export function prefsWritable() {
80
99
  * in step) — the caller reads this browser's copy.
81
100
  */
82
101
  export function prefInForce(key) {
83
- if (!own(SYNCED, key)) return null;
102
+ if (!hostKey(key)) return null;
84
103
  if (mine.has(key)) return { value: mine.get(key) };
85
104
  if (host && own(host, key)) return { value: host[key] };
86
105
  if (initial && inStep()) return { value: undefined };
87
106
  return null;
88
107
  }
89
108
  /** The host's value for a key as this page last saw it (undefined: not set there, or not known). */
90
- export function hostPref(key) { return host && own(host, key) ? host[key] : undefined; }
109
+ export function hostPref(key) { return hostKey(key) && host && own(host, key) ? host[key] : undefined; }
91
110
  /** Called with the keys another device changed, after they are cached and applied here. Returns the unsubscribe. */
92
111
  export function onPrefsChanged(fn) { listeners.add(fn); return () => listeners.delete(fn); }
93
112
 
@@ -97,7 +116,7 @@ const isView = (body) => !!body && typeof body.rev === "number" && !!body.values
97
116
  function take(body) {
98
117
  if (!isView(body) || (rev !== null && body.rev < rev)) return false;
99
118
  rev = body.rev;
100
- host = body.values;
119
+ host = hostValues(body.values);
101
120
  return true;
102
121
  }
103
122
 
@@ -106,7 +125,7 @@ function take(body) {
106
125
  * (the public link, the anonymous reader): this browser only, as before.
107
126
  */
108
127
  export function savePref(key, value) {
109
- if (!own(SYNCED, key)) return;
128
+ if (!hostKey(key)) return;
110
129
  cache(key, value);
111
130
  if (!prefsWritable()) return;
112
131
  pending.set(key, value);
@@ -165,8 +184,8 @@ export async function refresh() {
165
184
  body = res.ok ? await res.json() : null;
166
185
  } catch { body = null; }
167
186
  if (!isView(body)) return false;
168
- const before = new Map(Object.keys(SYNCED).map((key) => [key, own(host, key) ? SYNCED[key].toLocal(host[key]) : null]));
169
- if (take(body)) align(Object.keys(SYNCED), { before });
187
+ const before = new Map(HOST_KEYS.map((key) => [key, own(host, key) ? SYNCED[key].toLocal(host[key]) : null]));
188
+ if (take(body)) align(HOST_KEYS, { before });
170
189
  return true;
171
190
  }
172
191
 
@@ -179,6 +198,7 @@ export async function refresh() {
179
198
  function align(keys, opts = {}) {
180
199
  const changed = new Set();
181
200
  for (const key of keys) {
201
+ if (!hostKey(key)) continue;
182
202
  if (pending.has(key) || inflight.has(key)) continue;
183
203
  const k = SYNCED[key];
184
204
  const want = own(host, key) ? k.toLocal(host[key]) : null;
@@ -216,7 +236,7 @@ function apply(changed) {
216
236
  */
217
237
  export async function migratePrefs() {
218
238
  if (!prefsWritable() || inStep()) return false;
219
- for (const key of Object.keys(SYNCED)) {
239
+ for (const key of HOST_KEYS) {
220
240
  if (own(host, key)) continue;
221
241
  const local = SYNCED[key].fromLocal(readLocal(SYNCED[key].ls));
222
242
  if (local === undefined) continue;
@@ -2,7 +2,7 @@
2
2
  // a test runs both over one list of cases). Pure: no DOM, no history.
3
3
 
4
4
  export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "config"];
5
- export const SETTINGS_SECTIONS = ["device", "agents", "bots", "classic", "general", "web", "advanced"]; // #1604: device first
5
+ export const SETTINGS_SECTIONS = ["device", "appearance", "agents", "bots", "classic", "general", "web", "advanced"]; // #1604: device first
6
6
  const MAX_INSTANCE_NAME = 128;
7
7
  const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
8
8
 
@@ -62,6 +62,28 @@ export function routePath(route) {
62
62
  if (route.panel === "log") return logPath(route.instance);
63
63
  return route.instance ? chatPath(route.instance) : "/ui";
64
64
  }
65
+ /**
66
+ * #1990: a text with the in-app links the server put on it (ranges of the text: { offset, length, url }) as segments —
67
+ * { text } or { text, href }. Only a route this table parses as an instance's chat or View becomes a link (its href
68
+ * rebuilt here); a range out of order, out of the text, or anything else stays text. No target: the router opens it.
69
+ */
70
+ export function textLinkSegments(text, links) {
71
+ const s = String(text == null ? "" : text);
72
+ const out = [];
73
+ let at = 0;
74
+ for (const l of Array.isArray(links) ? links.slice(0, 16) : []) {
75
+ if (!l || !Number.isInteger(l.offset) || !Number.isInteger(l.length) || typeof l.url !== "string") continue;
76
+ if (l.offset < at || l.length < 1 || l.offset + l.length > s.length || !/^\/[^?#\s]*$/.test(l.url)) continue;
77
+ const route = parseRoute(l.url);
78
+ if (!route || !route.instance || (route.panel !== "chat" && route.panel !== "view")) continue;
79
+ if (l.offset > at) out.push({ text: s.slice(at, l.offset) });
80
+ out.push({ text: s.slice(l.offset, l.offset + l.length), href: routePath(route) });
81
+ at = l.offset + l.length;
82
+ }
83
+ if (at < s.length || !out.length) out.push({ text: s.slice(at) });
84
+ return out;
85
+ }
86
+
65
87
  /** One string per route: a change of it is a new navigation (a new lease for the panel, §4). */
66
88
  export function routeKey(route) {
67
89
  if (!route) return "none";
@@ -49,7 +49,7 @@ export function SessionMenu() {
49
49
  <button ref=${btn} type="button" class="side-row" aria-haspopup="dialog" aria-expanded=${open ? "true" : "false"}
50
50
  onClick=${() => { const next = !open; setOpen(next); if (next) refresh(); }}>
51
51
  <span class=${`dot${me ? " ok" : ""}`} aria-hidden="true"></span><span class="grow">${label}</span>${me ? html`<span class="meta">${me.label}</span>` : null}</button>
52
- ${open ? html`<div ref=${pop} class="pop" role="dialog" aria-label=${t("app.session")} onKeyDown=${onKey}>
52
+ ${open ? html`<div data-trust="session" ref=${pop} class="pop" role="dialog" aria-label=${t("app.session")} onKeyDown=${onKey}>
53
53
  ${!me ? html`<h3>${t("app.signedOut")}</h3><a class="btn" href=${`/signin?next=${encodeURIComponent(location.pathname)}`}>${t("app.signIn")}</a>` : html`
54
54
  <h3>${t("app.signedIn")} · ${me.label}</h3>
55
55
  <p class="sub">${t("app.expires", clock(me.expiresAt), span(me.idleMs))}</p>
@@ -5,6 +5,7 @@ import { html, useEffect, useLayoutEffect, useRef, useState } from "./app-html.j
5
5
  import { Icon } from "./ui-icons.js";
6
6
  import { Toasts } from "./ui-toast.js";
7
7
  import { ConfirmHost } from "./ui-confirm.js";
8
+ import { ColorDialogHost } from "./identity-color.js";
8
9
  import { t, lang, setLang, onLang } from "./app-i18n.js";
9
10
  import { appStore, createStore, useStore } from "./app-store.js";
10
11
  import { navStore } from "./app-nav.js";
@@ -14,6 +15,8 @@ import { savePref } from "./app-prefs.js";
14
15
  import { ErrorState, Skeleton } from "./ui-states.js";
15
16
  import { InstanceNav, RosterNav, namesStore, setNamesPrimary } from "./instance-nav.js";
16
17
  import { UsageRail, usageRailStore } from "./header-tools.js";
18
+ import { OwnerCssKeepBar } from "./owner-css.js";
19
+ import { AgentWidget, fleetBinds, sidebarWidget, sidebarWidgetStore } from "./agent-widget.js";
17
20
 
18
21
  /** Shell state shared with panels: the phone drawer, the collapsed sidebar, extensions a panel module adds. */
19
22
  export const shellStore = createStore({ drawer: false, collapsed: readCollapsed(), keyboard: false, dialog: null, side: null });
@@ -175,7 +178,7 @@ function SidebarResize() {
175
178
  handle.addEventListener("pointercancel", cancel);
176
179
  handle.addEventListener("lostpointercapture", cancel);
177
180
  };
178
- return html`<div class="side-resize" role="separator" aria-orientation="vertical" tabindex="0" aria-controls="sidebar"
181
+ return html`<div data-trust="navigation" class="side-resize" role="separator" aria-orientation="vertical" tabindex="0" aria-controls="sidebar"
179
182
  aria-label=${t("app.resizeSidebar")} title=${t("app.resizeSidebarHint")} aria-valuemin=${String(SIDEBAR_W.min)}
180
183
  aria-valuemax=${String(clampSidebarWidth(Infinity, mainBudget()))} aria-valuenow=${String(now)}
181
184
  onKeyDown=${onKey} onPointerDown=${onDown} ondblclick=${() => setSidebarWidth(null)}></div>`;
@@ -255,13 +258,13 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
255
258
  // View-only (an anonymous reader, #1408 §3): View and a way to sign in — nothing that needs a session is rendered.
256
259
  if (viewOnly) {
257
260
  return html`<aside id="sidebar" class="sidebar" aria-label=${t("app.menu")}>
258
- <div class="side-head">
261
+ <div class="side-head" data-trust="navigation">
259
262
  <a class="brand" href="/view" onClick=${closeDrawer}>${t("app.brand")}</a>
260
263
  <button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
261
264
  </div>
262
- <nav class="side-nav" aria-label=${t("app.menu")}>${navLink("view", "/view", "view", t("app.view"))}</nav>
265
+ <nav data-trust="navigation" class="side-nav" aria-label=${t("app.menu")}>${navLink("view", "/view", "view", t("app.view"))}</nav>
263
266
  ${shell.side ? html`<${shell.side.Component} />` : html`<${ViewOnlyNav} route=${route} />`}
264
- <div class="side-foot">
267
+ <div class="side-foot" data-trust="navigation">
265
268
  <a class="side-row" href=${signInHref()}><${Icon} name="user" /><span>${t("app.signIn")}</span></a>
266
269
  <${PrefsPopover} />
267
270
  </div>
@@ -274,12 +277,12 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
274
277
  const overview = html`<a class=${`side-row side-overview${onFleet ? " active" : ""}`} href=${fleetPath("org")}
275
278
  aria-current=${onFleet ? "page" : undefined} onClick=${closeDrawer}><${Icon} name="org" /><span class="grow">${t("app.fleetOverview")}</span></a>`;
276
279
  return html`<aside id="sidebar" class="sidebar" aria-label=${t("app.menu")}>
277
- <div class="side-head">
280
+ <div class="side-head" data-trust="navigation">
278
281
  <a class="brand" href="/ui" onClick=${closeDrawer}>${t("app.brand")}</a>
279
282
  <button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
280
283
  <button type="button" class="icon-btn" onClick=${onNewInstance} aria-label=${t("app.newInstance")} title=${t("app.newInstance")}><${Icon} name="plus" /></button>
281
284
  </div>
282
- <nav class="side-nav" aria-label=${t("app.menu")}>
285
+ <nav data-trust="navigation" class="side-nav" aria-label=${t("app.menu")}>
283
286
  ${overview}
284
287
  ${navLink("needs", NEEDS_PATH, "inbox", t("app.needsNav"), needsCount)}
285
288
  ${navLink("fleet", openInstance ? detailsPath(openInstance) : "/ui/fleet", "fleet", t("app.fleet"))}
@@ -287,7 +290,8 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
287
290
  </nav>
288
291
  ${shell.side ? html`<${shell.side.Component} />` : html`<${InstanceNav} route=${route} items=${app.instances} loaded=${app.ready !== false}
289
292
  exec=${app.exec} awaiting=${app.awaiting} onPick=${closeDrawer} />`}
290
- <div class="side-foot">
293
+ <${SidebarWidget} app=${app} />
294
+ <div class="side-foot" data-trust="navigation">
291
295
  ${navLink("settings", settingsPath(), "settings", t("app.settings"))}
292
296
  <${SessionMenu} />
293
297
  </div>
@@ -295,6 +299,13 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
295
299
  </aside>`;
296
300
  }
297
301
 
302
+ /** #1629 r4-4: the person's own sidebar widget (their synced preference), with the fleet's counts. */
303
+ function SidebarWidget({ app }) {
304
+ useStore(sidebarWidgetStore);
305
+ const widget = sidebarWidget();
306
+ return widget ? html`<div class="side-widget" data-trust="widget"><${AgentWidget} slot="sidebar" widget=${widget} binds=${fleetBinds(app.instances, app.exec, app.awaiting)} /></div>` : null;
307
+ }
308
+
298
309
  /** The anonymous View reader's list: View's roster read (no status frames without a session), the same component. */
299
310
  function ViewOnlyNav({ route }) {
300
311
  return html`<${RosterNav} route=${route} viewOnly=${true} onPick=${closeDrawer} />`;
@@ -328,7 +339,7 @@ export function DevicePrefs({ labels = false } = {}) {
328
339
  function PrefsPopover() {
329
340
  return html`<details class="prefs-pop">
330
341
  <summary class="icon-btn" aria-label=${t("app.devicePrefs")} title=${t("app.devicePrefs")}><${Icon} name="sun" /></summary>
331
- <div class="pop" role="group" aria-label=${t("app.devicePrefs")}><${DevicePrefs} labels=${true} /></div>
342
+ <div data-trust="popover" class="pop" role="group" aria-label=${t("app.devicePrefs")}><${DevicePrefs} labels=${true} /></div>
332
343
  </details>`;
333
344
  }
334
345
 
@@ -338,7 +349,7 @@ function PrefsPopover() {
338
349
  */
339
350
  export function PanelHeader({ title, sub, children, headingRef, nav }) {
340
351
  const shell = useStore(shellStore);
341
- return html`<header class=${`panel-head${shell.collapsed ? " collapsed" : ""}`}>
352
+ return html`<header data-trust="header" class=${`panel-head${shell.collapsed ? " collapsed" : ""}`}>
342
353
  <button type="button" id="sbOpen" class="icon-btn sb-open" onClick=${() => (narrow() ? openDrawer() : toggleSidebar())}
343
354
  aria-label=${narrow() ? t("app.openMenu") : t("app.expand")} title=${narrow() ? t("app.openMenu") : t("app.expand")} aria-controls="sidebar" aria-expanded=${shell.drawer ? "true" : "false"}><${Icon} name="menu" /></button>
344
355
  <div class="panel-title"><h1 ref=${headingRef} tabindex="-1">${title}</h1>${sub ? html`<div class="panel-sub">${sub}</div>` : null}</div>
@@ -368,12 +379,12 @@ function BottomTabs({ route, viewOnly }) {
368
379
  try { lastView = localStorage.getItem("agend_last_view"); } catch { /* none */ }
369
380
  const viewHref = route && (route.panel === "view" || route.panel === "log" || route.panel === "details") && route.instance ? viewPath(route.instance) : viewPath(lastView);
370
381
  if (viewOnly) {
371
- return html`<nav class="tabs" aria-label=${t("app.menu")}>
382
+ return html`<nav data-trust="navigation" class="tabs" aria-label=${t("app.menu")}>
372
383
  ${tab(viewHref, "view", t("app.view"), route && route.panel === "view")}
373
384
  ${tab(signInHref(), "user", t("app.signIn"), false)}
374
385
  </nav>`;
375
386
  }
376
- return html`<nav class="tabs" aria-label=${t("app.menu")}>
387
+ return html`<nav data-trust="navigation" class="tabs" aria-label=${t("app.menu")}>
377
388
  ${tab(chatHref, "chat", t("app.chat"), route && route.panel === "chat", anyAwaiting)}
378
389
  ${tab(NEEDS_PATH, "inbox", t("app.needsTab"), route && route.panel === "needs", (app.needs || []).length, t("app.needsCount", (app.needs || []).length))}
379
390
  ${tab(open ? detailsPath(open) : "/ui/fleet", "fleet", t("app.fleet"), route && (route.panel === "fleet" || route.panel === "details"))}
@@ -386,7 +397,7 @@ function ConnectionLine() {
386
397
  const { connection, hydration, retryHydration } = useStore(appStore);
387
398
  // A late catch-up that is not done: the chat may be missing what was open before it loaded (#1425 review).
388
399
  if (hydration === "retrying" || hydration === "failed") {
389
- return html`<div class="conn" role="status">${hydration === "failed" ? t("app.hydrateFailed") : t("app.hydrating")}
400
+ return html`<div data-trust="status" class="conn" role="status">${hydration === "failed" ? t("app.hydrateFailed") : t("app.hydrating")}
390
401
  ${hydration === "failed" && retryHydration ? html` <button type="button" class="btn btn-sm" onClick=${() => retryHydration()}>${t("app.retry")}</button>` : null}</div>`;
391
402
  }
392
403
  if (connection === "reconnecting") return html`<${Reconnecting} />`;
@@ -400,7 +411,7 @@ function Reconnecting() {
400
411
  const { retryAt } = useStore(appStore);
401
412
  useTick(true);
402
413
  const s = typeof retryAt === "number" ? Math.max(0, Math.ceil((retryAt - NOW()) / 1000)) : 0;
403
- return html`<div class="conn reconnecting" role="status">${s > 0 ? t("app.connReconnectingIn", s) : t("app.connReconnecting")}</div>`;
414
+ return html`<div data-trust="status" class="conn reconnecting" role="status">${s > 0 ? t("app.connReconnectingIn", s) : t("app.connReconnecting")}</div>`;
404
415
  }
405
416
 
406
417
  const NOW = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
@@ -425,7 +436,7 @@ export function PendingChanges({ inline = false } = {}) {
425
436
  const list = pendingChanges || [];
426
437
  useTick(list.some(p => p.state === "pending"));
427
438
  if (!list.length) return null;
428
- return html`<div class=${`pending-stack${inline ? " inline" : ""}`} role="region" aria-label=${t("app.pendingTitle")}>${list.map(p => html`<section key=${p.id} class=${`pending-card ${p.state}`} role="status">
439
+ return html`<div data-trust="confirmation" class=${`pending-stack${inline ? " inline" : ""}`} role="region" aria-label=${t("app.pendingTitle")}>${list.map(p => html`<section key=${p.id} class=${`pending-card ${p.state}`} role="status">
429
440
  <div class="pending-head"><${Icon} name=${p.state === "applied" ? "check" : p.state === "pending" || p.state === "applying" ? "clock" : "alert"} size=${16} />
430
441
  <strong>${t(`app.pending_${p.state}`)}</strong>
431
442
  ${p.state === "pending" ? html`<span class="pending-left">${t("app.pendingLeft", mmss(p.deadline - NOW()))}</span>` : null}
@@ -450,7 +461,7 @@ function OperationLine({ route }) {
450
461
  : op.phase === "posting" || op.phase === "watching" ? t("app.opApplying")
451
462
  : op.phase === "failed" ? t("app.opFailed") : restart ? t("app.opRestart") : failedRows ? t("app.opPartial") : t("app.opDone");
452
463
  const busy = op.phase === "writing" || op.phase === "posting" || op.phase === "watching";
453
- return html`<div class=${`conn op-line${busy ? "" : op.phase === "failed" || restart || failedRows ? " bad" : " ok"}`} role="status">
464
+ return html`<div data-trust="status" class=${`conn op-line${busy ? "" : op.phase === "failed" || restart || failedRows ? " bad" : " ok"}`} role="status">
454
465
  <span>${text}</span> <a class="btn btn-sm" href=${settingsPath()}>${t("app.openSettings")}</a></div>`;
455
466
  }
456
467
 
@@ -499,7 +510,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
499
510
  }, []);
500
511
  const cls = ["shell", shell.collapsed ? "sb-collapsed" : "", shell.drawer ? "sb-open" : "", shell.keyboard ? "kb-open" : ""].filter(Boolean).join(" ");
501
512
  return html`<div class=${cls}>
502
- <a class="skip" href="#main">${t("app.skip")}</a>
513
+ <a data-trust="navigation" class="skip" href="#main">${t("app.skip")}</a>
503
514
  <${Sidebar} route=${nav.route} onNewInstance=${onNewInstance} viewOnly=${viewOnly} />
504
515
  <div class="scrim" onClick=${closeDrawer} aria-hidden="true"></div>
505
516
  <main id="main" class="main" tabindex="-1" inert=${shell.drawer && narrow() ? true : undefined}>
@@ -512,6 +523,8 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
512
523
  ${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
513
524
  ${viewOnly || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
514
525
  <${ConfirmHost} />
526
+ <${ColorDialogHost} />
527
+ ${viewOnly ? null : html`<${OwnerCssKeepBar} />`}
515
528
  <${Toasts} />
516
529
  </div>`;
517
530
  }
@@ -39,6 +39,8 @@ export function applyStatus(d) {
39
39
  connections: typeof d.connections === "number" ? d.connections : null,
40
40
  // #1665: the host preferences' revision (app-prefs.js re-reads them when it moves).
41
41
  prefsRev: typeof d.prefs_rev === "number" ? d.prefs_rev : null,
42
+ // #1629: the agents' themes' revision (app-themes.js re-reads them when it moves).
43
+ themesRev: typeof d.themes_rev === "number" ? d.themes_rev : null,
42
44
  // #1685: host memory pressure slowing or holding new agents ("elevated" | "critical"), else null.
43
45
  hostMemory: d.host_memory === "elevated" || d.host_memory === "critical" ? d.host_memory : null });
44
46
  }