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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) 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 +21 -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 +2 -2
  12. package/dist/auth-api.js +4 -2
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/antigravity.d.ts +3 -0
  15. package/dist/backend/antigravity.js +9 -0
  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 +1 -1
  33. package/dist/backend/effort-metadata.js +3 -2
  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 +33 -0
  48. package/dist/backend/launch-effort.js +123 -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 +33 -4
  54. package/dist/backend/types.js +2 -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 +24 -1
  66. package/dist/channel/adapters/discord.js +87 -2
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +24 -2
  69. package/dist/channel/adapters/telegram.js +118 -20
  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 +24 -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.js +9 -3
  83. package/dist/channel/tool-router.js.map +1 -1
  84. package/dist/channel/types.d.ts +39 -3
  85. package/dist/cli.js +133 -28
  86. package/dist/cli.js.map +1 -1
  87. package/dist/command-table.d.ts +2 -2
  88. package/dist/command-table.js +6 -3
  89. package/dist/command-table.js.map +1 -1
  90. package/dist/config-validator.js +59 -0
  91. package/dist/config-validator.js.map +1 -1
  92. package/dist/cross-instance-envelope.d.ts +15 -2
  93. package/dist/cross-instance-envelope.js +29 -5
  94. package/dist/cross-instance-envelope.js.map +1 -1
  95. package/dist/daemon.d.ts +156 -1
  96. package/dist/daemon.js +764 -89
  97. package/dist/daemon.js.map +1 -1
  98. package/dist/delivery-outbox.d.ts +62 -7
  99. package/dist/delivery-outbox.js +309 -22
  100. package/dist/delivery-outbox.js.map +1 -1
  101. package/dist/delivery-reconciliation.d.ts +18 -1
  102. package/dist/delivery-reconciliation.js +67 -2
  103. package/dist/delivery-reconciliation.js.map +1 -1
  104. package/dist/fleet-context.d.ts +10 -1
  105. package/dist/fleet-manager.d.ts +345 -10
  106. package/dist/fleet-manager.js +1868 -174
  107. package/dist/fleet-manager.js.map +1 -1
  108. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  109. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
  110. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
  111. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  112. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  113. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  114. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  115. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  116. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  117. package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
  118. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  119. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  120. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  121. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  122. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
  123. package/dist/general-knowledge/steering/core-rules.md +8 -7
  124. package/dist/host-memory.d.ts +10 -0
  125. package/dist/host-memory.js +33 -1
  126. package/dist/host-memory.js.map +1 -1
  127. package/dist/human-delivery-receipts.d.ts +83 -0
  128. package/dist/human-delivery-receipts.js +194 -0
  129. package/dist/human-delivery-receipts.js.map +1 -0
  130. package/dist/instance-avatar.d.ts +36 -0
  131. package/dist/instance-avatar.js +57 -0
  132. package/dist/instance-avatar.js.map +1 -0
  133. package/dist/instance-config-impact.js +6 -0
  134. package/dist/instance-config-impact.js.map +1 -1
  135. package/dist/instance-lifecycle.d.ts +29 -6
  136. package/dist/instance-lifecycle.js +79 -28
  137. package/dist/instance-lifecycle.js.map +1 -1
  138. package/dist/instructions.d.ts +3 -1
  139. package/dist/instructions.js +11 -8
  140. package/dist/instructions.js.map +1 -1
  141. package/dist/kiro-log-gate.d.ts +42 -0
  142. package/dist/kiro-log-gate.js +39 -0
  143. package/dist/kiro-log-gate.js.map +1 -0
  144. package/dist/locale.js +200 -44
  145. package/dist/locale.js.map +1 -1
  146. package/dist/log-service.d.ts +99 -0
  147. package/dist/log-service.js +243 -0
  148. package/dist/log-service.js.map +1 -0
  149. package/dist/log-steps.d.ts +169 -0
  150. package/dist/log-steps.js +858 -0
  151. package/dist/log-steps.js.map +1 -0
  152. package/dist/login-controller.d.ts +20 -3
  153. package/dist/login-controller.js +71 -19
  154. package/dist/login-controller.js.map +1 -1
  155. package/dist/login-flows.d.ts +5 -2
  156. package/dist/login-flows.js +5 -4
  157. package/dist/login-flows.js.map +1 -1
  158. package/dist/memory-pressure.d.ts +13 -1
  159. package/dist/memory-pressure.js +50 -10
  160. package/dist/memory-pressure.js.map +1 -1
  161. package/dist/needs-you-hub.js +2 -0
  162. package/dist/needs-you-hub.js.map +1 -1
  163. package/dist/needs-you-live.d.ts +6 -0
  164. package/dist/needs-you-live.js +23 -3
  165. package/dist/needs-you-live.js.map +1 -1
  166. package/dist/next-steps.js +1 -1
  167. package/dist/next-steps.js.map +1 -1
  168. package/dist/outbound-handlers.d.ts +100 -0
  169. package/dist/outbound-handlers.js +263 -96
  170. package/dist/outbound-handlers.js.map +1 -1
  171. package/dist/outbound-schemas.d.ts +26 -15
  172. package/dist/outbound-schemas.js +24 -7
  173. package/dist/outbound-schemas.js.map +1 -1
  174. package/dist/public-link-progress.d.ts +1 -1
  175. package/dist/public-link-progress.js +8 -2
  176. package/dist/public-link-progress.js.map +1 -1
  177. package/dist/public-preview.d.ts +101 -0
  178. package/dist/public-preview.js +236 -0
  179. package/dist/public-preview.js.map +1 -0
  180. package/dist/public-web-gateway.js +11 -4
  181. package/dist/public-web-gateway.js.map +1 -1
  182. package/dist/public-web-link.d.ts +24 -1
  183. package/dist/public-web-link.js +63 -9
  184. package/dist/public-web-link.js.map +1 -1
  185. package/dist/reply-buttons-controller.d.ts +3 -0
  186. package/dist/reply-buttons-controller.js +3 -2
  187. package/dist/reply-buttons-controller.js.map +1 -1
  188. package/dist/restart-guard.d.ts +6 -3
  189. package/dist/restart-guard.js +6 -3
  190. package/dist/restart-guard.js.map +1 -1
  191. package/dist/restart-progress.d.ts +33 -0
  192. package/dist/restart-progress.js +30 -0
  193. package/dist/restart-progress.js.map +1 -1
  194. package/dist/service-activation.d.ts +15 -7
  195. package/dist/service-activation.js +46 -29
  196. package/dist/service-activation.js.map +1 -1
  197. package/dist/service-installer.d.ts +31 -0
  198. package/dist/service-installer.js +41 -2
  199. package/dist/service-installer.js.map +1 -1
  200. package/dist/settings-api.js +85 -25
  201. package/dist/settings-api.js.map +1 -1
  202. package/dist/settings-change.js +72 -1
  203. package/dist/settings-change.js.map +1 -1
  204. package/dist/settings-effect.js +28 -4
  205. package/dist/settings-effect.js.map +1 -1
  206. package/dist/settings-http-confirmation.js +7 -0
  207. package/dist/settings-http-confirmation.js.map +1 -1
  208. package/dist/setup-form.d.ts +1 -1
  209. package/dist/setup-form.js +1 -1
  210. package/dist/spawn-gate.js +5 -1
  211. package/dist/spawn-gate.js.map +1 -1
  212. package/dist/spawn-hold-reply.d.ts +12 -0
  213. package/dist/spawn-hold-reply.js +39 -0
  214. package/dist/spawn-hold-reply.js.map +1 -0
  215. package/dist/status-emojis.d.ts +10 -3
  216. package/dist/status-emojis.js +11 -2
  217. package/dist/status-emojis.js.map +1 -1
  218. package/dist/systemd-peer-bus.d.ts +95 -0
  219. package/dist/systemd-peer-bus.js +430 -0
  220. package/dist/systemd-peer-bus.js.map +1 -0
  221. package/dist/systemd-runtime.d.ts +27 -1
  222. package/dist/systemd-runtime.js +69 -7
  223. package/dist/systemd-runtime.js.map +1 -1
  224. package/dist/tips.js +4 -4
  225. package/dist/tips.js.map +1 -1
  226. package/dist/tool-progress.js +3 -2
  227. package/dist/tool-progress.js.map +1 -1
  228. package/dist/topic-commands.d.ts +42 -2
  229. package/dist/topic-commands.js +92 -7
  230. package/dist/topic-commands.js.map +1 -1
  231. package/dist/transcript-delta-reader.d.ts +3 -3
  232. package/dist/transcript-delta-reader.js +8 -8
  233. package/dist/transcript-delta-reader.js.map +1 -1
  234. package/dist/transcript-jsonl.d.ts +80 -3
  235. package/dist/transcript-jsonl.js +147 -10
  236. package/dist/transcript-jsonl.js.map +1 -1
  237. package/dist/transcript-monitor.d.ts +19 -0
  238. package/dist/transcript-monitor.js +87 -15
  239. package/dist/transcript-monitor.js.map +1 -1
  240. package/dist/transcript-sources.d.ts +78 -0
  241. package/dist/transcript-sources.js +375 -64
  242. package/dist/transcript-sources.js.map +1 -1
  243. package/dist/transcript-turns.d.ts +15 -2
  244. package/dist/transcript-turns.js +15 -2
  245. package/dist/transcript-turns.js.map +1 -1
  246. package/dist/tunnel/cloudflared.d.ts +7 -15
  247. package/dist/tunnel/cloudflared.js +36 -1
  248. package/dist/tunnel/cloudflared.js.map +1 -1
  249. package/dist/tunnel/purpose-lane.d.ts +5 -1
  250. package/dist/tunnel/purpose-lane.js +14 -7
  251. package/dist/tunnel/purpose-lane.js.map +1 -1
  252. package/dist/tunnel/types.d.ts +5 -1
  253. package/dist/tunnel/types.js.map +1 -1
  254. package/dist/turn-resume.d.ts +2 -0
  255. package/dist/turn-resume.js +1 -1
  256. package/dist/turn-resume.js.map +1 -1
  257. package/dist/types.d.ts +25 -0
  258. package/dist/ui/chat-commands.js +1 -0
  259. package/dist/ui/chat-emojis.js +33 -0
  260. package/dist/ui/chat-mentions.js +63 -0
  261. package/dist/ui/chat-render.js +42 -2
  262. package/dist/ui/chat-store.js +2 -2
  263. package/dist/ui/chat-strings.js +38 -20
  264. package/dist/ui/chat-thread.js +69 -12
  265. package/dist/ui/chat-time.js +80 -0
  266. package/dist/ui/panel-chat.js +39 -10
  267. package/dist/ui/panel-details.js +2 -0
  268. package/dist/ui/panel-fleet.js +199 -57
  269. package/dist/ui/panel-log.js +428 -0
  270. package/dist/ui/panel-needs.js +6 -2
  271. package/dist/ui/panel-settings.js +137 -32
  272. package/dist/ui/preview-panel.js +55 -15
  273. package/dist/ui/preview-status.js +28 -0
  274. package/dist/ui/preview.js +44 -3
  275. package/dist/ui/settings-dialogs.js +7 -2
  276. package/dist/ui/settings-model.js +2 -1
  277. package/dist/ui/settings-strings.js +46 -4
  278. package/dist/ui/settings-token.js +1 -1
  279. package/dist/ui/shared/app-i18n.js +43 -17
  280. package/dist/ui/shared/app-prefs.js +250 -0
  281. package/dist/ui/shared/app-route.js +7 -2
  282. package/dist/ui/shared/app-shell.js +165 -8
  283. package/dist/ui/shared/app-store.js +5 -1
  284. package/dist/ui/shared/app-stream.js +23 -5
  285. package/dist/ui/shared/app.css +155 -22
  286. package/dist/ui/shared/app.js +11 -1
  287. package/dist/ui/shared/fonts/README.md +1 -1
  288. package/dist/ui/shared/header-tools.js +55 -5
  289. package/dist/ui/shared/instance-nav.js +241 -45
  290. package/dist/ui/shared/instance-switch.js +4 -4
  291. package/dist/ui/shared/panel-view.js +6 -0
  292. package/dist/ui/shared/signin.js +7 -7
  293. package/dist/ui/shared/theme.js +39 -7
  294. package/dist/ui/shared/tokens.css +1 -1
  295. package/dist/ui/shared/ui-icons.js +1 -0
  296. package/dist/ui/shared/usage-dialog.js +23 -5
  297. package/dist/ui/shared/view-strings.js +32 -4
  298. package/dist/ui/signin.html +1 -1
  299. package/dist/unrecognised-prompt.d.ts +23 -0
  300. package/dist/unrecognised-prompt.js +74 -0
  301. package/dist/unrecognised-prompt.js.map +1 -0
  302. package/dist/update-install.d.ts +28 -2
  303. package/dist/update-install.js +22 -4
  304. package/dist/update-install.js.map +1 -1
  305. package/dist/update-marker.d.ts +51 -0
  306. package/dist/update-marker.js +61 -0
  307. package/dist/update-marker.js.map +1 -1
  308. package/dist/update-preflight.d.ts +100 -0
  309. package/dist/update-preflight.js +264 -0
  310. package/dist/update-preflight.js.map +1 -0
  311. package/dist/updater-caller.d.ts +31 -0
  312. package/dist/updater-caller.js +109 -0
  313. package/dist/updater-caller.js.map +1 -0
  314. package/dist/usage/i18n-keys.d.ts +1 -1
  315. package/dist/usage/i18n-keys.js +1 -1
  316. package/dist/usage/i18n-keys.js.map +1 -1
  317. package/dist/usage/providers.d.ts +34 -5
  318. package/dist/usage/providers.js +160 -27
  319. package/dist/usage/providers.js.map +1 -1
  320. package/dist/usage/statusline-usage.d.ts +6 -2
  321. package/dist/usage/statusline-usage.js +10 -4
  322. package/dist/usage/statusline-usage.js.map +1 -1
  323. package/dist/view-api.d.ts +1 -1
  324. package/dist/view-api.js +38 -8
  325. package/dist/view-api.js.map +1 -1
  326. package/dist/wake-coordinator.d.ts +11 -0
  327. package/dist/wake-coordinator.js +38 -4
  328. package/dist/wake-coordinator.js.map +1 -1
  329. package/dist/web-api.d.ts +49 -2
  330. package/dist/web-api.js +421 -31
  331. package/dist/web-api.js.map +1 -1
  332. package/dist/web-auth.d.ts +2 -2
  333. package/dist/web-auth.js +9 -4
  334. package/dist/web-auth.js.map +1 -1
  335. package/dist/web-commands.d.ts +27 -4
  336. package/dist/web-commands.js +19 -4
  337. package/dist/web-commands.js.map +1 -1
  338. package/dist/web-host-guard.d.ts +3 -11
  339. package/dist/web-host-guard.js +3 -12
  340. package/dist/web-host-guard.js.map +1 -1
  341. package/dist/web-images.d.ts +47 -0
  342. package/dist/web-images.js +169 -0
  343. package/dist/web-images.js.map +1 -0
  344. package/dist/web-mentions.d.ts +44 -0
  345. package/dist/web-mentions.js +75 -0
  346. package/dist/web-mentions.js.map +1 -0
  347. package/dist/web-prefs.d.ts +63 -0
  348. package/dist/web-prefs.js +265 -0
  349. package/dist/web-prefs.js.map +1 -0
  350. package/dist/web-preview.d.ts +14 -1
  351. package/dist/web-preview.js +35 -13
  352. package/dist/web-preview.js.map +1 -1
  353. package/dist/web-reply-files.d.ts +45 -0
  354. package/dist/web-reply-files.js +223 -0
  355. package/dist/web-reply-files.js.map +1 -0
  356. package/dist/web-schedules.d.ts +12 -0
  357. package/dist/web-schedules.js +34 -0
  358. package/dist/web-schedules.js.map +1 -0
  359. package/dist/web-server-drain.d.ts +10 -0
  360. package/dist/web-server-drain.js +50 -0
  361. package/dist/web-server-drain.js.map +1 -0
  362. package/dist/web-session.d.ts +36 -1
  363. package/dist/web-session.js +53 -6
  364. package/dist/web-session.js.map +1 -1
  365. package/dist/web-settings-patch.d.ts +23 -0
  366. package/dist/web-settings-patch.js +77 -0
  367. package/dist/web-settings-patch.js.map +1 -0
  368. package/dist/web-shell-routes.d.ts +5 -1
  369. package/dist/web-shell-routes.js +10 -1
  370. package/dist/web-shell-routes.js.map +1 -1
  371. package/dist/web-upload.d.ts +17 -4
  372. package/dist/web-upload.js +64 -13
  373. package/dist/web-upload.js.map +1 -1
  374. package/dist/workflow-templates/default.md +1 -1
  375. package/package.json +1 -1
  376. package/templates/launchd.plist.ejs +3 -1
  377. 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,11 +292,15 @@ 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`<section class="card s-device s-synced"><h3>${tn("syncedTitle")}</h3>
297
+ <p class="sub">${publicLink ? tn("syncedPublicLink") : tn("syncedLead")}</p>
295
298
  <${DevicePrefs} labels=${true} />
296
299
  ${P() ? html`<label class="check"><input type="checkbox" checked=${on} onChange=${(e) => { const want = e.target.checked; setPreviewOptIn(want).then(setOn); }} />
