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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/README.md +40 -24
  2. package/README.zh-TW.md +40 -24
  3. package/dist/agent-cli.js +248 -2
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +2 -0
  6. package/dist/agent-endpoint.js +28 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-file-read.d.ts +30 -0
  9. package/dist/agent-file-read.js +129 -0
  10. package/dist/agent-file-read.js.map +1 -0
  11. package/dist/agent-whats-new.js +20 -1
  12. package/dist/agent-whats-new.js.map +1 -1
  13. package/dist/atomic-write.js +9 -1
  14. package/dist/atomic-write.js.map +1 -1
  15. package/dist/auth-api.d.ts +3 -1
  16. package/dist/auth-api.js +32 -1
  17. package/dist/auth-api.js.map +1 -1
  18. package/dist/backend/antigravity.d.ts +33 -0
  19. package/dist/backend/antigravity.js +129 -21
  20. package/dist/backend/antigravity.js.map +1 -1
  21. package/dist/backend/claude-code.d.ts +3 -0
  22. package/dist/backend/claude-code.js +9 -0
  23. package/dist/backend/claude-code.js.map +1 -1
  24. package/dist/backend/codex-metadata.d.ts +33 -0
  25. package/dist/backend/codex-metadata.js +122 -18
  26. package/dist/backend/codex-metadata.js.map +1 -1
  27. package/dist/backend/codex.d.ts +3 -0
  28. package/dist/backend/codex.js +17 -1
  29. package/dist/backend/codex.js.map +1 -1
  30. package/dist/backend/effort-metadata.d.ts +37 -1
  31. package/dist/backend/effort-metadata.js +86 -15
  32. package/dist/backend/effort-metadata.js.map +1 -1
  33. package/dist/backend/grok.d.ts +3 -0
  34. package/dist/backend/grok.js +9 -0
  35. package/dist/backend/grok.js.map +1 -1
  36. package/dist/backend/kiro.d.ts +3 -1
  37. package/dist/backend/kiro.js +7 -3
  38. package/dist/backend/kiro.js.map +1 -1
  39. package/dist/backend/launch-effort.d.ts +61 -0
  40. package/dist/backend/launch-effort.js +170 -0
  41. package/dist/backend/launch-effort.js.map +1 -0
  42. package/dist/backend/muse.js +10 -2
  43. package/dist/backend/muse.js.map +1 -1
  44. package/dist/backend/types.d.ts +24 -0
  45. package/dist/backend/types.js +12 -1
  46. package/dist/backend/types.js.map +1 -1
  47. package/dist/backend-command.d.ts +91 -0
  48. package/dist/backend-command.js +122 -0
  49. package/dist/backend-command.js.map +1 -0
  50. package/dist/channel/adapters/discord.d.ts +14 -0
  51. package/dist/channel/adapters/discord.js +66 -4
  52. package/dist/channel/adapters/discord.js.map +1 -1
  53. package/dist/channel/adapters/telegram.d.ts +26 -1
  54. package/dist/channel/adapters/telegram.js +98 -7
  55. package/dist/channel/adapters/telegram.js.map +1 -1
  56. package/dist/channel/mcp-tools.js +36 -0
  57. package/dist/channel/mcp-tools.js.map +1 -1
  58. package/dist/channel/tool-router.d.ts +2 -0
  59. package/dist/channel/tool-router.js +1 -1
  60. package/dist/channel/tool-router.js.map +1 -1
  61. package/dist/channel/types.d.ts +31 -0
  62. package/dist/cli.js +124 -24
  63. package/dist/cli.js.map +1 -1
  64. package/dist/command-table.d.ts +2 -2
  65. package/dist/command-table.js +5 -2
  66. package/dist/command-table.js.map +1 -1
  67. package/dist/compact-watch.d.ts +86 -0
  68. package/dist/compact-watch.js +288 -0
  69. package/dist/compact-watch.js.map +1 -0
  70. package/dist/config-validator.js +55 -0
  71. package/dist/config-validator.js.map +1 -1
  72. package/dist/config.d.ts +1 -0
  73. package/dist/config.js +2 -1
  74. package/dist/config.js.map +1 -1
  75. package/dist/daemon.d.ts +9 -0
  76. package/dist/daemon.js +394 -29
  77. package/dist/daemon.js.map +1 -1
  78. package/dist/delivery-outbox.d.ts +8 -1
  79. package/dist/delivery-outbox.js +6 -2
  80. package/dist/delivery-outbox.js.map +1 -1
  81. package/dist/doctor.js +7 -0
  82. package/dist/doctor.js.map +1 -1
  83. package/dist/fleet-context.d.ts +12 -0
  84. package/dist/fleet-manager.d.ts +247 -9
  85. package/dist/fleet-manager.js +1227 -142
  86. package/dist/fleet-manager.js.map +1 -1
  87. package/dist/forged-envelope-scan.d.ts +4 -3
  88. package/dist/forged-envelope-scan.js +4 -3
  89. package/dist/forged-envelope-scan.js.map +1 -1
  90. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  91. package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
  92. package/dist/install-lock.d.ts +18 -1
  93. package/dist/install-lock.js +30 -3
  94. package/dist/install-lock.js.map +1 -1
  95. package/dist/instance-avatar.d.ts +5 -4
  96. package/dist/instance-avatar.js +15 -8
  97. package/dist/instance-avatar.js.map +1 -1
  98. package/dist/instance-config-impact.js +3 -0
  99. package/dist/instance-config-impact.js.map +1 -1
  100. package/dist/instance-lifecycle.js +9 -4
  101. package/dist/instance-lifecycle.js.map +1 -1
  102. package/dist/instructions.js +2 -2
  103. package/dist/instructions.js.map +1 -1
  104. package/dist/kiro-compact-pane.d.ts +38 -0
  105. package/dist/kiro-compact-pane.js +57 -0
  106. package/dist/kiro-compact-pane.js.map +1 -0
  107. package/dist/launchd-handoff.d.ts +43 -0
  108. package/dist/launchd-handoff.js +56 -0
  109. package/dist/launchd-handoff.js.map +1 -0
  110. package/dist/locale.js +202 -30
  111. package/dist/locale.js.map +1 -1
  112. package/dist/log-service.js +10 -2
  113. package/dist/log-service.js.map +1 -1
  114. package/dist/login-shell.d.ts +21 -0
  115. package/dist/login-shell.js +98 -0
  116. package/dist/login-shell.js.map +1 -0
  117. package/dist/needs-you-hub.d.ts +4 -0
  118. package/dist/needs-you-hub.js +31 -6
  119. package/dist/needs-you-hub.js.map +1 -1
  120. package/dist/needs-you.d.ts +1 -13
  121. package/dist/needs-you.js +3 -37
  122. package/dist/needs-you.js.map +1 -1
  123. package/dist/outbound-handlers.d.ts +32 -0
  124. package/dist/outbound-handlers.js +150 -45
  125. package/dist/outbound-handlers.js.map +1 -1
  126. package/dist/outbound-schemas.d.ts +76 -0
  127. package/dist/outbound-schemas.js +65 -4
  128. package/dist/outbound-schemas.js.map +1 -1
  129. package/dist/provider-probe.d.ts +29 -0
  130. package/dist/provider-probe.js +198 -2
  131. package/dist/provider-probe.js.map +1 -1
  132. package/dist/public-web-gateway.d.ts +2 -0
  133. package/dist/public-web-gateway.js +25 -1
  134. package/dist/public-web-gateway.js.map +1 -1
  135. package/dist/quickstart-api.d.ts +1 -1
  136. package/dist/quickstart-api.js +8 -6
  137. package/dist/quickstart-api.js.map +1 -1
  138. package/dist/quickstart.d.ts +12 -0
  139. package/dist/quickstart.js +36 -6
  140. package/dist/quickstart.js.map +1 -1
  141. package/dist/reply-buttons-controller.d.ts +86 -5
  142. package/dist/reply-buttons-controller.js +271 -7
  143. package/dist/reply-buttons-controller.js.map +1 -1
  144. package/dist/reply-buttons.d.ts +48 -3
  145. package/dist/reply-buttons.js +105 -10
  146. package/dist/reply-buttons.js.map +1 -1
  147. package/dist/reply-polls.d.ts +85 -0
  148. package/dist/reply-polls.js +154 -0
  149. package/dist/reply-polls.js.map +1 -0
  150. package/dist/repo-worktree-guard.d.ts +36 -0
  151. package/dist/repo-worktree-guard.js +143 -0
  152. package/dist/repo-worktree-guard.js.map +1 -0
  153. package/dist/restart-guard.d.ts +6 -3
  154. package/dist/restart-guard.js +6 -3
  155. package/dist/restart-guard.js.map +1 -1
  156. package/dist/restart-progress.d.ts +0 -2
  157. package/dist/restart-progress.js +0 -2
  158. package/dist/restart-progress.js.map +1 -1
  159. package/dist/scheduler/scheduler.d.ts +58 -1
  160. package/dist/scheduler/scheduler.js +176 -10
  161. package/dist/scheduler/scheduler.js.map +1 -1
  162. package/dist/sd-notify.d.ts +22 -1
  163. package/dist/sd-notify.js +63 -10
  164. package/dist/sd-notify.js.map +1 -1
  165. package/dist/service-activation.d.ts +15 -7
  166. package/dist/service-activation.js +42 -26
  167. package/dist/service-activation.js.map +1 -1
  168. package/dist/service-installer.d.ts +24 -0
  169. package/dist/service-installer.js +49 -1
  170. package/dist/service-installer.js.map +1 -1
  171. package/dist/service-plan.d.ts +11 -0
  172. package/dist/service-plan.js +23 -0
  173. package/dist/service-plan.js.map +1 -1
  174. package/dist/settings-api.js +74 -1
  175. package/dist/settings-api.js.map +1 -1
  176. package/dist/settings-change.js +8 -0
  177. package/dist/settings-change.js.map +1 -1
  178. package/dist/settings-effect.js +16 -2
  179. package/dist/settings-effect.js.map +1 -1
  180. package/dist/settings-http-confirmation.js +5 -0
  181. package/dist/settings-http-confirmation.js.map +1 -1
  182. package/dist/systemd-peer-bus.d.ts +95 -0
  183. package/dist/systemd-peer-bus.js +430 -0
  184. package/dist/systemd-peer-bus.js.map +1 -0
  185. package/dist/systemd-runtime.d.ts +27 -1
  186. package/dist/systemd-runtime.js +69 -7
  187. package/dist/systemd-runtime.js.map +1 -1
  188. package/dist/tool-permissions.js +53 -3
  189. package/dist/tool-permissions.js.map +1 -1
  190. package/dist/topic-commands.d.ts +72 -2
  191. package/dist/topic-commands.js +156 -38
  192. package/dist/topic-commands.js.map +1 -1
  193. package/dist/topic-link.d.ts +128 -0
  194. package/dist/topic-link.js +269 -0
  195. package/dist/topic-link.js.map +1 -0
  196. package/dist/transcript-jsonl.d.ts +7 -2
  197. package/dist/transcript-jsonl.js +13 -7
  198. package/dist/transcript-jsonl.js.map +1 -1
  199. package/dist/transcript-sources.js +6 -1
  200. package/dist/transcript-sources.js.map +1 -1
  201. package/dist/types.d.ts +21 -0
  202. package/dist/ui/app-effects.js +187 -0
  203. package/dist/ui/chat-commands.js +1 -0
  204. package/dist/ui/chat-effect-posters.js +42 -0
  205. package/dist/ui/chat-effects.js +72 -0
  206. package/dist/ui/chat-live-replies.js +58 -0
  207. package/dist/ui/chat-render.js +59 -14
  208. package/dist/ui/chat-store.js +78 -8
  209. package/dist/ui/chat-strings.js +43 -16
  210. package/dist/ui/chat-thread.js +175 -31
  211. package/dist/ui/chat-tour.js +18 -15
  212. package/dist/ui/effect-trial-controller.js +119 -0
  213. package/dist/ui/effect-trial-offer.js +31 -0
  214. package/dist/ui/effect-trial-panel.js +37 -0
  215. package/dist/ui/image-lightbox.js +1 -1
  216. package/dist/ui/panel-chat.js +101 -27
  217. package/dist/ui/panel-details.js +18 -5
  218. package/dist/ui/panel-fleet.js +18 -5
  219. package/dist/ui/panel-log.js +10 -10
  220. package/dist/ui/panel-needs.js +13 -10
  221. package/dist/ui/panel-settings.js +66 -12
  222. package/dist/ui/preview-panel.js +25 -9
  223. package/dist/ui/settings-appearance.js +325 -0
  224. package/dist/ui/settings-dialogs.js +11 -6
  225. package/dist/ui/settings-effects.js +201 -0
  226. package/dist/ui/settings-model.js +3 -1
  227. package/dist/ui/settings-mods.js +257 -0
  228. package/dist/ui/settings-owner-effects.js +80 -0
  229. package/dist/ui/settings-strings.js +269 -10
  230. package/dist/ui/settings-token.js +7 -0
  231. package/dist/ui/settings-wizard.js +5 -3
  232. package/dist/ui/shared/agend-auth.js +27 -3
  233. package/dist/ui/shared/agent-widget.js +248 -0
  234. package/dist/ui/shared/app-i18n.js +3 -1
  235. package/dist/ui/shared/app-prefs.js +30 -10
  236. package/dist/ui/shared/app-route.js +23 -1
  237. package/dist/ui/shared/app-session.js +1 -1
  238. package/dist/ui/shared/app-shell.js +29 -16
  239. package/dist/ui/shared/app-store.js +2 -0
  240. package/dist/ui/shared/app-stream.js +15 -7
  241. package/dist/ui/shared/app-themes.js +188 -0
  242. package/dist/ui/shared/app.css +266 -11
  243. package/dist/ui/shared/app.js +6 -0
  244. package/dist/ui/shared/card-strings.js +28 -0
  245. package/dist/ui/shared/effect-app-runtime.js +248 -0
  246. package/dist/ui/shared/effect-baseline.js +64 -0
  247. package/dist/ui/shared/effect-chat-runtime.js +154 -0
  248. package/dist/ui/shared/effect-cursor.js +125 -0
  249. package/dist/ui/shared/effect-frame.js +127 -0
  250. package/dist/ui/shared/effect-geometry.js +122 -0
  251. package/dist/ui/shared/effect-host-style.js +10 -0
  252. package/dist/ui/shared/effect-owner-regions.js +74 -0
  253. package/dist/ui/shared/effect-owner-source.js +115 -0
  254. package/dist/ui/shared/effect-policy.js +137 -0
  255. package/dist/ui/shared/effect-poster-reader.js +98 -0
  256. package/dist/ui/shared/effect-posters.js +117 -0
  257. package/dist/ui/shared/effect-profiler.js +162 -0
  258. package/dist/ui/shared/effect-profiles.js +65 -0
  259. package/dist/ui/shared/effect-registry.js +496 -0
  260. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  261. package/dist/ui/shared/effect-safety.js +153 -0
  262. package/dist/ui/shared/effect-stock-snapshot.js +34 -0
  263. package/dist/ui/shared/effect-stock-trust.js +165 -0
  264. package/dist/ui/shared/effect-trial-source.js +103 -0
  265. package/dist/ui/shared/effect-trial-stage.js +147 -0
  266. package/dist/ui/shared/header-tools.js +35 -15
  267. package/dist/ui/shared/identity-card.js +202 -0
  268. package/dist/ui/shared/identity-color.js +261 -0
  269. package/dist/ui/shared/instance-nav.js +22 -14
  270. package/dist/ui/shared/instance-switch.js +5 -2
  271. package/dist/ui/shared/owner-css.js +221 -0
  272. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  273. package/dist/ui/shared/panel-view.js +96 -12
  274. package/dist/ui/shared/signin.js +17 -9
  275. package/dist/ui/shared/ui-dialog.js +1 -1
  276. package/dist/ui/shared/ui-icons.js +3 -0
  277. package/dist/ui/shared/ui-menu.js +1 -1
  278. package/dist/ui/shared/ui-toast.js +1 -1
  279. package/dist/ui/shared/view-strings.js +40 -2
  280. package/dist/ui/signin.html +9 -3
  281. package/dist/update-check.d.ts +31 -0
  282. package/dist/update-check.js +47 -0
  283. package/dist/update-check.js.map +1 -1
  284. package/dist/update-launch.d.ts +48 -1
  285. package/dist/update-launch.js +159 -5
  286. package/dist/update-launch.js.map +1 -1
  287. package/dist/usage/providers.d.ts +28 -8
  288. package/dist/usage/providers.js +96 -41
  289. package/dist/usage/providers.js.map +1 -1
  290. package/dist/view-api.d.ts +29 -0
  291. package/dist/view-api.js +214 -17
  292. package/dist/view-api.js.map +1 -1
  293. package/dist/web-api.d.ts +86 -2
  294. package/dist/web-api.js +1160 -92
  295. package/dist/web-api.js.map +1 -1
  296. package/dist/web-appearance-limits.d.ts +82 -0
  297. package/dist/web-appearance-limits.js +83 -0
  298. package/dist/web-appearance-limits.js.map +1 -0
  299. package/dist/web-appearance-policy.d.ts +66 -0
  300. package/dist/web-appearance-policy.js +94 -0
  301. package/dist/web-appearance-policy.js.map +1 -0
  302. package/dist/web-appearance-tools.d.ts +50 -0
  303. package/dist/web-appearance-tools.js +105 -0
  304. package/dist/web-appearance-tools.js.map +1 -0
  305. package/dist/web-appearance-view.d.ts +99 -0
  306. package/dist/web-appearance-view.js +163 -0
  307. package/dist/web-appearance-view.js.map +1 -0
  308. package/dist/web-appearance.d.ts +79 -0
  309. package/dist/web-appearance.js +298 -0
  310. package/dist/web-appearance.js.map +1 -0
  311. package/dist/web-asset-parts.d.ts +98 -0
  312. package/dist/web-asset-parts.js +133 -0
  313. package/dist/web-asset-parts.js.map +1 -0
  314. package/dist/web-auth.js +17 -1
  315. package/dist/web-auth.js.map +1 -1
  316. package/dist/web-card.d.ts +96 -0
  317. package/dist/web-card.js +232 -0
  318. package/dist/web-card.js.map +1 -0
  319. package/dist/web-chat-history.d.ts +39 -1
  320. package/dist/web-chat-history.js +69 -9
  321. package/dist/web-chat-history.js.map +1 -1
  322. package/dist/web-commands.d.ts +39 -3
  323. package/dist/web-commands.js +22 -5
  324. package/dist/web-commands.js.map +1 -1
  325. package/dist/web-effect-bundle.d.ts +28 -0
  326. package/dist/web-effect-bundle.js +126 -0
  327. package/dist/web-effect-bundle.js.map +1 -0
  328. package/dist/web-effect-canonical.d.ts +8 -0
  329. package/dist/web-effect-canonical.js +27 -0
  330. package/dist/web-effect-canonical.js.map +1 -0
  331. package/dist/web-effect-library.d.ts +114 -0
  332. package/dist/web-effect-library.js +530 -0
  333. package/dist/web-effect-library.js.map +1 -0
  334. package/dist/web-effect-poster.d.ts +8 -0
  335. package/dist/web-effect-poster.js +52 -0
  336. package/dist/web-effect-poster.js.map +1 -0
  337. package/dist/web-effect-profiles.d.ts +118 -0
  338. package/dist/web-effect-profiles.js +180 -0
  339. package/dist/web-effect-profiles.js.map +1 -0
  340. package/dist/web-effect-service.d.ts +86 -0
  341. package/dist/web-effect-service.js +312 -0
  342. package/dist/web-effect-service.js.map +1 -0
  343. package/dist/web-effect-tools.d.ts +47 -0
  344. package/dist/web-effect-tools.js +126 -0
  345. package/dist/web-effect-tools.js.map +1 -0
  346. package/dist/web-effect-trials.d.ts +86 -0
  347. package/dist/web-effect-trials.js +204 -0
  348. package/dist/web-effect-trials.js.map +1 -0
  349. package/dist/web-effect-upload.d.ts +29 -0
  350. package/dist/web-effect-upload.js +190 -0
  351. package/dist/web-effect-upload.js.map +1 -0
  352. package/dist/web-effect-worker.d.ts +8 -0
  353. package/dist/web-effect-worker.js +170 -0
  354. package/dist/web-effect-worker.js.map +1 -0
  355. package/dist/web-effect.d.ts +89 -0
  356. package/dist/web-effect.js +132 -0
  357. package/dist/web-effect.js.map +1 -0
  358. package/dist/web-identity-color.d.ts +87 -0
  359. package/dist/web-identity-color.js +209 -0
  360. package/dist/web-identity-color.js.map +1 -0
  361. package/dist/web-identity-palette.d.ts +113 -0
  362. package/dist/web-identity-palette.js +84 -0
  363. package/dist/web-identity-palette.js.map +1 -0
  364. package/dist/web-image-meta.d.ts +25 -0
  365. package/dist/web-image-meta.js +200 -0
  366. package/dist/web-image-meta.js.map +1 -0
  367. package/dist/web-login.d.ts +14 -0
  368. package/dist/web-login.js +4 -2
  369. package/dist/web-login.js.map +1 -1
  370. package/dist/web-mod-agent.d.ts +50 -0
  371. package/dist/web-mod-agent.js +203 -0
  372. package/dist/web-mod-agent.js.map +1 -0
  373. package/dist/web-mod-bundle.d.ts +144 -0
  374. package/dist/web-mod-bundle.js +464 -0
  375. package/dist/web-mod-bundle.js.map +1 -0
  376. package/dist/web-mod-export.d.ts +59 -0
  377. package/dist/web-mod-export.js +79 -0
  378. package/dist/web-mod-export.js.map +1 -0
  379. package/dist/web-mod-store.d.ts +283 -0
  380. package/dist/web-mod-store.js +624 -0
  381. package/dist/web-mod-store.js.map +1 -0
  382. package/dist/web-org.d.ts +3 -3
  383. package/dist/web-org.js +11 -17
  384. package/dist/web-org.js.map +1 -1
  385. package/dist/web-owner-css-store.d.ts +180 -0
  386. package/dist/web-owner-css-store.js +384 -0
  387. package/dist/web-owner-css-store.js.map +1 -0
  388. package/dist/web-owner-css.d.ts +58 -0
  389. package/dist/web-owner-css.js +552 -0
  390. package/dist/web-owner-css.js.map +1 -0
  391. package/dist/web-prefs.d.ts +66 -0
  392. package/dist/web-prefs.js +243 -35
  393. package/dist/web-prefs.js.map +1 -1
  394. package/dist/web-preview.d.ts +9 -1
  395. package/dist/web-preview.js +24 -7
  396. package/dist/web-preview.js.map +1 -1
  397. package/dist/web-profile.d.ts +32 -0
  398. package/dist/web-profile.js +132 -0
  399. package/dist/web-profile.js.map +1 -0
  400. package/dist/web-session.d.ts +14 -0
  401. package/dist/web-session.js +10 -0
  402. package/dist/web-session.js.map +1 -1
  403. package/dist/web-settings-patch.d.ts +18 -0
  404. package/dist/web-settings-patch.js +51 -0
  405. package/dist/web-settings-patch.js.map +1 -1
  406. package/dist/web-shell-routes.d.ts +1 -1
  407. package/dist/web-shell-routes.js +1 -1
  408. package/dist/web-shell-routes.js.map +1 -1
  409. package/dist/web-theme-store.d.ts +218 -0
  410. package/dist/web-theme-store.js +516 -0
  411. package/dist/web-theme-store.js.map +1 -0
  412. package/dist/web-theme.d.ts +121 -0
  413. package/dist/web-theme.js +407 -0
  414. package/dist/web-theme.js.map +1 -0
  415. package/dist/web-upload.d.ts +119 -4
  416. package/dist/web-upload.js +252 -39
  417. package/dist/web-upload.js.map +1 -1
  418. package/dist/web-widget.d.ts +72 -0
  419. package/dist/web-widget.js +245 -0
  420. package/dist/web-widget.js.map +1 -0
  421. package/launcher/install-admission.cjs +61 -6
  422. package/package.json +3 -3
