@songsid/agend 2.3.0-beta.2 → 2.3.0-beta.20

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 (502) hide show
  1. package/README.md +7 -1
  2. package/README.zh-TW.md +7 -1
  3. package/dist/agent-cli-instructions.md +9 -2
  4. package/dist/agent-cli.js +63 -2
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +4 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-file-read.d.ts +4 -1
  9. package/dist/agent-file-read.js +189 -16
  10. package/dist/agent-file-read.js.map +1 -1
  11. package/dist/agent-whats-new.d.ts +11 -0
  12. package/dist/agent-whats-new.js +29 -5
  13. package/dist/agent-whats-new.js.map +1 -1
  14. package/dist/auth-api.d.ts +30 -1
  15. package/dist/auth-api.js +45 -13
  16. package/dist/auth-api.js.map +1 -1
  17. package/dist/authority/authority-state.d.ts +38 -0
  18. package/dist/authority/authority-state.js +114 -0
  19. package/dist/authority/authority-state.js.map +1 -0
  20. package/dist/authority/session-authorizations.d.ts +43 -0
  21. package/dist/authority/session-authorizations.js +102 -0
  22. package/dist/authority/session-authorizations.js.map +1 -0
  23. package/dist/authority/web-authority.d.ts +120 -0
  24. package/dist/authority/web-authority.js +157 -0
  25. package/dist/authority/web-authority.js.map +1 -0
  26. package/dist/backend/claude-code.d.ts +2 -2
  27. package/dist/backend/claude-code.js +18 -8
  28. package/dist/backend/claude-code.js.map +1 -1
  29. package/dist/backend/codex-metadata.d.ts +8 -0
  30. package/dist/backend/codex-metadata.js +74 -17
  31. package/dist/backend/codex-metadata.js.map +1 -1
  32. package/dist/backend/codex.js +15 -2
  33. package/dist/backend/codex.js.map +1 -1
  34. package/dist/backend/credential-profile.d.ts +2 -0
  35. package/dist/backend/credential-profile.js +2 -0
  36. package/dist/backend/credential-profile.js.map +1 -1
  37. package/dist/backend/types.d.ts +6 -0
  38. package/dist/backend/types.js +15 -0
  39. package/dist/backend/types.js.map +1 -1
  40. package/dist/backend-availability.d.ts +120 -0
  41. package/dist/backend-availability.js +396 -0
  42. package/dist/backend-availability.js.map +1 -0
  43. package/dist/backend-command.d.ts +23 -3
  44. package/dist/backend-command.js +25 -4
  45. package/dist/backend-command.js.map +1 -1
  46. package/dist/bug-report-cli.d.ts +30 -0
  47. package/dist/bug-report-cli.js +138 -0
  48. package/dist/bug-report-cli.js.map +1 -0
  49. package/dist/bug-report.d.ts +47 -0
  50. package/dist/bug-report.js +136 -0
  51. package/dist/bug-report.js.map +1 -0
  52. package/dist/bundled-skills.d.ts +14 -0
  53. package/dist/bundled-skills.js +29 -0
  54. package/dist/bundled-skills.js.map +1 -0
  55. package/dist/cache-ledger.d.ts +16 -3
  56. package/dist/cache-ledger.js +33 -13
  57. package/dist/cache-ledger.js.map +1 -1
  58. package/dist/cache-service.d.ts +55 -3
  59. package/dist/cache-service.js +152 -16
  60. package/dist/cache-service.js.map +1 -1
  61. package/dist/channel/adapters/discord-client-retirement.d.ts +5 -0
  62. package/dist/channel/adapters/discord-client-retirement.js +57 -0
  63. package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
  64. package/dist/channel/adapters/discord.d.ts +9 -0
  65. package/dist/channel/adapters/discord.js +49 -4
  66. package/dist/channel/adapters/discord.js.map +1 -1
  67. package/dist/channel/mcp-tools.js +22 -11
  68. package/dist/channel/mcp-tools.js.map +1 -1
  69. package/dist/channel/types.d.ts +6 -0
  70. package/dist/classic-channel-manager.d.ts +10 -5
  71. package/dist/classic-channel-manager.js +12 -9
  72. package/dist/classic-channel-manager.js.map +1 -1
  73. package/dist/cli.js +224 -111
  74. package/dist/cli.js.map +1 -1
  75. package/dist/config-validator.d.ts +6 -0
  76. package/dist/config-validator.js +129 -3
  77. package/dist/config-validator.js.map +1 -1
  78. package/dist/config.d.ts +13 -1
  79. package/dist/config.js +14 -5
  80. package/dist/config.js.map +1 -1
  81. package/dist/cross-instance-notice.d.ts +7 -0
  82. package/dist/cross-instance-notice.js +10 -0
  83. package/dist/cross-instance-notice.js.map +1 -1
  84. package/dist/daemon.d.ts +24 -1
  85. package/dist/daemon.js +151 -19
  86. package/dist/daemon.js.map +1 -1
  87. package/dist/decision-list.d.ts +25 -0
  88. package/dist/decision-list.js +30 -0
  89. package/dist/decision-list.js.map +1 -0
  90. package/dist/delivery-outbox.d.ts +10 -0
  91. package/dist/delivery-outbox.js +17 -1
  92. package/dist/delivery-outbox.js.map +1 -1
  93. package/dist/diagnostic-redaction.d.ts +10 -0
  94. package/dist/diagnostic-redaction.js +167 -0
  95. package/dist/diagnostic-redaction.js.map +1 -0
  96. package/dist/fleet-context.d.ts +10 -0
  97. package/dist/fleet-manager.d.ts +505 -15
  98. package/dist/fleet-manager.js +1870 -204
  99. package/dist/fleet-manager.js.map +1 -1
  100. package/dist/fleet-process-handlers.d.ts +18 -0
  101. package/dist/fleet-process-handlers.js +69 -0
  102. package/dist/fleet-process-handlers.js.map +1 -0
  103. package/dist/fleet-traffic.d.ts +74 -0
  104. package/dist/fleet-traffic.js +196 -0
  105. package/dist/fleet-traffic.js.map +1 -0
  106. package/dist/general-knowledge/skills/bug-report/SKILL.md +31 -0
  107. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +17 -6
  108. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
  109. package/dist/general-knowledge/skills/fleet-config/SKILL.md +2 -0
  110. package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
  111. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
  112. package/dist/general-knowledge/skills/scheduling/SKILL.md +4 -0
  113. package/dist/general-knowledge/skills/web-appearance/SKILL.md +190 -47
  114. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +545 -0
  115. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  116. package/dist/human-delivery-receipts.d.ts +24 -1
  117. package/dist/human-delivery-receipts.js +64 -11
  118. package/dist/human-delivery-receipts.js.map +1 -1
  119. package/dist/instance-config-impact.js +7 -1
  120. package/dist/instance-config-impact.js.map +1 -1
  121. package/dist/instance-lifecycle.d.ts +10 -1
  122. package/dist/instance-lifecycle.js +79 -18
  123. package/dist/instance-lifecycle.js.map +1 -1
  124. package/dist/instance-names.d.ts +4 -0
  125. package/dist/instance-names.js +12 -0
  126. package/dist/instance-names.js.map +1 -0
  127. package/dist/instructions.d.ts +12 -1
  128. package/dist/instructions.js +13 -2
  129. package/dist/instructions.js.map +1 -1
  130. package/dist/locale.js +173 -12
  131. package/dist/locale.js.map +1 -1
  132. package/dist/log-steps.d.ts +1 -0
  133. package/dist/log-steps.js +15 -0
  134. package/dist/log-steps.js.map +1 -1
  135. package/dist/login-controller.d.ts +45 -0
  136. package/dist/login-controller.js +101 -5
  137. package/dist/login-controller.js.map +1 -1
  138. package/dist/ls-attach-resolve.d.ts +96 -0
  139. package/dist/ls-attach-resolve.js +180 -0
  140. package/dist/ls-attach-resolve.js.map +1 -0
  141. package/dist/ls-rows.d.ts +12 -0
  142. package/dist/ls-rows.js +82 -3
  143. package/dist/ls-rows.js.map +1 -1
  144. package/dist/memory-hogs.d.ts +27 -0
  145. package/dist/memory-hogs.js +124 -0
  146. package/dist/memory-hogs.js.map +1 -0
  147. package/dist/needs-you.d.ts +10 -0
  148. package/dist/needs-you.js +2 -1
  149. package/dist/needs-you.js.map +1 -1
  150. package/dist/outbound-handlers.d.ts +11 -0
  151. package/dist/outbound-handlers.js +139 -11
  152. package/dist/outbound-handlers.js.map +1 -1
  153. package/dist/outbound-schemas.d.ts +200 -4
  154. package/dist/outbound-schemas.js +124 -11
  155. package/dist/outbound-schemas.js.map +1 -1
  156. package/dist/public-web-gateway.js +17 -4
  157. package/dist/public-web-gateway.js.map +1 -1
  158. package/dist/public-web-link.d.ts +9 -0
  159. package/dist/public-web-link.js +8 -0
  160. package/dist/public-web-link.js.map +1 -1
  161. package/dist/quickstart-api.d.ts +19 -1
  162. package/dist/quickstart-api.js +94 -4
  163. package/dist/quickstart-api.js.map +1 -1
  164. package/dist/reply-buttons-controller.d.ts +4 -0
  165. package/dist/reply-buttons-controller.js +2 -2
  166. package/dist/reply-buttons-controller.js.map +1 -1
  167. package/dist/restart-progress.d.ts +80 -1
  168. package/dist/restart-progress.js +306 -21
  169. package/dist/restart-progress.js.map +1 -1
  170. package/dist/rollout-index.d.ts +5 -0
  171. package/dist/rollout-index.js +26 -5
  172. package/dist/rollout-index.js.map +1 -1
  173. package/dist/scheduler/db.d.ts +40 -13
  174. package/dist/scheduler/db.js +123 -89
  175. package/dist/scheduler/db.js.map +1 -1
  176. package/dist/scheduler/task-board-text.d.ts +26 -0
  177. package/dist/scheduler/task-board-text.js +135 -0
  178. package/dist/scheduler/task-board-text.js.map +1 -0
  179. package/dist/scheduler/task-board.d.ts +650 -0
  180. package/dist/scheduler/task-board.js +1938 -0
  181. package/dist/scheduler/task-board.js.map +1 -0
  182. package/dist/scheduler/types.d.ts +6 -1
  183. package/dist/search/search-docs.d.ts +58 -0
  184. package/dist/search/search-docs.js +63 -0
  185. package/dist/search/search-docs.js.map +1 -0
  186. package/dist/search/search-index.d.ts +141 -0
  187. package/dist/search/search-index.js +440 -0
  188. package/dist/search/search-index.js.map +1 -0
  189. package/dist/search/search-store.d.ts +122 -0
  190. package/dist/search/search-store.js +236 -0
  191. package/dist/search/search-store.js.map +1 -0
  192. package/dist/search/search-worker.d.ts +1 -0
  193. package/dist/search/search-worker.js +163 -0
  194. package/dist/search/search-worker.js.map +1 -0
  195. package/dist/service-installer.js +12 -0
  196. package/dist/service-installer.js.map +1 -1
  197. package/dist/settings-api.d.ts +23 -0
  198. package/dist/settings-api.js +164 -3
  199. package/dist/settings-api.js.map +1 -1
  200. package/dist/settings-change.js +104 -0
  201. package/dist/settings-change.js.map +1 -1
  202. package/dist/settings-confirmation.d.ts +3 -1
  203. package/dist/settings-confirmation.js +2 -0
  204. package/dist/settings-confirmation.js.map +1 -1
  205. package/dist/settings-effect.js +34 -3
  206. package/dist/settings-effect.js.map +1 -1
  207. package/dist/settings-http-confirmation.d.ts +11 -0
  208. package/dist/settings-http-confirmation.js +10 -0
  209. package/dist/settings-http-confirmation.js.map +1 -1
  210. package/dist/setup-form.d.ts +1 -1
  211. package/dist/setup-form.js +23 -7
  212. package/dist/setup-form.js.map +1 -1
  213. package/dist/spawn-gate.d.ts +1 -0
  214. package/dist/spawn-gate.js +13 -3
  215. package/dist/spawn-gate.js.map +1 -1
  216. package/dist/task-board-tool.d.ts +25 -0
  217. package/dist/task-board-tool.js +154 -0
  218. package/dist/task-board-tool.js.map +1 -0
  219. package/dist/task-board-waker.d.ts +44 -0
  220. package/dist/task-board-waker.js +128 -0
  221. package/dist/task-board-waker.js.map +1 -0
  222. package/dist/tmux-exec-helper.d.ts +94 -0
  223. package/dist/tmux-exec-helper.js +407 -0
  224. package/dist/tmux-exec-helper.js.map +1 -0
  225. package/dist/tmux-manager.d.ts +5 -0
  226. package/dist/tmux-manager.js +42 -11
  227. package/dist/tmux-manager.js.map +1 -1
  228. package/dist/tool-permissions.d.ts +0 -7
  229. package/dist/tool-permissions.js +44 -5
  230. package/dist/tool-permissions.js.map +1 -1
  231. package/dist/topic-commands.d.ts +17 -1
  232. package/dist/topic-commands.js +45 -19
  233. package/dist/topic-commands.js.map +1 -1
  234. package/dist/tunnel/cloudflared.js +37 -2
  235. package/dist/tunnel/cloudflared.js.map +1 -1
  236. package/dist/tunnel/types.d.ts +3 -1
  237. package/dist/tunnel/types.js.map +1 -1
  238. package/dist/types.d.ts +63 -1
  239. package/dist/ui/app-effects.js +90 -32
  240. package/dist/ui/backend-signin.js +297 -0
  241. package/dist/ui/chat-ambient.js +307 -0
  242. package/dist/ui/chat-effect-posters.js +8 -8
  243. package/dist/ui/chat-effects.js +110 -15
  244. package/dist/ui/chat-msg-actions.js +176 -0
  245. package/dist/ui/chat-render.js +21 -3
  246. package/dist/ui/chat-store.js +64 -12
  247. package/dist/ui/chat-strings.js +85 -8
  248. package/dist/ui/chat-thread.js +204 -21
  249. package/dist/ui/effect-trial-controller.js +13 -8
  250. package/dist/ui/effect-trial-panel.js +19 -16
  251. package/dist/ui/effects-waker.js +70 -0
  252. package/dist/ui/fleet-canvas-layout.js +117 -0
  253. package/dist/ui/fleet-canvas.js +555 -0
  254. package/dist/ui/fleet-org.js +27 -12
  255. package/dist/ui/fleet-route-scene.js +207 -0
  256. package/dist/ui/fleet-route-worker.js +21 -0
  257. package/dist/ui/fleet-route.js +88 -0
  258. package/dist/ui/fleet-task-card.js +436 -0
  259. package/dist/ui/fleet-task-common.js +448 -0
  260. package/dist/ui/fleet-tasks.js +535 -0
  261. package/dist/ui/fleet-traffic.js +359 -0
  262. package/dist/ui/fleet-usage.js +435 -0
  263. package/dist/ui/panel-chat.js +264 -66
  264. package/dist/ui/panel-details.js +2 -1
  265. package/dist/ui/panel-fleet.js +65 -83
  266. package/dist/ui/panel-needs.js +4 -2
  267. package/dist/ui/panel-settings.js +56 -11
  268. package/dist/ui/preview.js +3 -2
  269. package/dist/ui/search-dialog.js +349 -0
  270. package/dist/ui/settings-appearance.js +44 -19
  271. package/dist/ui/settings-apply.js +3 -3
  272. package/dist/ui/settings-catalog.js +92 -0
  273. package/dist/ui/settings-confirm.js +7 -2
  274. package/dist/ui/settings-dialogs.js +40 -12
  275. package/dist/ui/settings-effects.js +4 -3
  276. package/dist/ui/settings-guests.js +117 -0
  277. package/dist/ui/settings-learn.js +25 -0
  278. package/dist/ui/settings-model.js +15 -6
  279. package/dist/ui/settings-mods.js +23 -9
  280. package/dist/ui/settings-owner-effects.js +15 -13
  281. package/dist/ui/settings-strings.js +122 -42
  282. package/dist/ui/settings-web-authority.js +91 -0
  283. package/dist/ui/shared/agend-auth.js +1 -1
  284. package/dist/ui/shared/app-i18n.js +11 -2
  285. package/dist/ui/shared/app-prefs.js +17 -4
  286. package/dist/ui/shared/app-route.js +19 -1
  287. package/dist/ui/shared/app-session.js +2 -0
  288. package/dist/ui/shared/app-shell.js +174 -30
  289. package/dist/ui/shared/app-store.js +26 -1
  290. package/dist/ui/shared/app-stream.js +5 -1
  291. package/dist/ui/shared/app-themes.js +140 -7
  292. package/dist/ui/shared/app.css +704 -20
  293. package/dist/ui/shared/app.js +26 -15
  294. package/dist/ui/shared/card-strings.js +8 -0
  295. package/dist/ui/shared/effect-app-runtime.js +68 -19
  296. package/dist/ui/shared/effect-chat-runtime.js +16 -3
  297. package/dist/ui/shared/effect-frame.js +26 -7
  298. package/dist/ui/shared/effect-geometry.js +24 -9
  299. package/dist/ui/shared/effect-owner-source.js +8 -1
  300. package/dist/ui/shared/effect-policy.js +21 -7
  301. package/dist/ui/shared/effect-profiler.js +31 -9
  302. package/dist/ui/shared/effect-registry.js +97 -50
  303. package/dist/ui/shared/effect-stock-snapshot.js +9 -0
  304. package/dist/ui/shared/effect-stock-trust.js +62 -10
  305. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  306. package/dist/ui/shared/effect-trial-source.js +1 -1
  307. package/dist/ui/shared/effect-trial-stage.js +54 -31
  308. package/dist/ui/shared/effect-trial.css +40 -0
  309. package/dist/ui/shared/fx-motion.js +67 -0
  310. package/dist/ui/shared/identity-card.js +100 -11
  311. package/dist/ui/shared/identity-color.js +14 -10
  312. package/dist/ui/shared/instance-nav.js +179 -23
  313. package/dist/ui/shared/owner-css.js +30 -1
  314. package/dist/ui/shared/panel-view.js +24 -4
  315. package/dist/ui/shared/tokens.css +9 -3
  316. package/dist/ui/shared/ui-icons.js +2 -0
  317. package/dist/ui/shared/ui-menu.js +20 -7
  318. package/dist/ui/shared/ui-toast.js +16 -3
  319. package/dist/ui/shared/usage-dialog.js +34 -3
  320. package/dist/ui/shared/view-strings.js +72 -0
  321. package/dist/ui/shared/web-authorize.js +198 -0
  322. package/dist/ui/web-terminal/terminal.css +4 -4
  323. package/dist/ui/web-terminal/terminal.html +1 -1
  324. package/dist/update-launch.d.ts +9 -0
  325. package/dist/update-launch.js +38 -12
  326. package/dist/update-launch.js.map +1 -1
  327. package/dist/usage/format-rich.js +6 -3
  328. package/dist/usage/format-rich.js.map +1 -1
  329. package/dist/usage/i18n-keys.d.ts +1 -1
  330. package/dist/usage/i18n-keys.js +2 -1
  331. package/dist/usage/i18n-keys.js.map +1 -1
  332. package/dist/usage/i18n.d.ts +13 -1
  333. package/dist/usage/i18n.js +29 -0
  334. package/dist/usage/i18n.js.map +1 -1
  335. package/dist/usage/providers.d.ts +81 -2
  336. package/dist/usage/providers.js +240 -38
  337. package/dist/usage/providers.js.map +1 -1
  338. package/dist/usage/usage-api.d.ts +5 -1
  339. package/dist/usage/usage-api.js +184 -58
  340. package/dist/usage/usage-api.js.map +1 -1
  341. package/dist/usage-report.d.ts +158 -0
  342. package/dist/usage-report.js +226 -0
  343. package/dist/usage-report.js.map +1 -0
  344. package/dist/usage-rollup.d.ts +51 -0
  345. package/dist/usage-rollup.js +168 -0
  346. package/dist/usage-rollup.js.map +1 -0
  347. package/dist/user-systemd.d.ts +24 -0
  348. package/dist/user-systemd.js +75 -0
  349. package/dist/user-systemd.js.map +1 -0
  350. package/dist/view-api.d.ts +39 -0
  351. package/dist/view-api.js +69 -6
  352. package/dist/view-api.js.map +1 -1
  353. package/dist/web-ambient.d.ts +72 -0
  354. package/dist/web-ambient.js +161 -0
  355. package/dist/web-ambient.js.map +1 -0
  356. package/dist/web-api.d.ts +98 -14
  357. package/dist/web-api.js +1027 -83
  358. package/dist/web-api.js.map +1 -1
  359. package/dist/web-appearance-policy.d.ts +4 -6
  360. package/dist/web-appearance-policy.js +5 -12
  361. package/dist/web-appearance-policy.js.map +1 -1
  362. package/dist/web-appearance-tools.d.ts +3 -0
  363. package/dist/web-appearance-tools.js +22 -3
  364. package/dist/web-appearance-tools.js.map +1 -1
  365. package/dist/web-appearance-view.d.ts +27 -2
  366. package/dist/web-appearance-view.js +129 -14
  367. package/dist/web-appearance-view.js.map +1 -1
  368. package/dist/web-appearance.d.ts +17 -3
  369. package/dist/web-appearance.js +85 -8
  370. package/dist/web-appearance.js.map +1 -1
  371. package/dist/web-asset-parts.d.ts +2 -2
  372. package/dist/web-asset-parts.js +3 -3
  373. package/dist/web-asset-parts.js.map +1 -1
  374. package/dist/web-auth.js +3 -1
  375. package/dist/web-auth.js.map +1 -1
  376. package/dist/web-backend-login.d.ts +130 -0
  377. package/dist/web-backend-login.js +225 -0
  378. package/dist/web-backend-login.js.map +1 -0
  379. package/dist/web-card.d.ts +14 -0
  380. package/dist/web-card.js +40 -6
  381. package/dist/web-card.js.map +1 -1
  382. package/dist/web-channel-echo.d.ts +7 -1
  383. package/dist/web-channel-echo.js +6 -2
  384. package/dist/web-channel-echo.js.map +1 -1
  385. package/dist/web-chat-history.d.ts +83 -0
  386. package/dist/web-chat-history.js +137 -5
  387. package/dist/web-chat-history.js.map +1 -1
  388. package/dist/web-commands.d.ts +6 -1
  389. package/dist/web-commands.js +1 -1
  390. package/dist/web-commands.js.map +1 -1
  391. package/dist/web-effect-bootstrap.d.ts +12 -0
  392. package/dist/web-effect-bootstrap.js +49 -0
  393. package/dist/web-effect-bootstrap.js.map +1 -0
  394. package/dist/web-effect-bundle.js +46 -1
  395. package/dist/web-effect-bundle.js.map +1 -1
  396. package/dist/web-effect-playback.d.ts +30 -0
  397. package/dist/web-effect-playback.js +71 -0
  398. package/dist/web-effect-playback.js.map +1 -0
  399. package/dist/web-effect-profiles.d.ts +4 -4
  400. package/dist/web-effect-service.d.ts +11 -4
  401. package/dist/web-effect-service.js +25 -4
  402. package/dist/web-effect-service.js.map +1 -1
  403. package/dist/web-effect-tools.d.ts +1 -0
  404. package/dist/web-effect-tools.js +4 -4
  405. package/dist/web-effect-tools.js.map +1 -1
  406. package/dist/web-effect-trials.d.ts +2 -2
  407. package/dist/web-effect-trials.js +3 -3
  408. package/dist/web-effect-trials.js.map +1 -1
  409. package/dist/web-effect-upload.js +1 -1
  410. package/dist/web-effect-upload.js.map +1 -1
  411. package/dist/web-effect-worker.d.ts +1 -1
  412. package/dist/web-effect-worker.js +2 -2
  413. package/dist/web-effect-worker.js.map +1 -1
  414. package/dist/web-effect.d.ts +1 -1
  415. package/dist/web-guest-access.d.ts +126 -0
  416. package/dist/web-guest-access.js +238 -0
  417. package/dist/web-guest-access.js.map +1 -0
  418. package/dist/web-guest-rooms.d.ts +82 -0
  419. package/dist/web-guest-rooms.js +135 -0
  420. package/dist/web-guest-rooms.js.map +1 -0
  421. package/dist/web-host-guard.d.ts +11 -0
  422. package/dist/web-host-guard.js +11 -0
  423. package/dist/web-host-guard.js.map +1 -1
  424. package/dist/web-identity-color.d.ts +1 -1
  425. package/dist/web-identity-color.js +2 -2
  426. package/dist/web-identity-color.js.map +1 -1
  427. package/dist/web-identity-palette.d.ts +1 -1
  428. package/dist/web-identity-palette.js +2 -2
  429. package/dist/web-identity-palette.js.map +1 -1
  430. package/dist/web-image-meta.d.ts +13 -0
  431. package/dist/web-image-meta.js +62 -0
  432. package/dist/web-image-meta.js.map +1 -1
  433. package/dist/web-libavoid.d.ts +14 -0
  434. package/dist/web-libavoid.js +46 -0
  435. package/dist/web-libavoid.js.map +1 -0
  436. package/dist/web-login.d.ts +47 -0
  437. package/dist/web-login.js +115 -0
  438. package/dist/web-login.js.map +1 -1
  439. package/dist/web-mod-agent.d.ts +1 -1
  440. package/dist/web-mod-agent.js +7 -5
  441. package/dist/web-mod-agent.js.map +1 -1
  442. package/dist/web-mod-bundle.d.ts +9 -1
  443. package/dist/web-mod-bundle.js +62 -3
  444. package/dist/web-mod-bundle.js.map +1 -1
  445. package/dist/web-mod-export.d.ts +9 -0
  446. package/dist/web-mod-export.js +21 -0
  447. package/dist/web-mod-export.js.map +1 -1
  448. package/dist/web-mod-store.d.ts +7 -1
  449. package/dist/web-mod-store.js +19 -6
  450. package/dist/web-mod-store.js.map +1 -1
  451. package/dist/web-owner-css-store.d.ts +44 -1
  452. package/dist/web-owner-css-store.js +70 -9
  453. package/dist/web-owner-css-store.js.map +1 -1
  454. package/dist/web-prefs.d.ts +14 -0
  455. package/dist/web-prefs.js +37 -4
  456. package/dist/web-prefs.js.map +1 -1
  457. package/dist/web-preview.d.ts +6 -2
  458. package/dist/web-preview.js +4 -2
  459. package/dist/web-preview.js.map +1 -1
  460. package/dist/web-returning-device.d.ts +41 -0
  461. package/dist/web-returning-device.js +57 -5
  462. package/dist/web-returning-device.js.map +1 -1
  463. package/dist/web-session-authorization.d.ts +132 -0
  464. package/dist/web-session-authorization.js +193 -0
  465. package/dist/web-session-authorization.js.map +1 -0
  466. package/dist/web-session.d.ts +70 -10
  467. package/dist/web-session.js +137 -22
  468. package/dist/web-session.js.map +1 -1
  469. package/dist/web-settings-patch.d.ts +49 -2
  470. package/dist/web-settings-patch.js +145 -1
  471. package/dist/web-settings-patch.js.map +1 -1
  472. package/dist/web-settings-tools.d.ts +94 -0
  473. package/dist/web-settings-tools.js +301 -0
  474. package/dist/web-settings-tools.js.map +1 -0
  475. package/dist/web-shell-routes.d.ts +1 -1
  476. package/dist/web-shell-routes.js +1 -1
  477. package/dist/web-shell-routes.js.map +1 -1
  478. package/dist/web-terminal-http.js +15 -2
  479. package/dist/web-terminal-http.js.map +1 -1
  480. package/dist/web-theme-store.d.ts +39 -0
  481. package/dist/web-theme-store.js +122 -11
  482. package/dist/web-theme-store.js.map +1 -1
  483. package/dist/web-theme.d.ts +31 -0
  484. package/dist/web-theme.js +99 -2
  485. package/dist/web-theme.js.map +1 -1
  486. package/dist/web-upload.d.ts +13 -0
  487. package/dist/web-upload.js +39 -3
  488. package/dist/web-upload.js.map +1 -1
  489. package/dist/windows-shortcuts.d.ts +78 -0
  490. package/dist/windows-shortcuts.js +203 -0
  491. package/dist/windows-shortcuts.js.map +1 -0
  492. package/dist/workflow-templates/default.md +4 -1
  493. package/dist/yaml-shared-nodes.d.ts +25 -0
  494. package/dist/yaml-shared-nodes.js +185 -0
  495. package/dist/yaml-shared-nodes.js.map +1 -0
  496. package/launcher/agend-install.cjs +553 -0
  497. package/launcher/install-admission.cjs +4 -39
  498. package/launcher/postinstall.cjs +14 -12
  499. package/package.json +4 -2
  500. package/scripts/preinstall-guard.cjs +2 -5
  501. package/templates/windows-shortcuts.ps1 +181 -0
  502. package/launcher/old-updater-note.cjs +0 -48