297
300
  <span>${t("chat.previews")}</span></label>` : null}
301
+ </section>
302
+ <section class="card s-device s-local"><h3>${tn("deviceTitle")}</h3>
303
+ <p class="sub">${tn("deviceLead")}</p>
298
304
  <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
305
  </section>`;
300
306
  }
@@ -366,11 +372,13 @@ function makeCtx(data, setData, stage, unstage, reload, reloadLive, alive) {
366
372
  async pauseWake(name, action) {
367
373
  const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
368
374
  if (!res.ok) toast((res.body && res.body.error) || tn("actionFailed", action), false);
375
+ else if (res.body && res.body.held) toast(res.body.message);
369
376
  await reloadLive();
370
377
  },
371
378
  async startStop(name, running) {
372
379
  const res = await api(running ? `/stop/${encodeURIComponent(name)}` : `/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
373
380
  if (!res.ok) toast(`${tn("actionFailed", running ? tn("stop") : tn("start"))}: ${(res.body && res.body.error) || res.status || ""}`, false);
381
+ else if (res.body && res.body.held) toast(res.body.message);
374
382
  await new Promise((r) => setTimeout(r, 800));
375
383
  await reloadLive();
376
384
  },
@@ -570,17 +578,13 @@ function Classic({ ctx, search, openDialog }) {
570
578
  * ClassicBot defaults, web) — each only with what differs from what this form showed, so changing one hot default
571
579
  * never also stages untouched settings as a fleet restart.
572
580
  */
573
- function General({ ctx, staged }) {
581
+ function General({ ctx }) {
574
582
  const { schema } = ctx;
575
583
  const d = ctx.fleet.defaults || {}, c = (ctx.classic && ctx.classic.defaults) || {};
576
584
  const hang = d.hang_detector || {}, startup = d.startup || {};
577
585
  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) }), []);
586
+ const original = useMemo(() => ({ c: structuredClone(c) }), []);
582
587
  const [f, setF] = useState(() => ({
583
- echo: originalEcho, pub: originalPublic.allow_public, ttl: String(originalPublic.ttl_minutes), protocol: originalPublic.protocol,
584
588
  backend: d.backend || "claude-code", model: d.model || "", locale: d.locale || "", auto: String(d.auto_pause_after ?? 0),
585
589
  agent_mode: d.agent_mode || "mcp", tool_set: d.tool_set || "full", tool_progress: d.tool_progress ?? "off",
586
590
  visibility: visibilityDefault(d), reply: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
@@ -598,7 +602,6 @@ function General({ ctx, staged }) {
598
602
  auto: nonNegative(f.auto), hang: positive(f.hang),
599
603
  concurrency: !Number.isInteger(num(f.concurrency)) || num(f.concurrency) < 1 || num(f.concurrency) > 20 ? "concurrencyRange" : "",
600
604
  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
605
  };
603
606
  const warnings = { concurrency: num(f.concurrency) > 4 ? "concurrencyWarning" : "", stagger: num(f.stagger) < 100 ? "staggerWarning" : "" };
604
607
  const ready = !Object.values(errors).some(Boolean);
@@ -638,29 +641,12 @@ function General({ ctx, staged }) {
638
641
  ctx.stage("defaults:classic", { label: tn("updateClassicDefaults"), impact: hotOnly ? "now" : "fleet",
639
642
  request: { method: "PUT", url: "/api/settings/classic/defaults", body: classicPatch } });
640
643
  }
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
644
  setMsg(tn("readyApply"));
649
645
  };
650
646
  const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
651
647
  const fleetReply = replyGuardSupported(f.backend, f.agent_mode, d.kiro_ui) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", f.backend, f.agent_mode);
652
648
  const classicReply = replyGuardSupported(f.classicBackend, f.agent_mode, d.kiro_ui) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", f.classicBackend, f.agent_mode);
653
649
  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
650
  <section class="card"><h3>${tn("defaultsTitle")}</h3>
665
651
  <div class="grid2">
666
652
  <div class="field">${L(tn("language"), "defaults.locale", "g-locale")}<${Select} id="g-locale" value=${f.locale} onChange=${set("locale")}
@@ -714,6 +700,125 @@ function General({ ctx, staged }) {
714
700
  </div>`;
715
701
  }
716
702
 
703
+ /**
704
+ * #1620: Settings → Web — the dashboard's own settings, in one write (PUT /api/settings/fleet/web). Editable: the local
705
+ * session limits, the browser terminal (on/off, its minutes), web echo and the public link's menu; everything else
706
+ * under web: / web_terminal: is listed read-only (it decides what the dashboard listens on and exposes: fleet.yaml, then
707
+ * agend reload). The server judges each change: loosening one waits for a fleet admin in General, tightening applies at
708
+ * once. Over
709
+ * the temporary public link none of it can be changed — the form says so and stays disabled (the server refuses too).
710
+ */
711
+ export const WEB_READ_ONLY = [
712
+ "web.allowed_hosts", "web.view_access", "web.notify_login", "web.preview", "web.preview_port", "web.preview_origin", "web.preview_on_public_link",
713
+ "web.usage_panel", "web.provider_secrets", "web_terminal.bind", "web_terminal.tunnel",
714
+ ];
715
+ function WebSection({ ctx, staged }) {
716
+ const { schema } = ctx;
717
+ const web = ctx.fleet.web || {}, local = (web.session && web.session.local) || {}, term = ctx.fleet.web_terminal || {};
718
+ const overPublicLink = globalThis.document?.body?.dataset?.publicLink === "1";
719
+ const original = useMemo(() => ({
720
+ echo: web.echo_to_channel ?? true,
721
+ log: web.log_tab === true, // #1218: off by default
722
+ 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" },
723
+ session: { absolute_hours: local.absolute_hours ?? null, idle_hours: local.idle_hours ?? null },
724
+ terminal: { enabled: term.enabled ?? null, ttl_minutes: term.ttl_minutes ?? null },
725
+ }), []);
726
+ const text = (v) => (v === null || v === undefined ? "" : String(v));
727
+ const [f, setF] = useState(() => ({
728
+ echo: original.echo, log: original.log, pub: original.pub.allow_public, ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol,
729
+ abs: text(original.session.absolute_hours), idle: text(original.session.idle_hours),
730
+ termOn: original.terminal.enabled !== false, termTtl: text(original.terminal.ttl_minutes),
731
+ }));
732
+ const [msg, setMsg] = useState(null);
733
+ const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
734
+ // Empty = the default (null in the write: the key is removed from fleet.yaml).
735
+ const value = (v) => (v.trim() === "" ? null : Number(v));
736
+ const hours = (v) => { const n = value(v); return n !== null && (!Number.isFinite(n) || n < 1 || n > 720) ? "sessionRange" : ""; };
737
+ // The defaults (#1670: 48 h / 48 h, DEFAULT_SESSION_POLICY); an idle time left blank follows a shorter session, as
738
+ // the store and the validator apply it — so only an idle time that is typed can exceed the session length.
739
+ const abs = value(f.abs) ?? 48, idle = value(f.idle) ?? Math.min(48, abs);
740
+ const errors = {
741
+ ttl: !Number.isInteger(Number(f.ttl)) || Number(f.ttl) < 1 || Number(f.ttl) > 480 ? "publicTtl" : "",
742
+ abs: hours(f.abs),
743
+ idle: hours(f.idle) || (!hours(f.abs) && idle > abs ? "sessionIdleOver" : ""),
744
+ termTtl: value(f.termTtl) !== null && (!Number.isInteger(value(f.termTtl)) || value(f.termTtl) < 1 || value(f.termTtl) > 20) ? "webTerminalTtlRange" : "",
745
+ };
746
+ const ready = !overPublicLink && !Object.values(errors).some(Boolean);
747
+ const review = () => {
748
+ if (!ready) return;
749
+ const publicPatch = changedFields({ allow_public: f.pub, ttl_minutes: Number(f.ttl), protocol: f.protocol }, original.pub);
750
+ const sessionPatch = changedFields({ absolute_hours: value(f.abs), idle_hours: value(f.idle) }, original.session);
751
+ // The checkbox shows the default (on) for an unset key: only a real toggle is written; on again = back to the default.
752
+ const terminalPatch = changedFields({ enabled: f.termOn === (original.terminal.enabled !== false) ? original.terminal.enabled : f.termOn ? null : false,
753
+ ttl_minutes: value(f.termTtl) }, original.terminal);
754
+ const body = {
755
+ ...(f.echo !== original.echo ? { echo_to_channel: f.echo } : {}),
756
+ ...(f.log !== original.log ? { log_tab: f.log } : {}),
757
+ ...(Object.keys(publicPatch).length ? { public_link: publicPatch } : {}),
758
+ ...(Object.keys(sessionPatch).length ? { session: { local: sessionPatch } } : {}),
759
+ ...(Object.keys(terminalPatch).length ? { web_terminal: terminalPatch } : {}),
760
+ };
761
+ const fields = [
762
+ ...(body.echo_to_channel !== undefined ? ["web.echo_to_channel"] : []),
763
+ ...(body.log_tab !== undefined ? ["web.log_tab"] : []),
764
+ ...Object.keys(publicPatch).map((k) => `web.public_link.${k}`),
765
+ ...Object.keys(sessionPatch).map((k) => `web.session.local.${k}`),
766
+ ...Object.keys(terminalPatch).map((k) => `web_terminal.${k}`),
767
+ ];
768
+ if (fields.length) {
769
+ ctx.stage("web", { label: tn("sec_web"), impact: batchImpact(schema, fields, (k) => impactOf(schema, k)),
770
+ request: { method: "PUT", url: "/api/settings/fleet/web", body } });
771
+ } else if (staged.has("web")) ctx.unstage("web");
772
+ setMsg(tn("readyApply"));
773
+ };
774
+ const shown = (path) => {
775
+ const [top, key] = path.split(".");
776
+ const v = (top === "web" ? web : term)[key];
777
+ return v === undefined ? tn("notSet") : typeof v === "object" ? JSON.stringify(v) : String(v);
778
+ };
779
+ return html`<div class="form s-web">
780
+ <p class="sub">${tn("webLead")}</p>
781
+ ${overPublicLink ? html`<p class="note" role="status" id="webPublicRefused">${tn("webPublicRefused")}</p>` : null}
782
+ <fieldset class="s-web-fields" disabled=${overPublicLink}>
783
+ <section class="card"><h3>${tn("webSessionTitle")}</h3>
784
+ <p class="note">${tn("webSessionHint")}</p>
785
+ <div class="grid2">
786
+ <div class="field"><label for="webSessionAbsolute">${tn("sessionAbsolute")}</label><input id="webSessionAbsolute" type="number" min="1" max="720" step="any" placeholder=${tn("defaultHours", 48)}
787
+ class=${errors.abs ? "invalid" : ""} value=${f.abs} onInput=${(e) => set("abs")(e.target.value)} /><${Feedback} error=${errors.abs} /></div>
788
+ <div class="field"><label for="webSessionIdle">${tn("sessionIdle")}</label><input id="webSessionIdle" type="number" min="1" max="720" step="any" placeholder=${tn("defaultHours", 48)}
789
+ class=${errors.idle ? "invalid" : ""} value=${f.idle} onInput=${(e) => set("idle")(e.target.value)} /><${Feedback} error=${errors.idle} /></div>
790
+ </div>
791
+ </section>
792
+ <section class="card"><h3>${tn("webTerminalTitle")}</h3>
793
+ <p class="note">${tn("webTerminalHint")}</p>
794
+ <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>
795
+ <div class="grid2">
796
+ <div class="field"><label for="webTerminalTtl">${tn("webTerminalTtl")}</label><input id="webTerminalTtl" type="number" min="1" max="20" step="1" placeholder=${tn("defaultHours", 10)}
797
+ class=${errors.termTtl ? "invalid" : ""} value=${f.termTtl} onInput=${(e) => set("termTtl")(e.target.value)} /><${Feedback} error=${errors.termTtl} /></div>
798
+ </div>
799
+ </section>
800
+ <section class="card"><h3>${tn("webChat")}</h3>
801
+ <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>
802
+ <p class="note">${tn("webEchoHint")}</p>
803
+ <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>
804
+ <p class="note">${tn("webLogTabHint")}</p>
805
+ <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>
806
+ <p class="note">${tn("publicLinkHint")}</p>
807
+ <div class="grid2">
808
+ <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>
809
+ <div class="field"><label for="publicWebProtocol">${tn("publicProtocol")}</label><${Select} id="publicWebProtocol" value=${f.protocol} onChange=${set("protocol")} options=${["http2", "quic", "auto"]} /></div>
810
+ </div>
811
+ </section>
812
+ </fieldset>
813
+ <section class="card s-web-readonly"><h3>${tn("webReadOnlyTitle")}</h3>
814
+ <p class="note">${tn("webReadOnlyLead")}</p>
815
+ <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>
816
+ </section>
817
+ <div class="save-row"><button type="button" class="btn btn-primary" disabled=${!ready} onClick=${review}>${tn("reviewChanges")}</button>
818
+ ${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>
819
+ </div>`;
820
+ }
821
+
717
822
  /** Developer: the fleet model as YAML or JSON, to read, copy, download — or edit and apply as one operation. */
718
823
  function Developer({ ctx }) {
719
824
  const [fmt, setFmt] = useState("yaml");
@@ -749,7 +854,7 @@ function Developer({ ctx }) {
749
854
  <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
855
  <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
856
  </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>
857
+ ${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck=${false} wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
753
858
  <div class="save-row"><button type="button" class="btn btn-primary" onClick=${save}>${tn("devApply")}</button><span class="note">${tn("devApplyHint")}</span></div>`
754
859
  : html`<pre class="s-yaml">${text}</pre>`}
755
860
  ${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.
@@ -24,8 +24,9 @@ export function changedFields(next, baseline) {
24
24
  /** #1302: cross-instance notices. Unset, or a value the fleet does not know, means full. */
25
25
  export const visibilityDefault = (defaults) => (VISIBILITY_MODES.includes(defaults && defaults.cross_instance_visibility) ? defaults.cross_instance_visibility : "full");
26
26
 
27
+ /** The backends whose turn end the reply guard can see (backend replyCompletionGuard: claude-code, codex and muse since #1510, kiro legacy/tui), with the MCP server it needs. */
27
28
  export const replyGuardSupported = (backend, mode, kiroUi = "legacy") =>
28
- mode === "mcp" && (backend === "claude-code" || (backend === "kiro-cli" && ["legacy", "tui"].includes(kiroUi || "legacy")));
29
+ mode === "mcp" && (["claude-code", "codex", "muse"].includes(backend) || (backend === "kiro-cli" && ["legacy", "tui"].includes(kiroUi || "legacy")));
29
30
 
30
31
  /**
31
32
  * The backend picker's options. A current value it does not offer (the removed gemini-cli, #1280 review) stays first