@songsid/agend 2.2.0-beta.1 → 2.2.0-beta.11

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 (401) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +16 -9
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +24 -9
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +47 -0
  9. package/dist/agent-whats-new.js +147 -0
  10. package/dist/agent-whats-new.js.map +1 -0
  11. package/dist/auth-api.d.ts +5 -3
  12. package/dist/auth-api.js +19 -3
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/antigravity.d.ts +8 -0
  15. package/dist/backend/antigravity.js +32 -9
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/claude-carry.d.ts +22 -0
  18. package/dist/backend/claude-carry.js +170 -0
  19. package/dist/backend/claude-carry.js.map +1 -0
  20. package/dist/backend/claude-code.d.ts +65 -2
  21. package/dist/backend/claude-code.js +226 -33
  22. package/dist/backend/claude-code.js.map +1 -1
  23. package/dist/backend/codex-metadata.d.ts +33 -0
  24. package/dist/backend/codex-metadata.js +122 -18
  25. package/dist/backend/codex-metadata.js.map +1 -1
  26. package/dist/backend/codex.d.ts +3 -0
  27. package/dist/backend/codex.js +17 -1
  28. package/dist/backend/codex.js.map +1 -1
  29. package/dist/backend/credential-profile.d.ts +40 -3
  30. package/dist/backend/credential-profile.js +113 -5
  31. package/dist/backend/credential-profile.js.map +1 -1
  32. package/dist/backend/effort-metadata.d.ts +37 -1
  33. package/dist/backend/effort-metadata.js +86 -15
  34. package/dist/backend/effort-metadata.js.map +1 -1
  35. package/dist/backend/grok.d.ts +4 -0
  36. package/dist/backend/grok.js +14 -0
  37. package/dist/backend/grok.js.map +1 -1
  38. package/dist/backend/kiro-engine-ledger.d.ts +5 -0
  39. package/dist/backend/kiro-engine-ledger.js +20 -0
  40. package/dist/backend/kiro-engine-ledger.js.map +1 -1
  41. package/dist/backend/kiro-identity.d.ts +6 -0
  42. package/dist/backend/kiro-identity.js +19 -5
  43. package/dist/backend/kiro-identity.js.map +1 -1
  44. package/dist/backend/kiro.d.ts +39 -3
  45. package/dist/backend/kiro.js +143 -19
  46. package/dist/backend/kiro.js.map +1 -1
  47. package/dist/backend/launch-effort.d.ts +61 -0
  48. package/dist/backend/launch-effort.js +170 -0
  49. package/dist/backend/launch-effort.js.map +1 -0
  50. package/dist/backend/muse.d.ts +84 -0
  51. package/dist/backend/muse.js +449 -53
  52. package/dist/backend/muse.js.map +1 -1
  53. package/dist/backend/types.d.ts +47 -4
  54. package/dist/backend/types.js +12 -1
  55. package/dist/backend/types.js.map +1 -1
  56. package/dist/backend-command.d.ts +91 -0
  57. package/dist/backend-command.js +122 -0
  58. package/dist/backend-command.js.map +1 -0
  59. package/dist/background-work-ended.d.ts +18 -0
  60. package/dist/background-work-ended.js +30 -0
  61. package/dist/background-work-ended.js.map +1 -0
  62. package/dist/cache-service.d.ts +2 -0
  63. package/dist/cache-service.js +2 -1
  64. package/dist/cache-service.js.map +1 -1
  65. package/dist/channel/adapters/discord.d.ts +36 -1
  66. package/dist/channel/adapters/discord.js +126 -4
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +42 -2
  69. package/dist/channel/adapters/telegram.js +142 -23
  70. package/dist/channel/adapters/telegram.js.map +1 -1
  71. package/dist/channel/mcp-server.js +9 -1
  72. package/dist/channel/mcp-server.js.map +1 -1
  73. package/dist/channel/mcp-tools.d.ts +11 -0
  74. package/dist/channel/mcp-tools.js +26 -13
  75. package/dist/channel/mcp-tools.js.map +1 -1
  76. package/dist/channel/message-edit-errors.d.ts +5 -0
  77. package/dist/channel/message-edit-errors.js +24 -0
  78. package/dist/channel/message-edit-errors.js.map +1 -0
  79. package/dist/channel/telegram-markdown.d.ts +6 -0
  80. package/dist/channel/telegram-markdown.js +104 -0
  81. package/dist/channel/telegram-markdown.js.map +1 -0
  82. package/dist/channel/tool-router.d.ts +2 -0
  83. package/dist/channel/tool-router.js +10 -4
  84. package/dist/channel/tool-router.js.map +1 -1
  85. package/dist/channel/types.d.ts +55 -3
  86. package/dist/cli.js +167 -39
  87. package/dist/cli.js.map +1 -1
  88. package/dist/command-table.d.ts +2 -2
  89. package/dist/command-table.js +6 -3
  90. package/dist/command-table.js.map +1 -1
  91. package/dist/config-validator.js +59 -0
  92. package/dist/config-validator.js.map +1 -1
  93. package/dist/cross-instance-envelope.d.ts +15 -2
  94. package/dist/cross-instance-envelope.js +29 -5
  95. package/dist/cross-instance-envelope.js.map +1 -1
  96. package/dist/daemon.d.ts +156 -1
  97. package/dist/daemon.js +773 -89
  98. package/dist/daemon.js.map +1 -1
  99. package/dist/delivery-outbox.d.ts +70 -8
  100. package/dist/delivery-outbox.js +315 -24
  101. package/dist/delivery-outbox.js.map +1 -1
  102. package/dist/delivery-reconciliation.d.ts +18 -1
  103. package/dist/delivery-reconciliation.js +67 -2
  104. package/dist/delivery-reconciliation.js.map +1 -1
  105. package/dist/fleet-context.d.ts +10 -1
  106. package/dist/fleet-manager.d.ts +404 -13
  107. package/dist/fleet-manager.js +2039 -210
  108. package/dist/fleet-manager.js.map +1 -1
  109. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  110. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
  111. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
  112. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  113. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  114. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  115. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  116. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  117. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  118. package/dist/general-knowledge/skills/model-discovery/SKILL.md +12 -9
  119. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  120. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  121. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  122. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  123. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
  124. package/dist/general-knowledge/steering/core-rules.md +8 -7
  125. package/dist/host-memory.d.ts +10 -0
  126. package/dist/host-memory.js +33 -1
  127. package/dist/host-memory.js.map +1 -1
  128. package/dist/human-delivery-receipts.d.ts +83 -0
  129. package/dist/human-delivery-receipts.js +194 -0
  130. package/dist/human-delivery-receipts.js.map +1 -0
  131. package/dist/instance-avatar.d.ts +36 -0
  132. package/dist/instance-avatar.js +57 -0
  133. package/dist/instance-avatar.js.map +1 -0
  134. package/dist/instance-config-impact.js +6 -0
  135. package/dist/instance-config-impact.js.map +1 -1
  136. package/dist/instance-lifecycle.d.ts +29 -6
  137. package/dist/instance-lifecycle.js +79 -28
  138. package/dist/instance-lifecycle.js.map +1 -1
  139. package/dist/instructions.d.ts +3 -1
  140. package/dist/instructions.js +11 -8
  141. package/dist/instructions.js.map +1 -1
  142. package/dist/kiro-log-gate.d.ts +42 -0
  143. package/dist/kiro-log-gate.js +39 -0
  144. package/dist/kiro-log-gate.js.map +1 -0
  145. package/dist/locale.js +226 -44
  146. package/dist/locale.js.map +1 -1
  147. package/dist/log-service.d.ts +99 -0
  148. package/dist/log-service.js +243 -0
  149. package/dist/log-service.js.map +1 -0
  150. package/dist/log-steps.d.ts +169 -0
  151. package/dist/log-steps.js +858 -0
  152. package/dist/log-steps.js.map +1 -0
  153. package/dist/login-controller.d.ts +20 -3
  154. package/dist/login-controller.js +71 -19
  155. package/dist/login-controller.js.map +1 -1
  156. package/dist/login-flows.d.ts +5 -2
  157. package/dist/login-flows.js +5 -4
  158. package/dist/login-flows.js.map +1 -1
  159. package/dist/memory-pressure.d.ts +13 -1
  160. package/dist/memory-pressure.js +50 -10
  161. package/dist/memory-pressure.js.map +1 -1
  162. package/dist/needs-you-hub.js +2 -0
  163. package/dist/needs-you-hub.js.map +1 -1
  164. package/dist/needs-you-live.d.ts +6 -0
  165. package/dist/needs-you-live.js +23 -3
  166. package/dist/needs-you-live.js.map +1 -1
  167. package/dist/next-steps.js +1 -1
  168. package/dist/next-steps.js.map +1 -1
  169. package/dist/outbound-handlers.d.ts +100 -0
  170. package/dist/outbound-handlers.js +284 -97
  171. package/dist/outbound-handlers.js.map +1 -1
  172. package/dist/outbound-schemas.d.ts +38 -15
  173. package/dist/outbound-schemas.js +36 -8
  174. package/dist/outbound-schemas.js.map +1 -1
  175. package/dist/public-link-progress.d.ts +1 -1
  176. package/dist/public-link-progress.js +8 -2
  177. package/dist/public-link-progress.js.map +1 -1
  178. package/dist/public-preview.d.ts +101 -0
  179. package/dist/public-preview.js +236 -0
  180. package/dist/public-preview.js.map +1 -0
  181. package/dist/public-web-gateway.js +14 -4
  182. package/dist/public-web-gateway.js.map +1 -1
  183. package/dist/public-web-link.d.ts +24 -1
  184. package/dist/public-web-link.js +63 -9
  185. package/dist/public-web-link.js.map +1 -1
  186. package/dist/reply-buttons-controller.d.ts +89 -5
  187. package/dist/reply-buttons-controller.js +273 -8
  188. package/dist/reply-buttons-controller.js.map +1 -1
  189. package/dist/reply-buttons.d.ts +48 -3
  190. package/dist/reply-buttons.js +105 -10
  191. package/dist/reply-buttons.js.map +1 -1
  192. package/dist/reply-polls.d.ts +85 -0
  193. package/dist/reply-polls.js +154 -0
  194. package/dist/reply-polls.js.map +1 -0
  195. package/dist/restart-guard.d.ts +6 -3
  196. package/dist/restart-guard.js +6 -3
  197. package/dist/restart-guard.js.map +1 -1
  198. package/dist/restart-progress.d.ts +33 -0
  199. package/dist/restart-progress.js +30 -0
  200. package/dist/restart-progress.js.map +1 -1
  201. package/dist/service-activation.d.ts +15 -7
  202. package/dist/service-activation.js +46 -29
  203. package/dist/service-activation.js.map +1 -1
  204. package/dist/service-installer.d.ts +31 -0
  205. package/dist/service-installer.js +41 -2
  206. package/dist/service-installer.js.map +1 -1
  207. package/dist/settings-api.js +85 -25
  208. package/dist/settings-api.js.map +1 -1
  209. package/dist/settings-change.js +72 -1
  210. package/dist/settings-change.js.map +1 -1
  211. package/dist/settings-effect.js +28 -4
  212. package/dist/settings-effect.js.map +1 -1
  213. package/dist/settings-http-confirmation.js +7 -0
  214. package/dist/settings-http-confirmation.js.map +1 -1
  215. package/dist/setup-form.d.ts +1 -1
  216. package/dist/setup-form.js +1 -1
  217. package/dist/spawn-gate.js +5 -1
  218. package/dist/spawn-gate.js.map +1 -1
  219. package/dist/spawn-hold-reply.d.ts +12 -0
  220. package/dist/spawn-hold-reply.js +39 -0
  221. package/dist/spawn-hold-reply.js.map +1 -0
  222. package/dist/status-emojis.d.ts +10 -3
  223. package/dist/status-emojis.js +11 -2
  224. package/dist/status-emojis.js.map +1 -1
  225. package/dist/systemd-peer-bus.d.ts +95 -0
  226. package/dist/systemd-peer-bus.js +430 -0
  227. package/dist/systemd-peer-bus.js.map +1 -0
  228. package/dist/systemd-runtime.d.ts +27 -1
  229. package/dist/systemd-runtime.js +69 -7
  230. package/dist/systemd-runtime.js.map +1 -1
  231. package/dist/tips.js +4 -4
  232. package/dist/tips.js.map +1 -1
  233. package/dist/tool-permissions.js +3 -3
  234. package/dist/tool-permissions.js.map +1 -1
  235. package/dist/tool-progress.js +3 -2
  236. package/dist/tool-progress.js.map +1 -1
  237. package/dist/topic-commands.d.ts +42 -2
  238. package/dist/topic-commands.js +101 -11
  239. package/dist/topic-commands.js.map +1 -1
  240. package/dist/transcript-delta-reader.d.ts +3 -3
  241. package/dist/transcript-delta-reader.js +8 -8
  242. package/dist/transcript-delta-reader.js.map +1 -1
  243. package/dist/transcript-jsonl.d.ts +80 -3
  244. package/dist/transcript-jsonl.js +147 -10
  245. package/dist/transcript-jsonl.js.map +1 -1
  246. package/dist/transcript-monitor.d.ts +19 -0
  247. package/dist/transcript-monitor.js +87 -15
  248. package/dist/transcript-monitor.js.map +1 -1
  249. package/dist/transcript-sources.d.ts +78 -0
  250. package/dist/transcript-sources.js +375 -64
  251. package/dist/transcript-sources.js.map +1 -1
  252. package/dist/transcript-turns.d.ts +15 -2
  253. package/dist/transcript-turns.js +15 -2
  254. package/dist/transcript-turns.js.map +1 -1
  255. package/dist/tunnel/cloudflared.d.ts +7 -15
  256. package/dist/tunnel/cloudflared.js +36 -1
  257. package/dist/tunnel/cloudflared.js.map +1 -1
  258. package/dist/tunnel/purpose-lane.d.ts +5 -1
  259. package/dist/tunnel/purpose-lane.js +14 -7
  260. package/dist/tunnel/purpose-lane.js.map +1 -1
  261. package/dist/tunnel/types.d.ts +5 -1
  262. package/dist/tunnel/types.js.map +1 -1
  263. package/dist/turn-resume.d.ts +2 -0
  264. package/dist/turn-resume.js +1 -1
  265. package/dist/turn-resume.js.map +1 -1
  266. package/dist/types.d.ts +25 -0
  267. package/dist/ui/chat-commands.js +1 -0
  268. package/dist/ui/chat-emojis.js +33 -0
  269. package/dist/ui/chat-mentions.js +63 -0
  270. package/dist/ui/chat-render.js +61 -4
  271. package/dist/ui/chat-store.js +37 -3
  272. package/dist/ui/chat-strings.js +40 -20
  273. package/dist/ui/chat-thread.js +111 -12
  274. package/dist/ui/chat-time.js +80 -0
  275. package/dist/ui/panel-chat.js +41 -10
  276. package/dist/ui/panel-details.js +2 -0
  277. package/dist/ui/panel-fleet.js +199 -57
  278. package/dist/ui/panel-log.js +428 -0
  279. package/dist/ui/panel-needs.js +6 -2
  280. package/dist/ui/panel-settings.js +184 -32
  281. package/dist/ui/preview-panel.js +55 -15
  282. package/dist/ui/preview-status.js +28 -0
  283. package/dist/ui/preview.js +44 -3
  284. package/dist/ui/settings-dialogs.js +7 -2
  285. package/dist/ui/settings-model.js +2 -1
  286. package/dist/ui/settings-strings.js +56 -4
  287. package/dist/ui/settings-token.js +1 -1
  288. package/dist/ui/shared/app-i18n.js +43 -17
  289. package/dist/ui/shared/app-prefs.js +250 -0
  290. package/dist/ui/shared/app-route.js +7 -2
  291. package/dist/ui/shared/app-shell.js +165 -8
  292. package/dist/ui/shared/app-store.js +5 -1
  293. package/dist/ui/shared/app-stream.js +26 -5
  294. package/dist/ui/shared/app.css +163 -22
  295. package/dist/ui/shared/app.js +11 -1
  296. package/dist/ui/shared/fonts/README.md +1 -1
  297. package/dist/ui/shared/header-tools.js +55 -5
  298. package/dist/ui/shared/instance-nav.js +241 -45
  299. package/dist/ui/shared/instance-switch.js +4 -4
  300. package/dist/ui/shared/panel-view.js +6 -0
  301. package/dist/ui/shared/signin.js +7 -7
  302. package/dist/ui/shared/theme.js +39 -7
  303. package/dist/ui/shared/tokens.css +1 -1
  304. package/dist/ui/shared/ui-icons.js +1 -0
  305. package/dist/ui/shared/usage-dialog.js +23 -5
  306. package/dist/ui/shared/view-strings.js +32 -4
  307. package/dist/ui/signin.html +1 -1
  308. package/dist/unrecognised-prompt.d.ts +23 -0
  309. package/dist/unrecognised-prompt.js +74 -0
  310. package/dist/unrecognised-prompt.js.map +1 -0
  311. package/dist/update-check.d.ts +31 -0
  312. package/dist/update-check.js +47 -0
  313. package/dist/update-check.js.map +1 -1
  314. package/dist/update-install.d.ts +28 -2
  315. package/dist/update-install.js +22 -4
  316. package/dist/update-install.js.map +1 -1
  317. package/dist/update-launch.d.ts +46 -1
  318. package/dist/update-launch.js +146 -3
  319. package/dist/update-launch.js.map +1 -1
  320. package/dist/update-marker.d.ts +51 -0
  321. package/dist/update-marker.js +61 -0
  322. package/dist/update-marker.js.map +1 -1
  323. package/dist/update-preflight.d.ts +100 -0
  324. package/dist/update-preflight.js +264 -0
  325. package/dist/update-preflight.js.map +1 -0
  326. package/dist/updater-caller.d.ts +31 -0
  327. package/dist/updater-caller.js +109 -0
  328. package/dist/updater-caller.js.map +1 -0
  329. package/dist/usage/i18n-keys.d.ts +1 -1
  330. package/dist/usage/i18n-keys.js +1 -1
  331. package/dist/usage/i18n-keys.js.map +1 -1
  332. package/dist/usage/providers.d.ts +34 -5
  333. package/dist/usage/providers.js +160 -27
  334. package/dist/usage/providers.js.map +1 -1
  335. package/dist/usage/statusline-usage.d.ts +6 -2
  336. package/dist/usage/statusline-usage.js +10 -4
  337. package/dist/usage/statusline-usage.js.map +1 -1
  338. package/dist/view-api.d.ts +1 -1
  339. package/dist/view-api.js +38 -8
  340. package/dist/view-api.js.map +1 -1
  341. package/dist/wake-coordinator.d.ts +11 -0
  342. package/dist/wake-coordinator.js +38 -4
  343. package/dist/wake-coordinator.js.map +1 -1
  344. package/dist/web-api.d.ts +89 -4
  345. package/dist/web-api.js +551 -45
  346. package/dist/web-api.js.map +1 -1
  347. package/dist/web-auth.d.ts +2 -2
  348. package/dist/web-auth.js +9 -4
  349. package/dist/web-auth.js.map +1 -1
  350. package/dist/web-chat-history.d.ts +12 -1
  351. package/dist/web-chat-history.js +29 -7
  352. package/dist/web-chat-history.js.map +1 -1
  353. package/dist/web-commands.d.ts +27 -4
  354. package/dist/web-commands.js +19 -4
  355. package/dist/web-commands.js.map +1 -1
  356. package/dist/web-host-guard.d.ts +3 -11
  357. package/dist/web-host-guard.js +3 -12
  358. package/dist/web-host-guard.js.map +1 -1
  359. package/dist/web-images.d.ts +47 -0
  360. package/dist/web-images.js +169 -0
  361. package/dist/web-images.js.map +1 -0
  362. package/dist/web-login.d.ts +14 -0
  363. package/dist/web-login.js +4 -2
  364. package/dist/web-login.js.map +1 -1
  365. package/dist/web-mentions.d.ts +44 -0
  366. package/dist/web-mentions.js +75 -0
  367. package/dist/web-mentions.js.map +1 -0
  368. package/dist/web-prefs.d.ts +81 -0
  369. package/dist/web-prefs.js +297 -0
  370. package/dist/web-prefs.js.map +1 -0
  371. package/dist/web-preview.d.ts +14 -1
  372. package/dist/web-preview.js +35 -13
  373. package/dist/web-preview.js.map +1 -1
  374. package/dist/web-profile.d.ts +32 -0
  375. package/dist/web-profile.js +132 -0
  376. package/dist/web-profile.js.map +1 -0
  377. package/dist/web-reply-files.d.ts +45 -0
  378. package/dist/web-reply-files.js +223 -0
  379. package/dist/web-reply-files.js.map +1 -0
  380. package/dist/web-schedules.d.ts +12 -0
  381. package/dist/web-schedules.js +34 -0
  382. package/dist/web-schedules.js.map +1 -0
  383. package/dist/web-server-drain.d.ts +10 -0
  384. package/dist/web-server-drain.js +50 -0
  385. package/dist/web-server-drain.js.map +1 -0
  386. package/dist/web-session.d.ts +50 -1
  387. package/dist/web-session.js +63 -6
  388. package/dist/web-session.js.map +1 -1
  389. package/dist/web-settings-patch.d.ts +23 -0
  390. package/dist/web-settings-patch.js +77 -0
  391. package/dist/web-settings-patch.js.map +1 -0
  392. package/dist/web-shell-routes.d.ts +5 -1
  393. package/dist/web-shell-routes.js +10 -1
  394. package/dist/web-shell-routes.js.map +1 -1
  395. package/dist/web-upload.d.ts +17 -4
  396. package/dist/web-upload.js +64 -13
  397. package/dist/web-upload.js.map +1 -1
  398. package/dist/workflow-templates/default.md +1 -1
  399. package/package.json +1 -1
  400. package/templates/launchd.plist.ejs +3 -1
  401. package/templates/systemd.service.ejs +2 -1
