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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (389) hide show
  1. package/README.md +40 -24
  2. package/README.zh-TW.md +40 -24
  3. package/dist/agent-cli-instructions.md +6 -2
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +3 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.js +16 -4
  9. package/dist/agent-whats-new.js.map +1 -1
  10. package/dist/auth-api.d.ts +3 -1
  11. package/dist/auth-api.js +17 -1
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/backend/antigravity.d.ts +33 -0
  14. package/dist/backend/antigravity.js +126 -18
  15. package/dist/backend/antigravity.js.map +1 -1
  16. package/dist/backend/claude-carry.d.ts +22 -0
  17. package/dist/backend/claude-carry.js +170 -0
  18. package/dist/backend/claude-carry.js.map +1 -0
  19. package/dist/backend/claude-code.d.ts +65 -2
  20. package/dist/backend/claude-code.js +226 -33
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/codex-metadata.d.ts +33 -0
  23. package/dist/backend/codex-metadata.js +122 -18
  24. package/dist/backend/codex-metadata.js.map +1 -1
  25. package/dist/backend/codex.d.ts +3 -0
  26. package/dist/backend/codex.js +17 -1
  27. package/dist/backend/codex.js.map +1 -1
  28. package/dist/backend/credential-profile.d.ts +40 -3
  29. package/dist/backend/credential-profile.js +113 -5
  30. package/dist/backend/credential-profile.js.map +1 -1
  31. package/dist/backend/effort-metadata.d.ts +37 -1
  32. package/dist/backend/effort-metadata.js +86 -15
  33. package/dist/backend/effort-metadata.js.map +1 -1
  34. package/dist/backend/grok.d.ts +4 -0
  35. package/dist/backend/grok.js +14 -0
  36. package/dist/backend/grok.js.map +1 -1
  37. package/dist/backend/kiro.d.ts +27 -1
  38. package/dist/backend/kiro.js +63 -10
  39. package/dist/backend/kiro.js.map +1 -1
  40. package/dist/backend/launch-effort.d.ts +61 -0
  41. package/dist/backend/launch-effort.js +170 -0
  42. package/dist/backend/launch-effort.js.map +1 -0
  43. package/dist/backend/muse.d.ts +84 -0
  44. package/dist/backend/muse.js +449 -53
  45. package/dist/backend/muse.js.map +1 -1
  46. package/dist/backend/types.d.ts +48 -2
  47. package/dist/backend/types.js +12 -1
  48. package/dist/backend/types.js.map +1 -1
  49. package/dist/backend-command.d.ts +91 -0
  50. package/dist/backend-command.js +122 -0
  51. package/dist/backend-command.js.map +1 -0
  52. package/dist/background-work-ended.d.ts +18 -0
  53. package/dist/background-work-ended.js +30 -0
  54. package/dist/background-work-ended.js.map +1 -0
  55. package/dist/cache-service.d.ts +2 -0
  56. package/dist/cache-service.js +2 -1
  57. package/dist/cache-service.js.map +1 -1
  58. package/dist/channel/adapters/discord.d.ts +26 -1
  59. package/dist/channel/adapters/discord.js +103 -3
  60. package/dist/channel/adapters/discord.js.map +1 -1
  61. package/dist/channel/adapters/telegram.d.ts +35 -3
  62. package/dist/channel/adapters/telegram.js +122 -23
  63. package/dist/channel/adapters/telegram.js.map +1 -1
  64. package/dist/channel/mcp-tools.js +5 -3
  65. package/dist/channel/mcp-tools.js.map +1 -1
  66. package/dist/channel/message-edit-errors.d.ts +5 -0
  67. package/dist/channel/message-edit-errors.js +24 -0
  68. package/dist/channel/message-edit-errors.js.map +1 -0
  69. package/dist/channel/telegram-markdown.d.ts +6 -0
  70. package/dist/channel/telegram-markdown.js +104 -0
  71. package/dist/channel/telegram-markdown.js.map +1 -0
  72. package/dist/channel/tool-router.d.ts +2 -0
  73. package/dist/channel/tool-router.js +6 -5
  74. package/dist/channel/tool-router.js.map +1 -1
  75. package/dist/channel/types.d.ts +46 -4
  76. package/dist/cli.js +184 -34
  77. package/dist/cli.js.map +1 -1
  78. package/dist/command-table.d.ts +2 -2
  79. package/dist/command-table.js +5 -2
  80. package/dist/command-table.js.map +1 -1
  81. package/dist/compact-watch.d.ts +86 -0
  82. package/dist/compact-watch.js +288 -0
  83. package/dist/compact-watch.js.map +1 -0
  84. package/dist/config-validator.js +55 -5
  85. package/dist/config-validator.js.map +1 -1
  86. package/dist/config.d.ts +1 -0
  87. package/dist/config.js +2 -1
  88. package/dist/config.js.map +1 -1
  89. package/dist/cross-instance-envelope.js +2 -0
  90. package/dist/cross-instance-envelope.js.map +1 -1
  91. package/dist/daemon.d.ts +129 -1
  92. package/dist/daemon.js +754 -94
  93. package/dist/daemon.js.map +1 -1
  94. package/dist/delivery-outbox.d.ts +64 -8
  95. package/dist/delivery-outbox.js +315 -24
  96. package/dist/delivery-outbox.js.map +1 -1
  97. package/dist/delivery-reconciliation.d.ts +18 -1
  98. package/dist/delivery-reconciliation.js +67 -2
  99. package/dist/delivery-reconciliation.js.map +1 -1
  100. package/dist/doctor.js +7 -0
  101. package/dist/doctor.js.map +1 -1
  102. package/dist/fleet-context.d.ts +12 -1
  103. package/dist/fleet-manager.d.ts +392 -19
  104. package/dist/fleet-manager.js +2030 -275
  105. package/dist/fleet-manager.js.map +1 -1
  106. package/dist/forged-envelope-scan.d.ts +4 -3
  107. package/dist/forged-envelope-scan.js +4 -3
  108. package/dist/forged-envelope-scan.js.map +1 -1
  109. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +84 -16
  110. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -1
  111. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  112. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
  113. package/dist/host-memory.d.ts +10 -0
  114. package/dist/host-memory.js +33 -1
  115. package/dist/host-memory.js.map +1 -1
  116. package/dist/human-delivery-receipts.d.ts +83 -0
  117. package/dist/human-delivery-receipts.js +194 -0
  118. package/dist/human-delivery-receipts.js.map +1 -0
  119. package/dist/install-lock.d.ts +18 -1
  120. package/dist/install-lock.js +30 -3
  121. package/dist/install-lock.js.map +1 -1
  122. package/dist/instance-avatar.d.ts +5 -4
  123. package/dist/instance-avatar.js +12 -8
  124. package/dist/instance-avatar.js.map +1 -1
  125. package/dist/instance-config-impact.js +1 -0
  126. package/dist/instance-config-impact.js.map +1 -1
  127. package/dist/instance-lifecycle.d.ts +25 -6
  128. package/dist/instance-lifecycle.js +76 -27
  129. package/dist/instance-lifecycle.js.map +1 -1
  130. package/dist/instructions.d.ts +1 -1
  131. package/dist/instructions.js +3 -3
  132. package/dist/instructions.js.map +1 -1
  133. package/dist/kiro-compact-pane.d.ts +38 -0
  134. package/dist/kiro-compact-pane.js +57 -0
  135. package/dist/kiro-compact-pane.js.map +1 -0
  136. package/dist/kiro-log-gate.d.ts +42 -0
  137. package/dist/kiro-log-gate.js +39 -0
  138. package/dist/kiro-log-gate.js.map +1 -0
  139. package/dist/launchd-handoff.d.ts +43 -0
  140. package/dist/launchd-handoff.js +56 -0
  141. package/dist/launchd-handoff.js.map +1 -0
  142. package/dist/locale.js +268 -42
  143. package/dist/locale.js.map +1 -1
  144. package/dist/log-service.d.ts +99 -0
  145. package/dist/log-service.js +243 -0
  146. package/dist/log-service.js.map +1 -0
  147. package/dist/log-steps.d.ts +169 -0
  148. package/dist/log-steps.js +858 -0
  149. package/dist/log-steps.js.map +1 -0
  150. package/dist/login-controller.d.ts +20 -3
  151. package/dist/login-controller.js +71 -19
  152. package/dist/login-controller.js.map +1 -1
  153. package/dist/login-flows.d.ts +5 -2
  154. package/dist/login-flows.js +5 -4
  155. package/dist/login-flows.js.map +1 -1
  156. package/dist/login-shell.d.ts +21 -0
  157. package/dist/login-shell.js +98 -0
  158. package/dist/login-shell.js.map +1 -0
  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/outbound-handlers.d.ts +100 -0
  168. package/dist/outbound-handlers.js +277 -93
  169. package/dist/outbound-handlers.js.map +1 -1
  170. package/dist/outbound-schemas.d.ts +30 -15
  171. package/dist/outbound-schemas.js +23 -5
  172. package/dist/outbound-schemas.js.map +1 -1
  173. package/dist/provider-probe.d.ts +29 -0
  174. package/dist/provider-probe.js +198 -2
  175. package/dist/provider-probe.js.map +1 -1
  176. package/dist/public-link-progress.d.ts +1 -1
  177. package/dist/public-link-progress.js +8 -2
  178. package/dist/public-link-progress.js.map +1 -1
  179. package/dist/public-preview.d.ts +101 -0
  180. package/dist/public-preview.js +236 -0
  181. package/dist/public-preview.js.map +1 -0
  182. package/dist/public-web-gateway.d.ts +2 -0
  183. package/dist/public-web-gateway.js +20 -6
  184. package/dist/public-web-gateway.js.map +1 -1
  185. package/dist/public-web-link.d.ts +24 -1
  186. package/dist/public-web-link.js +63 -9
  187. package/dist/public-web-link.js.map +1 -1
  188. package/dist/quickstart-api.d.ts +1 -1
  189. package/dist/quickstart-api.js +8 -6
  190. package/dist/quickstart-api.js.map +1 -1
  191. package/dist/quickstart.d.ts +12 -0
  192. package/dist/quickstart.js +36 -6
  193. package/dist/quickstart.js.map +1 -1
  194. package/dist/reply-buttons-controller.d.ts +89 -5
  195. package/dist/reply-buttons-controller.js +273 -8
  196. package/dist/reply-buttons-controller.js.map +1 -1
  197. package/dist/reply-buttons.d.ts +48 -3
  198. package/dist/reply-buttons.js +105 -10
  199. package/dist/reply-buttons.js.map +1 -1
  200. package/dist/reply-polls.d.ts +85 -0
  201. package/dist/reply-polls.js +154 -0
  202. package/dist/reply-polls.js.map +1 -0
  203. package/dist/restart-guard.d.ts +6 -3
  204. package/dist/restart-guard.js +6 -3
  205. package/dist/restart-guard.js.map +1 -1
  206. package/dist/restart-progress.d.ts +33 -2
  207. package/dist/restart-progress.js +30 -2
  208. package/dist/restart-progress.js.map +1 -1
  209. package/dist/sd-notify.d.ts +22 -1
  210. package/dist/sd-notify.js +63 -10
  211. package/dist/sd-notify.js.map +1 -1
  212. package/dist/service-activation.d.ts +15 -7
  213. package/dist/service-activation.js +42 -26
  214. package/dist/service-activation.js.map +1 -1
  215. package/dist/service-installer.d.ts +55 -0
  216. package/dist/service-installer.js +90 -3
  217. package/dist/service-installer.js.map +1 -1
  218. package/dist/service-plan.d.ts +11 -0
  219. package/dist/service-plan.js +23 -0
  220. package/dist/service-plan.js.map +1 -1
  221. package/dist/settings-api.js +14 -4
  222. package/dist/settings-api.js.map +1 -1
  223. package/dist/settings-change.js +4 -2
  224. package/dist/settings-change.js.map +1 -1
  225. package/dist/settings-effect.js +2 -0
  226. package/dist/settings-effect.js.map +1 -1
  227. package/dist/spawn-gate.js +5 -1
  228. package/dist/spawn-gate.js.map +1 -1
  229. package/dist/spawn-hold-reply.d.ts +12 -0
  230. package/dist/spawn-hold-reply.js +39 -0
  231. package/dist/spawn-hold-reply.js.map +1 -0
  232. package/dist/systemd-peer-bus.d.ts +95 -0
  233. package/dist/systemd-peer-bus.js +430 -0
  234. package/dist/systemd-peer-bus.js.map +1 -0
  235. package/dist/systemd-runtime.d.ts +27 -1
  236. package/dist/systemd-runtime.js +69 -7
  237. package/dist/systemd-runtime.js.map +1 -1
  238. package/dist/tips.js +1 -1
  239. package/dist/tips.js.map +1 -1
  240. package/dist/tool-permissions.js +3 -3
  241. package/dist/tool-permissions.js.map +1 -1
  242. package/dist/tool-progress.js +3 -2
  243. package/dist/tool-progress.js.map +1 -1
  244. package/dist/topic-commands.d.ts +71 -4
  245. package/dist/topic-commands.js +167 -34
  246. package/dist/topic-commands.js.map +1 -1
  247. package/dist/transcript-delta-reader.d.ts +3 -3
  248. package/dist/transcript-delta-reader.js +8 -8
  249. package/dist/transcript-delta-reader.js.map +1 -1
  250. package/dist/transcript-jsonl.d.ts +85 -3
  251. package/dist/transcript-jsonl.js +156 -13
  252. package/dist/transcript-jsonl.js.map +1 -1
  253. package/dist/transcript-monitor.d.ts +11 -0
  254. package/dist/transcript-monitor.js +61 -13
  255. package/dist/transcript-monitor.js.map +1 -1
  256. package/dist/transcript-sources.d.ts +24 -0
  257. package/dist/transcript-sources.js +211 -56
  258. package/dist/transcript-sources.js.map +1 -1
  259. package/dist/tunnel/cloudflared.d.ts +7 -15
  260. package/dist/tunnel/cloudflared.js +36 -1
  261. package/dist/tunnel/cloudflared.js.map +1 -1
  262. package/dist/tunnel/purpose-lane.d.ts +5 -1
  263. package/dist/tunnel/purpose-lane.js +14 -7
  264. package/dist/tunnel/purpose-lane.js.map +1 -1
  265. package/dist/tunnel/types.d.ts +5 -1
  266. package/dist/tunnel/types.js.map +1 -1
  267. package/dist/turn-resume.d.ts +2 -0
  268. package/dist/turn-resume.js +1 -1
  269. package/dist/turn-resume.js.map +1 -1
  270. package/dist/types.d.ts +22 -0
  271. package/dist/ui/chat-commands.js +1 -0
  272. package/dist/ui/chat-mentions.js +63 -0
  273. package/dist/ui/chat-render.js +80 -10
  274. package/dist/ui/chat-store.js +70 -6
  275. package/dist/ui/chat-strings.js +62 -32
  276. package/dist/ui/chat-thread.js +199 -24
  277. package/dist/ui/chat-time.js +80 -0
  278. package/dist/ui/chat-tour.js +17 -14
  279. package/dist/ui/panel-chat.js +45 -21
  280. package/dist/ui/panel-details.js +2 -0
  281. package/dist/ui/panel-fleet.js +213 -58
  282. package/dist/ui/panel-log.js +428 -0
  283. package/dist/ui/panel-needs.js +14 -10
  284. package/dist/ui/panel-settings.js +82 -17
  285. package/dist/ui/preview-panel.js +55 -15
  286. package/dist/ui/preview-status.js +28 -0
  287. package/dist/ui/preview.js +44 -3
  288. package/dist/ui/settings-dialogs.js +11 -6
  289. package/dist/ui/settings-model.js +3 -1
  290. package/dist/ui/settings-strings.js +32 -12
  291. package/dist/ui/settings-token.js +8 -1
  292. package/dist/ui/settings-wizard.js +5 -3
  293. package/dist/ui/shared/app-i18n.js +41 -15
  294. package/dist/ui/shared/app-prefs.js +250 -0
  295. package/dist/ui/shared/app-route.js +6 -1
  296. package/dist/ui/shared/app-shell.js +165 -8
  297. package/dist/ui/shared/app-store.js +5 -1
  298. package/dist/ui/shared/app-stream.js +26 -5
  299. package/dist/ui/shared/app.css +170 -30
  300. package/dist/ui/shared/app.js +11 -1
  301. package/dist/ui/shared/fonts/README.md +1 -1
  302. package/dist/ui/shared/header-tools.js +83 -13
  303. package/dist/ui/shared/instance-nav.js +178 -51
  304. package/dist/ui/shared/instance-switch.js +8 -5
  305. package/dist/ui/shared/panel-view.js +21 -4
  306. package/dist/ui/shared/signin.js +15 -9
  307. package/dist/ui/shared/theme.js +39 -7
  308. package/dist/ui/shared/tokens.css +1 -1
  309. package/dist/ui/shared/ui-icons.js +2 -0
  310. package/dist/ui/shared/usage-dialog.js +23 -5
  311. package/dist/ui/shared/view-strings.js +40 -4
  312. package/dist/ui/signin.html +8 -3
  313. package/dist/unrecognised-prompt.d.ts +23 -0
  314. package/dist/unrecognised-prompt.js +74 -0
  315. package/dist/unrecognised-prompt.js.map +1 -0
  316. package/dist/update-check.d.ts +31 -0
  317. package/dist/update-check.js +47 -0
  318. package/dist/update-check.js.map +1 -1
  319. package/dist/update-install.d.ts +13 -1
  320. package/dist/update-install.js +16 -4
  321. package/dist/update-install.js.map +1 -1
  322. package/dist/update-launch.d.ts +48 -1
  323. package/dist/update-launch.js +159 -5
  324. package/dist/update-launch.js.map +1 -1
  325. package/dist/update-marker.d.ts +51 -0
  326. package/dist/update-marker.js +61 -0
  327. package/dist/update-marker.js.map +1 -1
  328. package/dist/usage/i18n-keys.d.ts +1 -1
  329. package/dist/usage/i18n-keys.js +1 -1
  330. package/dist/usage/i18n-keys.js.map +1 -1
  331. package/dist/usage/providers.d.ts +62 -13
  332. package/dist/usage/providers.js +256 -68
  333. package/dist/usage/providers.js.map +1 -1
  334. package/dist/usage/statusline-usage.d.ts +6 -2
  335. package/dist/usage/statusline-usage.js +10 -4
  336. package/dist/usage/statusline-usage.js.map +1 -1
  337. package/dist/view-api.js +31 -9
  338. package/dist/view-api.js.map +1 -1
  339. package/dist/web-api.d.ts +82 -4
  340. package/dist/web-api.js +632 -88
  341. package/dist/web-api.js.map +1 -1
  342. package/dist/web-auth.js +5 -2
  343. package/dist/web-auth.js.map +1 -1
  344. package/dist/web-chat-history.d.ts +26 -1
  345. package/dist/web-chat-history.js +47 -8
  346. package/dist/web-chat-history.js.map +1 -1
  347. package/dist/web-commands.d.ts +41 -5
  348. package/dist/web-commands.js +25 -7
  349. package/dist/web-commands.js.map +1 -1
  350. package/dist/web-login.d.ts +14 -0
  351. package/dist/web-login.js +4 -2
  352. package/dist/web-login.js.map +1 -1
  353. package/dist/web-mentions.d.ts +44 -0
  354. package/dist/web-mentions.js +75 -0
  355. package/dist/web-mentions.js.map +1 -0
  356. package/dist/web-prefs.d.ts +81 -0
  357. package/dist/web-prefs.js +297 -0
  358. package/dist/web-prefs.js.map +1 -0
  359. package/dist/web-preview.d.ts +14 -1
  360. package/dist/web-preview.js +35 -13
  361. package/dist/web-preview.js.map +1 -1
  362. package/dist/web-profile.d.ts +32 -0
  363. package/dist/web-profile.js +132 -0
  364. package/dist/web-profile.js.map +1 -0
  365. package/dist/web-reply-files.d.ts +45 -0
  366. package/dist/web-reply-files.js +223 -0
  367. package/dist/web-reply-files.js.map +1 -0
  368. package/dist/web-schedules.d.ts +12 -0
  369. package/dist/web-schedules.js +34 -0
  370. package/dist/web-schedules.js.map +1 -0
  371. package/dist/web-server-drain.d.ts +10 -0
  372. package/dist/web-server-drain.js +50 -0
  373. package/dist/web-server-drain.js.map +1 -0
  374. package/dist/web-session.d.ts +14 -0
  375. package/dist/web-session.js +11 -1
  376. package/dist/web-session.js.map +1 -1
  377. package/dist/web-settings-patch.d.ts +3 -1
  378. package/dist/web-settings-patch.js +4 -1
  379. package/dist/web-settings-patch.js.map +1 -1
  380. package/dist/web-shell-routes.d.ts +4 -0
  381. package/dist/web-shell-routes.js +9 -0
  382. package/dist/web-shell-routes.js.map +1 -1
  383. package/dist/web-upload.d.ts +135 -7
  384. package/dist/web-upload.js +315 -51
  385. package/dist/web-upload.js.map +1 -1
  386. package/launcher/install-admission.cjs +61 -6
  387. package/package.json +3 -3
  388. package/templates/launchd.plist.ejs +3 -1
  389. package/templates/systemd.service.ejs +2 -1