@@ -8,8 +8,10 @@
8
8
  // Unrelated updates (a new message, another message's tick) stop nothing (#1306 §6.2, #1408 §4).
9
9
  import "./chat-render.js";
10
10
  import "./preview.js";
11
+ import { effectTrialMessage } from "./effect-trial-offer.js";
11
12
  import { loadHtmlAttachment } from "./html-attachment.js";
12
13
  import { chatTime, chatTimeFull, onDayChange } from "./chat-time.js";
14
+ import { textLinkSegments } from "/assets/app-route.js";
13
15
 
14
16
  const R = () => globalThis.AgendChatRender;
15
17
  /** #1589: the public link shows attachments as downloads only (its page says so: <body data-public-link="1">). */
@@ -31,6 +33,12 @@ const COPY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill=
31
33
  /** Escapes & " ' < >: safe for text and inside a quoted attribute. */
32
34
  export function escAttr(s) { return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
33
35
  export const msgKey = (x) => `${x.boot}-${x.id}`;
36
+ /**
37
+ * Board 00ef2430: a thread opens with only its newest messages drawn — enough for a screen — so switching chats paints
38
+ * at once; the older ones are drawn after that, a chunk at a time between frames, the reader's place kept.
39
+ */
40
+ export const THREAD_TAIL = 24;
41
+ export const THREAD_CHUNK = 40;
34
42
 
35
43
  /**
36
44
  * Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
@@ -47,6 +55,10 @@ export function createThread(list, scroller, opts) {
47
55
  // moves down or stays), and those must not count as the reader leaving the bottom. Reaching the bottom pins again.
48
56
  let pinned = true;
49
57
  let lastTop = 0;
58
+ // The oldest message drawn (by key): everything after it is drawn, so new messages never push the window. Null: none
59
+ // drawn yet — the next render draws the newest THREAD_TAIL and the rest follows (grow).
60
+ let firstKey = null;
61
+ let growTimer = null, growFrame = null, disposed = false;
50
62
  const cardRefresh = new WeakMap(); // card node → re-read availability (weak: a card that left takes its source with it)
51
63
  const tr = opts.t, trf = opts.tf;
52
64
  const stopIn = (node) => (P() ? P().stopIn(node) : []);
@@ -58,7 +70,7 @@ export function createThread(list, scroller, opts) {
58
70
  if (el.textContent !== v) el.textContent = v;
59
71
  }
60
72
  // #1770: "today" ends at the fleet's midnight; the labels are rewritten then (and on every render), while there are any.
61
- let offDay = null;
73
+ let offDay = null, trialTimer = null;
62
74
  function armDay() {
63
75
  if (nodes.size && !offDay) offDay = onDayChange(() => { for (const v of nodes.values()) timeLabel(v.node); });
64
76
  else if (!nodes.size && offDay) { offDay(); offDay = null; }
@@ -70,13 +82,39 @@ export function createThread(list, scroller, opts) {
70
82
  // #1770: the label ("17:38", "Yesterday 17:38", "10/11 17:38") depends on today, so it is not part of the html a node
71
83
  // is kept by: it is written into the node (timeLabels) — crossing midnight relabels in place and reloads no preview.
72
84
  const time = x.ts ? `<span class="time" data-ts="${escAttr(x.ts)}" title="${escAttr(chatTimeFull(x.ts))}"></span>` : `<span class="time"></span>`;
85
+ const trial = effectTrialMessage(x);
86
+ if (trial) return trialHtml(trial, time, x);
73
87
  if (x.role === "status") return statusHtml(x, time);
74
88
  const u = opts.isUser(x);
75
89
  // renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
76
90
  const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
77
- const tools = u ? "" : `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
91
+ const tools = u ? "" : `<div data-trust="thread-control" class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
78
92
  const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
79
- return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div><div class="body"><div class="md">${R().renderMarkdown(x.text, { htmlCards: x.role === "agent", emojis: opts.emojis, mentions: opts.mentions })}</div>${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
93
+ // A person's click on a reply button shows as the button's label, never the line the agent was given (its value).
94
+ const clicked = x.role === "user" && x.click && typeof x.click.label === "string";
95
+ return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div><div class="body">${clicked ? clickHtml(x.click) : `<div class="md">${R().renderMarkdown(x.text, { htmlCards: x.role === "agent", emojis: opts.emojis, mentions: opts.mentions })}</div>`}${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
96
+ }
97
+ /**
98
+ * A button click as a chip: 🔘 and the label (escaped: the label is the agent's text). With `of` (live, while its
99
+ * reply is in this thread) it is a button that shows that reply; restored from disk, plain.
100
+ */
101
+ function clickHtml(click) {
102
+ const inner = `<span class="click-mark" aria-hidden="true">🔘</span><span class="sr-only">${escAttr(tr("chat.clicked"))} </span>${escAttr(click.label)}`;
103
+ const of = typeof click.of === "string" && /^[0-9a-f]{32}$/.test(click.of) ? click.of : null;
104
+ return `<div class="click-chip" data-trust="thread-control">${of
105
+ ? `<button type="button" class="click-link" data-act="showButtons" data-arg="${escAttr(of)}" title="${escAttr(tr("chat.clickShow"))}">${inner}</button>`
106
+ : `<span class="click-text">${inner}</span>`}</div>`;
107
+ }
108
+ function trialNow() { try { const t = opts.serverNow ? opts.serverNow() : Date.now(); return Number.isFinite(t) ? t : Infinity; } catch { return Infinity; } }
109
+ function trialHtml(offer, time, message) {
110
+ const expired = trialNow() >= offer.expiresAt, available = !PUBLIC_LINK() && !expired && opts.canTryEffect?.(message) === true;
111
+ const button = available ? `<button type="button" class="btn btn-sm effect-trial-start" data-act="effectTrial" data-arg="${escAttr(msgKey(message))}">${escAttr(tr("chat.effectTrialStart"))}</button>` : "";
112
+ return `<div class="msg status-line"><div class="meta">${time}</div><div class="body"><div class="effect-trial-offer" data-trust="thread-control"><strong>${escAttr(tr("chat.effectTrial"))}: ${escAttr(offer.title)}</strong><div>${escAttr(tr(expired ? "chat.effectTrialExpired" : "chat.effectTrialLocal"))}</div>${button}</div></div></div>`;
113
+ }
114
+ function armTrials() {
115
+ if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
116
+ const now = trialNow(), next = msgs.map(x => effectTrialMessage(x)).filter(x => x && x.expiresAt > now).reduce((n, x) => Math.min(n, x.expiresAt), Infinity);
117
+ if (Number.isFinite(next)) trialTimer = setTimeout(() => { trialTimer = null; render(msgs); }, Math.max(1, Math.min(2147483647, next - now)));
80
118
  }
81
119
  /**
82
120
  * #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
@@ -88,10 +126,12 @@ export function createThread(list, scroller, opts) {
88
126
  const lines = R().lineCount(text);
89
127
  const manyLines = lines > STATUS_FOLD_LINES;
90
128
  const more = manyLines || text.length > STATUS_FOLD_CHARS
91
- ? `<button type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
129
+ ? `<button data-trust="thread-control" type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
92
130
  : "";
93
- const tools = `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
94
- return `<div class="msg status-line${more ? " folded" : ""}"><div class="meta">${time}</div><div class="body"><div class="st-text">${escAttr(text)}</div>${more}</div>${tools}</div>`;
131
+ const tools = `<div data-trust="thread-control" class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
132
+ // #1990: the other instance's name opens its chat (an in-app link the router takes: no target).
133
+ const body = textLinkSegments(text, x.links).map(sg => sg.href ? `<a data-trust="link" class="st-link" href="${escAttr(sg.href)}">${escAttr(sg.text)}</a>` : escAttr(sg.text)).join("");
134
+ return `<div class="msg status-line${more ? " folded" : ""}"><div class="meta">${time}</div><div class="body"><div class="st-text">${body}</div>${more}</div>${tools}</div>`;
95
135
  }
96
136
  /**
97
137
  * #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
@@ -99,6 +139,7 @@ export function createThread(list, scroller, opts) {
99
139
  */
100
140
  function replyButtonsHtml(b) {
101
141
  if (!b || typeof b.id !== "string" || !Array.isArray(b.labels) || !b.labels.length) return "";
142
+ if (b.poll && typeof b.poll === "object") return pollHtml(b);
102
143
  const busy = !!(opts.replyButtonBusy && opts.replyButtonBusy(b.id));
103
144
  const off = b.state !== "open" || busy;
104
145
  const items = b.labels.map((label, i) => {
@@ -106,14 +147,53 @@ export function createThread(list, scroller, opts) {
106
147
  return `<button type="button" class="btn btn-sm rb-btn${chosen ? " chosen" : ""}" data-act="replyButton" data-arg="${escAttr(`${b.id}:${i}`)}"${off ? " disabled" : ""}>${escAttr(chosen ? `✓ ${label}` : label)}</button>`;
107
148
  }).join("");
108
149
  const note = b.state === "chosen" ? trf("chat.rbChosen", b.by || "") : b.state === "expired" ? tr("chat.rbExpired") : "";
109
- return `<div class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
150
+ return `<div data-trust="reply" class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
151
+ }
152
+ /**
153
+ * #1823: a poll. Anyone here may click while it is open; the page marks its own vote (opts.pollMine), which no one else
154
+ * sees. Counts only when the server sends them (revealed: live, or closed); names only at close with show_voters. The
155
+ * time left is not in the html (a node is kept by its html): pollLeft writes it, every second.
156
+ */
157
+ function pollHtml(b) {
158
+ const p = b.poll, closed = b.state === "closed";
159
+ const busy = !!(opts.replyButtonBusy && opts.replyButtonBusy(b.id));
160
+ const mine = (opts.pollMine && opts.pollMine(b.id)) || [];
161
+ const counts = Array.isArray(p.counts) && p.counts.length === b.labels.length ? p.counts : null;
162
+ const items = b.labels.map((label, i) => {
163
+ const me = !closed && mine.includes(i);
164
+ const tally = counts ? ` · ${counts[i]}${closed && p.answered ? ` (${Math.round((counts[i] / p.answered) * 100)}%)` : ""}` : "";
165
+ return `<button type="button" class="btn btn-sm rb-btn${me ? " chosen" : ""}" data-act="replyButton" data-arg="${escAttr(`${b.id}:${i}`)}" aria-pressed="${me ? "true" : "false"}"${closed || busy ? " disabled" : ""}>${escAttr(`${me ? "✓ " : ""}${label}${tally}`)}</button>`;
166
+ }).join("");
167
+ const status = closed
168
+ ? escAttr(trf("chat.pollClosed", p.answered))
169
+ : `${escAttr(trf("chat.pollAnswered", p.answered))} · ⏳ <span class="poll-left" data-deadline="${escAttr(p.deadline)}" title="${escAttr(tr("chat.pollLeft"))}"></span>`;
170
+ const voters = closed && Array.isArray(p.voters)
171
+ ? `<ul data-trust="reply" class="poll-voters" aria-label="${escAttr(tr("chat.pollVoters"))}">${b.labels.map((label, i) => `<li>${escAttr(`${label}: ${(p.voters[i] || []).join(", ") || "—"}`)}</li>`).join("")}</ul>`
172
+ : "";
173
+ return `<div data-trust="reply" class="rb-row poll" role="group" aria-label="${escAttr(tr("chat.pollGroup"))}">${items}<span class="rb-note">${status}</span></div>${voters}`;
174
+ }
175
+ /** #1823: the time left on this node's open polls, now. */
176
+ function pollLeft(node) {
177
+ for (const el of node.querySelectorAll(".poll-left[data-deadline]")) {
178
+ const v = R().pollRemaining(Number(el.dataset.deadline) - (opts.serverNow ? opts.serverNow() : Date.now()));
179
+ if (el.textContent !== v) el.textContent = v;
180
+ }
181
+ }
182
+ // #1823: while an open poll is on screen, its time left is rewritten every second.
183
+ let pollTick = null;
184
+ function armPolls() {
185
+ const open = nodes.size > 0 && !!list.querySelector(".poll-left[data-deadline]");
186
+ if (open && !pollTick) pollTick = setInterval(() => pollLeft(list), 1000);
187
+ else if (!open && pollTick) { clearInterval(pollTick); pollTick = null; }
110
188
  }
189
+
111
190
  function msgNode(html, x, stoppedKeys) {
112
191
  const tpl = list.ownerDocument.createElement("template");
113
192
  tpl.innerHTML = html;
114
193
  const node = tpl.content.firstElementChild;
115
194
  decorateCode(node);
116
195
  timeLabel(node);
196
+ pollLeft(node);
117
197
  if (x) decorateHtmlCards(node, x, stoppedKeys || []);
118
198
  if (x) decorateTextCards(node, x);
119
199
  return node;
@@ -124,7 +204,7 @@ export function createThread(list, scroller, opts) {
124
204
  const doc = list.ownerDocument;
125
205
  for (const pre of root.querySelectorAll(".md pre")) {
126
206
  const box = doc.createElement("div"); box.className = "codeblock";
127
- const head = doc.createElement("div"); head.className = "cb-head";
207
+ const head = doc.createElement("div"); head.className = "cb-head"; head.setAttribute("data-trust", "thread-control");
128
208
  const lang = doc.createElement("span"); lang.className = "cb-lang"; lang.textContent = pre.dataset.lang || "text";
129
209
  const wrap = doc.createElement("button"); wrap.type = "button"; wrap.className = "chip-btn"; wrap.dataset.act = "toggleWrap"; wrap.textContent = tr("chat.wrap");
130
210
  const copy = doc.createElement("button"); copy.type = "button"; copy.className = "chip-btn"; copy.dataset.act = "copyCode"; copy.textContent = tr("chat.copy");
@@ -133,7 +213,7 @@ export function createThread(list, scroller, opts) {
133
213
  const lines = R().lineCount(pre.textContent);
134
214
  if (lines > R().CODE_FOLD_LINES) {
135
215
  box.classList.add("folded");
136
- const more = doc.createElement("button"); more.type = "button"; more.className = "cb-more"; more.dataset.act = "toggleFold";
216
+ const more = doc.createElement("button"); more.type = "button"; more.className = "cb-more"; more.setAttribute("data-trust", "thread-control"); more.dataset.act = "toggleFold";
137
217
  more.dataset.lines = String(lines); more.textContent = trf("chat.showAll", lines);
138
218
  box.append(more);
139
219
  }
@@ -151,7 +231,7 @@ export function createThread(list, scroller, opts) {
151
231
  if (!fence) continue;
152
232
  ph.textContent = "";
153
233
  if (ph.classList.contains("truncated") || !fence.terminated) {
154
- const note = doc.createElement("div"); note.className = "pv-note"; note.textContent = tr("chat.pvTruncated"); ph.append(note);
234
+ const note = doc.createElement("div"); note.className = "pv-note"; note.setAttribute("data-trust", "thread-control"); note.textContent = tr("chat.pvTruncated"); ph.append(note);
155
235
  continue;
156
236
  }
157
237
  buildCard(ph, x, `${msgKey(x)}:f${n}`, { n, code: fence.code }, stoppedKeys);
@@ -181,12 +261,12 @@ export function createThread(list, scroller, opts) {
181
261
  const doc = list.ownerDocument;
182
262
  const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
183
263
  ph.textContent = "";
184
- const head = el("div", "tc-head");
264
+ const head = el("div", "tc-head"); head.setAttribute("data-trust", "thread-control");
185
265
  const label = el("span", "tc-label", att.name);
186
266
  const show = el("button", "btn btn-sm tc-show", tr("chat.tcShow")); show.type = "button";
187
267
  const dl = el("a", "btn btn-sm tc-dl", tr("chat.tcDownload")); dl.href = `/ui/file/${att.id}`; dl.setAttribute("download", att.name);
188
268
  head.append(label, show, dl);
189
- const note = el("div", "pv-note");
269
+ const note = el("div", "pv-note"); note.setAttribute("data-trust", "thread-control");
190
270
  const body = el("div", "tc-body"); body.hidden = true;
191
271
  ph.append(head, note, body);
192
272
  let text = null, reading = false, all = false;
@@ -199,7 +279,8 @@ export function createThread(list, scroller, opts) {
199
279
  // Formatting is built only within TEXT_PREVIEW_NODES: the markup is measured as a string first, never as DOM.
200
280
  const plain = () => {
201
281
  const pre = el("pre", "tc-pre"), code = el("code", null, src);
202
- pre.append(code); body.append(el("div", "pv-note", tr("chat.tcPlain")), pre);
282
+ const note = el("div", "pv-note", tr("chat.tcPlain")); note.setAttribute("data-trust", "thread-control");
283
+ pre.append(code); body.append(note, pre);
203
284
  };
204
285
  if (type === "md") {
205
286
  const html = R().renderMarkdown(src);
@@ -219,7 +300,7 @@ export function createThread(list, scroller, opts) {
219
300
  }
220
301
  }
221
302
  if (cut) {
222
- const more = el("button", "btn btn-sm tc-more", trf("chat.tcShowAll", lines.length)); more.type = "button";
303
+ const more = el("button", "btn btn-sm tc-more", trf("chat.tcShowAll", lines.length)); more.setAttribute("data-trust", "thread-control"); more.type = "button";
223
304
  more.onclick = () => { all = true; draw(); };
224
305
  body.append(more);
225
306
  }
@@ -280,7 +361,7 @@ export function createThread(list, scroller, opts) {
280
361
  const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
281
362
  const att = source.att || null;
282
363
  let code = att ? null : source.code;
283
- const head = el("div", "pv-head");
364
+ const head = el("div", "pv-head"); head.setAttribute("data-trust", "thread-control");
284
365
  const label = el("span", "pv-label", att ? `${tr("chat.pvHtml")} · ${att.name}` : tr("chat.pvHtml"));
285
366
  const run = el("button", "btn btn-sm pv-run", tr("chat.pvPreview")); run.type = "button";
286
367
  const stopB = el("button", "btn btn-sm pv-stop", tr("chat.pvStop")); stopB.type = "button"; stopB.hidden = true;
@@ -290,15 +371,15 @@ export function createThread(list, scroller, opts) {
290
371
  const panel = opts.panel || null;
291
372
  const toPanel = panel ? el("button", "btn btn-sm btn-ghost pv-open", tr("chat.pvOpenPanel")) : null;
292
373
  if (toPanel) toPanel.type = "button";
293
- const menu = el("details", "pv-menu");
374
+ const menu = el("details", "pv-menu"); menu.setAttribute("data-trust", "preview-menu");
294
375
  const sum = el("summary", null, "⋯"); sum.title = tr("chat.pvMenu"); sum.setAttribute("aria-label", tr("chat.pvMenu"));
295
376
  const optB = el("button", "chip-btn pv-opt"); optB.type = "button";
296
377
  const neverB = el("button", "chip-btn pv-never"); neverB.type = "button";
297
378
  menu.append(sum, optB, neverB);
298
379
  head.append(label, run, stopB, ...(toPanel ? [toPanel] : []), ...(dl ? [dl] : []), menu);
299
- const note = el("div", "pv-note");
380
+ const note = el("div", "pv-note"); note.setAttribute("data-trust", "thread-control");
300
381
  // #1568: the banner, with its "I understand" (hidden until the next sign-in; the frame's border stays).
301
- const banner = el("div", "pv-banner"); banner.hidden = true;
382
+ const banner = el("div", "pv-banner"); banner.setAttribute("data-trust", "thread-control"); banner.hidden = true;
302
383
  const ack = el("button", "btn btn-sm btn-ghost pv-ack", P().bannerAckLabel()); ack.type = "button"; ack.title = P().bannerAckTitle();
303
384
  banner.append(el("span", "pv-banner-text", P().banner()), ack);
304
385
  const holder = el("div", "pv-holder");
@@ -388,44 +469,97 @@ export function createThread(list, scroller, opts) {
388
469
  const ro = typeof ResizeObserver === "function" ? new ResizeObserver(resized) : null;
389
470
  if (ro) { ro.observe(list); ro.observe(scroller); }
390
471
 
472
+ /** Where drawing starts in `msgs`: at the oldest drawn message, or (none yet) the newest THREAD_TAIL. */
473
+ function startIndex() {
474
+ const i = firstKey === null ? -1 : msgs.findIndex((x) => msgKey(x) === firstKey);
475
+ // Whatever is drawn stays drawn: the list may have reordered (merged across fleet restarts) or lost its oldest to
476
+ // the cap — so the window starts at the earlier of the oldest drawn key and the first message still drawn.
477
+ const still = nodes.size ? msgs.findIndex((x) => nodes.has(msgKey(x))) : -1;
478
+ const at = [i, still].filter((n) => n >= 0);
479
+ return at.length ? Math.min(...at) : Math.max(0, msgs.length - THREAD_TAIL);
480
+ }
481
+ /** Draw the next chunk of older messages, keeping the reader where they were; then the next, until all are drawn. */
482
+ function grow() {
483
+ growTimer = growFrame = null;
484
+ if (disposed) return; // the chat left: nothing more is drawn
485
+ const at = startIndex();
486
+ if (at <= 0) return;
487
+ firstKey = msgKey(msgs[Math.max(0, at - THREAD_CHUNK)]);
488
+ const fromBottom = scroller.scrollHeight - scroller.scrollTop;
489
+ draw({ older: true }); // drawn above the reader: never "new below" (render alone counts those)
490
+ if (pinned) toBottom();
491
+ else { scroller.scrollTop = scroller.scrollHeight - fromBottom; lastTop = scroller.scrollTop; }
492
+ scheduleGrow();
493
+ }
494
+ /** After the next frame has painted (the click's own frame stays light), draw a chunk more. */
495
+ function scheduleGrow() {
496
+ if (growTimer !== null || growFrame !== null || startIndex() <= 0) return;
497
+ const later = () => { growFrame = null; growTimer = setTimeout(grow, 0); };
498
+ if (typeof requestAnimationFrame === "function") growFrame = requestAnimationFrame(later); else later();
499
+ }
500
+ /** Everything drawn now (a remembered scroll position, a message to show that is not drawn yet). */
501
+ function growAll() {
502
+ if (startIndex() <= 0) return;
503
+ if (growFrame !== null && typeof cancelAnimationFrame === "function") cancelAnimationFrame(growFrame);
504
+ if (growTimer !== null) clearTimeout(growTimer);
505
+ growTimer = growFrame = null;
506
+ firstKey = msgs.length ? msgKey(msgs[0]) : null;
507
+ draw({ older: true });
508
+ }
509
+
391
510
  /** Draw `next` (this instance's messages). `restore`: the scroll position to return to (null = the bottom). */
392
511
  function render(next, renderOpts = {}) {
393
512
  const stick = pinned || nearBottom();
394
513
  msgs = next || [];
514
+ armTrials();
395
515
  if (!msgs.length) {
396
516
  for (const v of nodes.values()) stopIn(v.node);
397
517
  nodes.clear();
518
+ firstKey = null;
398
519
  armDay();
399
520
  list.textContent = "";
400
521
  opts.onEmpty(true);
401
522
  return;
402
523
  }
403
524
  opts.onEmpty(false);
404
- const keep = new Set(msgs.map(msgKey));
525
+ // A position to return to that is not the bottom needs the messages above it: all of them, at once (as before).
526
+ if ("restore" in renderOpts && renderOpts.restore != null) firstKey = msgKey(msgs[0]);
527
+ const added = draw({});
528
+ if (firstKey === null || startIndex() > 0) { firstKey = msgKey(msgs[startIndex()]); scheduleGrow(); }
529
+ if ("restore" in renderOpts) {
530
+ unseen = 0;
531
+ if (renderOpts.restore == null) toBottom();
532
+ else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); lastTop = scroller.scrollTop; }
533
+ } else if (stick) toBottom();
534
+ else unseen += added;
535
+ reportJump();
536
+ }
537
+ /** Put the messages from startIndex() on in the list (nodes reused when unchanged). Returns how many are new. */
538
+ function draw(drawOpts) {
539
+ const shown = msgs.slice(startIndex());
540
+ const keep = new Set(shown.map(msgKey));
405
541
  for (const [k, v] of nodes) if (!keep.has(k)) { stopIn(v.node); v.node.remove(); nodes.delete(k); }
406
542
  let prev = null, added = 0;
407
- for (const x of msgs) {
408
- const key = msgKey(x), html = msgHtml(x);
543
+ for (const x of shown) {
544
+ const key = msgKey(x);
409
545
  let have = nodes.get(key);
546
+ // Filling in older messages: a node drawn from this very message object is current — not drawn again.
547
+ if (drawOpts.older && have && have.x === x) { prev = have.node; continue; }
548
+ const html = msgHtml(x);
410
549
  if (!have || have.html !== html) {
411
550
  const stopped = have ? stopIn(have.node) : [];
412
551
  const node = msgNode(html, x, stopped);
413
552
  if (have) have.node.replaceWith(node); else added++;
414
- have = { html, node }; nodes.set(key, have);
415
- } else timeLabel(have.node);
553
+ have = { html, node, x, trialButton: node.querySelector(".effect-trial-offer > button.effect-trial-start") }; nodes.set(key, have);
554
+ } else { have.x = x; timeLabel(have.node); }
416
555
  const want = prev ? prev.nextSibling : list.firstChild;
417
556
  // A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
418
557
  if (have.node !== want) { if (have.node.parentNode === list) stopIn(have.node); list.insertBefore(have.node, want); }
419
558
  prev = have.node;
420
559
  }
421
560
  armDay();
422
- if ("restore" in renderOpts) {
423
- unseen = 0;
424
- if (renderOpts.restore == null) toBottom();
425
- else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); lastTop = scroller.scrollTop; }
426
- } else if (stick) toBottom();
427
- else unseen += added;
428
- reportJump();
561
+ armPolls();
562
+ return added;
429
563
  }
430
564
 
431
565
  function onScroll() {
@@ -440,6 +574,7 @@ export function createThread(list, scroller, opts) {
440
574
  function jumpLatest() { toBottom(); unseen = 0; reportJump(); }
441
575
  /** Show one message (the panel's "from …" link, #1481): scrolled into view and briefly marked. */
442
576
  function reveal(key) {
577
+ if (!nodes.has(key) && msgs.some((x) => msgKey(x) === key)) growAll(); // an older message not drawn yet
443
578
  const have = nodes.get(key); if (!have) return false;
444
579
  if (typeof have.node.scrollIntoView === "function") have.node.scrollIntoView({ block: "center" });
445
580
  have.node.classList.add("flash");
@@ -489,15 +624,24 @@ export function createThread(list, scroller, opts) {
489
624
  else if (act === "replyButton") {
490
625
  const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
491
626
  if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
492
- }
627
+ } else if (act === "effectTrial") {
628
+ // Only the current host-produced keyed card and a real local click can request admission. No token is read from DOM.
629
+ const key = el.dataset.arg, row = msgs.find(x => msgKey(x) === key), owner = nodes.get(key), node = owner?.node, offer = effectTrialMessage(row);
630
+ if (PUBLIC_LINK() || e.defaultPrevented || e.isTrusted !== true || owner?.trialButton !== el || el.className !== "btn btn-sm effect-trial-start" || !node?.contains(el) || el.closest(".effect-trial-offer")?.getAttribute("data-trust") !== "thread-control"
631
+ || !offer || trialNow() >= offer.expiresAt || opts.canTryEffect?.(row) !== true) return;
632
+ opts.startEffectTrial?.(row);
633
+ } else if (act === "showButtons") { const x = msgs.find(y => y.buttons && y.buttons.id === el.dataset.arg); if (x) reveal(msgKey(x)); }
493
634
  }
494
635
 
495
636
  /** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
496
637
  function dispose() {
638
+ disposed = true;
639
+ if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
497
640
  if (ro) ro.disconnect();
498
641
  if (P()) P().stopAll("leave");
499
642
  nodes.clear();
500
643
  armDay();
644
+ armPolls();
501
645
  }
502
646
 
503
647
  return { render, onScroll, onClick, jumpLatest, reveal, refreshCards, dispose, nodes, get unseen() { return unseen; } };
@@ -1,7 +1,8 @@
1
- // #1366: the first sign-in tour, in the app shell (#1408). Shown once on this device (agend_tour_done), dismissed by
2
- // Got it, Skip or Esc, and replayed from Tour in the sidebar. A card that outlines one thing at a time: no overlay,
3
- // nothing blocked, and no style attribute — the outline is a class (#1300). When this browser's storage cannot be read
4
- // it is not shown at all: a tour that comes back on every load is worse than none.
1
+ // #1366: the chat tour, in the app shell (#1408). Since #1854 (rank 6) it never starts by itself: a first visit opens
2
+ // on a quiet page, and Settings → This device → Chat tour starts it on an agent's chat. It walks only the steps whose
3
+ // target is on the page. Got it, Skip or Esc ends it and records it (agend_tour_done, kept as before: no new key). A
4
+ // card that outlines one thing at a time: no overlay, nothing blocked, and no style attribute — the outline is a class
5
+ // (#1300).
5
6
  //
6
7
  // Esc order: the phone drawer covers the card, so it closes first (the shell); then the tour; only then may Esc reach
7
8
  // the chat and stop a reply. The Esc that closed the tour is spent: its key repeats (a held key) must not go on to stop
@@ -24,6 +25,8 @@ const STEPS = [
24
25
  : document.querySelector(".badge-await") || byId("instanceList")) },
25
26
  ];
26
27
  let at = -1, spot = null, returnTo = null, escHeld = false, installed = false;
28
+ /** The steps of the running tour: those whose target was on the page when it started (#1854: target-aware). */
29
+ let steps = STEPS;
27
30
 
28
31
  export function tourDone() { try { return localStorage.getItem(TOUR_KEY) === "1"; } catch { return true; } }
29
32
  export function tourStep() { return at; }
@@ -32,13 +35,15 @@ export function startTour() {
32
35
  // Replayed from the drawer: the drawer closes first (focus goes back to ☰), so the card is seen.
33
36
  closeDrawer();
34
37
  if (at < 0) returnTo = document.activeElement;
38
+ steps = STEPS.filter((s) => s.spot());
39
+ if (!steps.length) return;
35
40
  show(0);
36
41
  }
37
42
 
38
43
  /** The current step's outline follows a redraw (a new badge, Send swapped for Stop reply) — same card, same focus. */
39
44
  export function refreshTourSpot() {
40
45
  if (at < 0) return;
41
- const next = STEPS[at].spot() || null;
46
+ const next = steps[at].spot() || null;
42
47
  if (next === spot && (!next || next.classList.contains("tour-spot"))) return;
43
48
  if (spot) spot.classList.remove("tour-spot");
44
49
  spot = next;
@@ -46,32 +51,32 @@ export function refreshTourSpot() {
46
51
  }
47
52
 
48
53
  function show(i) {
49
- if (i < 0 || i >= STEPS.length) return;
54
+ if (i < 0 || i >= steps.length) return;
50
55
  at = i;
51
56
  if (spot) spot.classList.remove("tour-spot");
52
- spot = STEPS[i].spot() || null;
57
+ spot = steps[i].spot() || null;
53
58
  if (spot) spot.classList.add("tour-spot");
54
59
  let card = byId("tour");
55
60
  if (!card) {
56
- card = document.createElement("section"); card.id = "tour"; card.className = "tour";
61
+ card = document.createElement("section"); card.id = "tour"; card.className = "tour"; card.setAttribute("data-trust", "tour");
57
62
  card.setAttribute("role", "dialog"); card.setAttribute("aria-labelledby", "tourTitle");
58
63
  card.addEventListener("click", onCardClick);
59
64
  document.body.appendChild(card);
60
65
  }
61
66
  card.textContent = "";
62
67
  const title = document.createElement("h2"); title.id = "tourTitle"; title.textContent = t("chat.tourTitle");
63
- const text = document.createElement("p"); text.className = "tour-text"; text.setAttribute("aria-live", "polite"); text.textContent = t(STEPS[i].text);
68
+ const text = document.createElement("p"); text.className = "tour-text"; text.setAttribute("aria-live", "polite"); text.textContent = t(steps[i].text);
64
69
  const foot = document.createElement("div"); foot.className = "tour-foot";
65
- const count = document.createElement("span"); count.className = "tour-count"; count.textContent = t("chat.tourStep", i + 1, STEPS.length);
70
+ const count = document.createElement("span"); count.className = "tour-count"; count.textContent = t("chat.tourStep", i + 1, steps.length);
66
71
  const button = (label, act, arg, cls) => {
67
72
  const b = document.createElement("button"); b.type = "button"; b.className = `btn btn-sm ${cls}`; b.dataset.tour = act;
68
73
  if (arg != null) b.dataset.arg = String(arg);
69
74
  b.textContent = t(label); return b;
70
75
  };
71
76
  foot.append(count);
72
- if (i < STEPS.length - 1) foot.append(button("chat.tourSkip", "end", null, "btn-ghost"));
77
+ if (i < steps.length - 1) foot.append(button("chat.tourSkip", "end", null, "btn-ghost"));
73
78
  if (i > 0) foot.append(button("chat.tourBack", "move", -1, "btn-ghost"));
74
- const next = i < STEPS.length - 1 ? button("chat.tourNext", "move", 1, "btn-primary") : button("chat.tourDone", "end", null, "btn-primary");
79
+ const next = i < steps.length - 1 ? button("chat.tourNext", "move", 1, "btn-primary") : button("chat.tourDone", "end", null, "btn-primary");
75
80
  foot.append(next);
76
81
  card.append(title, text, foot);
77
82
  next.focus();
@@ -105,12 +110,10 @@ export function onTourKey(e) {
105
110
  if (at >= 0) { e.preventDefault(); e.stopPropagation(); endTour(); escHeld = true; }
106
111
  }
107
112
 
108
- /** Once per page: the key handler, the language follow-up, and the first showing on a device that has not seen it. */
113
+ /** Once per page: the key handler and the language follow-up. Nothing is shown: the tour is started by a person (#1854). */
109
114
  export function installTour() {
110
115
  if (installed) return;
111
116
  installed = true;
112
117
  document.addEventListener("keydown", onTourKey, true);
113
118
  onLang(() => { if (at >= 0) show(at); });
114
- // Shown first on the chat (it points at the chat's own controls), never over View or another panel.
115
- if (!tourDone() && /^\/ui(\/chat\/|$)/.test(location.pathname)) setTimeout(startTour, 0);
116
119
  }
@@ -0,0 +1,119 @@
1
+ /** #1629: explicit-click control lane. Responses never grant authority; Start reauthorizes on the server. */
2
+ import { effectTrialMessage } from "./effect-trial-offer.js";
3
+ import { checkEffectTrialStart } from "/assets/effect-runtime-loader.js";
4
+ import { chatEffectIdentity } from "/assets/effect-chat-runtime.js";
5
+ const owners = new WeakSet(), safe = fn => { try { return fn(); } catch { return null; } };
6
+ const LIMIT = 128 * 1024;
7
+ export function createEffectTrialController({ window: host, current, allowed, onStart, onChange = () => {} }) {
8
+ let dead = false, active = null, physical = null, previous = null, broken = false, clockBusy = false;
9
+ const cleanup = [], releases = [], identity = safe(() => chatEffectIdentity(host));
10
+ const tell = () => { if (!dead) safe(onChange); };
11
+ function clock() {
12
+ if (broken || clockBusy) { broken = true; return null; }
13
+ clockBusy = true; const t = safe(() => host.performance.now()); clockBusy = false;
14
+ if (broken || typeof t !== "number" || !Number.isFinite(t) || t < 0 || t > Number.MAX_SAFE_INTEGER || previous !== null && t < previous) { broken = true; return null; }
15
+ return previous = t;
16
+ }
17
+ function eligible(op) {
18
+ if (dead || broken || !op || active !== op || op.dead || op.checking || host.AgendAuth?.sessionEnded?.() !== false) return false;
19
+ op.checking = true;
20
+ try {
21
+ const t = clock();
22
+ if (t === null || t < op.began || t >= op.until || safe(() => current(op.instance) === true && allowed() === true) !== true) return false;
23
+ const after = clock();
24
+ return !dead && !op.dead && active === op && after !== null && after >= op.began && after < op.until;
25
+ } finally { op.checking = false; }
26
+ }
27
+ function end(op, reason = "stopped") {
28
+ if (!op || op.dead) return false;
29
+ op.dead = true; if (active === op) active = null; safe(() => op.resolve?.(false));
30
+ safe(() => host.clearTimeout(op.timer)); safe(() => op.controller.abort());
31
+ if (op.reply && !op.stopQueued) { op.stopQueued = true; cleanup.push(op); }
32
+ // Physical paint is removed before callbacks, cleanup I/O or another explicit click.
33
+ safe(() => op.dispose?.()); op.dispose = null; tell(); drain(); return true;
34
+ }
35
+ function arm(op) {
36
+ safe(() => host.clearTimeout(op.timer));
37
+ if (!eligible(op)) { end(op, "trial_ended"); return; }
38
+ const t = clock(); if (t === null || t >= op.until) { end(op, "trial_expired"); return; }
39
+ op.timer = host.setTimeout(() => { if (!op.dead) arm(op); }, Math.max(1, Math.ceil(op.until - t)));
40
+ }
41
+ async function read(response) {
42
+ let reader = null;
43
+ try {
44
+ if (!response || response.redirected || response.status !== 200 || !/^application\/json(?:\s*;|$)/i.test(response.headers.get("content-type") || "")) return null;
45
+ const length = response.headers.get("content-length"); if (length !== null && (!/^\d+$/.test(length) || Number(length) > LIMIT)) return null;
46
+ reader = response.body?.getReader(); if (!reader) return null;
47
+ const chunks = []; let size = 0;
48
+ for (;;) { const r = await reader.read(); if (r.done) break; if (!(r.value instanceof Uint8Array) || (size += r.value.byteLength) > LIMIT) return null; chunks.push(r.value); }
49
+ const bytes = new Uint8Array(size); let offset = 0; for (const c of chunks) { bytes.set(c, offset); offset += c.byteLength; }
50
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
51
+ } catch { return null; }
52
+ finally { try { if (reader) await reader.cancel(); else await response?.body?.cancel(); } catch { /* retained until cancellation settles */ } }
53
+ }
54
+ function drain() {
55
+ if (physical || !cleanup.length) return;
56
+ const op = cleanup.shift(), token = {}; physical = token; const controller = new host.AbortController();
57
+ const timer = host.setTimeout(() => controller.abort(), 10000);
58
+ // Stop remains permitted after CSS/gate/session changes. It cannot mount or mutate appearance.
59
+ Promise.resolve().then(async () => {
60
+ const response = await host.fetch(`/ui/effect-trials/${op.reply.trial}/stop?ownerEpoch=${identity.epoch}`, {
61
+ method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" }, body: "{}", signal: controller.signal,
62
+ });
63
+ try { await response.body?.cancel(); } catch { /* no response fields are consumed */ }
64
+ }).catch(() => {}).finally(() => { safe(() => host.clearTimeout(timer)); if (physical === token) physical = null; tell(); drain(); });
65
+ }
66
+ function dispose() {
67
+ if (dead) return; dead = true; end(active); for (const off of releases.splice(0)) safe(off); tell();
68
+ }
69
+ const api = { ok: false, start: async () => false, stop: () => end(active), sync() { const op = active; if (op && !eligible(op)) end(op, "trial_ended"); },
70
+ snapshot: () => Object.freeze({ busy: !!physical || cleanup.length > 0, phase: active ? active.reply ? "running" : "starting" : "idle" }), dispose };
71
+ if (!identity || owners.has(host) || typeof current !== "function" || typeof allowed !== "function" || typeof onStart !== "function"
72
+ || typeof host.AgendAuth?.trialSession !== "function" || typeof host.AgendAuth?.onSessionEnd !== "function") return Object.freeze(api);
73
+ owners.add(host);
74
+ try {
75
+ const off = host.AgendAuth.onSessionEnd(dispose);
76
+ if (dead) { safe(off); return Object.freeze(api); }
77
+ releases.push(off);
78
+ for (const [target, type, fn] of [[host, "blur", () => end(active)], [host, "pagehide", dispose], [host.document, "visibilitychange", () => { if (host.document.visibilityState !== "visible") end(active); }]]) {
79
+ target.addEventListener(type, fn); releases.push(() => target.removeEventListener(type, fn));
80
+ }
81
+ } catch { dispose(); return Object.freeze(api); }
82
+ if (dead) return Object.freeze(api);
83
+ api.ok = true;
84
+ api.start = async message => {
85
+ if (dead || active || physical || cleanup.length) return false;
86
+ const offer = safe(() => effectTrialMessage(message)), instance = safe(() => message.instance), began = clock();
87
+ if (!offer || began === null || began + 10000 > Number.MAX_SAFE_INTEGER || safe(() => host.Date.now() < offer.expiresAt) !== true) return false;
88
+ const op = { instance, offer, began, until: began + 10000, controller: new host.AbortController(), timer: null, reply: null, dead: false, stopQueued: false, dispose: null };
89
+ active = op; const token = {}; physical = token;
90
+ const cancelled = new Promise(resolve => { op.resolve = resolve; });
91
+ const work = Promise.resolve().then(async () => {
92
+ if (!eligible(op)) return false;
93
+ arm(op); const session = await host.AgendAuth.trialSession();
94
+ if (!eligible(op) || !session || session.localAdmin !== true || session.epoch !== identity.epoch) return false;
95
+ const response = await host.fetch(`/ui/effect-trials/start?ownerEpoch=${identity.epoch}`, {
96
+ method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" },
97
+ body: JSON.stringify({ offer: offer.offer, instance }), signal: op.controller.signal,
98
+ });
99
+ const reply = checkEffectTrialStart(await read(response), offer.offer, offer.id);
100
+ if (!reply) return false;
101
+ op.reply = reply;
102
+ if (op.dead && !op.stopQueued) { op.stopQueued = true; cleanup.push(op); }
103
+ if (reply.offer.title !== offer.title || reply.offer.expiresAt !== offer.expiresAt) return false;
104
+ if (!eligible(op)) { if (!op.stopQueued) { op.stopQueued = true; cleanup.push(op); } return false; }
105
+ op.until = began + reply.remainingMs;
106
+ if (op.until > Number.MAX_SAFE_INTEGER || !eligible(op)) return false;
107
+ arm(op);
108
+ const handle = Object.freeze({ reply, startedAt: began, instance, current: () => eligible(op), stop: () => end(op), });
109
+ const disposer = onStart(handle);
110
+ if (typeof disposer !== "function") return false;
111
+ if (!eligible(op)) { safe(disposer); return false; }
112
+ op.dispose = disposer; tell(); return eligible(op);
113
+ }).catch(() => false).then(ok => { if (!ok) end(op); return ok; }).finally(() => { if (physical === token) physical = null; tell(); drain(); });
114
+ // The logical caller may stop immediately, while the physical request/cancel still owns the lane.
115
+ tell();
116
+ return Promise.race([work, cancelled]);
117
+ };
118
+ return Object.freeze(api);
119
+ }