@@ -43,7 +43,7 @@ import { startTour } from "./chat-tour.js";
43
43
  import "./chat-strings.js";
44
44
 
45
45
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
46
- const ICONS = { device: "user", agents: "bot", bots: "plug", classic: "room", general: "sliders", advanced: "code" };
46
+ const ICONS = { device: "user", agents: "bot", bots: "plug", classic: "room", general: "sliders", web: "globe", advanced: "code" };
47
47
  const RELEASES = "https://github.com/songsid/AgEnD/releases";
48
48
 
49
49
  /** GET under the panel's lease → { ok, status, body }. */
@@ -229,7 +229,8 @@ export function SettingsPanel({ route, navKey }) {
229
229
  : section === "agents" ? html`<${Agents} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
230
230
  : section === "bots" ? html`<${Bots} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
231
231
  : section === "classic" ? html`<${Classic} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
232
- : section === "general" ? html`<${General} key=${data.loadedAt} ctx=${ctx} staged=${staged} />`
232
+ : section === "general" ? html`<${General} key=${data.loadedAt} ctx=${ctx} />`
233
+ : section === "web" ? html`<${WebSection} key=${data.loadedAt} ctx=${ctx} staged=${staged} />`
233
234
  : html`<${Developer} ctx=${ctx} />`;
234
235
  // A dialog belongs to the navigation that opened it (its draft names one object, never another).
235
236
  const open = dialog && dialog.key === navKey ? dialog : null;
@@ -257,9 +258,10 @@ export function SettingsPanel({ route, navKey }) {
257
258
  }
258
259
 
259
260
  /**
260
- * #1604: what this browser chooses for itself — moved here from the sidebar footer, each with the storage it always had:
261
- * theme (theme.js), language (app-i18n.js), HTML previews on this device (preview.js, through the app's confirm), and
262
- * the chat tour (replayed on the chat it points at).
261
+ * #1604, #1665: Preferences. Synced across the person's devices (kept on the host, app-prefs.js): theme (theme.js),
262
+ * language (app-i18n.js) and HTML previews (preview.js, through the app's confirm). On the public link they are
263
+ * read-only: a change there stays in that browser, and it says so. This device only: the chat tour (replayed on the
264
+ * chat it points at), and the layout choices named there (their controls are where they always were).
263
265
  */
264
266
  export function DeviceSection() {
265
267
  const P = () => globalThis.AgendPreview;
@@ -290,15 +292,66 @@ export function DeviceSection() {
290
292
  };
291
293
  setTimeout(go, 0);
292
294
  };
293
- return html`<section class="card s-device"><h3>${tn("sec_device")}</h3>
294
- <p class="sub">${tn("deviceLead")}</p>
295
+ const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
296
+ return html`<${ProfileCard} />
297
+ <section class="card s-device s-synced"><h3>${tn("syncedTitle")}</h3>
298
+ <p class="sub">${publicLink ? tn("syncedPublicLink") : tn("syncedLead")}</p>
295
299
  <${DevicePrefs} labels=${true} />
296
300
  ${P() ? html`<label class="check"><input type="checkbox" checked=${on} onChange=${(e) => { const want = e.target.checked; setPreviewOptIn(want).then(setOn); }} />
297
301
  <span>${t("chat.previews")}</span></label>` : null}
302
+ </section>
303
+ <section class="card s-device s-local"><h3>${tn("deviceTitle")}</h3>
304
+ <p class="sub">${tn("deviceLead")}</p>
298
305
  <div class="s-device-row"><button type="button" id="tourBtn" class="btn" title=${t("chat.tourBtnTitle")} onClick=${replay}><${Icon} name="info" size=${16} />${t("chat.tour")}</button></div>
299
306
  </section>`;
300
307
  }