@@ -24,7 +24,7 @@ import "./settings-strings.js";
24
24
  import { api, attach, confirmedWrite, newKey } from "./settings-confirm.js";
25
25
  import { dismissOperation, operationActive, restartFleet, startOperation, takeLeftover } from "./settings-apply.js";
26
26
  import {
27
- ACCESS_MODES, AGENT_MODES, DEFAULT_SCHEMA, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS, VISIBILITY_MODES, batchImpact,
27
+ ACCESS_MODES, AGENT_MODES, DEFAULT_SCHEMA, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS, VISIBILITY_MODES, batchImpact,
28
28
  changedFields, channelId, channelsOf, chLabel, connectionState, effectiveSummary, fleetModel, fromYaml, fullModelRequests, groupAgents,
29
29
  hasOwn, impactOf, localeOptions, defaultsImpact, requestImpact, nonNegative, positive, replyGuardSupported, shortName, STATUS_ORDER, toYaml,
30
30
  visibilityDefault, worstImpact,
@@ -258,9 +258,10 @@ export function SettingsPanel({ route, navKey }) {
258
258
  }
259
259
 
260
260
  /**
261
- * #1604: what this browser chooses for itself — moved here from the sidebar footer, each with the storage it always had:
262
- * theme (theme.js), language (app-i18n.js), HTML previews on this device (preview.js, through the app's confirm), and
263
- * 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).
264
265
  */
265
266
  export function DeviceSection() {
266
267
  const P = () => globalThis.AgendPreview;
@@ -269,7 +270,11 @@ export function DeviceSection() {
269
270
  const replay = () => {
270
271
  let last = null;
271
272
  try { last = localStorage.getItem("agend_last_instance"); } catch { /* none */ }
272
- navigate(last ? chatPath(last) : "/ui");
273
+ // #1854: the tour is about an agent's chat — with no agent to open (none remembered, or it is gone), say so instead
274
+ // of opening an empty chat that waits ~3 s and shows nothing.
275
+ const { instances } = appStore.get();
276
+ if (!last || !(instances || []).some((i) => i && i.name === last)) { toast(t("chat.tourNoAgent"), false); return; }
277
+ navigate(chatPath(last));
273
278
  // This replay belongs to the navigation it just made — that one, by its sequence number, not by URL (A → B → A, or
274
279
  // the same path again, is a new navigation). It did not happen (a leave guard said no) or it is not the chat:
275
280
  // nothing. Left meanwhile (another page, another navigation): nothing (#1609 review).
@@ -284,22 +289,73 @@ export function DeviceSection() {
284
289
  let tries = 0;
285
290
  const go = () => {
286
291
  if (!current()) return;
287
- if (!document.getElementById("sendBtn")) { if (++tries <= 30) setTimeout(go, 100); return; }
292
+ if (!document.getElementById("sendBtn")) { if (++tries <= 30) setTimeout(go, 100); else toast(t("chat.tourNoAgent"), false); return; }
288
293
  const here = document.getElementById("msgIn") || document.getElementById("main");
289
294
  if (here && typeof here.focus === "function") here.focus();
290
295
  if (current()) startTour();
291
296
  };
292
297
  setTimeout(go, 0);
293
298
  };
294
- return html`<section class="card s-device"><h3>${tn("sec_device")}</h3>
295
- <p class="sub">${tn("deviceLead")}</p>
299
+ const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
300
+ return html`<${ProfileCard} />
301
+ <section class="card s-device s-synced"><h3>${tn("syncedTitle")}</h3>
302
+ <p class="sub">${publicLink ? tn("syncedPublicLink") : tn("syncedLead")}</p>
296
303
  <${DevicePrefs} labels=${true} />
297
304
  ${P() ? html`<label class="check"><input type="checkbox" checked=${on} onChange=${(e) => { const want = e.target.checked; setPreviewOptIn(want).then(setOn); }} />
298
305
  <span>${t("chat.previews")}</span></label>` : null}
306
+ </section>
307
+ <section class="card s-device s-local"><h3>${tn("deviceTitle")}</h3>
308
+ <p class="sub">${tn("deviceLead")}</p>
299
309
  <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>
300
310
  </section>`;
301
311
  }
302
312
 
313
+ /**
314
+ * #1827: who agents see when this person writes from the web. Signed in with a code sent to their Discord or Telegram
315
+ * account, they are that account — its user id, and its name unless they choose another (it follows them to every
316
+ * device, the public link included). Otherwise "web-user", as before. The host checks a name in full and says why it
317
+ * refuses one; this only shows what it says.
318
+ */
319
+ const view = (b) => ({ phase: "ready", name: b.name || "", shown: b.shown || "", id: b.id || "", bound: b.bound === true, platform: b.platform_name || "", error: "" });
320
+ export function ProfileCard() {
321
+ const [state, setState] = useState({ phase: "loading", name: "", shown: "", id: "", bound: false, platform: "", error: "" });
322
+ const [draft, setDraft] = useState("");
323
+ const lease = useLease();
324
+ useEffect(() => {
325
+ let live = true;
326
+ api("/ui/profile").then((r) => {
327
+ if (!live) return;
328
+ if (!r.ok || !r.body) { setState((s) => ({ ...s, phase: "unavailable" })); return; }
329
+ setState(view(r.body));
330
+ setDraft(r.body.name || "");
331
+ }).catch(() => { if (live) setState((s) => ({ ...s, phase: "unavailable" })); });
332
+ return () => { live = false; };
333
+ }, []);
334
+ if (state.phase === "unavailable") return null;
335
+ const save = (clear) => {
336
+ setState((s) => ({ ...s, phase: "saving", error: "" }));
337
+ api("/ui/profile", { method: "POST", body: JSON.stringify({ name: clear ? null : draft }) }).then((r) => {
338
+ if (lease && !lease.current()) return;
339
+ if (!r.ok) { setState((s) => ({ ...s, phase: "ready", error: (r.body && r.body.error) || tn("profileFailed") })); return; }
340
+ setState(view(r.body));
341
+ setDraft(r.body.name || "");
342
+ toast(tn("profileSaved", r.body.shown || "web-user"));
343
+ }).catch(() => setState((s) => ({ ...s, phase: "ready", error: tn("profileFailed") })));
344
+ };
345
+ const busy = state.phase !== "ready";
346
+ const changed = draft.trim() !== state.name;
347
+ return html`<section class="card s-device s-profile"><h3>${tn("profileTitle")}</h3>
348
+ <p class="sub">${state.bound ? tn("profileLeadBound") : tn("profileLeadUnbound")}</p>
349
+ <div class="field"><label for="profile-name">${tn("profileName")}</label>
350
+ <input id="profile-name" type="text" maxlength="200" autocomplete="nickname" value=${draft} disabled=${busy}
351
+ placeholder=${state.platform || "web-user"} aria-invalid=${state.error ? "true" : "false"} aria-describedby="profile-help"
352
+ onInput=${(e) => setDraft(e.target.value)} onKeyDown=${(e) => { if (e.key === "Enter" && changed && draft.trim()) save(false); }} />
353
+ <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>
354
+ <div class="s-device-row"><button type="button" class="btn primary" disabled=${busy || !changed || !draft.trim()} onClick=${() => save(false)}>${tn("profileSave")}</button>
355
+ ${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>
356
+ </section>`;
357
+ }
358
+
303
359
  /** The operation in hand still owns Apply: its writes and job, or a restart of AgEnD from the press to its end. */
304
360
  const opBusy = (op) => !!op && (["writing", "posting", "watching"].includes(op.phase) || op.restart === "busy");
305
361
  function dropSecrets(staged) { for (const c of staged.values()) if (c.connectionSecret) c.connectionSecret.secret = ""; }
@@ -367,11 +423,13 @@ function makeCtx(data, setData, stage, unstage, reload, reloadLive, alive) {
367
423
  async pauseWake(name, action) {
368
424
  const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
369
425
  if (!res.ok) toast((res.body && res.body.error) || tn("actionFailed", action), false);
426
+ else if (res.body && res.body.held) toast(res.body.message);
370
427
  await reloadLive();
371
428
  },
372
429
  async startStop(name, running) {
373
430
  const res = await api(running ? `/stop/${encodeURIComponent(name)}` : `/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
374
431
  if (!res.ok) toast(`${tn("actionFailed", running ? tn("stop") : tn("start"))}: ${(res.body && res.body.error) || res.status || ""}`, false);
432
+ else if (res.body && res.body.held) toast(res.body.message);
375
433
  await new Promise((r) => setTimeout(r, 800));
376
434
  await reloadLive();
377
435
  },
@@ -581,7 +639,7 @@ function General({ ctx }) {
581
639
  backend: d.backend || "claude-code", model: d.model || "", locale: d.locale || "", auto: String(d.auto_pause_after ?? 0),
582
640
  agent_mode: d.agent_mode || "mcp", tool_set: d.tool_set || "full", tool_progress: d.tool_progress ?? "off",
583
641
  visibility: visibilityDefault(d), reply: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
584
- hangOn: hang.enabled !== false, hang: String(hang.timeout_minutes ?? 15),
642
+ hangOn: hang.enabled !== false, hang: String(hang.timeout_minutes ?? HANG_TIMEOUT_DEFAULT_MINUTES),
585
643
  concurrency: String(startup.concurrency ?? 10), stagger: String(startup.stagger_delay_ms ?? 500),
586
644
  classicBackend: c.backend || d.backend || "claude-code",
587
645
  classicToolProgress: c.tool_progress ?? d.tool_progress ?? "off", classicToolProgressInherit: !hasOwn(c, "tool_progress"),
@@ -610,7 +668,7 @@ function General({ ctx }) {
610
668
  backend: d.backend || "claude-code", model: d.model || undefined, locale: d.locale || null, auto_pause_after: d.auto_pause_after ?? 0,
611
669
  agent_mode: d.agent_mode || "mcp", tool_set: d.tool_set || "full", tool_progress: d.tool_progress ?? "off",
612
670
  cross_instance_visibility: visibilityDefault(d), reply_completion_guard: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
613
- hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? 15 },
671
+ hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? HANG_TIMEOUT_DEFAULT_MINUTES },
614
672
  startup: { concurrency: startup.concurrency ?? 10, stagger_delay_ms: startup.stagger_delay_ms ?? 500 },
615
673
  });
616
674
  const oc = original.c;
@@ -702,8 +760,8 @@ function General({ ctx }) {
702
760
  * the temporary public link none of it can be changed — the form says so and stays disabled (the server refuses too).
703
761
  */
704
762
  export const WEB_READ_ONLY = [
705
- "web.allowed_hosts", "web.view_access", "web.notify_login", "web.preview", "web.preview_port", "web.preview_origin",
706
- "web.usage_panel", "web.provider_secrets", "web_terminal.bind", "web_terminal.tunnel",
763
+ "web.allowed_hosts", "web.view_access", "web.notify_login", "web.preview", "web.preview_port", "web.preview_origin", "web.preview_on_public_link",
764
+ "web.usage_panel", "web.provider_secrets", "web.upload_max_file_mb", "web.upload_max_total_mb", "web_terminal.bind", "web_terminal.tunnel",
707
765
  ];
708
766
  function WebSection({ ctx, staged }) {
709
767
  const { schema } = ctx;
@@ -711,13 +769,14 @@ function WebSection({ ctx, staged }) {
711
769
  const overPublicLink = globalThis.document?.body?.dataset?.publicLink === "1";
712
770
  const original = useMemo(() => ({
713
771
  echo: web.echo_to_channel ?? true,
772
+ log: web.log_tab === true, // #1218: off by default
714
773
  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" },
715
774
  session: { absolute_hours: local.absolute_hours ?? null, idle_hours: local.idle_hours ?? null },
716
775
  terminal: { enabled: term.enabled ?? null, ttl_minutes: term.ttl_minutes ?? null },
717
776
  }), []);
718
777
  const text = (v) => (v === null || v === undefined ? "" : String(v));
719
778
  const [f, setF] = useState(() => ({
720
- echo: original.echo, pub: original.pub.allow_public, ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol,
779
+ echo: original.echo, log: original.log, pub: original.pub.allow_public, ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol,
721
780
  abs: text(original.session.absolute_hours), idle: text(original.session.idle_hours),
722
781
  termOn: original.terminal.enabled !== false, termTtl: text(original.terminal.ttl_minutes),
723
782
  }));
@@ -726,7 +785,9 @@ function WebSection({ ctx, staged }) {
726
785
  // Empty = the default (null in the write: the key is removed from fleet.yaml).
727
786
  const value = (v) => (v.trim() === "" ? null : Number(v));
728
787
  const hours = (v) => { const n = value(v); return n !== null && (!Number.isFinite(n) || n < 1 || n > 720) ? "sessionRange" : ""; };
729
- const abs = value(f.abs) ?? 12, idle = value(f.idle) ?? 2;
788
+ // The defaults (#1670: 48 h / 48 h, DEFAULT_SESSION_POLICY); an idle time left blank follows a shorter session, as
789
+ // the store and the validator apply it — so only an idle time that is typed can exceed the session length.
790
+ const abs = value(f.abs) ?? 48, idle = value(f.idle) ?? Math.min(48, abs);
730
791
  const errors = {
731
792
  ttl: !Number.isInteger(Number(f.ttl)) || Number(f.ttl) < 1 || Number(f.ttl) > 480 ? "publicTtl" : "",
732
793
  abs: hours(f.abs),
@@ -743,12 +804,14 @@ function WebSection({ ctx, staged }) {
743
804
  ttl_minutes: value(f.termTtl) }, original.terminal);
744
805
  const body = {
745
806
  ...(f.echo !== original.echo ? { echo_to_channel: f.echo } : {}),
807
+ ...(f.log !== original.log ? { log_tab: f.log } : {}),
746
808
  ...(Object.keys(publicPatch).length ? { public_link: publicPatch } : {}),
747
809
  ...(Object.keys(sessionPatch).length ? { session: { local: sessionPatch } } : {}),
748
810
  ...(Object.keys(terminalPatch).length ? { web_terminal: terminalPatch } : {}),
749
811
  };
750
812
  const fields = [
751
813
  ...(body.echo_to_channel !== undefined ? ["web.echo_to_channel"] : []),
814
+ ...(body.log_tab !== undefined ? ["web.log_tab"] : []),
752
815
  ...Object.keys(publicPatch).map((k) => `web.public_link.${k}`),
753
816
  ...Object.keys(sessionPatch).map((k) => `web.session.local.${k}`),
754
817
  ...Object.keys(terminalPatch).map((k) => `web_terminal.${k}`),
@@ -771,9 +834,9 @@ function WebSection({ ctx, staged }) {
771
834
  <section class="card"><h3>${tn("webSessionTitle")}</h3>
772
835
  <p class="note">${tn("webSessionHint")}</p>
773
836
  <div class="grid2">
774
- <div class="field"><label for="webSessionAbsolute">${tn("sessionAbsolute")}</label><input id="webSessionAbsolute" type="number" min="1" max="720" step="any" placeholder=${tn("defaultHours", 12)}
837
+ <div class="field"><label for="webSessionAbsolute">${tn("sessionAbsolute")}</label><input id="webSessionAbsolute" type="number" min="1" max="720" step="any" placeholder=${tn("defaultHours", 48)}
775
838
  class=${errors.abs ? "invalid" : ""} value=${f.abs} onInput=${(e) => set("abs")(e.target.value)} /><${Feedback} error=${errors.abs} /></div>
776
- <div class="field"><label for="webSessionIdle">${tn("sessionIdle")}</label><input id="webSessionIdle" type="number" min="1" max="720" step="any" placeholder=${tn("defaultHours", 2)}
839
+ <div class="field"><label for="webSessionIdle">${tn("sessionIdle")}</label><input id="webSessionIdle" type="number" min="1" max="720" step="any" placeholder=${tn("defaultHours", 48)}
777
840
  class=${errors.idle ? "invalid" : ""} value=${f.idle} onInput=${(e) => set("idle")(e.target.value)} /><${Feedback} error=${errors.idle} /></div>
778
841
  </div>
779
842
  </section>
@@ -788,6 +851,8 @@ function WebSection({ ctx, staged }) {
788
851
  <section class="card"><h3>${tn("webChat")}</h3>
789
852
  <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>
790
853
  <p class="note">${tn("webEchoHint")}</p>
854
+ <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>
855
+ <p class="note">${tn("webLogTabHint")}</p>
791
856
  <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>
792
857
  <p class="note">${tn("publicLinkHint")}</p>
793
858
  <div class="grid2">
@@ -840,7 +905,7 @@ function Developer({ ctx }) {
840
905
  <button type="button" class="btn btn-sm" title=${tn("copy")} onClick=${copy}><${Icon} name="copy" size=${14} /><span class="lbl">${tn("copy")}</span></button>
841
906
  <button type="button" class="btn btn-sm" title=${tn("devDownload")} onClick=${download}><${Icon} name="download" size=${14} /><span class="lbl">${tn("devDownload")}</span></button>
842
907
  </div>
843
- ${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck="false" wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
908
+ ${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck=${false} wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
844
909
  <div class="save-row"><button type="button" class="btn btn-primary" onClick=${save}>${tn("devApply")}</button><span class="note">${tn("devApplyHint")}</span></div>`
845
910
  : html`<pre class="s-yaml">${text}</pre>`}
846
911
  ${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
  });
@@ -10,11 +10,11 @@ import { Icon } from "/assets/ui-icons.js";
10
10
  import { toast } from "/assets/ui-toast.js";
11
11
  import { confirmDialog } from "/assets/ui-confirm.js";
12
12
  import { api, confirmedWrite, newKey } from "./settings-confirm.js";
13
- import { botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
13
+ import { adminUserIdError, adminUserIdPattern, botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
14
14
  import { DiscordServerPicker } from "./settings-discord.js";
15
15
  import { startOperation } from "./settings-apply.js";
16
16
  import {
17
- ACCESS_MODES, AGENT_FIELDS, AGENT_MODES, BOT_FIELDS, CH_TYPES, CLASSIC_FIELDS, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
17
+ ACCESS_MODES, AGENT_FIELDS, AGENT_MODES, BOT_FIELDS, CH_TYPES, CLASSIC_FIELDS, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
18
18
  VISIBILITY_MODES, accessConfirmations, agentForm, agentPatch, backendOptions, changedFields, channelId, channelsOf,
19
19
  chLabel, hasOwn, impactOf, nonNegative, nonNegativeInteger, positive, replyGuardSupported, sameValue, visibilityDefault,
20
20
  webEchoIsGroup, worstImpact,
@@ -242,7 +242,7 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
242
242
  <div class="field">${L(`${tn("hangTimeout")} (${tn("minutes")})`, "instance.hang_detector", "ag-hang")}
243
243
  <${Override} inherit=${f.inherit.hang} onInherit=${setInherit("hang")}>
244
244
  <input id="ag-hang" type="number" min="0.1" step="0.5" class=${errors.hang ? "invalid" : ""} disabled=${f.inherit.hang}
245
- value=${shown("hang", f.hang, String((defaults.hang_detector && defaults.hang_detector.timeout_minutes) ?? 15))} onInput=${(e) => set("hang")(e.target.value)} /></${Override}>
245
+ value=${shown("hang", f.hang, String((defaults.hang_detector && defaults.hang_detector.timeout_minutes) ?? HANG_TIMEOUT_DEFAULT_MINUTES))} onInput=${(e) => set("hang")(e.target.value)} /></${Override}>
246
246
  <${Feedback} error=${errors.hang} /></div>
247
247
  ${[["agent_mode", "agentMode", AGENT_MODES, defaults.agent_mode || "mcp"], ["tool_set", "toolSet", TOOL_SETS, defaults.tool_set || "full"],
248
248
  ["tool_progress", "toolProgress", TOOL_PROGRESS, defaults.tool_progress ?? "off"],
@@ -466,17 +466,18 @@ export function BotDialog({ id, ctx, onClose }) {
466
466
  */
467
467
  export function NewBotDialog({ ctx, onClose }) {
468
468
  const lease = useLease("new-bot");
469
- const [f, setF] = useState({ type: "discord", id: "", token: "", identity: null, guilds: [], group: "", general: "" });
469
+ const [f, setF] = useState({ type: "discord", id: "", token: "", identity: null, guilds: [], group: "", general: "", admin: "" });
470
470
  const [plan, setPlan] = useState(null);
471
471
  const [busy, setBusy] = useState(false);
472
472
  const [err, setErr] = useState("");
473
473
  // The form's revision (#1529 review): any change to the token, platform, id or group moves it; a Verify or a plan
474
474
  // that began before is dropped when it lands — never restoring an old token, naming it, or planning for it.
475
475
  const rev = useRef(0);
476
- const body = (x) => ({ platform: x.type, connection_only: true, ...(x.id.trim() ? { channel_id: x.id.trim() } : {}),
476
+ const body = (x) => ({ platform: x.type, connection_only: true, admin_user_id: x.admin.trim(), ...(x.id.trim() ? { channel_id: x.id.trim() } : {}),
477
477
  ...(x.group.trim() ? (x.type === "discord" ? { guild_id: x.group.trim() } : { group_id: x.group.trim() }) : {}),
478
478
  ...(x.type === "discord" && x.general.trim() ? { general_channel_id: x.general.trim() } : {}) });
479
479
  const planFor = async (x) => {
480
+ if (adminUserIdError(x.type, x.admin.trim())) { setPlan(null); return null; }
480
481
  const at = rev.current;
481
482
  const res = await api("/api/settings/quickstart/plan", { method: "POST", body: JSON.stringify(body(x)) }).catch(() => ({ ok: false, body: {} }));
482
483
  if (!lease.current() || rev.current !== at) return null;
@@ -501,6 +502,8 @@ export function NewBotDialog({ ctx, onClose }) {
501
502
  };
502
503
  const save = async () => {
503
504
  if (!f.identity || !f.identity.valid) { setErr(tn("wizardNeedVerify")); return; }
505
+ const adminError = adminUserIdError(f.type, f.admin.trim());
506
+ if (adminError) { setErr(adminError); return; }
504
507
  const x = f;
505
508
  setBusy(true);
506
509
  const p = await planFor(x);
@@ -514,7 +517,7 @@ export function NewBotDialog({ ctx, onClose }) {
514
517
  };
515
518
  // A change of platform or id is another connection: what was verified and planned no longer applies. The server or
516
519
  // group does not change the id or the token's name, so the plan (and its name under Advanced) stays.
517
- const set = (k) => (v) => { rev.current++; setF((x) => ({ ...x, [k]: v, ...(k === "type" ? { identity: null, guilds: [], group: "", general: "" } : {}) })); if (k !== "group" && k !== "general") setPlan(null); };
520
+ const set = (k) => (v) => { rev.current++; setF((x) => ({ ...x, [k]: v, ...(k === "type" ? { identity: null, guilds: [], group: "", general: "", admin: "" } : {}) })); if (k !== "group" && k !== "general") setPlan(null); };
518
521
  return html`<${Dialog} title=${tn("newBot")} onClose=${onClose} busy=${busy}
519
522
  actions=${html`<button type="button" class="btn" disabled=${busy} onClick=${onClose}>${tn("cancel")}</button>
520
523
  <button type="button" class="btn btn-primary" disabled=${busy || !f.identity || !f.identity.valid} onClick=${save}>${tn("save")}</button>`}>
@@ -528,6 +531,8 @@ export function NewBotDialog({ ctx, onClose }) {
528
531
  guild=${f.group} onGuild=${set("group")} channel=${f.general} onChannel=${set("general")} />`
529
532
  : html`<div class="field"><label for="nb-group">${tn("groupIdField")}</label>
530
533
  <input id="nb-group" type="text" value=${f.group} onInput=${(e) => set("group")(e.target.value.trim())} /></div>`}
534
+ <div class="field"><label for="nb-user">${tn("wizardAdminUser")}</label>
535
+ <input id="nb-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(f.type)} value=${f.admin} onInput=${(e) => set("admin")(e.target.value.trim())} /></div>
531
536
  <${Drawer} title=${tn("advancedSection")}>
532
537
  <div class="field"><label for="nb-id">${tn("connectionIdOptional")}</label><input id="nb-id" type="text" placeholder=${plan ? plan.channel_id : f.type}
533
538
  value=${f.id} onInput=${(e) => set("id")(e.target.value)} onBlur=${() => { if (f.identity && f.identity.valid) planFor(f); }} /></div>
@@ -2,6 +2,8 @@
2
2
  // Ported from settings.html with their reasons: every rule here was a fix (the review or issue named beside it).
3
3
  // No DOM, no fetch; the panel (panel-settings.js) and the tests call these directly.
4
4
 
5
+ /** #1854: the stuck timeout when none is set — the daemon's DEFAULT_STUCK_TIMEOUT_MS (src/daemon.ts), in minutes. */
6
+ export const HANG_TIMEOUT_DEFAULT_MINUTES = 10;
5
7
  export const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "antigravity", "grok", "muse"];
6
8
  export const CH_TYPES = ["discord", "telegram"];
7
9
  export const LOCALES = [["en", "English"], ["zh-TW", "繁體中文 (zh-TW)"]];
@@ -166,7 +168,7 @@ export function agentForm(inst, defaults) {
166
168
  description: inst.description || "", systemPrompt: inst.systemPrompt || "", tags: [...(inst.tags || [])],
167
169
  general_topic: !!inst.general_topic,
168
170
  auto_pause_after: String(inst.auto_pause_after ?? defaults.auto_pause_after ?? 0),
169
- hang: String(hang.timeout_minutes ?? (defaults.hang_detector && defaults.hang_detector.timeout_minutes) ?? 15),
171
+ hang: String(hang.timeout_minutes ?? (defaults.hang_detector && defaults.hang_detector.timeout_minutes) ?? HANG_TIMEOUT_DEFAULT_MINUTES),
170
172
  agent_mode: inst.agent_mode || defaults.agent_mode || "mcp",
171
173
  tool_set: inst.tool_set || defaults.tool_set || "full",
172
174
  tool_progress: inst.tool_progress ?? defaults.tool_progress ?? "off",