@@ -0,0 +1,92 @@
1
+ // #2072 part 3: what a backend or model field in Settings offers — the host's published availability (installed,
2
+ // signed in, new enough: /ui/backends) and the model catalog of that backend for that instance's account (/ui/models).
3
+ // Both answers are the server's cached ones; nothing here makes the server probe. A page asks once and reuses the answer
4
+ // for a short while, so opening several dialogs costs one request each.
5
+ import { useEffect, useState } from "/assets/app-html.js";
6
+
7
+ const TTL_MS = 30_000;
8
+ const cache = new Map(); // url → { at, value, pending }
9
+
10
+ async function getJson(url) {
11
+ const res = await fetch(url, { credentials: "same-origin" });
12
+ if (!res.ok) throw new Error(String(res.status));
13
+ return res.json();
14
+ }
15
+ /** The answer for `url`, shared by every field on the page while it is fresh; null when it cannot be read. */
16
+ function cached(url) {
17
+ const hit = cache.get(url), now = performance.now();
18
+ if (hit && (hit.pending || now - hit.at < TTL_MS)) return hit.pending ?? Promise.resolve(hit.value);
19
+ const pending = getJson(url).then((value) => { cache.set(url, { at: performance.now(), value, pending: null }); return value; })
20
+ .catch(() => { cache.delete(url); return null; });
21
+ cache.set(url, { at: now, value: null, pending });
22
+ return pending;
23
+ }
24
+ /**
25
+ * Drop every remembered answer. Only tests call it today: a page keeps an answer TTL_MS, so a sign-in or a newly
26
+ * cached catalog shows within that time, or at once on the next page load.
27
+ */
28
+ export function forgetCatalogs() { cache.clear(); }
29
+
30
+ function useAnswer(url) {
31
+ const [value, setValue] = useState(undefined); // undefined: loading; null: unavailable
32
+ useEffect(() => {
33
+ if (!url) { setValue(null); return undefined; }
34
+ let live = true;
35
+ setValue(undefined);
36
+ cached(url).then((v) => { if (live) setValue(v); });
37
+ return () => { live = false; };
38
+ }, [url]);
39
+ return value;
40
+ }
41
+
42
+ /** The host's backends with their state ({ name, state, minVersion, … }), or null while loading or unavailable. */
43
+ export function useBackendRows() {
44
+ const answer = useAnswer("/ui/backends");
45
+ return answer && Array.isArray(answer.backends) ? answer.backends : answer === undefined ? undefined : null;
46
+ }
47
+
48
+ /** The model catalog of `backend` for `instance` (its account) — [] when none is cached; undefined while loading. */
49
+ export function useModelCatalog(backend, instance) {
50
+ const url = backend ? `/ui/models?backend=${encodeURIComponent(backend)}${instance ? `&instance=${encodeURIComponent(instance)}` : ""}` : null;
51
+ const answer = useAnswer(url);
52
+ if (answer === undefined) return undefined;
53
+ return answer && Array.isArray(answer.models) ? answer.models : [];
54
+ }
55
+
56
+ /** A state that keeps a backend from being chosen (shown, disabled, with why); "unknown" never does. */
57
+ export const BLOCKING_STATES = ["not_installed", "not_signed_in", "too_old"];
58
+
59
+ /**
60
+ * The backend select's options: every backend the page knows, in its order, each with what the host said about it.
61
+ * The current value is always selectable — a probe never disables what an agent already runs — and one the page does
62
+ * not know (a removed backend) stays first, as before.
63
+ */
64
+ export function backendChoices(known, rows, value, words) {
65
+ const byName = new Map((rows || []).map((r) => [r.name, r]));
66
+ const choice = (name) => {
67
+ const row = byName.get(name), state = row ? row.state ?? (row.installed ? "usable" : row.unknown ? "unknown" : "not_installed") : null;
68
+ const blocked = !!state && BLOCKING_STATES.includes(state) && name !== value;
69
+ const note = state === "not_installed" ? words.notInstalled : state === "not_signed_in" ? words.notSignedIn
70
+ : state === "too_old" ? words.tooOld(row.minVersion ?? "?") : state === "unknown" && name !== value ? words.unchecked : "";
71
+ return { value: name, label: note ? `${name} — ${note}` : name, disabled: blocked };
72
+ };
73
+ const list = known.map(choice);
74
+ return known.includes(value) || !value ? list : [{ value, label: words.removed(value), disabled: false }, ...list];
75
+ }
76
+
77
+ /** The id of the "type another model" choice (never a model name: it has a space). */
78
+ export const OTHER_MODEL = "\u0000other";
79
+
80
+ /**
81
+ * The model select's options: the inherited choice (empty value), the current model first when the catalog does not
82
+ * list it (marked), the catalog, and "Other…" for a name the CLI does not list.
83
+ */
84
+ export function modelChoices(catalog, value, words, inherit) {
85
+ const list = [];
86
+ if (inherit) list.push({ value: "", label: inherit });
87
+ const ids = new Set((catalog || []).map((m) => m.id));
88
+ if (value && !ids.has(value)) list.push({ value, label: `${value} — ${words.notListed}` });
89
+ for (const m of catalog || []) list.push({ value: m.id, label: m.description ? `${m.label || m.id} — ${m.description}` : (m.label || m.id) });
90
+ list.push({ value: OTHER_MODEL, label: words.other });
91
+ return list;
92
+ }
@@ -33,6 +33,10 @@ export async function api(path, opts = {}) {
33
33
 
34
34
  /** A request id we can follow: a non-empty string (the server's are 32 hex characters). */
35
35
  const usableId = (v) => typeof v === "string" && v.length > 0;
36
+ /** #2170: a held request its own web-verified account confirmed (the server's answer says so): applying, no wait for chat. */
37
+ export function selfConfirmed(view) {
38
+ return !!view && !!view.confirmation && view.confirmation.kind === "web";
39
+ }
36
40
  /** The 202 that means "waiting for an admin", not "done". */
37
41
  export function isPendingResponse(res) {
38
42
  return !!res && res.status === 202 && !!res.body && res.body.result === "pending_confirmation" && !!res.body.pending_change
@@ -159,13 +163,14 @@ export async function attach() {
159
163
  * itself would have been: applied → { ok: true, body: what the write answered }, anything else → { ok: false } with
160
164
  * the reason. An accepted answer that cannot be read is { ok: false, ambiguous: true } (see isUnreadableAccept); a
161
165
  * network failure throws. Every other answer is returned as it is. `key` is the request's Idempotency-Key: pass the same one to
162
- * retry the same request. `onPending(id)` is told when it starts waiting.
166
+ * retry the same request. `onPending(id, view)` is told when it starts waiting — or, held and confirmed by its own
167
+ * web-verified account (selfConfirmed(view), #2170), applying.
163
168
  */
164
169
  export async function confirmedWrite(path, { method = "POST", body, key = newKey("w"), label, headers, onPending } = {}) {
165
170
  const res = await api(path, { method, headers: { ...(headers || {}), "Idempotency-Key": key }, body: body === undefined ? undefined : JSON.stringify(body) });
166
171
  if (isUnreadableAccept(res)) return { ok: false, status: 202, ambiguous: true, body: { error: "unreadable_confirmation_response" } };
167
172
  if (!isPendingResponse(res)) return res;
168
- if (onPending) onPending(res.body.pending_change.id);
173
+ if (onPending) onPending(res.body.pending_change.id, res.body.pending_change);
169
174
  const final = await track(res.body.pending_change, label);
170
175
  return outcomeResponse(final);
171
176
  }
@@ -13,11 +13,13 @@ import { api, confirmedWrite, newKey } from "./settings-confirm.js";
13
13
  import { adminUserIdError, adminUserIdPattern, botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
14
14
  import { DiscordServerPicker } from "./settings-discord.js";
15
15
  import { startOperation } from "./settings-apply.js";
16
+ import "./settings-strings.js"; // its words, wherever a picker of this file is used (#2072: Fleet's create dialog too)
17
+ import { backendChoices, modelChoices, OTHER_MODEL, useBackendRows, useModelCatalog } from "./settings-catalog.js";
16
18
  import {
17
19
  ACCESS_MODES, AGENT_FIELDS, AGENT_MODES, BOT_FIELDS, CH_TYPES, CLASSIC_FIELDS, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
18
20
  VISIBILITY_MODES, accessConfirmations, agentForm, agentPatch, backendOptions, changedFields, channelId, channelsOf,
19
21
  chLabel, hasOwn, impactOf, nonNegative, nonNegativeInteger, positive, replyGuardSupported, sameValue, visibilityDefault,
20
- webEchoIsGroup, worstImpact,
22
+ classicWebEchoEffective, worstImpact,
21
23
  } from "./settings-model.js";
22
24
 
23
25
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
@@ -32,9 +34,36 @@ export function Select({ value, options, onChange, disabled, id, label }) {
32
34
  return html`<select id=${id} value=${value} disabled=${disabled} aria-label=${label} onChange=${(e) => onChange(e.target.value)}>
33
35
  ${opts.map((o) => html`<option key=${o.value} value=${o.value}>${o.label}</option>`)}</select>`;
34
36
  }
37
+ /**
38
+ * #2072: a backend field shows every backend with what this host says about it — one it cannot run is disabled with
39
+ * why (not installed, not signed in, too old); one not checked yet stays selectable; the current value always can be.
40
+ */
35
41
  export function BackendSelect({ value, onChange, disabled, id }) {
36
- return html`<${Select} id=${id} value=${value} disabled=${disabled} onChange=${onChange}
37
- options=${backendOptions(value).map((o) => (o.unavailable ? { value: o.value, label: tn("backendUnavailable", o.value) } : o))} />`;
42
+ const rows = useBackendRows();
43
+ const known = backendOptions(value).filter((o) => !o.unavailable).map((o) => o.value);
44
+ const opts = backendChoices(known, rows || [], value, {
45
+ notInstalled: tn("backendNotInstalled"), notSignedIn: tn("backendNotSignedIn"), tooOld: (v) => tn("backendTooOld", v),
46
+ unchecked: tn("backendUnchecked"), removed: (v) => tn("backendUnavailable", v),
47
+ });
48
+ return html`<select id=${id} value=${value} disabled=${disabled} onChange=${(e) => onChange(e.target.value)}>
49
+ ${opts.map((o) => html`<option key=${o.value} value=${o.value} disabled=${o.disabled}>${o.label}</option>`)}</select>`;
50
+ }
51
+ /**
52
+ * #2072: a model field is a list — that backend's models for this account (cached; never a probe), the current one
53
+ * first and marked when the CLI does not list it, and "Other…" for a name it does not list (a text box opens with
54
+ * the current value in it: choosing "Other…" alone changes nothing — emptying the box is what inherits, #2098 review).
55
+ * `inherit`: the label of the empty choice (inherit / none), or omit it when the field has none.
56
+ */
57
+ export function ModelSelect({ value, onChange, disabled, id, backend, instance, inherit }) {
58
+ const catalog = useModelCatalog(backend, instance);
59
+ const [typing, setTyping] = useState(false);
60
+ const opts = modelChoices(catalog || [], value, { notListed: tn("modelNotListed"), other: tn("modelOther") }, inherit);
61
+ return html`<div class="model-pick">
62
+ <select id=${id} value=${typing ? OTHER_MODEL : value} disabled=${disabled}
63
+ onChange=${(e) => { const v = e.target.value; if (v === OTHER_MODEL) setTyping(true); else { setTyping(false); onChange(v); } }}>
64
+ ${opts.map((o) => html`<option key=${o.value} value=${o.value}>${o.label}</option>`)}</select>
65
+ ${typing ? html`<input type="text" aria-label=${tn("modelOtherLabel")} placeholder=${tn("modelOtherPlaceholder")} disabled=${disabled}
66
+ value=${value} onInput=${(e) => onChange(e.target.value)} />` : null}</div>`;
38
67
  }
39
68
  export function Impact({ schema, field }) { return html`<span class="impact">${impactText(impactOf(schema, field))}</span>`; }
40
69
  export function Label({ text, schema, field, htmlFor }) {
@@ -228,7 +257,8 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
228
257
  <input id="ag-dn" type="text" placeholder=${name} disabled=${f.inherit.display_name} value=${shown("display_name", f.display_name, defaults.display_name || "")} onInput=${(e) => set("display_name")(e.target.value)} /></${Override}></div>
229
258
  <div class="field">${L(tn("description"), "instance.description", "ag-desc")}<input id="ag-desc" type="text" value=${f.description} onInput=${(e) => set("description")(e.target.value)} /></div>
230
259
  <div class="field">${L(tn("backend"), "instance.backend", "ag-be")}<${BackendSelect} id="ag-be" value=${f.backend} onChange=${set("backend")} /></div>
231
- <div class="field">${L(tn("model"), "instance.model", "ag-model")}<input id="ag-model" type="text" placeholder=${tn("inheritPlaceholder")} value=${f.model} onInput=${(e) => set("model")(e.target.value)} /></div>
260
+ <div class="field">${L(tn("model"), "instance.model", "ag-model")}<${ModelSelect} id="ag-model" value=${f.model} onChange=${set("model")}
261
+ backend=${f.backend || defaults.backend || "claude-code"} instance=${name} inherit=${tn("inheritPlaceholder")} /></div>
232
262
  </div>
233
263
  <div class="field">${L(tn("workingDir"), "instance.working_directory", "ag-wd")}<input id="ag-wd" type="text" value=${f.working_directory} onInput=${(e) => set("working_directory")(e.target.value)} /></div>
234
264
  <div class="field">${L(`${tn("autoPause")} (${tn("minutes")})`, "instance.auto_pause_after", "ag-auto")}
@@ -264,8 +294,8 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
264
294
  onChange=${(e) => set("lightweight")(e.target.checked)} /><span>${tn("lightweight")}</span></label></${Override}></div>
265
295
  <div class="field">${L(tn("modelFailover"), "instance.model_failover", "ag-fo")}
266
296
  <${Override} inherit=${f.inherit.model_failover} onInherit=${setInherit("model_failover")}>
267
- <input id="ag-fo" type="text" placeholder="e.g. sonnet" disabled=${f.inherit.model_failover}
268
- value=${shown("model_failover", f.model_failover, (defaults.model_failover && defaults.model_failover[0]) || "")} onInput=${(e) => set("model_failover")(e.target.value)} /></${Override}>
297
+ <${ModelSelect} id="ag-fo" disabled=${f.inherit.model_failover} backend=${f.backend || defaults.backend || "claude-code"} instance=${name} inherit=${tn("modelNone")}
298
+ value=${shown("model_failover", f.model_failover, (defaults.model_failover && defaults.model_failover[0]) || "")} onChange=${set("model_failover")} /></${Override}>
269
299
  <${Feedback} warning=${failoverWarn} /></div>
270
300
  </div>
271
301
  <div class="field">${L(tn("systemPrompt"), "instance.systemPrompt", "ag-sys")}<input id="ag-sys" type="text" placeholder="e.g. file:prompts/dev.md" value=${f.systemPrompt} onInput=${(e) => set("systemPrompt")(e.target.value)} /></div>
@@ -557,7 +587,7 @@ export function ClassicDialog({ room, ctx, onClose }) {
557
587
  backend: c.backend || inherited.backend, model: c.model || "",
558
588
  auto_pause_after: String(c.auto_pause_after ?? inherited.auto_pause_after), tool_progress: c.tool_progress ?? inherited.tool_progress,
559
589
  reply_completion_guard: c.reply_completion_guard ?? inherited.reply_completion_guard,
560
- collab: !!c.collab, web_echo: !!c.web_echo, context_lines: String(c.context_lines ?? cd.context_lines ?? 5),
590
+ collab: !!c.collab, web_echo: classicWebEchoEffective(c, fleet), context_lines: String(c.context_lines ?? cd.context_lines ?? 5),
561
591
  inherit: { auto_pause_after: !hasOwn(c, "auto_pause_after"), tool_progress: !hasOwn(c, "tool_progress"), reply_completion_guard: !hasOwn(c, "reply_completion_guard") },
562
592
  }));
563
593
  const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
@@ -580,12 +610,9 @@ export function ClassicDialog({ room, ctx, onClose }) {
580
610
  auto_pause_after: hasOwn(c, "auto_pause_after") ? c.auto_pause_after : null,
581
611
  tool_progress: hasOwn(c, "tool_progress") ? c.tool_progress : null,
582
612
  reply_completion_guard: hasOwn(c, "reply_completion_guard") ? c.reply_completion_guard : null,
583
- collab: !!c.collab, context_lines: c.context_lines ?? cd.context_lines ?? 5, web_echo: !!c.web_echo,
613
+ collab: !!c.collab, context_lines: c.context_lines ?? cd.context_lines ?? 5, web_echo: classicWebEchoEffective(c, fleet),
584
614
  });
585
615
  if (!Object.keys(patch).length) { onClose(); return; }
586
- // #1320: turning web echo on for a group posts every web message where all its members can read it.
587
- if (patch.web_echo === true && !c.web_echo && webEchoIsGroup(c.channelId)
588
- && !(await ask(tn("webEchoGroupConfirm"), { confirmLabel: tn("webEchoEnable") }))) return;
589
616
  if (!lease.current()) return;
590
617
  ctx.stageClassic(c, patch);
591
618
  onClose();
@@ -594,7 +621,8 @@ export function ClassicDialog({ room, ctx, onClose }) {
594
621
  return html`<${EditDialog} title=${c.name || c.instanceName} subtitle=${`ID: ${c.instanceName} · ${tn(`exec_${runtime.execution}`)}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${CLASSIC_FIELDS} schema=${schema}>
595
622
  <div class="grid2">
596
623
  <div class="field">${L(tn("backend"), "classic.backend", "cl-be")}<${BackendSelect} id="cl-be" value=${f.backend} onChange=${set("backend")} /></div>
597
- <div class="field">${L(tn("model"), "classic.model", "cl-model")}<input id="cl-model" type="text" placeholder=${inherited.model || tn("inheritPlaceholder")} value=${f.model} onInput=${(e) => set("model")(e.target.value)} /></div>
624
+ <div class="field">${L(tn("model"), "classic.model", "cl-model")}<${ModelSelect} id="cl-model" value=${f.model} onChange=${set("model")}
625
+ backend=${f.backend} instance=${c.instanceName} inherit=${inherited.model ? tn("inheritValue", inherited.model) : tn("inheritPlaceholder")} /></div>
598
626
  <div class="field">${L(`${tn("autoPause")} (${tn("minutes")})`, "classic.auto_pause_after", "cl-auto")}
599
627
  <${Override} inherit=${f.inherit.auto_pause_after} onInherit=${setInherit("auto_pause_after")}>
600
628
  <input id="cl-auto" type="number" min="0" step="1" class=${errors.auto ? "invalid" : ""} disabled=${f.inherit.auto_pause_after}
@@ -4,10 +4,11 @@ import { register, t } from "/assets/app-i18n.js";
4
4
  import { appStore, useStore } from "/assets/app-store.js";
5
5
  import { confirmDialog } from "/assets/ui-confirm.js";
6
6
  import { OwnerEffects } from "./settings-owner-effects.js";
7
+ import { CardTitle } from "./settings-learn.js";
7
8
 
8
9
  register("effectSettings", {
9
10
  en: {
10
- title: "Effects", lead: "Manage effect files and save selections for a chat. Saving replaces that chat’s parameters with the values below.", folder: "Upload an effect folder", files: "Choose effect.json and worker.js, with optional raster assets. Up to 1 MiB after processing.",
11
+ title: "Effects", lead: "Your effect files, and which one a chat uses.", folder: "Upload an effect folder", files: "effect.json + worker.js (and pictures), up to 1 MiB.",
11
12
  inUse: "This effect is still in use and cannot be deleted.", quota: "Your library reached its 10-effect or 8 MiB limit. Delete unused copies first.", rateLimited: "Too many changes. Wait a moment, then try again.",
12
13
  loading: "Loading effects…", retry: "Reload library", empty: "No effects in your library.", failed: "Couldn't load the library. Try again.", actionFailed: "Couldn't save this change. Try again.", savedRefresh: "Saved. Reload the library to see the result.",
13
14
  uploadFailed: "Couldn't upload this folder. Check its files and size, then try again.", public: "Effect management isn't available on a temporary public link.", readOnly: "Read-only session: sign in as an admin to change effects.",
@@ -16,7 +17,7 @@ register("effectSettings", {
16
17
  fps: "Host FPS p50 / p10", tasks: "Host long task p95", noTasks: "Not observed", standard: "Standard device", constrained: "Constrained device", params: "Parameters", badParams: "Check the parameter values.", more: "Show more", count: "{0} effects", one: "1 effect", profileFailed: "Couldn't read profile history. Try again.",
17
18
  },
18
19
  "zh-TW": {
19
- title: "效果", lead: "管理效果檔案,並儲存聊天的效果選擇。儲存時會以下方數值取代該聊天的效果參數。", folder: "上傳效果資料夾", files: "選擇 effect.json、worker.js,以及可選的點陣圖素材;處理後最多 1 MiB。",
20
+ title: "效果", lead: "你的效果檔案,以及每個聊天使用哪一個。", folder: "上傳效果資料夾", files: "effect.json + worker.js(可加圖片),最多 1 MiB。",
20
21
  inUse: "這個效果仍在使用中,無法刪除。", quota: "你的程式庫已達 10 個效果或 8 MiB 上限,請先刪除未使用的副本。", rateLimited: "變更太頻繁,請稍候再試。",
21
22
  loading: "正在載入效果…", retry: "重新載入程式庫", empty: "程式庫中還沒有效果。", failed: "無法載入程式庫,請重試。", actionFailed: "無法儲存變更,請重試。", savedRefresh: "已儲存,請重新載入程式庫查看結果。",
22
23
  uploadFailed: "無法上傳這個資料夾,請檢查檔案與大小後重試。", public: "臨時公開連結不提供效果管理。", readOnly: "目前為唯讀登入,請以管理員身分登入才能修改效果。",
@@ -180,7 +181,7 @@ export function EffectsCard() {
180
181
  if (!current(op.seq)) return; finish(op.seq);
181
182
  if (yes) await change(`/ui/effects/${id}`, undefined, "DELETE");
182
183
  };
183
- return html`<section class="card s-appearance s-effects"><h3>${tn("title")}</h3><p class="sub">${tn("lead")}</p>
184
+ return html`<section class="card s-appearance s-effects"><${CardTitle} title=${tn("title")} topic="effects" /><p class="sub">${tn("lead")}</p>
184
185
  <div class="s-fx-actions"><button type="button" class="btn" disabled=${busy} onClick=${reload}>${tn("retry")}</button>
185
186
  ${view?.canWrite ? html`<button class="btn s-fx-choose" type="button" disabled=${busy} onClick=${() => uploadInput.current?.click()}>${tn("folder")}</button><input ref=${uploadInput} class="offscreen s-fx-upload" aria-label=${tn("folder")} tabindex="-1" type="file" webkitdirectory=${true} directory=${true} multiple disabled=${busy} onChange=${upload} />` : null}</div>
186
187
  ${busy ? html`<p class="sub" role="status">${tn("loading")}</p>` : null}${error ? html`<p class="err" role="status">${error}</p>` : null}${message ? html`<p class="sub" role="status">${message}</p>` : null}
@@ -0,0 +1,117 @@
1
+ // #1920 phase 3 (P3b-2): Settings → Web → Guests — guest sign-in for people who are not admins. The switches (guest
2
+ // sign-in, guests may chat), the agents an open connection's guests may reach, and every guest account: who, on which
3
+ // platform, what it reaches now, its sessions and devices, with Sign out (at once) and Narrow (staged with the rest).
4
+ // Changes go through Review changes → Apply like every Settings edit: one that widens what guests reach waits for a
5
+ // fleet admin's confirmation (the server's settings-change.ts), one that narrows it applies at once.
6
+ import { html, useEffect, useMemo, useState } from "/assets/app-html.js";
7
+ import { t } from "/assets/app-i18n.js";
8
+ import { toast } from "/assets/ui-toast.js";
9
+ import { confirmDialog } from "/assets/ui-confirm.js";
10
+ import { api } from "./settings-confirm.js";
11
+ import "./settings-strings.js";
12
+
13
+ const tn = (k, ...v) => t(`settings.${k}`, ...v);
14
+ const PLATFORMS = { discord: "Discord", telegram: "Telegram" };
15
+ const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
16
+ const copyLists = (m) => { const out = {}; for (const [k, v] of Object.entries(m || {})) if (Array.isArray(v)) out[k] = [...v]; return out; };
17
+ const sameList = (a, b) => (a || []).length === (b || []).length && (a || []).every((n) => (b || []).includes(n));
18
+ const accountKey = (g) => `${g.adapterId}:${g.userId}`;
19
+
20
+ /** What changed against the configuration, as the PUT /api/settings/fleet/web body's `guests` (null: nothing). */
21
+ export function guestsBody(original, f) {
22
+ const out = {};
23
+ if (f.enabled !== original.enabled) out.enabled = f.enabled ? true : null; // off is the default
24
+ if (f.chat !== original.chat) out.chat = f.chat ? null : false; // on is the default
25
+ for (const key of ["open_instances", "narrow"]) {
26
+ const was = original[key], now = f[key], diff = {};
27
+ for (const k of new Set([...Object.keys(was), ...Object.keys(now)])) {
28
+ const a = own(was, k) ? was[k] : undefined, b = own(now, k) ? now[k] : undefined;
29
+ if (a === undefined && b === undefined) continue;
30
+ if (a !== undefined && b !== undefined && sameList(a, b)) continue;
31
+ // open_instances: none named = the entry leaves. narrow: undefined = lifted; [] = narrowed to nothing.
32
+ diff[k] = b === undefined || (key === "open_instances" && !b.length) ? null : [...b];
33
+ }
34
+ if (Object.keys(diff).length) out[key] = diff;
35
+ }
36
+ return Object.keys(out).length ? out : null;
37
+ }
38
+
39
+ /** `away`: narrowed names it no longer reaches — listed (marked) only so they can be unticked; unticked, they leave. */
40
+ function Names({ id, all, value, onChange, away = new Set() }) {
41
+ const chosen = new Set(value || []);
42
+ return html`<div class="guest-names" id=${id} role="group">${all.map((n) => html`<label key=${n} class=${`check guest-name${away.has(n) ? " guest-away" : ""}`}>
43
+ <input type="checkbox" checked=${chosen.has(n)} onChange=${(e) => { const s = new Set(chosen); if (e.target.checked) s.add(n); else s.delete(n); onChange(all.filter((x) => s.has(x))); }} />
44
+ <span>${n}${away.has(n) ? html` <span class="note">${tn("guestNotReachable")}</span>` : null}</span></label>`)}</div>`;
45
+ }
46
+
47
+ export function GuestsCard({ ctx, staged, disabled = false }) {
48
+ const g = (ctx.fleet.web && ctx.fleet.web.guests) || {};
49
+ const original = useMemo(() => ({ enabled: g.enabled === true, chat: g.chat !== false, open_instances: copyLists(g.open_instances), narrow: copyLists(g.narrow) }), []);
50
+ const [f, setF] = useState(() => ({ ...original, open_instances: copyLists(original.open_instances), narrow: copyLists(original.narrow) }));
51
+ const [overview, setOverview] = useState(null), [failed, setFailed] = useState(false), [narrowing, setNarrowing] = useState(null), [msg, setMsg] = useState(null);
52
+ const load = async () => {
53
+ let r = null;
54
+ try { r = await api("/api/settings/guests"); } catch { r = null; }
55
+ if (r && r.ok && r.body) { setOverview(r.body); setFailed(false); } else setFailed(true);
56
+ };
57
+ useEffect(() => { load(); }, []);
58
+ const set = (patch) => setF((x) => ({ ...x, ...patch }));
59
+ const setList = (key, k, list) => setF((x) => { const m = { ...x[key] }; if (list === undefined) delete m[k]; else m[k] = list; return { ...x, [key]: m }; });
60
+ const review = () => {
61
+ const body = guestsBody(original, f);
62
+ if (body) ctx.stage("guests", { label: tn("guestsTitle"), impact: "now", request: { method: "PUT", url: "/api/settings/fleet/web", body: { guests: body } } });
63
+ else if (staged.has("guests")) ctx.unstage("guests");
64
+ setMsg(tn("readyApply"));
65
+ };
66
+ const signOut = async (guest) => {
67
+ const yes = await confirmDialog({ title: tn("guestSignOutTitle"), message: tn("guestSignOutConfirm", guest.name), confirmLabel: tn("guestSignOut"), danger: true });
68
+ if (!yes) return;
69
+ let r = null;
70
+ try { r = await api("/api/settings/guests/revoke", { method: "POST", body: JSON.stringify({ adapterId: guest.adapterId, userId: guest.userId }) }); } catch { r = null; }
71
+ if (r && r.ok) { toast(tn(r.body && r.body.durable === false ? "guestSignedOutNotDurable" : "guestSignedOut", guest.name)); load(); }
72
+ else toast(tn("guestSignOutFailed"), false);
73
+ };
74
+ const instances = (overview && overview.instances) || [];
75
+ const open = ((overview && overview.connections) || []).filter((c) => c.mode === "open");
76
+ const guests = (overview && overview.guests) || [];
77
+ const when = (ms) => (typeof ms === "number" ? tn("guestLastActive", new Date(ms).toLocaleString()) : tn("guestNotSignedIn"));
78
+ return html`<section class="card s-guests"><h3>${tn("guestsTitle")}</h3>
79
+ <p class="note">${tn("guestsHint")}</p>
80
+ <fieldset class="s-guest-fields" disabled=${disabled}>
81
+ <label class="check"><input id="guestsEnabled" type="checkbox" checked=${f.enabled} onChange=${(e) => set({ enabled: e.target.checked })} /><span>${tn("guestsEnabled")}</span></label>
82
+ <p class="note">${tn("guestsEnabledHint")}</p>
83
+ <label class="check"><input id="guestsChat" type="checkbox" checked=${f.chat} onChange=${(e) => set({ chat: e.target.checked })} /><span>${tn("guestsChat")}</span></label>
84
+ <p class="note">${tn("guestsChatHint")}</p>
85
+ ${open.map((c) => html`<div key=${c.id} class="field guest-open"><span class="label">${tn("guestsOpenFor", c.id, PLATFORMS[c.type] || c.type)}</span>
86
+ <p class="note">${tn("guestsOpenHint")}</p>
87
+ <${Names} id=${`guestsOpen-${c.id}`} all=${instances} value=${own(f.open_instances, c.id) ? f.open_instances[c.id] : []} onChange=${(list) => setList("open_instances", c.id, list)} /></div>`)}
88
+ <h4>${tn("guestsList")}</h4>
89
+ ${failed ? html`<p class="note" role="status">${tn("guestsLoadFailed")} <button type="button" class="btn btn-sm" onClick=${load}>${t("app.retry")}</button></p>`
90
+ : !overview ? html`<p class="note">${t("app.loading")}</p>`
91
+ : !guests.length ? html`<p class="note" id="guestsNone">${tn("guestsNone")}</p>`
92
+ : html`<ul class="guest-list">${guests.map((guest) => {
93
+ const k = accountKey(guest), narrowed = own(f.narrow, k) ? f.narrow[k] : undefined;
94
+ return html`<li key=${k} class="guest-row" data-account=${k}>
95
+ <div class="guest-who"><strong>${guest.name}</strong> <span class="note">${PLATFORMS[guest.platform] || guest.platform} · ${guest.userId}</span></div>
96
+ <div class="note guest-facts">${tn("guestFacts", guest.scope.length, guest.sessions, guest.devices)} · ${when(guest.lastSeen)}</div>
97
+ <div class="note guest-narrow">${narrowed === undefined ? tn("guestNotNarrowed") : tn("guestNarrowedTo", narrowed.length ? narrowed.join(", ") : tn("guestNothing"))}</div>
98
+ <div class="guest-actions">
99
+ <button type="button" class="btn btn-sm" aria-expanded=${narrowing === k ? "true" : "false"} onClick=${() => setNarrowing(narrowing === k ? null : k)}>${tn("guestNarrow")}</button>
100
+ ${narrowed !== undefined ? html`<button type="button" class="btn btn-sm" onClick=${() => setList("narrow", k, undefined)}>${tn("guestNarrowLift")}</button>` : null}
101
+ ${guest.sessions || guest.devices ? html`<button type="button" class="btn btn-sm btn-danger" onClick=${() => signOut(guest)}>${tn("guestSignOut")}</button>` : null}
102
+ </div>
103
+ ${narrowing === k ? (() => {
104
+ // #2104 review: only what this account can reach (narrowing never widens); a narrowed name it no longer
105
+ // reaches is listed to be unticked, marked.
106
+ const reach = guest.reach || guest.scope, away = new Set((narrowed || []).filter((n) => !reach.includes(n)));
107
+ return html`<div class="guest-narrow-edit"><p class="note">${tn("guestNarrowHint")}</p>
108
+ ${reach.length || away.size ? html`<${Names} id=${`guestNarrow-${k}`} all=${[...reach, ...away]} away=${away} value=${narrowed === undefined ? guest.scope : narrowed} onChange=${(list) => setList("narrow", k, list)} />`
109
+ : html`<p class="note">${tn("guestReachesNothing")}</p>`}</div>`;
110
+ })() : null}
111
+ </li>`;
112
+ })}</ul>`}
113
+ </fieldset>
114
+ <div class="save-row"><button type="button" class="btn btn-primary" id="guestsReview" disabled=${disabled} onClick=${review}>${tn("guestsReview")}</button>
115
+ ${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>
116
+ </section>`;
117
+ }
@@ -0,0 +1,25 @@
1
+ // #2056 B: a card title with a small "?" that opens the card's section of the docs, so Settings → Web appearance
2
+ // keeps one short line per control and the long explanations live in docs/web-dashboard(.zh-TW).md.
3
+ import { html } from "/assets/app-html.js";
4
+ import { lang, t } from "/assets/app-i18n.js";
5
+ import "./settings-strings.js";
6
+
7
+ /**
8
+ * #2056 B: where each card's long explanation lives — a heading of docs/web-dashboard(.zh-TW).md on GitHub, so the
9
+ * page keeps one short line per control and a small "?" for the rest.
10
+ */
11
+ const DOCS = { en: "https://github.com/songsid/AgEnD/blob/main/docs/web-dashboard.md", "zh-TW": "https://github.com/songsid/AgEnD/blob/main/docs/web-dashboard.zh-TW.md" };
12
+ export const LEARN_ANCHORS = {
13
+ colors: ["identity-colours", "識別色"], css: ["custom-css-settings--web-appearance", "自訂-css設定--網頁外觀"], themes: ["agents-themes", "agent-的主題"],
14
+ cards: ["identity-cards", "身分卡"], limits: ["card-upload-limits", "身分卡上傳限制"], widgets: ["widgets", "小工具"], mods: ["mods", "mod"],
15
+ effects: ["effects", "效果"], restore: ["restore-defaults", "還原預設值"],
16
+ };
17
+ export function learnHref(topic, language = lang()) {
18
+ const zh = language === "zh-TW";
19
+ return `${DOCS[zh ? "zh-TW" : "en"]}#${encodeURIComponent(LEARN_ANCHORS[topic][zh ? 1 : 0])}`;
20
+ }
21
+ /** A card title with its "?" link to the docs (a new tab; the page itself never fetches anything). */
22
+ export function CardTitle({ title, topic }) {
23
+ return html`<h3 class="s-ap-title"><span>${title}</span>${topic ? html`<a class="s-learn" href=${learnHref(topic)} target="_blank" rel="noopener noreferrer"
24
+ aria-label=${t("settings.learnMore", title)} title=${t("settings.learnMore", title)}>?</a>` : null}</h3>`;
25
+ }
@@ -97,15 +97,20 @@ export function effectiveSummary(live, config, fleetDefaults, classicDefaults) {
97
97
  return { backend, model, effort };
98
98
  }
99
99
 
100
- /** Group by first tag, "Other" last; within a group Running → Paused → Crashed → Stopped, then by name. */
101
- export function groupAgents(instances, live, filter) {
100
+ /**
101
+ * Group by `groupFor(name)` — #2172: the sidebar's rule (instance-nav groupOf: its first team, else its first tag); the
102
+ * first tag when not given — teams first, then tags, "Other" last; within a group Running → Paused → Crashed → Stopped,
103
+ * then by name. `labelOf(group)` sorts by what the heading says.
104
+ */
105
+ export function groupAgents(instances, live, filter, groupFor = (n) => groupOf(instances[n]), labelOf = (g) => g) {
102
106
  const needle = String(filter || "").trim().toLowerCase();
103
107
  let names = Object.keys(instances || {});
104
108
  if (needle) names = names.filter((n) => n.toLowerCase().includes(needle) || shortName(n, instances[n]).toLowerCase().includes(needle));
105
109
  const groups = {};
106
- for (const n of names) (groups[groupOf(instances[n])] ??= []).push(n);
110
+ for (const n of names) (groups[groupFor(n)] ??= []).push(n);
107
111
  const order = (n) => STATUS_ORDER[(live[n] && live[n].status) || "stopped"] ?? 9;
108
- return Object.keys(groups).sort((a, b) => (a === "Other" ? 1 : b === "Other" ? -1 : a.localeCompare(b)))
112
+ const team = (g) => g.charCodeAt(0) < 32; // a team's key (instance-nav TEAM_GROUP)
113
+ return Object.keys(groups).sort((a, b) => (a === "Other" ? 1 : b === "Other" ? -1 : team(a) !== team(b) ? (team(a) ? -1 : 1) : labelOf(a).localeCompare(labelOf(b))))
109
114
  .map((g) => ({ group: g, names: groups[g].sort((a, b) => order(a) - order(b) || a.localeCompare(b)) }));
110
115
  }
111
116
 
@@ -222,8 +227,12 @@ export function accessConfirmations(previousAccess, nextMode, nextUsers) {
222
227
  return out;
223
228
  }
224
229
 
225
- /** #1320: only a short positive numeric id is a Telegram private chat; Discord snowflakes and negative ids are groups. */
226
- export const webEchoIsGroup = (channelIdValue) => !/^\d{1,12}$/.test(String(channelIdValue || ""));
230
+ /**
231
+ * #2054: whether a ClassicBot room copies web chat, as the fleet decides it (ClassicChannelManager's load +
232
+ * classicWebEchoOn): an absent `web_echo` follows the fleet-wide `web.echo_to_channel` (default on); any other value
233
+ * echoes only when it is `true` (a `null`, `0` or string reads as off, as the manager reads it).
234
+ */
235
+ export const classicWebEchoEffective = (room, fleet) => (room?.web_echo === undefined ? fleet?.web?.echo_to_channel !== false : room.web_echo === true);
227
236
 
228
237
  // ── Developer YAML: the config subset's writer and reader, exactly as settings.html had them (round-trip verified) ──
229
238
  export function toYaml(value, indent = 0) {
@@ -12,6 +12,7 @@ import { toast } from "/assets/ui-toast.js";
12
12
  import { confirmDialog } from "/assets/ui-confirm.js";
13
13
  import { startOwnerCssTrial } from "/assets/owner-css.js";
14
14
  import { AgentWidget, checkWidget, fleetBinds, instanceBinds } from "/assets/agent-widget.js";
15
+ import { CardTitle } from "./settings-learn.js";
15
16
  import "./settings-strings.js";
16
17
 
17
18
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
@@ -40,9 +41,9 @@ function why(body) {
40
41
 
41
42
  /** The parts a plan offers, each with its default: everything but custom CSS. */
42
43
  export function defaultChoice(plan) {
43
- const c = { theme: false, identity: false, effects: false, ownerCss: false, widgets: {}, replace: false };
44
+ const c = { theme: false, identity: false, effects: false, ownerCss: false, ambient: false, widgets: {}, replace: false };
44
45
  for (const it of plan.items) {
45
- if (it.part === "theme" || it.part === "identity") c[it.part] = true;
46
+ if (it.part === "theme" || it.part === "identity" || it.part === "ambient") c[it.part] = true;
46
47
  else if (it.part === "widget") c.widgets[it.slot] = true;
47
48
  else if (it.part === "effect") c.effects = true;
48
49
  // ownerCss stays false: "read it before turning it on" (r4 §4).
@@ -99,6 +100,19 @@ function PlanItem({ it, choice, setChoice, binds, fleet }) {
99
100
  return html`<li class="s-mod-item">${tick(tn(`modPartWidget_${it.slot}`), !!choice.widgets[it.slot], (v) => setChoice({ ...choice, widgets: { ...choice.widgets, [it.slot]: v } }))}
100
101
  <div class="s-mod-detail s-mod-widget">${w ? html`<${AgentWidget} slot=${it.slot} widget=${w} binds=${it.slot === "sidebar" ? fleet : binds} />` : null}</div></li>`;
101
102
  }
103
+ if (it.part === "ambient") {
104
+ // #2117: each ambient as it will be drawn: its preset, glyphs (pictures shown) and colours; every field below.
105
+ const glyph = (g, i) => typeof g === "string" ? html`<span key=${i} class="s-mod-text">${g}</span>`
106
+ : html`<img key=${i} class="s-mod-glyph" src=${`/ui/mods/staged/${g.asset}`} alt="" loading="lazy" />`;
107
+ const one = (kind) => {
108
+ const a = it[kind];
109
+ if (!a || !Array.isArray(a.glyphs)) return null;
110
+ return html`<div key=${kind} class="s-mod-kv"><span class="k">${a.preset}</span><span class="v s-mod-sws">${a.glyphs.map(glyph)}${(a.colors || []).map((c) => html`<${Swatch} key=${c} color=${c} />`)}</span></div>`;
111
+ };
112
+ return html`<li class="s-mod-item">${tick(tn("modPartAmbient"), !!choice.ambient, (v) => setChoice({ ...choice, ambient: v }))}
113
+ <div class="s-mod-detail">${one("loop")}${one("click")}
114
+ <details class="s-mod-raw"><summary>${tn("modAllFields")}</summary><pre class="s-mod-css-text">${JSON.stringify({ loop: it.loop, click: it.click }, null, 2)}</pre></details></div></li>`;
115
+ }
102
116
  if (it.part === "effect") {
103
117
  return html`<li class="s-mod-item">${tick(tn("modPartEffect", it.title), choice.effects, (v) => setChoice({ ...choice, effects: v }))}
104
118
  <div class="s-mod-detail"><span>${kib(it.bytes)} · ${tn("modRegions", it.regions.join(", "))}</span>${it.wants.length ? html`<span>${tn("modWants", it.wants.join(", "))}</span>` : null}</div></li>`;
@@ -119,7 +133,7 @@ function Plan({ staged, onDone }) {
119
133
  const [target, setTarget] = useState("");
120
134
  const [busy, setBusy] = useState(false);
121
135
  const [error, setError] = useState("");
122
- const toInstance = choice.theme || choice.identity || !!choice.widgets.chat_header || !!choice.widgets.details;
136
+ const toInstance = choice.theme || choice.identity || choice.ambient || !!choice.widgets.chat_header || !!choice.widgets.details;
123
137
  const names = (app.instances || []).map((i) => i.name);
124
138
  const install = async () => {
125
139
  setBusy(true); setError("");
@@ -161,13 +175,13 @@ function Plan({ staged, onDone }) {
161
175
 
162
176
  function ExportForm() {
163
177
  const app = useStore(appStore);
164
- const [f, setF] = useState({ instance: "", name: "", version: "1.0.0", title: "", theme: true, identity: true, header: true, details: true, sidebar: false, ownerCss: false });
178
+ const [f, setF] = useState({ instance: "", name: "", version: "1.0.0", title: "", theme: true, identity: true, header: true, details: true, ambient: true, sidebar: false, ownerCss: false });
165
179
  const [error, setError] = useState("");
166
180
  const set = (k) => (e) => setF({ ...f, [k]: e.target.type === "checkbox" ? e.target.checked : e.target.value });
167
181
  const go = async () => {
168
182
  setError("");
169
183
  const body = { instance: f.instance || undefined, name: f.name, version: f.version, title: f.title,
170
- include: { theme: f.theme, identity: f.identity, ownerCss: f.ownerCss, widgets: { chat_header: f.header, details: f.details, sidebar: f.sidebar } } };
184
+ include: { theme: f.theme, identity: f.identity, ambient: f.ambient, ownerCss: f.ownerCss, widgets: { chat_header: f.header, details: f.details, sidebar: f.sidebar } } };
171
185
  let res;
172
186
  try { res = await fetch("/ui/mods/export", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); }
173
187
  catch { setError(tn("modFailed", "network")); return; }
@@ -181,7 +195,7 @@ function ExportForm() {
181
195
  return html`<details class="s-mod-export"><summary>${tn("modExport")}</summary>
182
196
  <p class="sub">${tn("modExportLead")}</p>
183
197
  <label class="field"><span>${tn("modExportInstance")}</span><select value=${f.instance} onChange=${set("instance")}><option value="">—</option>${(app.instances || []).map((i) => html`<option key=${i.name} value=${i.name}>${i.name}</option>`)}</select></label>
184
- <div class="s-mod-boxes">${box("theme", tn("modPartTheme"))}${box("identity", tn("modPartIdentity"))}${box("header", tn("modPartWidget_chat_header"))}${box("details", tn("modPartWidget_details"))}${box("sidebar", tn("modPartWidget_sidebar"))}${box("ownerCss", tn("modPartCss"))}</div>
198
+ <div class="s-mod-boxes">${box("theme", tn("modPartTheme"))}${box("identity", tn("modPartIdentity"))}${box("header", tn("modPartWidget_chat_header"))}${box("details", tn("modPartWidget_details"))}${box("ambient", tn("modPartAmbient"))}${box("sidebar", tn("modPartWidget_sidebar"))}${box("ownerCss", tn("modPartCss"))}</div>
185
199
  <label class="field"><span>${tn("modExportName")}</span><input value=${f.name} placeholder="my-look" onInput=${set("name")} /></label>
186
200
  <label class="field"><span>${tn("modExportVersion")}</span><input value=${f.version} onInput=${set("version")} /></label>
187
201
  <label class="field"><span>${tn("modExportTitle")}</span><input value=${f.title} onInput=${set("title")} /></label>
@@ -204,8 +218,8 @@ export function ModsCard() {
204
218
  setState(b && Array.isArray(b.mods) ? { mods: b.mods.filter((m) => m && typeof m.id === "string"), staged, proposals } : { failed: true });
205
219
  };
206
220
  useEffect(() => { load(); }, []);
207
- if (!state) return html`<section class="card s-appearance s-mods"><h3>${tn("modsTitle")}</h3><p class="sub">…</p></section>`;
208
- if (state.failed) return html`<section class="card s-appearance s-mods"><h3>${tn("modsTitle")}</h3><p class="err">${tn("modsLoadFailed")}</p></section>`;
221
+ if (!state) return html`<section class="card s-appearance s-mods"><${CardTitle} title=${tn("modsTitle")} topic="mods" /><p class="sub">…</p></section>`;
222
+ if (state.failed) return html`<section class="card s-appearance s-mods"><${CardTitle} title=${tn("modsTitle")} topic="mods" /><p class="err">${tn("modsLoadFailed")}</p></section>`;
209
223
  const stage = async () => {
210
224
  const f = file.current && file.current.files && file.current.files[0];
211
225
  if (!f) return;
@@ -238,7 +252,7 @@ export function ModsCard() {
238
252
  if (!r || !r.ok) { toast(tn("modFailed", r ? r.status : "network"), false); return; }
239
253
  download(await r.text(), `${m.name}-${m.version}.agendmod.json`);
240
254
  };
241
- return html`<section class="card s-appearance s-mods"><h3>${tn("modsTitle")}</h3>
255
+ return html`<section class="card s-appearance s-mods"><${CardTitle} title=${tn("modsTitle")} topic="mods" />
242
256
  <p class="sub">${tn("modsLead")}</p>
243
257
  ${state.staged ? html`<${Plan} key=${state.staged.stage} staged=${state.staged} onDone=${load} />` : html`<div class="s-mod-import">
244
258
  <label class="btn"><${Icon} name="attach" size=${16} /><span>${tn("modChoose")}</span>