301
308
 
309
+ /**
310
+ * #1827: who agents see when this person writes from the web. Signed in with a code sent to their Discord or Telegram
311
+ * account, they are that account — its user id, and its name unless they choose another (it follows them to every
312
+ * device, the public link included). Otherwise "web-user", as before. The host checks a name in full and says why it
313
+ * refuses one; this only shows what it says.
314
+ */
315
+ const view = (b) => ({ phase: "ready", name: b.name || "", shown: b.shown || "", id: b.id || "", bound: b.bound === true, platform: b.platform_name || "", error: "" });
316
+ export function ProfileCard() {
317
+ const [state, setState] = useState({ phase: "loading", name: "", shown: "", id: "", bound: false, platform: "", error: "" });
318
+ const [draft, setDraft] = useState("");
319
+ const lease = useLease();
320
+ useEffect(() => {
321
+ let live = true;
322
+ api("/ui/profile").then((r) => {
323
+ if (!live) return;
324
+ if (!r.ok || !r.body) { setState((s) => ({ ...s, phase: "unavailable" })); return; }
325
+ setState(view(r.body));
326
+ setDraft(r.body.name || "");
327
+ }).catch(() => { if (live) setState((s) => ({ ...s, phase: "unavailable" })); });
328
+ return () => { live = false; };
329
+ }, []);
330
+ if (state.phase === "unavailable") return null;
331
+ const save = (clear) => {
332
+ setState((s) => ({ ...s, phase: "saving", error: "" }));
333
+ api("/ui/profile", { method: "POST", body: JSON.stringify({ name: clear ? null : draft }) }).then((r) => {
334
+ if (lease && !lease.current()) return;
335
+ if (!r.ok) { setState((s) => ({ ...s, phase: "ready", error: (r.body && r.body.error) || tn("profileFailed") })); return; }
336
+ setState(view(r.body));
337
+ setDraft(r.body.name || "");
338
+ toast(tn("profileSaved", r.body.shown || "web-user"));
339
+ }).catch(() => setState((s) => ({ ...s, phase: "ready", error: tn("profileFailed") })));
340
+ };
341
+ const busy = state.phase !== "ready";
342
+ const changed = draft.trim() !== state.name;
343
+ return html`<section class="card s-device s-profile"><h3>${tn("profileTitle")}</h3>
344
+ <p class="sub">${state.bound ? tn("profileLeadBound") : tn("profileLeadUnbound")}</p>
345
+ <div class="field"><label for="profile-name">${tn("profileName")}</label>
346
+ <input id="profile-name" type="text" maxlength="200" autocomplete="nickname" value=${draft} disabled=${busy}
347
+ placeholder=${state.platform || "web-user"} aria-invalid=${state.error ? "true" : "false"} aria-describedby="profile-help"
348
+ onInput=${(e) => setDraft(e.target.value)} onKeyDown=${(e) => { if (e.key === "Enter" && changed && draft.trim()) save(false); }} />
349
+ <p class="sub" id="profile-help">${state.error ? html`<span class="err" role="alert">${state.error}</span>` : tn("profileHelp", state.shown || "web-user", state.id)}</p></div>
350
+ <div class="s-device-row"><button type="button" class="btn primary" disabled=${busy || !changed || !draft.trim()} onClick=${() => save(false)}>${tn("profileSave")}</button>
351
+ ${state.name ? html`<button type="button" class="btn" disabled=${busy} onClick=${() => save(true)}>${state.bound ? tn("profileUseAccount", state.platform || "web-user") : tn("profileClear")}</button>` : null}</div>
352
+ </section>`;
353
+ }
354
+
302
355
  /** The operation in hand still owns Apply: its writes and job, or a restart of AgEnD from the press to its end. */
303
356
  const opBusy = (op) => !!op && (["writing", "posting", "watching"].includes(op.phase) || op.restart === "busy");
304
357
  function dropSecrets(staged) { for (const c of staged.values()) if (c.connectionSecret) c.connectionSecret.secret = ""; }
@@ -366,11 +419,13 @@ function makeCtx(data, setData, stage, unstage, reload, reloadLive, alive) {
366
419
  async pauseWake(name, action) {
367
420
  const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
368
421
  if (!res.ok) toast((res.body && res.body.error) || tn("actionFailed", action), false);
422
+ else if (res.body && res.body.held) toast(res.body.message);
369
423
  await reloadLive();
370
424
  },
371
425
  async startStop(name, running) {
372
426
  const res = await api(running ? `/stop/${encodeURIComponent(name)}` : `/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
373
427
  if (!res.ok) toast(`${tn("actionFailed", running ? tn("stop") : tn("start"))}: ${(res.body && res.body.error) || res.status || ""}`, false);
428
+ else if (res.body && res.body.held) toast(res.body.message);
374
429
  await new Promise((r) => setTimeout(r, 800));
375
430
  await reloadLive();
376
431
  },
@@ -570,17 +625,13 @@ function Classic({ ctx, search, openDialog }) {
570
625
  * ClassicBot defaults, web) — each only with what differs from what this form showed, so changing one hot default
571
626
  * never also stages untouched settings as a fleet restart.
572
627
  */
573
- function General({ ctx, staged }) {
628
+ function General({ ctx }) {
574
629
  const { schema } = ctx;
575
630
  const d = ctx.fleet.defaults || {}, c = (ctx.classic && ctx.classic.defaults) || {};
576
631
  const hang = d.hang_detector || {}, startup = d.startup || {};
577
632
  const primary = channelsOf(ctx.fleet)[0];
578
- const web = ctx.fleet.web || {};
579
- const originalPublic = useMemo(() => ({ allow_public: (web.public_link && web.public_link.allow_public) ?? true, ttl_minutes: (web.public_link && web.public_link.ttl_minutes) ?? 120, protocol: (web.public_link && web.public_link.protocol) ?? "http2" }), []);
580
- const originalEcho = web.echo_to_channel ?? true;
581
- const original = useMemo(() => ({ c: structuredClone(c), web: structuredClone(ctx.fleet.web) }), []);
633
+ const original = useMemo(() => ({ c: structuredClone(c) }), []);
582
634
  const [f, setF] = useState(() => ({
583
- echo: originalEcho, pub: originalPublic.allow_public, ttl: String(originalPublic.ttl_minutes), protocol: originalPublic.protocol,
584
635
  backend: d.backend || "claude-code", model: d.model || "", locale: d.locale || "", auto: String(d.auto_pause_after ?? 0),
585
636
  agent_mode: d.agent_mode || "mcp", tool_set: d.tool_set || "full", tool_progress: d.tool_progress ?? "off",
586
637
  visibility: visibilityDefault(d), reply: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
@@ -598,7 +649,6 @@ function General({ ctx, staged }) {
598
649
  auto: nonNegative(f.auto), hang: positive(f.hang),
599
650
  concurrency: !Number.isInteger(num(f.concurrency)) || num(f.concurrency) < 1 || num(f.concurrency) > 20 ? "concurrencyRange" : "",
600
651
  stagger: !Number.isFinite(num(f.stagger)) || f.stagger.trim() === "" || num(f.stagger) < 0 || num(f.stagger) > 30000 ? "staggerRange" : "",
601
- ttl: !Number.isInteger(num(f.ttl)) || num(f.ttl) < 1 || num(f.ttl) > 480 ? "publicTtl" : "",
602
652
  };
603
653
  const warnings = { concurrency: num(f.concurrency) > 4 ? "concurrencyWarning" : "", stagger: num(f.stagger) < 100 ? "staggerWarning" : "" };
604
654
  const ready = !Object.values(errors).some(Boolean);
@@ -638,29 +688,12 @@ function General({ ctx, staged }) {
638
688
  ctx.stage("defaults:classic", { label: tn("updateClassicDefaults"), impact: hotOnly ? "now" : "fleet",
639
689
  request: { method: "PUT", url: "/api/settings/classic/defaults", body: classicPatch } });
640
690
  }
641
- const publicPatch = changedFields({ allow_public: f.pub, ttl_minutes: num(f.ttl), protocol: f.protocol }, originalPublic);
642
- const webPatch = { ...(f.echo !== originalEcho ? { echo_to_channel: f.echo } : {}), ...(Object.keys(publicPatch).length ? { public_link: publicPatch } : {}) };
643
- if (Object.keys(webPatch).length) {
644
- ctx.stage("web:echo", { label: tn("publicLink"),
645
- impact: batchImpact(schema, [...(webPatch.echo_to_channel !== undefined ? ["web.echo_to_channel"] : []), ...Object.keys(publicPatch).map((k) => `web.public_link.${k}`)], (k) => impactOf(schema, k)),
646
- request: { method: "PUT", url: "/api/settings/fleet/web", body: webPatch } });
647
- } else if (staged.has("web:echo")) ctx.unstage("web:echo");
648
691
  setMsg(tn("readyApply"));
649
692
  };
650
693
  const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
651
694
  const fleetReply = replyGuardSupported(f.backend, f.agent_mode, d.kiro_ui) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", f.backend, f.agent_mode);
652
695
  const classicReply = replyGuardSupported(f.classicBackend, f.agent_mode, d.kiro_ui) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", f.classicBackend, f.agent_mode);
653
696
  return html`<div class="form s-general">
654
- <section class="card"><h3>${tn("webChat")}</h3>
655
- <label class="check"><input id="webEchoToChannel" type="checkbox" checked=${f.echo} onChange=${(e) => set("echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="web.echo_to_channel" /></label>
656
- <p class="note">${tn("webEchoHint")}</p>
657
- <label class="check"><input id="publicWebLink" type="checkbox" checked=${f.pub} onChange=${(e) => set("pub")(e.target.checked)} /><span>${tn("publicLink")}</span><${Impact} schema=${schema} field="web.public_link.allow_public" /></label>
658
- <p class="note">${tn("publicLinkHint")}</p>
659
- <div class="grid2">
660
- <div class="field"><label for="publicWebTtl">${tn("publicTtl")}</label><input id="publicWebTtl" type="number" min="1" max="480" class=${errors.ttl ? "invalid" : ""} value=${f.ttl} onInput=${(e) => set("ttl")(e.target.value)} /><${Feedback} error=${errors.ttl} /></div>
661
- <div class="field"><label for="publicWebProtocol">${tn("publicProtocol")}</label><${Select} id="publicWebProtocol" value=${f.protocol} onChange=${set("protocol")} options=${["http2", "quic", "auto"]} /></div>
662
- </div>
663
- </section>
664
697
  <section class="card"><h3>${tn("defaultsTitle")}</h3>
665
698
  <div class="grid2">
666
699
  <div class="field">${L(tn("language"), "defaults.locale", "g-locale")}<${Select} id="g-locale" value=${f.locale} onChange=${set("locale")}
@@ -714,6 +747,125 @@ function General({ ctx, staged }) {
714
747
  </div>`;
715
748
  }
716
749
 
750
+ /**
751
+ * #1620: Settings → Web — the dashboard's own settings, in one write (PUT /api/settings/fleet/web). Editable: the local
752
+ * session limits, the browser terminal (on/off, its minutes), web echo and the public link's menu; everything else
753
+ * under web: / web_terminal: is listed read-only (it decides what the dashboard listens on and exposes: fleet.yaml, then
754
+ * agend reload). The server judges each change: loosening one waits for a fleet admin in General, tightening applies at
755
+ * once. Over
756
+ * the temporary public link none of it can be changed — the form says so and stays disabled (the server refuses too).
757
+ */
758
+ export const WEB_READ_ONLY = [
759
+ "web.allowed_hosts", "web.view_access", "web.notify_login", "web.preview", "web.preview_port", "web.preview_origin", "web.preview_on_public_link",
760
+ "web.usage_panel", "web.provider_secrets", "web_terminal.bind", "web_terminal.tunnel",
761
+ ];
762
+ function WebSection({ ctx, staged }) {
763
+ const { schema } = ctx;
764
+ const web = ctx.fleet.web || {}, local = (web.session && web.session.local) || {}, term = ctx.fleet.web_terminal || {};
765
+ const overPublicLink = globalThis.document?.body?.dataset?.publicLink === "1";
766
+ const original = useMemo(() => ({
767
+ echo: web.echo_to_channel ?? true,
768
+ log: web.log_tab === true, // #1218: off by default
769
+ pub: { allow_public: (web.public_link && web.public_link.allow_public) ?? true, ttl_minutes: (web.public_link && web.public_link.ttl_minutes) ?? 120, protocol: (web.public_link && web.public_link.protocol) ?? "http2" },
770
+ session: { absolute_hours: local.absolute_hours ?? null, idle_hours: local.idle_hours ?? null },
771
+ terminal: { enabled: term.enabled ?? null, ttl_minutes: term.ttl_minutes ?? null },
772
+ }), []);
773
+ const text = (v) => (v === null || v === undefined ? "" : String(v));
774
+ const [f, setF] = useState(() => ({
775
+ echo: original.echo, log: original.log, pub: original.pub.allow_public, ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol,
776
+ abs: text(original.session.absolute_hours), idle: text(original.session.idle_hours),
777
+ termOn: original.terminal.enabled !== false, termTtl: text(original.terminal.ttl_minutes),
778
+ }));
779
+ const [msg, setMsg] = useState(null);
780
+ const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
781
+ // Empty = the default (null in the write: the key is removed from fleet.yaml).
782
+ const value = (v) => (v.trim() === "" ? null : Number(v));
783
+ const hours = (v) => { const n = value(v); return n !== null && (!Number.isFinite(n) || n < 1 || n > 720) ? "sessionRange" : ""; };
784
+ // The defaults (#1670: 48 h / 48 h, DEFAULT_SESSION_POLICY); an idle time left blank follows a shorter session, as
785
+ // the store and the validator apply it — so only an idle time that is typed can exceed the session length.
786
+ const abs = value(f.abs) ?? 48, idle = value(f.idle) ?? Math.min(48, abs);
787
+ const errors = {
788
+ ttl: !Number.isInteger(Number(f.ttl)) || Number(f.ttl) < 1 || Number(f.ttl) > 480 ? "publicTtl" : "",
789
+ abs: hours(f.abs),
790
+ idle: hours(f.idle) || (!hours(f.abs) && idle > abs ? "sessionIdleOver" : ""),
791
+ termTtl: value(f.termTtl) !== null && (!Number.isInteger(value(f.termTtl)) || value(f.termTtl) < 1 || value(f.termTtl) > 20) ? "webTerminalTtlRange" : "",
792
+ };
793
+ const ready = !overPublicLink && !Object.values(errors).some(Boolean);
794
+ const review = () => {
795
+ if (!ready) return;
796
+ const publicPatch = changedFields({ allow_public: f.pub, ttl_minutes: Number(f.ttl), protocol: f.protocol }, original.pub);
797
+ const sessionPatch = changedFields({ absolute_hours: value(f.abs), idle_hours: value(f.idle) }, original.session);
798
+ // The checkbox shows the default (on) for an unset key: only a real toggle is written; on again = back to the default.
799
+ const terminalPatch = changedFields({ enabled: f.termOn === (original.terminal.enabled !== false) ? original.terminal.enabled : f.termOn ? null : false,
800
+ ttl_minutes: value(f.termTtl) }, original.terminal);
801
+ const body = {
802
+ ...(f.echo !== original.echo ? { echo_to_channel: f.echo } : {}),
803
+ ...(f.log !== original.log ? { log_tab: f.log } : {}),
804
+ ...(Object.keys(publicPatch).length ? { public_link: publicPatch } : {}),
805
+ ...(Object.keys(sessionPatch).length ? { session: { local: sessionPatch } } : {}),
806
+ ...(Object.keys(terminalPatch).length ? { web_terminal: terminalPatch } : {}),
807
+ };
808
+ const fields = [
809
+ ...(body.echo_to_channel !== undefined ? ["web.echo_to_channel"] : []),
810
+ ...(body.log_tab !== undefined ? ["web.log_tab"] : []),
811
+ ...Object.keys(publicPatch).map((k) => `web.public_link.${k}`),
812
+ ...Object.keys(sessionPatch).map((k) => `web.session.local.${k}`),
813
+ ...Object.keys(terminalPatch).map((k) => `web_terminal.${k}`),
814
+ ];
815
+ if (fields.length) {
816
+ ctx.stage("web", { label: tn("sec_web"), impact: batchImpact(schema, fields, (k) => impactOf(schema, k)),
817
+ request: { method: "PUT", url: "/api/settings/fleet/web", body } });
818
+ } else if (staged.has("web")) ctx.unstage("web");
819
+ setMsg(tn("readyApply"));
820
+ };
821
+ const shown = (path) => {
822
+ const [top, key] = path.split(".");
823
+ const v = (top === "web" ? web : term)[key];
824
+ return v === undefined ? tn("notSet") : typeof v === "object" ? JSON.stringify(v) : String(v);
825
+ };
826
+ return html`<div class="form s-web">
827
+ <p class="sub">${tn("webLead")}</p>
828
+ ${overPublicLink ? html`<p class="note" role="status" id="webPublicRefused">${tn("webPublicRefused")}</p>` : null}
829
+ <fieldset class="s-web-fields" disabled=${overPublicLink}>
830
+ <section class="card"><h3>${tn("webSessionTitle")}</h3>
831
+ <p class="note">${tn("webSessionHint")}</p>
832
+ <div class="grid2">
833
+ <div class="field"><label for="webSessionAbsolute">${tn("sessionAbsolute")}</label><input id="webSessionAbsolute" type="number" min="1" max="720" step="any" placeholder=${tn("defaultHours", 48)}
834
+ class=${errors.abs ? "invalid" : ""} value=${f.abs} onInput=${(e) => set("abs")(e.target.value)} /><${Feedback} error=${errors.abs} /></div>
835
+ <div class="field"><label for="webSessionIdle">${tn("sessionIdle")}</label><input id="webSessionIdle" type="number" min="1" max="720" step="any" placeholder=${tn("defaultHours", 48)}
836
+ class=${errors.idle ? "invalid" : ""} value=${f.idle} onInput=${(e) => set("idle")(e.target.value)} /><${Feedback} error=${errors.idle} /></div>
837
+ </div>
838
+ </section>
839
+ <section class="card"><h3>${tn("webTerminalTitle")}</h3>
840
+ <p class="note">${tn("webTerminalHint")}</p>
841
+ <label class="check"><input id="webTerminalEnabled" type="checkbox" checked=${f.termOn} onChange=${(e) => set("termOn")(e.target.checked)} /><span>${tn("webTerminalEnabled")}</span><${Impact} schema=${schema} field="web_terminal.enabled" /></label>
842
+ <div class="grid2">
843
+ <div class="field"><label for="webTerminalTtl">${tn("webTerminalTtl")}</label><input id="webTerminalTtl" type="number" min="1" max="20" step="1" placeholder=${tn("defaultHours", 10)}
844
+ class=${errors.termTtl ? "invalid" : ""} value=${f.termTtl} onInput=${(e) => set("termTtl")(e.target.value)} /><${Feedback} error=${errors.termTtl} /></div>
845
+ </div>
846
+ </section>
847
+ <section class="card"><h3>${tn("webChat")}</h3>
848
+ <label class="check"><input id="webEchoToChannel" type="checkbox" checked=${f.echo} onChange=${(e) => set("echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="web.echo_to_channel" /></label>
849
+ <p class="note">${tn("webEchoHint")}</p>
850
+ <label class="check"><input id="webLogTab" type="checkbox" checked=${f.log} onChange=${(e) => set("log")(e.target.checked)} /><span>${tn("webLogTab")}</span><${Impact} schema=${schema} field="web.log_tab" /></label>
851
+ <p class="note">${tn("webLogTabHint")}</p>
852
+ <label class="check"><input id="publicWebLink" type="checkbox" checked=${f.pub} onChange=${(e) => set("pub")(e.target.checked)} /><span>${tn("publicLink")}</span><${Impact} schema=${schema} field="web.public_link.allow_public" /></label>
853
+ <p class="note">${tn("publicLinkHint")}</p>
854
+ <div class="grid2">
855
+ <div class="field"><label for="publicWebTtl">${tn("publicTtl")}</label><input id="publicWebTtl" type="number" min="1" max="480" class=${errors.ttl ? "invalid" : ""} value=${f.ttl} onInput=${(e) => set("ttl")(e.target.value)} /><${Feedback} error=${errors.ttl} /></div>
856
+ <div class="field"><label for="publicWebProtocol">${tn("publicProtocol")}</label><${Select} id="publicWebProtocol" value=${f.protocol} onChange=${set("protocol")} options=${["http2", "quic", "auto"]} /></div>
857
+ </div>
858
+ </section>
859
+ </fieldset>
860
+ <section class="card s-web-readonly"><h3>${tn("webReadOnlyTitle")}</h3>
861
+ <p class="note">${tn("webReadOnlyLead")}</p>
862
+ <dl class="s-web-ro">${WEB_READ_ONLY.map((k) => html`<div key=${k}><dt><code>${k}</code></dt><dd>${shown(k)}</dd></div>`)}</dl>
863
+ </section>
864
+ <div class="save-row"><button type="button" class="btn btn-primary" disabled=${!ready} onClick=${review}>${tn("reviewChanges")}</button>
865
+ ${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>
866
+ </div>`;
867
+ }
868
+
717
869
  /** Developer: the fleet model as YAML or JSON, to read, copy, download — or edit and apply as one operation. */
718
870
  function Developer({ ctx }) {
719
871
  const [fmt, setFmt] = useState("yaml");
@@ -749,7 +901,7 @@ function Developer({ ctx }) {
749
901
  <button type="button" class="btn btn-sm" title=${tn("copy")} onClick=${copy}><${Icon} name="copy" size=${14} /><span class="lbl">${tn("copy")}</span></button>
750
902
  <button type="button" class="btn btn-sm" title=${tn("devDownload")} onClick=${download}><${Icon} name="download" size=${14} /><span class="lbl">${tn("devDownload")}</span></button>
751
903
  </div>
752
- ${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck="false" wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
904
+ ${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck=${false} wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
753
905
  <div class="save-row"><button type="button" class="btn btn-primary" onClick=${save}>${tn("devApply")}</button><span class="note">${tn("devApplyHint")}</span></div>`
754
906
  : html`<pre class="s-yaml">${text}</pre>`}
755
907
  ${msg ? html`<p class=${`feedback${msg.error ? " error" : ""}`} role="status">${msg.error || msg.text}</p>` : null}
@@ -1,11 +1,14 @@
1
1
  // #1481: an agent's HTML preview (#1306) in a panel beside the conversation — a split with a draggable divider on a
2
2
  // wide screen, a full-screen sheet on a narrow one. Nothing about the preview itself changes here: the frame is the one
3
3
  // preview.js makes (mountPreview: the preview origin, sandbox allow-scripts only), started by AgendPreview.start like a
4
- // card's, only told to fill the panel. It runs on a click, as a card's does:
5
- // - "Open in panel" on an idle card opens the panel with its Preview button — the panel runs nothing by itself;
4
+ // card's, only told to fill the panel. It runs on a person's click, as a card's does — and "Open in panel" is that click:
5
+ // - on an idle card it opens the panel and starts the preview there, as if Preview were clicked in the panel (#1704);
6
+ // a device that has not allowed previews gets the "Allow previews" offer instead, and allowing it starts the run;
7
+ // where previews cannot run (web.preview: false, a public link), the panel opens with nothing running;
6
8
  // - on a card whose preview is running, the same button moves that run to the panel (stopped in the card, started in
7
9
  // the panel: a moved frame reloads anyway);
8
- // - a newer version of the block in a later reply is offered, never swapped in, and offered with Preview to click.
10
+ // - nothing runs by itself: not when the page loads, not when the panel is shown again, and a newer version of the
11
+ // block in a later reply is offered, never swapped in, and shown with Preview to click.
9
12
  // Closing the panel or leaving the chat stops its frame; one preview per page still holds (start() stops any other).
10
13
  import { html, useEffect, useRef, useState } from "/assets/app-html.js";
11
14
  import { t } from "/assets/app-i18n.js";
@@ -13,24 +16,36 @@ import { Icon } from "/assets/ui-icons.js";
13
16
  import "./chat-render.js";
14
17
  import "./preview.js";
15
18
  import { loadHtmlAttachment } from "./html-attachment.js";
19
+ import { chatTime, onDayChange } from "./chat-time.js";
16
20
 
17
21
  const R = () => globalThis.AgendChatRender;
18
22
  const P = () => globalThis.AgendPreview;
19
23
 
20
24
  // ── The panel's state, one per page (the chat shows one instance at a time) ──
21
25
 
22
- /** open: { key, instance, msgKey, n | att, code, sender, ts } | null (`att`: an attached .html file, #1306 Q4); run: start it on mount (a moved run). */
26
+ /**
27
+ * open: { key, instance, msgKey, n | att, code, sender, ts } | null (`att`: an attached .html file, #1306 Q4);
28
+ * run: start it on mount — the card's click, as a token no later open ever reuses (0: nothing to run).
29
+ */
23
30
  let state = { open: null, run: 0 };
31
+ let runs = 0, version = 0, claims = 0;
24
32
  const subs = new Set();
25
- function set(next) { state = next; for (const fn of [...subs]) { try { fn(state); } catch { /* one listener's error stops no other */ } } }
33
+ function set(next) { state = next; version++; for (const fn of [...subs]) { try { fn(state); } catch { /* one listener's error stops no other */ } } }
26
34
  export const panelStore = { get: () => state, subscribe(fn) { subs.add(fn); return () => { subs.delete(fn); }; } };
35
+ /** Moves on every open and close, and on every claim: what a card's click compares when its HTML arrives late. */
36
+ export const panelVersion = () => version;
37
+ /** A click that will show something in the panel (an attachment's HTML still to read): from now on it is the newest —
38
+ * any earlier click still reading is dropped when its answer comes. Returns the version the click owns. */
39
+ export function claimPanel() { claims++; return ++version; }
40
+ /** How many clicks have claimed: an inline Preview still reading asks only whether a newer click came (not a close). */
41
+ export const panelClaims = () => claims;
27
42
 
28
43
  /** The frame's key in preview.js: its own, so the card that opened it keeps its own state. */
29
44
  export const frameKey = (key) => `panel:${key}`;
30
- /** Show `spec` in the panel; `run`: start it there (a click that moves a running preview). The block shown before stops. */
45
+ /** Show `spec` in the panel; `run`: start it there (the card's "Open in panel" click). The block shown before stops. */
31
46
  export function openPanel(spec, { run = false } = {}) {
32
47
  if (state.open && P()) P().stop(frameKey(state.open.key), "stopped", "");
33
- set({ open: { ...spec }, run: run ? state.run + 1 : 0 });
48
+ set({ open: { ...spec }, run: run ? ++runs : 0 });
34
49
  }
35
50
  /** Close the panel: its frame stops first. */
36
51
  export function closePanel() {
@@ -98,14 +113,15 @@ export function clampWidth(w, total) {
98
113
  return Math.round(Math.min(max, Math.max(MIN_PANEL, w)));
99
114
  }
100
115
 
101
- const timeOf = (ts) => (ts ? new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "");
116
+ /** #1770: as the thread shows it — the fleet's zone, with the day when it is not today. */
117
+ const timeOf = (ts) => (ts ? chatTime(ts, { yesterday: (hm) => t("chat.yesterdayAt", hm) }) : "");
102
118
 
103
119
  /**
104
120
  * The panel, inside the chat's split (`split`: that element — the divider sets its width there, through CSSOM).
105
121
  * props: name (the instance), msgs() → its messages, subscribe(fn) → unsubscribe (the chat store's), keyOf(x),
106
122
  * reveal(msgKey) (the thread shows that message), download(code).
107
123
  */
108
- /** allow(): this device's opt-in through the chat's one path (setPreviewOptIn: the confirm, then setOptIn) → the choice. */
124
+ /** allow(commit): this device's opt-in through the chat's one path (setPreviewOptIn: the confirm, then commit(setOptIn)) → the choice. */
109
125
  export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, download, allow }) {
110
126
  const [st, setSt] = useState(state);
111
127
  const [view, setView] = useState("preview");
@@ -113,15 +129,23 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
113
129
  const [newer, setNewer] = useState(null);
114
130
  const [newerNote, setNewerNote] = useState("");
115
131
  const reading = useRef(null); // the newer version whose file is being read (one at a time)
132
+ const pending = useRef(0); // #1704: the card's click, waiting for this device's Allow (the st.run it came with)
133
+ const ownAllow = useRef(0); // while this panel's own Allow commits its choice: the click it answers (not cancelled by it)
116
134
  const holder = useRef(null), divider = useRef(null), panel = useRef(null);
117
135
  const open = st.open && st.open.instance === name ? st.open : null;
118
136
 
119
137
  useEffect(() => panelStore.subscribe(setSt), []);
120
138
  // This device's choice (here or in another tab) decides whether Preview is offered: re-read it when it changes.
139
+ // A click waiting for Allow is cancelled by any other change of that choice (Never, a card's menu, another tab):
140
+ // after it, running needs a new click — only this panel's own Allow runs the click it was offered for.
121
141
  const [, choice] = useState(0);
122
- useEffect(() => (P() ? P().onChange(() => choice(n => n + 1)) : undefined), []);
142
+ useEffect(() => (P() ? P().onChange(() => { if (pending.current && ownAllow.current !== pending.current) pending.current = 0; choice(n => n + 1); }) : undefined), []);
123
143
  // Leaving the chat (another instance, another panel) closes the panel: its frame stops with the thread's.
124
144
  useEffect(() => () => closePanel(), []);
145
+ // #1770: the source and newer-version labels say the day as the thread does — drawn again when the fleet's day
146
+ // changes, while a block is open. A render: the frame and the run are left as they are.
147
+ const [, dayTick] = useState(0);
148
+ useEffect(() => (open ? onDayChange(() => dayTick(n => n + 1)) : undefined), [!!open]);
125
149
 
126
150
  const ui = { state: (s, reason) => setRun({ state: s === "starting" || s === "running" ? s : "idle", reason: reason || "" }) };
127
151
  const start = () => {
@@ -130,11 +154,17 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
130
154
  P().start(frameKey(open.key), holder.current, open.code, ui, { fill: true });
131
155
  };
132
156
  const stop = () => { if (open) P().stop(frameKey(open.key), "stopped", ""); };
133
- // A new block in the panel: what ran before is gone (openPanel stopped it); a moved run starts here.
157
+ // A new block in the panel: what ran before is gone (openPanel stopped it); the card's click starts it here — or,
158
+ // where it cannot run, waits as `pending`: only the Allow below can use it (offered to a device that has not opted
159
+ // in), only while the panel shows what that click opened (st.run is a token no later open reuses), and any other
160
+ // change of this device's choice cancels it.
134
161
  useEffect(() => {
135
162
  setRun({ state: "idle", reason: "" });
136
163
  setView("preview");
137
- if (open && st.run) start();
164
+ if (open && st.run) {
165
+ if (P().availability().ok) start();
166
+ else pending.current = st.run;
167
+ }
138
168
  // Focus goes to the panel's close control that is on screen (Back on a phone, × beside the thread).
139
169
  if (panel.current) { const f = [...panel.current.querySelectorAll(".pv-panel-close")].find(b => b.offsetParent !== null); if (f) f.focus(); }
140
170
  }, [open && open.key, st.run]);
@@ -204,11 +234,21 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
204
234
  if (!open) return null;
205
235
  const going = run.state === "starting" || run.state === "running";
206
236
  const a = P().availability();
207
- // #1554: a device that has not allowed previews is offered the same "Allow previews" here (the same confirm); Preview
208
- // is then enabled — still a click. Why a run stopped (e.g. switched off in another tab) is still said first.
237
+ // #1554: a device that has not allowed previews is offered the same "Allow previews" here (the same confirm). Allowing
238
+ // runs the block the card's click opened (#1704); otherwise Preview is then enabled — still a click. Why a run stopped
239
+ // (e.g. switched off in another tab) is still said first.
209
240
  const askOptIn = !going && !a.ok && a.why === "optin" && typeof allow === "function";
210
241
  const note = run.reason || (going ? (run.state === "starting" ? t("chat.pvStarting") : "") : a.ok ? t("chat.pvPanelIdle") : askOptIn ? t("chat.pvWhyOptinPanel") : a.reason);
211
- const allowHere = async () => { if (await allow()) setRun({ state: "idle", reason: "" }); };
242
+ // Allowed here: the click that opened the panel runs now — if it is still waiting (no other change of the choice
243
+ // cancelled it, even while the confirm was open: only the change this Allow commits is its own) and the panel still
244
+ // shows what it opened (the confirm is a wait: the panel may have moved on meanwhile).
245
+ const allowHere = async () => {
246
+ const want = pending.current;
247
+ const yes = await allow((apply) => { ownAllow.current = want; try { apply(); } finally { ownAllow.current = 0; } });
248
+ if (!yes) return;
249
+ setRun({ state: "idle", reason: "" });
250
+ if (want && pending.current === want && panelStore.get().run === want) start();
251
+ };
212
252
  const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
213
253
  return html`<div class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
214
254
  aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>
@@ -0,0 +1,28 @@
1
+ // #1723: the public link's preview tunnel starts beside the link and is never waited for, so a page may load while it
2
+ // is still starting. This asks the page's own server (/ui/preview-status) until it is up or failed, and hands each answer
3
+ // to preview.js — which takes only what it may use, and itself reaches no network (#1306).
4
+
5
+ /** Every 2 s, for at most 5 minutes. */
6
+ export const POLL_MS = 2000;
7
+ export const POLL_MAX = 150;
8
+
9
+ /**
10
+ * Ask while `P.waitingForPublicPreview()`; stop once `P.applyPublicPreviewStatus(answer)` says there is nothing more to
11
+ * wait for, or after POLL_MAX asks. Returns a stop function.
12
+ */
13
+ export function pollPublicPreview(P, deps = {}) {
14
+ const get = deps.fetch ?? ((...a) => fetch(...a));
15
+ const later = deps.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
16
+ if (!P || typeof P.waitingForPublicPreview !== "function" || !P.waitingForPublicPreview()) return () => {};
17
+ let attempts = 0, stopped = false;
18
+ const again = () => { if (!stopped && attempts < POLL_MAX) later(tick, POLL_MS); };
19
+ const tick = () => {
20
+ if (stopped || !P.waitingForPublicPreview()) return;
21
+ attempts++;
22
+ get("/ui/preview-status", { credentials: "same-origin", cache: "no-store" })
23
+ .then((r) => (r.ok ? r.json() : null))
24
+ .then((j) => { if (!stopped && !P.applyPublicPreviewStatus(j)) again(); }, again);
25
+ };
26
+ later(tick, POLL_MS);
27
+ return () => { stopped = true; };
28
+ }
@@ -28,7 +28,14 @@
28
28
  var BANNER_ACK_KEY = "agend_html_preview_banner_ack";
29
29
  var BANNER = "Previews run the agent's HTML in an isolated frame. It cannot use your login, but it may be able to send data out. Only preview content you trust. A preview can slow or freeze this tab.";
30
30
 
31
- var cfg = { dashboardOrigin: "", previewOrigin: "", boot: "", reason: "", reasonCode: "" };
31
+ var cfg = { dashboardOrigin: "", previewOrigin: "", boot: "", reason: "", reasonCode: "", detail: "" };
32
+ // Why it failed, in English when the app's dictionary has no word for it (chat-strings.js pvFail_*).
33
+ var FAIL_EN = { exited: "its address closed unexpectedly", "spawn-failed": "cloudflared could not start for it", "no-address": "no address was given for it",
34
+ unreachable: "its address could not be reached", "lease-held": "an earlier preview tunnel is still being cleaned up", "gateway-failed": "its local listener could not start",
35
+ // #1767: what cloudflared's exit before an address really was.
36
+ "rate-limited": "Cloudflare is limiting new temporary links from this machine — try again in a while",
37
+ "provisioning-failed": "Cloudflare refused to create its address right now", "process-exited": "cloudflared stopped before it gave an address",
38
+ "bad-address": "Cloudflare gave an address that could not be used" };
32
39
  // #1554: the page's words. `say(key, english, …values)` asks the app's dictionary (init's opts.text) and keeps the
33
40
  // English here when it has none — so this file still works, and reads the same, without the app.
34
41
  var text = null;
@@ -66,6 +73,7 @@
66
73
  boot: String((data && data.previewBoot) || ""),
67
74
  reason: String((data && data.previewReason) || ""),
68
75
  reasonCode: String((data && data.previewReasonCode) || ""),
76
+ detail: String((data && data.previewDetail) || ""),
69
77
  };
70
78
  if (opts && typeof opts.text === "function") text = opts.text;
71
79
  if (!listening && typeof root.addEventListener === "function") {
@@ -76,6 +84,29 @@
76
84
  listening = true;
77
85
  }
78
86
  }
87
+ /**
88
+ * #1723: the public link's own preview tunnel was still starting when this page loaded; preview-status.js asks the
89
+ * page's server and hands each answer here (this file reaches no network). Ready → previews run at the origin it
90
+ * names — only an https quick-tunnel origin other than this page's is ever taken: the page's frame-src allows no
91
+ * other; failed or off → the page says why. Returns true once there is nothing more to wait for.
92
+ */
93
+ function waitingForPublicPreview() { return cfg.reasonCode === "publicLinkStarting"; }
94
+ function applyPublicPreviewStatus(j) {
95
+ if (cfg.reasonCode !== "publicLinkStarting") return true;
96
+ var at = root.location && root.location.origin;
97
+ if (j && typeof j.previewOrigin === "string" && /^https:\/\/[a-z0-9-]+\.trycloudflare\.com$/.test(j.previewOrigin) && j.previewOrigin !== at
98
+ && typeof j.boot === "string" && /^[0-9a-f]{32}$/.test(j.boot)) {
99
+ cfg.previewOrigin = j.previewOrigin; cfg.boot = j.boot; cfg.reasonCode = ""; cfg.reason = "";
100
+ notify();
101
+ return true;
102
+ }
103
+ if (j && (j.code === "publicLinkPreviewFailed" || j.code === "publicLink" || j.code === "fleetOff")) {
104
+ cfg.reasonCode = j.code; cfg.reason = ""; cfg.detail = typeof j.detail === "string" ? j.detail : "";
105
+ notify();
106
+ return true;
107
+ }
108
+ return false;
109
+ }
79
110
  function onStorage(event) {
80
111
  // #1568: the banner acknowledged in another tab of this sign-in: hide it here too (nothing starts or stops).
81
112
  if (event && event.key === BANNER_ACK_KEY) { notify(); return; }
@@ -159,7 +190,11 @@
159
190
  function availability() {
160
191
  if (!cfg.previewOrigin || !cfg.boot) {
161
192
  // The server's reason, in the page's language when the server named it (a code), else as the server wrote it.
162
- var server = cfg.reasonCode ? say("pvServer_" + cfg.reasonCode, cfg.reason || "Previews are not available here.", cfg.dashboardOrigin) : "";
193
+ // #1723: a failed public-link preview names why ({0} is the reason, in the page's words).
194
+ var arg = cfg.reasonCode === "publicLinkPreviewFailed" ? say("pvFail_" + (cfg.detail || "unknown"), Object.prototype.hasOwnProperty.call(FAIL_EN, cfg.detail) ? FAIL_EN[cfg.detail] : "an unexpected error") : cfg.dashboardOrigin;
195
+ var english = cfg.reasonCode === "publicLinkPreviewFailed" ? "Previews are unavailable on this public link: {0}. The link itself keeps working."
196
+ : cfg.reason || "Previews are not available here.";
197
+ var server = cfg.reasonCode ? say("pvServer_" + cfg.reasonCode, english, arg) : "";
163
198
  return { ok: false, why: "server", reason: server || cfg.reason || say("pvWhyServer", "Previews are not available here.") };
164
199
  }
165
200
  var at = root.location && root.location.origin;
@@ -197,11 +232,15 @@
197
232
  * opts.fill (#1481, the side panel): the frame fills its holder — the same frame from mountPreview, only sized by the
198
233
  * panel instead of by the frame's height messages, which it then ignores.
199
234
  */
235
+ /** How many previews this page has started: a click whose HTML arrives late asks whether another started meanwhile. */
236
+ var starts = 0;
237
+ function startCount() { return starts; }
200
238
  function start(key, holder, html, ui, opts) {
201
239
  var a = availability();
202
240
  if (!a.ok) { ui.state("unavailable", a.reason); return false; }
203
241
  if (typeof html !== "string" || utf8Bytes(html) > LIMITS.maxBytes) { ui.state("unavailable", say("pvHtmlOver", "This HTML is over 1 MiB; it is not previewed.")); return false; }
204
242
  stopAll("another");
243
+ starts++;
205
244
  var doc = holder.ownerDocument;
206
245
  var iframe = mountPreview(doc);
207
246
  var card = {
@@ -330,7 +369,7 @@
330
369
 
331
370
  return {
332
371
  init: init, availability: availability, optedIn: optedIn, setOptIn: setOptIn, never: never, setNever: setNever, onChange: onChange, listeners: listeners, onStorage: onStorage,
333
- start: start, stop: stop, stopAll: stopAll, stopIn: stopIn, running: running, liveCount: liveCount, liveFrame: liveFrame,
372
+ start: start, startCount: startCount, stop: stop, stopAll: stopAll, stopIn: stopIn, running: running, liveCount: liveCount, liveFrame: liveFrame,
334
373
  mountPreview: mountPreview, onMessage: onMessage, BANNER: BANNER, LIMITS: LIMITS,
335
374
  /** The banner every running preview carries, in the page's language (#1554); BANNER is its English. */
336
375
  banner: function () { return say("pvBanner", BANNER); },
@@ -338,5 +377,7 @@
338
377
  bannerAckLabel: function () { return say("pvBannerAck", "I understand"); },
339
378
  bannerAckTitle: function () { return say("pvBannerAckTitle", "Hide this notice until you sign in again"); },
340
379
  bannerAcked: bannerAcked, ackBanner: ackBanner, setBannerScope: setBannerScope,
380
+ /** #1723: the public link's preview, while it starts (preview-status.js asks; this only takes the answer). */
381
+ waitingForPublicPreview: waitingForPublicPreview, applyPublicPreviewStatus: applyPublicPreviewStatus,
341
382
  };
342
383
  });
@@ -108,8 +108,13 @@ export function orderedStatusMap(map, keys) {
108
108
  for (const key of keys) { const v = map && map[key]; if (typeof v === "string" && v.trim()) out[key] = v.trim(); }
109
109
  return Object.keys(out).length ? out : null;
110
110
  }
111
- // no-referrer: nothing here should tell a third-party image host where it was loaded from.
112
- const EmojiImg = ({ url, alt }) => html`<img src=${url} alt=${alt} referrerpolicy="no-referrer" loading="lazy" />`;
111
+ // #1630: from the dashboard's own route (/ui/img/emoji/<id>). An emoji the fleet's servers do not have is not served
112
+ // there: then its :name: as text, never a broken image.
113
+ const EmojiImg = ({ url, alt }) => {
114
+ const [broken, setBroken] = useState("");
115
+ return broken === url ? html`<span class="se-alt">${alt}</span>`
116
+ : html`<img src=${url} alt=${alt} referrerpolicy="no-referrer" loading="lazy" onError=${() => setBroken(url)} />`;
117
+ };
113
118
 
114
119
  /**
115
120
  * One status_emojis map. `platform` and `channel` say where it applies; `previewBody(map)` is what the preview resolves.