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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +16 -9
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +24 -9
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +47 -0
  9. package/dist/agent-whats-new.js +147 -0
  10. package/dist/agent-whats-new.js.map +1 -0
  11. package/dist/auth-api.d.ts +5 -3
  12. package/dist/auth-api.js +19 -3
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/antigravity.d.ts +8 -0
  15. package/dist/backend/antigravity.js +32 -9
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/claude-carry.d.ts +22 -0
  18. package/dist/backend/claude-carry.js +170 -0
  19. package/dist/backend/claude-carry.js.map +1 -0
  20. package/dist/backend/claude-code.d.ts +65 -2
  21. package/dist/backend/claude-code.js +226 -33
  22. package/dist/backend/claude-code.js.map +1 -1
  23. package/dist/backend/codex-metadata.d.ts +33 -0
  24. package/dist/backend/codex-metadata.js +122 -18
  25. package/dist/backend/codex-metadata.js.map +1 -1
  26. package/dist/backend/codex.d.ts +3 -0
  27. package/dist/backend/codex.js +17 -1
  28. package/dist/backend/codex.js.map +1 -1
  29. package/dist/backend/credential-profile.d.ts +40 -3
  30. package/dist/backend/credential-profile.js +113 -5
  31. package/dist/backend/credential-profile.js.map +1 -1
  32. package/dist/backend/effort-metadata.d.ts +37 -1
  33. package/dist/backend/effort-metadata.js +86 -15
  34. package/dist/backend/effort-metadata.js.map +1 -1
  35. package/dist/backend/grok.d.ts +4 -0
  36. package/dist/backend/grok.js +14 -0
  37. package/dist/backend/grok.js.map +1 -1
  38. package/dist/backend/kiro-engine-ledger.d.ts +5 -0
  39. package/dist/backend/kiro-engine-ledger.js +20 -0
  40. package/dist/backend/kiro-engine-ledger.js.map +1 -1
  41. package/dist/backend/kiro-identity.d.ts +6 -0
  42. package/dist/backend/kiro-identity.js +19 -5
  43. package/dist/backend/kiro-identity.js.map +1 -1
  44. package/dist/backend/kiro.d.ts +39 -3
  45. package/dist/backend/kiro.js +143 -19
  46. package/dist/backend/kiro.js.map +1 -1
  47. package/dist/backend/launch-effort.d.ts +61 -0
  48. package/dist/backend/launch-effort.js +170 -0
  49. package/dist/backend/launch-effort.js.map +1 -0
  50. package/dist/backend/muse.d.ts +84 -0
  51. package/dist/backend/muse.js +449 -53
  52. package/dist/backend/muse.js.map +1 -1
  53. package/dist/backend/types.d.ts +47 -4
  54. package/dist/backend/types.js +12 -1
  55. package/dist/backend/types.js.map +1 -1
  56. package/dist/backend-command.d.ts +91 -0
  57. package/dist/backend-command.js +122 -0
  58. package/dist/backend-command.js.map +1 -0
  59. package/dist/background-work-ended.d.ts +18 -0
  60. package/dist/background-work-ended.js +30 -0
  61. package/dist/background-work-ended.js.map +1 -0
  62. package/dist/cache-service.d.ts +2 -0
  63. package/dist/cache-service.js +2 -1
  64. package/dist/cache-service.js.map +1 -1
  65. package/dist/channel/adapters/discord.d.ts +36 -1
  66. package/dist/channel/adapters/discord.js +126 -4
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +42 -2
  69. package/dist/channel/adapters/telegram.js +142 -23
  70. package/dist/channel/adapters/telegram.js.map +1 -1
  71. package/dist/channel/mcp-server.js +9 -1
  72. package/dist/channel/mcp-server.js.map +1 -1
  73. package/dist/channel/mcp-tools.d.ts +11 -0
  74. package/dist/channel/mcp-tools.js +26 -13
  75. package/dist/channel/mcp-tools.js.map +1 -1
  76. package/dist/channel/message-edit-errors.d.ts +5 -0
  77. package/dist/channel/message-edit-errors.js +24 -0
  78. package/dist/channel/message-edit-errors.js.map +1 -0
  79. package/dist/channel/telegram-markdown.d.ts +6 -0
  80. package/dist/channel/telegram-markdown.js +104 -0
  81. package/dist/channel/telegram-markdown.js.map +1 -0
  82. package/dist/channel/tool-router.d.ts +2 -0
  83. package/dist/channel/tool-router.js +10 -4
  84. package/dist/channel/tool-router.js.map +1 -1
  85. package/dist/channel/types.d.ts +55 -3
  86. package/dist/cli.js +167 -39
  87. package/dist/cli.js.map +1 -1
  88. package/dist/command-table.d.ts +2 -2
  89. package/dist/command-table.js +6 -3
  90. package/dist/command-table.js.map +1 -1
  91. package/dist/config-validator.js +59 -0
  92. package/dist/config-validator.js.map +1 -1
  93. package/dist/cross-instance-envelope.d.ts +15 -2
  94. package/dist/cross-instance-envelope.js +29 -5
  95. package/dist/cross-instance-envelope.js.map +1 -1
  96. package/dist/daemon.d.ts +156 -1
  97. package/dist/daemon.js +773 -89
  98. package/dist/daemon.js.map +1 -1
  99. package/dist/delivery-outbox.d.ts +70 -8
  100. package/dist/delivery-outbox.js +315 -24
  101. package/dist/delivery-outbox.js.map +1 -1
  102. package/dist/delivery-reconciliation.d.ts +18 -1
  103. package/dist/delivery-reconciliation.js +67 -2
  104. package/dist/delivery-reconciliation.js.map +1 -1
  105. package/dist/fleet-context.d.ts +10 -1
  106. package/dist/fleet-manager.d.ts +404 -13
  107. package/dist/fleet-manager.js +2039 -210
  108. package/dist/fleet-manager.js.map +1 -1
  109. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  110. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
  111. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
  112. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  113. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  114. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  115. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  116. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  117. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  118. package/dist/general-knowledge/skills/model-discovery/SKILL.md +12 -9
  119. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  120. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  121. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  122. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  123. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
  124. package/dist/general-knowledge/steering/core-rules.md +8 -7
  125. package/dist/host-memory.d.ts +10 -0
  126. package/dist/host-memory.js +33 -1
  127. package/dist/host-memory.js.map +1 -1
  128. package/dist/human-delivery-receipts.d.ts +83 -0
  129. package/dist/human-delivery-receipts.js +194 -0
  130. package/dist/human-delivery-receipts.js.map +1 -0
  131. package/dist/instance-avatar.d.ts +36 -0
  132. package/dist/instance-avatar.js +57 -0
  133. package/dist/instance-avatar.js.map +1 -0
  134. package/dist/instance-config-impact.js +6 -0
  135. package/dist/instance-config-impact.js.map +1 -1
  136. package/dist/instance-lifecycle.d.ts +29 -6
  137. package/dist/instance-lifecycle.js +79 -28
  138. package/dist/instance-lifecycle.js.map +1 -1
  139. package/dist/instructions.d.ts +3 -1
  140. package/dist/instructions.js +11 -8
  141. package/dist/instructions.js.map +1 -1
  142. package/dist/kiro-log-gate.d.ts +42 -0
  143. package/dist/kiro-log-gate.js +39 -0
  144. package/dist/kiro-log-gate.js.map +1 -0
  145. package/dist/locale.js +226 -44
  146. package/dist/locale.js.map +1 -1
  147. package/dist/log-service.d.ts +99 -0
  148. package/dist/log-service.js +243 -0
  149. package/dist/log-service.js.map +1 -0
  150. package/dist/log-steps.d.ts +169 -0
  151. package/dist/log-steps.js +858 -0
  152. package/dist/log-steps.js.map +1 -0
  153. package/dist/login-controller.d.ts +20 -3
  154. package/dist/login-controller.js +71 -19
  155. package/dist/login-controller.js.map +1 -1
  156. package/dist/login-flows.d.ts +5 -2
  157. package/dist/login-flows.js +5 -4
  158. package/dist/login-flows.js.map +1 -1
  159. package/dist/memory-pressure.d.ts +13 -1
  160. package/dist/memory-pressure.js +50 -10
  161. package/dist/memory-pressure.js.map +1 -1
  162. package/dist/needs-you-hub.js +2 -0
  163. package/dist/needs-you-hub.js.map +1 -1
  164. package/dist/needs-you-live.d.ts +6 -0
  165. package/dist/needs-you-live.js +23 -3
  166. package/dist/needs-you-live.js.map +1 -1
  167. package/dist/next-steps.js +1 -1
  168. package/dist/next-steps.js.map +1 -1
  169. package/dist/outbound-handlers.d.ts +100 -0
  170. package/dist/outbound-handlers.js +284 -97
  171. package/dist/outbound-handlers.js.map +1 -1
  172. package/dist/outbound-schemas.d.ts +38 -15
  173. package/dist/outbound-schemas.js +36 -8
  174. package/dist/outbound-schemas.js.map +1 -1
  175. package/dist/public-link-progress.d.ts +1 -1
  176. package/dist/public-link-progress.js +8 -2
  177. package/dist/public-link-progress.js.map +1 -1
  178. package/dist/public-preview.d.ts +101 -0
  179. package/dist/public-preview.js +236 -0
  180. package/dist/public-preview.js.map +1 -0
  181. package/dist/public-web-gateway.js +14 -4
  182. package/dist/public-web-gateway.js.map +1 -1
  183. package/dist/public-web-link.d.ts +24 -1
  184. package/dist/public-web-link.js +63 -9
  185. package/dist/public-web-link.js.map +1 -1
  186. package/dist/reply-buttons-controller.d.ts +89 -5
  187. package/dist/reply-buttons-controller.js +273 -8
  188. package/dist/reply-buttons-controller.js.map +1 -1
  189. package/dist/reply-buttons.d.ts +48 -3
  190. package/dist/reply-buttons.js +105 -10
  191. package/dist/reply-buttons.js.map +1 -1
  192. package/dist/reply-polls.d.ts +85 -0
  193. package/dist/reply-polls.js +154 -0
  194. package/dist/reply-polls.js.map +1 -0
  195. package/dist/restart-guard.d.ts +6 -3
  196. package/dist/restart-guard.js +6 -3
  197. package/dist/restart-guard.js.map +1 -1
  198. package/dist/restart-progress.d.ts +33 -0
  199. package/dist/restart-progress.js +30 -0
  200. package/dist/restart-progress.js.map +1 -1
  201. package/dist/service-activation.d.ts +15 -7
  202. package/dist/service-activation.js +46 -29
  203. package/dist/service-activation.js.map +1 -1
  204. package/dist/service-installer.d.ts +31 -0
  205. package/dist/service-installer.js +41 -2
  206. package/dist/service-installer.js.map +1 -1
  207. package/dist/settings-api.js +85 -25
  208. package/dist/settings-api.js.map +1 -1
  209. package/dist/settings-change.js +72 -1
  210. package/dist/settings-change.js.map +1 -1
  211. package/dist/settings-effect.js +28 -4
  212. package/dist/settings-effect.js.map +1 -1
  213. package/dist/settings-http-confirmation.js +7 -0
  214. package/dist/settings-http-confirmation.js.map +1 -1
  215. package/dist/setup-form.d.ts +1 -1
  216. package/dist/setup-form.js +1 -1
  217. package/dist/spawn-gate.js +5 -1
  218. package/dist/spawn-gate.js.map +1 -1
  219. package/dist/spawn-hold-reply.d.ts +12 -0
  220. package/dist/spawn-hold-reply.js +39 -0
  221. package/dist/spawn-hold-reply.js.map +1 -0
  222. package/dist/status-emojis.d.ts +10 -3
  223. package/dist/status-emojis.js +11 -2
  224. package/dist/status-emojis.js.map +1 -1
  225. package/dist/systemd-peer-bus.d.ts +95 -0
  226. package/dist/systemd-peer-bus.js +430 -0
  227. package/dist/systemd-peer-bus.js.map +1 -0
  228. package/dist/systemd-runtime.d.ts +27 -1
  229. package/dist/systemd-runtime.js +69 -7
  230. package/dist/systemd-runtime.js.map +1 -1
  231. package/dist/tips.js +4 -4
  232. package/dist/tips.js.map +1 -1
  233. package/dist/tool-permissions.js +3 -3
  234. package/dist/tool-permissions.js.map +1 -1
  235. package/dist/tool-progress.js +3 -2
  236. package/dist/tool-progress.js.map +1 -1
  237. package/dist/topic-commands.d.ts +42 -2
  238. package/dist/topic-commands.js +101 -11
  239. package/dist/topic-commands.js.map +1 -1
  240. package/dist/transcript-delta-reader.d.ts +3 -3
  241. package/dist/transcript-delta-reader.js +8 -8
  242. package/dist/transcript-delta-reader.js.map +1 -1
  243. package/dist/transcript-jsonl.d.ts +80 -3
  244. package/dist/transcript-jsonl.js +147 -10
  245. package/dist/transcript-jsonl.js.map +1 -1
  246. package/dist/transcript-monitor.d.ts +19 -0
  247. package/dist/transcript-monitor.js +87 -15
  248. package/dist/transcript-monitor.js.map +1 -1
  249. package/dist/transcript-sources.d.ts +78 -0
  250. package/dist/transcript-sources.js +375 -64
  251. package/dist/transcript-sources.js.map +1 -1
  252. package/dist/transcript-turns.d.ts +15 -2
  253. package/dist/transcript-turns.js +15 -2
  254. package/dist/transcript-turns.js.map +1 -1
  255. package/dist/tunnel/cloudflared.d.ts +7 -15
  256. package/dist/tunnel/cloudflared.js +36 -1
  257. package/dist/tunnel/cloudflared.js.map +1 -1
  258. package/dist/tunnel/purpose-lane.d.ts +5 -1
  259. package/dist/tunnel/purpose-lane.js +14 -7
  260. package/dist/tunnel/purpose-lane.js.map +1 -1
  261. package/dist/tunnel/types.d.ts +5 -1
  262. package/dist/tunnel/types.js.map +1 -1
  263. package/dist/turn-resume.d.ts +2 -0
  264. package/dist/turn-resume.js +1 -1
  265. package/dist/turn-resume.js.map +1 -1
  266. package/dist/types.d.ts +25 -0
  267. package/dist/ui/chat-commands.js +1 -0
  268. package/dist/ui/chat-emojis.js +33 -0
  269. package/dist/ui/chat-mentions.js +63 -0
  270. package/dist/ui/chat-render.js +61 -4
  271. package/dist/ui/chat-store.js +37 -3
  272. package/dist/ui/chat-strings.js +40 -20
  273. package/dist/ui/chat-thread.js +111 -12
  274. package/dist/ui/chat-time.js +80 -0
  275. package/dist/ui/panel-chat.js +41 -10
  276. package/dist/ui/panel-details.js +2 -0
  277. package/dist/ui/panel-fleet.js +199 -57
  278. package/dist/ui/panel-log.js +428 -0
  279. package/dist/ui/panel-needs.js +6 -2
  280. package/dist/ui/panel-settings.js +184 -32
  281. package/dist/ui/preview-panel.js +55 -15
  282. package/dist/ui/preview-status.js +28 -0
  283. package/dist/ui/preview.js +44 -3
  284. package/dist/ui/settings-dialogs.js +7 -2
  285. package/dist/ui/settings-model.js +2 -1
  286. package/dist/ui/settings-strings.js +56 -4
  287. package/dist/ui/settings-token.js +1 -1
  288. package/dist/ui/shared/app-i18n.js +43 -17
  289. package/dist/ui/shared/app-prefs.js +250 -0
  290. package/dist/ui/shared/app-route.js +7 -2
  291. package/dist/ui/shared/app-shell.js +165 -8
  292. package/dist/ui/shared/app-store.js +5 -1
  293. package/dist/ui/shared/app-stream.js +26 -5
  294. package/dist/ui/shared/app.css +163 -22
  295. package/dist/ui/shared/app.js +11 -1
  296. package/dist/ui/shared/fonts/README.md +1 -1
  297. package/dist/ui/shared/header-tools.js +55 -5
  298. package/dist/ui/shared/instance-nav.js +241 -45
  299. package/dist/ui/shared/instance-switch.js +4 -4
  300. package/dist/ui/shared/panel-view.js +6 -0
  301. package/dist/ui/shared/signin.js +7 -7
  302. package/dist/ui/shared/theme.js +39 -7
  303. package/dist/ui/shared/tokens.css +1 -1
  304. package/dist/ui/shared/ui-icons.js +1 -0
  305. package/dist/ui/shared/usage-dialog.js +23 -5
  306. package/dist/ui/shared/view-strings.js +32 -4
  307. package/dist/ui/signin.html +1 -1
  308. package/dist/unrecognised-prompt.d.ts +23 -0
  309. package/dist/unrecognised-prompt.js +74 -0
  310. package/dist/unrecognised-prompt.js.map +1 -0
  311. package/dist/update-check.d.ts +31 -0
  312. package/dist/update-check.js +47 -0
  313. package/dist/update-check.js.map +1 -1
  314. package/dist/update-install.d.ts +28 -2
  315. package/dist/update-install.js +22 -4
  316. package/dist/update-install.js.map +1 -1
  317. package/dist/update-launch.d.ts +46 -1
  318. package/dist/update-launch.js +146 -3
  319. package/dist/update-launch.js.map +1 -1
  320. package/dist/update-marker.d.ts +51 -0
  321. package/dist/update-marker.js +61 -0
  322. package/dist/update-marker.js.map +1 -1
  323. package/dist/update-preflight.d.ts +100 -0
  324. package/dist/update-preflight.js +264 -0
  325. package/dist/update-preflight.js.map +1 -0
  326. package/dist/updater-caller.d.ts +31 -0
  327. package/dist/updater-caller.js +109 -0
  328. package/dist/updater-caller.js.map +1 -0
  329. package/dist/usage/i18n-keys.d.ts +1 -1
  330. package/dist/usage/i18n-keys.js +1 -1
  331. package/dist/usage/i18n-keys.js.map +1 -1
  332. package/dist/usage/providers.d.ts +34 -5
  333. package/dist/usage/providers.js +160 -27
  334. package/dist/usage/providers.js.map +1 -1
  335. package/dist/usage/statusline-usage.d.ts +6 -2
  336. package/dist/usage/statusline-usage.js +10 -4
  337. package/dist/usage/statusline-usage.js.map +1 -1
  338. package/dist/view-api.d.ts +1 -1
  339. package/dist/view-api.js +38 -8
  340. package/dist/view-api.js.map +1 -1
  341. package/dist/wake-coordinator.d.ts +11 -0
  342. package/dist/wake-coordinator.js +38 -4
  343. package/dist/wake-coordinator.js.map +1 -1
  344. package/dist/web-api.d.ts +89 -4
  345. package/dist/web-api.js +551 -45
  346. package/dist/web-api.js.map +1 -1
  347. package/dist/web-auth.d.ts +2 -2
  348. package/dist/web-auth.js +9 -4
  349. package/dist/web-auth.js.map +1 -1
  350. package/dist/web-chat-history.d.ts +12 -1
  351. package/dist/web-chat-history.js +29 -7
  352. package/dist/web-chat-history.js.map +1 -1
  353. package/dist/web-commands.d.ts +27 -4
  354. package/dist/web-commands.js +19 -4
  355. package/dist/web-commands.js.map +1 -1
  356. package/dist/web-host-guard.d.ts +3 -11
  357. package/dist/web-host-guard.js +3 -12
  358. package/dist/web-host-guard.js.map +1 -1
  359. package/dist/web-images.d.ts +47 -0
  360. package/dist/web-images.js +169 -0
  361. package/dist/web-images.js.map +1 -0
  362. package/dist/web-login.d.ts +14 -0
  363. package/dist/web-login.js +4 -2
  364. package/dist/web-login.js.map +1 -1
  365. package/dist/web-mentions.d.ts +44 -0
  366. package/dist/web-mentions.js +75 -0
  367. package/dist/web-mentions.js.map +1 -0
  368. package/dist/web-prefs.d.ts +81 -0
  369. package/dist/web-prefs.js +297 -0
  370. package/dist/web-prefs.js.map +1 -0
  371. package/dist/web-preview.d.ts +14 -1
  372. package/dist/web-preview.js +35 -13
  373. package/dist/web-preview.js.map +1 -1
  374. package/dist/web-profile.d.ts +32 -0
  375. package/dist/web-profile.js +132 -0
  376. package/dist/web-profile.js.map +1 -0
  377. package/dist/web-reply-files.d.ts +45 -0
  378. package/dist/web-reply-files.js +223 -0
  379. package/dist/web-reply-files.js.map +1 -0
  380. package/dist/web-schedules.d.ts +12 -0
  381. package/dist/web-schedules.js +34 -0
  382. package/dist/web-schedules.js.map +1 -0
  383. package/dist/web-server-drain.d.ts +10 -0
  384. package/dist/web-server-drain.js +50 -0
  385. package/dist/web-server-drain.js.map +1 -0
  386. package/dist/web-session.d.ts +50 -1
  387. package/dist/web-session.js +63 -6
  388. package/dist/web-session.js.map +1 -1
  389. package/dist/web-settings-patch.d.ts +23 -0
  390. package/dist/web-settings-patch.js +77 -0
  391. package/dist/web-settings-patch.js.map +1 -0
  392. package/dist/web-shell-routes.d.ts +5 -1
  393. package/dist/web-shell-routes.js +10 -1
  394. package/dist/web-shell-routes.js.map +1 -1
  395. package/dist/web-upload.d.ts +17 -4
  396. package/dist/web-upload.js +64 -13
  397. package/dist/web-upload.js.map +1 -1
  398. package/dist/workflow-templates/default.md +1 -1
  399. package/package.json +1 -1
  400. package/templates/launchd.plist.ejs +3 -1
  401. package/templates/systemd.service.ejs +2 -1
@@ -9,6 +9,7 @@
9
9
  import "./chat-render.js";
10
10
  import "./preview.js";
11
11
  import { loadHtmlAttachment } from "./html-attachment.js";
12
+ import { chatTime, chatTimeFull, onDayChange } from "./chat-time.js";
12
13
 
13
14
  const R = () => globalThis.AgendChatRender;
14
15
  /** #1589: the public link shows attachments as downloads only (its page says so: <body data-public-link="1">). */
@@ -22,6 +23,9 @@ export const TEXT_PREVIEW_NODES = 4000;
22
23
  /** A csv/tsv wider than this is shown as text, as Markdown tables are capped at 20 columns. */
23
24
  export const TEXT_PREVIEW_COLUMNS = 50;
24
25
  const P = () => globalThis.AgendPreview;
26
+ /** #1672: a status line longer than this (lines, or characters) is folded until it is opened. */
27
+ export const STATUS_FOLD_LINES = 6;
28
+ export const STATUS_FOLD_CHARS = 600;
25
29
  const COPY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
26
30
 
27
31
  /** Escapes & " ' < >: safe for text and inside a quoted attribute. */
@@ -31,7 +35,7 @@ export const msgKey = (x) => `${x.boot}-${x.id}`;
31
35
  /**
32
36
  * Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
33
37
  * setPreviewOptIn(on), copyText(text) → Promise<boolean>, download(code), panel? }. panel (#1481): { shown() → the card
34
- * key the side panel shows, or null; open(spec, { run }) } — without it a card has no "Open in panel".
38
+ * key the side panel shows, or null; open(spec, { run }); version(); claim(); claims() } — without it a card has no "Open in panel".
35
39
  */
36
40
  export function createThread(list, scroller, opts) {
37
41
  const nodes = new Map(); // key → { html, node }
@@ -46,17 +50,48 @@ export function createThread(list, scroller, opts) {
46
50
  const cardRefresh = new WeakMap(); // card node → re-read availability (weak: a card that left takes its source with it)
47
51
  const tr = opts.t, trf = opts.tf;
48
52
  const stopIn = (node) => (P() ? P().stopIn(node) : []);
53
+ /** #1770: write a message node's time label for now; unchanged text is not touched. */
54
+ function timeLabel(node) {
55
+ const el = node.querySelector(".meta .time[data-ts]");
56
+ if (!el) return;
57
+ const v = chatTime(el.dataset.ts, { yesterday: (hm) => trf("chat.yesterdayAt", hm) });
58
+ if (el.textContent !== v) el.textContent = v;
59
+ }
60
+ // #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;
62
+ function armDay() {
63
+ if (nodes.size && !offDay) offDay = onDayChange(() => { for (const v of nodes.values()) timeLabel(v.node); });
64
+ else if (!nodes.size && offDay) { offDay(); offDay = null; }
65
+ }
49
66
 
50
67
  const TICKS = () => ({ queued: tr("chat.tick_queued"), processing: tr("chat.tick_processing"), delivered: tr("chat.tick_delivered"), failed: tr("chat.tick_failed"), cancelled: tr("chat.tick_cancelled") });
51
68
 
52
69
  function msgHtml(x) {
70
+ // #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
+ // is kept by: it is written into the node (timeLabels) — crossing midnight relabels in place and reloads no preview.
72
+ const time = x.ts ? `<span class="time" data-ts="${escAttr(x.ts)}" title="${escAttr(chatTimeFull(x.ts))}"></span>` : `<span class="time"></span>`;
73
+ if (x.role === "status") return statusHtml(x, time);
53
74
  const u = opts.isUser(x);
54
- const time = x.ts ? new Date(x.ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
55
75
  // renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
56
76
  const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
57
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>`;
58
78
  const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
59
- return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span><span class="time">${time}</span>${ticks}</div><div class="body"><div class="md">${R().renderMarkdown(x.text, x.role === "agent" ? { htmlCards: true } : undefined)}</div>${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
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>`;
80
+ }
81
+ /**
82
+ * #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
83
+ * a preview card, a button or a person's bubble. Checked before isUser, which reads the sender: a fleet name need not
84
+ * start with "agend". A long one is folded until it is opened.
85
+ */
86
+ function statusHtml(x, time) {
87
+ const text = String(x.text == null ? "" : x.text);
88
+ const lines = R().lineCount(text);
89
+ const manyLines = lines > STATUS_FOLD_LINES;
90
+ 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>`
92
+ : "";
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>`;
60
95
  }
61
96
  /**
62
97
  * #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
@@ -64,6 +99,7 @@ export function createThread(list, scroller, opts) {
64
99
  */
65
100
  function replyButtonsHtml(b) {
66
101
  if (!b || typeof b.id !== "string" || !Array.isArray(b.labels) || !b.labels.length) return "";
102
+ if (b.poll && typeof b.poll === "object") return pollHtml(b);
67
103
  const busy = !!(opts.replyButtonBusy && opts.replyButtonBusy(b.id));
68
104
  const off = b.state !== "open" || busy;
69
105
  const items = b.labels.map((label, i) => {
@@ -73,11 +109,51 @@ export function createThread(list, scroller, opts) {
73
109
  const note = b.state === "chosen" ? trf("chat.rbChosen", b.by || "") : b.state === "expired" ? tr("chat.rbExpired") : "";
74
110
  return `<div class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
75
111
  }
112
+ /**
113
+ * #1823: a poll. Anyone here may click while it is open; the page marks its own vote (opts.pollMine), which no one else
114
+ * sees. Counts only when the server sends them (revealed: live, or closed); names only at close with show_voters. The
115
+ * time left is not in the html (a node is kept by its html): pollLeft writes it, every second.
116
+ */
117
+ function pollHtml(b) {
118
+ const p = b.poll, closed = b.state === "closed";
119
+ const busy = !!(opts.replyButtonBusy && opts.replyButtonBusy(b.id));
120
+ const mine = (opts.pollMine && opts.pollMine(b.id)) || [];
121
+ const counts = Array.isArray(p.counts) && p.counts.length === b.labels.length ? p.counts : null;
122
+ const items = b.labels.map((label, i) => {
123
+ const me = !closed && mine.includes(i);
124
+ const tally = counts ? ` · ${counts[i]}${closed && p.answered ? ` (${Math.round((counts[i] / p.answered) * 100)}%)` : ""}` : "";
125
+ 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>`;
126
+ }).join("");
127
+ const status = closed
128
+ ? escAttr(trf("chat.pollClosed", p.answered))
129
+ : `${escAttr(trf("chat.pollAnswered", p.answered))} · ⏳ <span class="poll-left" data-deadline="${escAttr(p.deadline)}" title="${escAttr(tr("chat.pollLeft"))}"></span>`;
130
+ const voters = closed && Array.isArray(p.voters)
131
+ ? `<ul class="poll-voters" aria-label="${escAttr(tr("chat.pollVoters"))}">${b.labels.map((label, i) => `<li>${escAttr(`${label}: ${(p.voters[i] || []).join(", ") || "—"}`)}</li>`).join("")}</ul>`
132
+ : "";
133
+ return `<div class="rb-row poll" role="group" aria-label="${escAttr(tr("chat.pollGroup"))}">${items}<span class="rb-note">${status}</span></div>${voters}`;
134
+ }
135
+ /** #1823: the time left on this node's open polls, now. */
136
+ function pollLeft(node) {
137
+ for (const el of node.querySelectorAll(".poll-left[data-deadline]")) {
138
+ const v = R().pollRemaining(Number(el.dataset.deadline) - (opts.serverNow ? opts.serverNow() : Date.now()));
139
+ if (el.textContent !== v) el.textContent = v;
140
+ }
141
+ }
142
+ // #1823: while an open poll is on screen, its time left is rewritten every second.
143
+ let pollTick = null;
144
+ function armPolls() {
145
+ const open = nodes.size > 0 && !!list.querySelector(".poll-left[data-deadline]");
146
+ if (open && !pollTick) pollTick = setInterval(() => pollLeft(list), 1000);
147
+ else if (!open && pollTick) { clearInterval(pollTick); pollTick = null; }
148
+ }
149
+
76
150
  function msgNode(html, x, stoppedKeys) {
77
151
  const tpl = list.ownerDocument.createElement("template");
78
152
  tpl.innerHTML = html;
79
153
  const node = tpl.content.firstElementChild;
80
154
  decorateCode(node);
155
+ timeLabel(node);
156
+ pollLeft(node);
81
157
  if (x) decorateHtmlCards(node, x, stoppedKeys || []);
82
158
  if (x) decorateTextCards(node, x);
83
159
  return node;
@@ -299,14 +375,32 @@ export function createThread(list, scroller, opts) {
299
375
  };
300
376
  const ui = { state: (name, reason) => { state = name === "starting" || name === "running" ? name : "idle"; refresh(reason || null); } };
301
377
  // Nothing is read for Preview on a device that does not allow previews: the button is not even shown there.
302
- run.onclick = () => { if (P().availability().ok) withCode((c) => P().start(key, holder, c, ui)); };
378
+ // A Preview click is newer than any Open in panel still reading (#1704): it claims the panel's turn too — and, its
379
+ // own HTML read, starts only if no newer click claimed meanwhile and no preview was started since (the panel's own
380
+ // Preview): a late read never undoes a newer choice. A close is neither, so it does not drop this click.
381
+ run.onclick = () => {
382
+ if (!P().availability().ok || reading) return; // this card already reading: that click stays the one
383
+ const mine = panel ? (panel.claim(), panel.claims()) : 0, started = P().startCount();
384
+ withCode((c) => {
385
+ if ((panel && panel.claims() !== mine) || P().startCount() !== started) { refresh(); return; }
386
+ P().start(key, holder, c, ui);
387
+ });
388
+ };
303
389
  stopB.onclick = () => { if (reading) { reading = 0; refresh(); } else P().stop(key, "stopped", ""); };
304
- // A running preview moves: it stops here and starts in the panel (the click that ran it). An idle one opens the
305
- // panel with its own Preview to click.
390
+ // #1704: this click runs it in the panel, as Preview there would — a running preview moves (it stops here and
391
+ // starts there), an idle one starts there. Whether it can run is the panel's to say: a device that has not allowed
392
+ // previews is offered Allow first (and allowing runs it); where previews are off, nothing runs.
393
+ // An attachment's HTML is read first. The click claims the panel at once, so a newer click (even one still reading)
394
+ // wins: if the panel changed (another click, block, close) or any preview started meanwhile, this one is dropped —
395
+ // a late read never undoes a newer click.
306
396
  if (toPanel) toPanel.onclick = () => {
307
- const moving = P().running(key);
308
- if (moving) P().stop(key, "stopped", "");
309
- withCode((c) => panel.open({ key, instance: x.instance, msgKey: msgKey(x), ...(att ? { att } : { n: source.n }), code: c, sender: x.sender, ts: x.ts }, { run: moving }));
397
+ if (reading) return; // this card already reading: that click stays the one
398
+ if (P().running(key)) P().stop(key, "stopped", "");
399
+ const at = panel.claim(), started = P().startCount();
400
+ withCode((c) => {
401
+ if (panel.version() !== at || P().startCount() !== started) { refresh(); return; }
402
+ panel.open({ key, instance: x.instance, msgKey: msgKey(x), ...(att ? { att } : { n: source.n }), code: c, sender: x.sender, ts: x.ts }, { run: true });
403
+ });
310
404
  };
311
405
  if (dl) dl.onclick = () => opts.download(code);
312
406
  ack.onclick = () => P().ackBanner(); // every card and the panel hear it (onChange)
@@ -341,6 +435,7 @@ export function createThread(list, scroller, opts) {
341
435
  if (!msgs.length) {
342
436
  for (const v of nodes.values()) stopIn(v.node);
343
437
  nodes.clear();
438
+ armDay();
344
439
  list.textContent = "";
345
440
  opts.onEmpty(true);
346
441
  return;
@@ -357,12 +452,14 @@ export function createThread(list, scroller, opts) {
357
452
  const node = msgNode(html, x, stopped);
358
453
  if (have) have.node.replaceWith(node); else added++;
359
454
  have = { html, node }; nodes.set(key, have);
360
- }
455
+ } else timeLabel(have.node);
361
456
  const want = prev ? prev.nextSibling : list.firstChild;
362
457
  // A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
363
458
  if (have.node !== want) { if (have.node.parentNode === list) stopIn(have.node); list.insertBefore(have.node, want); }
364
459
  prev = have.node;
365
460
  }
461
+ armDay();
462
+ armPolls();
366
463
  if ("restore" in renderOpts) {
367
464
  unseen = 0;
368
465
  if (renderOpts.restore == null) toBottom();
@@ -426,9 +523,9 @@ export function createThread(list, scroller, opts) {
426
523
  if (act === "copyMsg") { const x = msgs.find(y => msgKey(y) === el.dataset.arg); if (x) copyFrom(el, x.text); }
427
524
  else if (act === "copyCode") { const pre = el.closest(".codeblock")?.querySelector("pre"); if (pre) copyFrom(el, pre.textContent); }
428
525
  else if (act === "toggleFold") {
429
- const box = el.closest(".codeblock"); if (!box) return;
526
+ const box = el.closest(".codeblock, .msg.status-line"); if (!box) return;
430
527
  const folded = box.classList.toggle("folded");
431
- el.textContent = folded ? trf("chat.showAll", el.dataset.lines) : tr("chat.showLess");
528
+ el.textContent = !folded ? tr("chat.showLess") : el.dataset.lines ? trf("chat.showAll", el.dataset.lines) : tr("chat.showMore");
432
529
  } else if (act === "toggleWrap") opts.toggleWrap();
433
530
  else if (act === "replyButton") {
434
531
  const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
@@ -441,6 +538,8 @@ export function createThread(list, scroller, opts) {
441
538
  if (ro) ro.disconnect();
442
539
  if (P()) P().stopAll("leave");
443
540
  nodes.clear();
541
+ armDay();
542
+ armPolls();
444
543
  }
445
544
 
446
545
  return { render, onScroll, onClick, jumpLatest, reveal, refreshCards, dispose, nodes, get unseen() { return unseen; } };
@@ -0,0 +1,80 @@
1
+ // #1770: the time a chat line shows, read in the fleet's zone (the one chat-logs, fleet.log and the #1586 DMs use) —
2
+ // "17:38" today, "Yesterday 17:38", "10/11 17:38" earlier this year, "2025/10/11 17:38" before. The hour and minute keep
3
+ // the browser's own format (12- or 24-hour), as they always had. No imports: the thread, the preview panel and the
4
+ // tests read it as it is.
5
+
6
+ const cache = new Map();
7
+ /** One formatter per (kind, zone, locale): a thread may label 500 lines on every render. */
8
+ function fmt(kind, tz, locale) {
9
+ const key = `${kind}|${tz || ""}|${locale || ""}`;
10
+ let f = cache.get(key);
11
+ if (!f) {
12
+ const o = kind === "hm" ? { hour: "2-digit", minute: "2-digit" }
13
+ : kind === "ymd" ? { year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric", hourCycle: "h23" }
14
+ : { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", timeZoneName: "short" };
15
+ f = new Intl.DateTimeFormat(kind === "ymd" ? "en-US" : (locale || []), { ...o, timeZone: tz || undefined });
16
+ cache.set(key, f);
17
+ }
18
+ return f;
19
+ }
20
+ /** `tz` when this browser knows it, else undefined (the browser's own zone): a label must still be drawn. */
21
+ function known(tz) {
22
+ if (!tz) return undefined;
23
+ try { fmt("ymd", tz); return tz; } catch { return undefined; }
24
+ }
25
+ /** The fleet's zone, as the page was served it (<body data-fleet-tz>); undefined when absent or unknown here. */
26
+ export function fleetTz() { return known(globalThis.document?.body?.dataset?.fleetTz); }
27
+
28
+ const toMs = (ts) => (typeof ts === "number" ? ts : Date.parse(String(ts || "")));
29
+ function wall(ms, tz) {
30
+ const p = {};
31
+ for (const x of fmt("ymd", tz).formatToParts(ms)) p[x.type] = Number(x.value);
32
+ return p;
33
+ }
34
+
35
+ /**
36
+ * The label for `ts` (ISO string or epoch ms), "" when it cannot be read. opts: tz (default the fleet's), now (epoch ms),
37
+ * yesterday(hm) → the word for "yesterday" around hm (default: hm alone), locale (tests).
38
+ */
39
+ export function chatTime(ts, opts = {}) {
40
+ const ms = toMs(ts);
41
+ if (!Number.isFinite(ms)) return "";
42
+ const tz = "tz" in opts ? known(opts.tz) : fleetTz();
43
+ const hm = fmt("hm", tz, opts.locale).format(ms);
44
+ const d = wall(ms, tz), today = wall(opts.now ?? Date.now(), tz);
45
+ if (d.year === today.year && d.month === today.month && d.day === today.day) return hm;
46
+ const y = new Date(Date.UTC(today.year, today.month - 1, today.day - 1));
47
+ if (d.year === y.getUTCFullYear() && d.month === y.getUTCMonth() + 1 && d.day === y.getUTCDate()) return opts.yesterday ? opts.yesterday(hm) : hm;
48
+ return d.year === today.year ? `${d.month}/${d.day} ${hm}` : `${d.year}/${d.month}/${d.day} ${hm}`;
49
+ }
50
+
51
+ /** The full date, time and zone of `ts`, for a tooltip; "" when it cannot be read. */
52
+ export function chatTimeFull(ts, opts = {}) {
53
+ const ms = toMs(ts);
54
+ if (!Number.isFinite(ms)) return "";
55
+ return fmt("full", "tz" in opts ? known(opts.tz) : fleetTz(), opts.locale).format(ms);
56
+ }
57
+
58
+ /** Milliseconds until the fleet's next midnight (when "today" labels change), at most an hour (a DST day self-corrects). */
59
+ export function msToNextDay(now = Date.now(), tz = fleetTz()) {
60
+ const w = wall(now, known(tz));
61
+ const into = ((w.hour * 60 + w.minute) * 60 + w.second) * 1000 + (now % 1000);
62
+ return Math.max(1000, Math.min(3_600_000, 86_400_000 - into + 1000));
63
+ }
64
+
65
+ /**
66
+ * Call `fn` when the fleet's day may have changed — at its next midnight, then again for each one after (an hour at
67
+ * most between looks, so a DST day or a late timer corrects itself). Each subscriber holds its own timer, and no
68
+ * module-wide state: the unsubscribe it returns leaves nothing armed.
69
+ */
70
+ export function onDayChange(fn) {
71
+ let timer = null, live = true;
72
+ const arm = () => {
73
+ timer = setTimeout(() => {
74
+ timer = null;
75
+ try { fn(); } finally { if (live) arm(); }
76
+ }, msToNextDay());
77
+ };
78
+ arm();
79
+ return () => { live = false; if (timer !== null) { clearTimeout(timer); timer = null; } };
80
+ }
@@ -10,6 +10,7 @@ import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-
10
10
  import { t } from "/assets/app-i18n.js";
11
11
  import { appStore, useStore } from "/assets/app-store.js";
12
12
  import { useLease } from "/assets/app-ctx.js";
13
+ import { namesOf, namesStore } from "/assets/instance-nav.js";
13
14
  import { PanelHeader, onPanelKey, setTitle, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
14
15
  import { detailsPath } from "/assets/app-route.js";
15
16
  import { InstanceSwitch } from "/assets/instance-switch.js";
@@ -21,11 +22,15 @@ import { Empty, Skeleton } from "/assets/ui-states.js";
21
22
  import { Icon } from "/assets/ui-icons.js";
22
23
  import { toast } from "/assets/ui-toast.js";
23
24
  import { confirmDialog } from "/assets/ui-confirm.js";
25
+ import { savePref } from "/assets/app-prefs.js";
24
26
  import "./chat-strings.js";
25
27
  import { confirmedWrite } from "./settings-confirm.js";
26
28
  import { createChatStore } from "./chat-store.js";
27
29
  import { createThread, msgKey } from "./chat-thread.js";
28
- import { PreviewPanel, openPanel, panelStore, shownKey } from "./preview-panel.js";
30
+ import { createEmojiIndex } from "./chat-emojis.js";
31
+ import { pollPublicPreview } from "./preview-status.js";
32
+ import { createMentionIndex } from "./chat-mentions.js";
33
+ import { PreviewPanel, claimPanel, openPanel, panelClaims, panelStore, panelVersion, shownKey } from "./preview-panel.js";
29
34
  import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
30
35
  import { installTour, refreshTourSpot } from "./chat-tour.js";
31
36
  import { FirstRunCard } from "./first-run.js";
@@ -40,6 +45,9 @@ export let store = null;
40
45
  // ── Announcements: one polite, visually hidden line for the coarse events only (a reply arrived, the agent started,
41
46
  // finished, or waits on you). Messages themselves are not read out as they arrive; the list is a log to browse.
42
47
  let announceTimer = null;
48
+ /** #1630: which Discord server emoji this page may draw (created with the store, in boot). */
49
+ let emojiIndex = null;
50
+ let mentionIndex = null; // #1682: the names of the mentions shown
43
51
  function announce(text) {
44
52
  let el = document.getElementById("announcer");
45
53
  if (!el) { el = document.createElement("div"); el.id = "announcer"; el.className = "sr-only"; el.setAttribute("role", "status"); el.setAttribute("aria-live", "polite"); el.setAttribute("aria-atomic", "true"); document.body.appendChild(el); }
@@ -51,11 +59,14 @@ function announce(text) {
51
59
 
52
60
  /**
53
61
  * This device's preview opt-in: asks once (the app's dialog) and says what it means; turning it off stops every
54
- * running preview. Resolves to the choice in force.
62
+ * running preview. Resolves to the choice in force. commit(apply): runs `apply` (the change itself) — the panel's Allow
63
+ * wraps it to know which change of the choice is its own (#1704).
55
64
  */
56
- export async function setPreviewOptIn(on) {
65
+ export async function setPreviewOptIn(on, commit = (apply) => apply()) {
57
66
  if (on && !P().optedIn() && !(await confirmDialog({ message: t("chat.pvConfirm"), confirmLabel: t("chat.pvAllowButton") }))) on = false;
58
- P().setOptIn(on);
67
+ commit(() => P().setOptIn(on));
68
+ // #1665: kept on the host for every device (only a signed-in page writes it; never the public link).
69
+ savePref("preview.optIn", P().optedIn());
59
70
  return P().optedIn();
60
71
  }
61
72
 
@@ -65,11 +76,15 @@ export function boot({ stream, boot: bootData, deps = {} }) {
65
76
  store = createChatStore({
66
77
  fetch: (...a) => fetch(...a), toast, announce, t, setTimeout: (fn, ms) => setTimeout(fn, ms), confirm: confirmDialog, ...deps,
67
78
  });
79
+ emojiIndex = createEmojiIndex((url) => (deps.fetch || fetch)(url, { credentials: "same-origin" }));
80
+ mentionIndex = createMentionIndex((url) => (deps.fetch || fetch)(url, { credentials: "same-origin" }));
68
81
  store.attach(stream);
69
82
  // #1306: what the server said about previews for this load. The page-wide message and storage listeners this adds
70
83
  // belong to the app, once (they are never per mount).
71
84
  // #1554: its words in the page's language (chat.pv*); a key the dictionary does not have keeps preview.js's English.
72
85
  if (P() && bootData) P().init(bootData, { text: (key, ...v) => { const k = `chat.${key}`, s = t(k, ...v); return s === k ? null : s; } });
86
+ // #1723: the public link's preview was still starting when this page loaded: ask until it is up (or failed).
87
+ if (P() && bootData) pollPublicPreview(P());
73
88
  // #1568: the banner's "I understand" lasts until the next sign-in — preview.js is told which sign-in this page is
74
89
  // (the session handle, from the one /auth/session read); none (no session, or no AgendAuth): this tab only.
75
90
  if (P() && typeof P().setBannerScope === "function") {
@@ -119,6 +134,9 @@ function useChatTick(name, kinds) {
119
134
 
120
135
  // ── The panel ──
121
136
 
137
+ /** #1722: the chat's title — the name that leads, the other on hover. */
138
+ const headerName = ({ primary, secondary }) => (secondary ? html`<span title=${secondary}>${primary}</span>` : primary);
139
+
122
140
  export function ChatPanel({ route, navKey }) {
123
141
  const lease = useLease(navKey);
124
142
  const app = useStore(appStore);
@@ -154,6 +172,7 @@ function NotFound({ name }) {
154
172
  }
155
173
 
156
174
  function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
175
+ const names = useStore(namesStore); // #1722: which name leads (the title)
157
176
  const [dialog, setDialog] = useState(null); // "delete" | null
158
177
  const [wrap, setWrap] = useState(codeWrap);
159
178
  const view = useRef(null), split = useRef(null), thread = useRef(null);
@@ -189,6 +208,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
189
208
  try { const res = await fetch(`/ui/${verb}/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" } }); r = await res.json(); }
190
209
  catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
191
210
  if (r && r.error) toast(r.error, false);
211
+ else if (r && r.held) toast(r.message);
192
212
  else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
193
213
  };
194
214
  const items = [
@@ -206,14 +226,14 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
206
226
  ${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", inst.model)} aria-label=${t("chat.chipModel", inst.model)} onClick=${() => pick("model")}>${inst.model}</button>` : null}
207
227
  ${inst.effort ? html`<button type="button" class="hd-chip" title=${t("chat.chipEffort", inst.effort)} aria-label=${t("chat.chipEffort", inst.effort)} onClick=${() => pick("effort")}>${inst.effort}</button>` : null}`;
208
228
  return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
209
- <${PanelHeader} title=${name} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
229
+ <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
210
230
  <div class="chat-split" ref=${split}>
211
231
  <div class="chat-main">
212
232
  <${Thread} name=${name} th=${thread} />
213
233
  <${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} />
214
234
  </div>
215
235
  <${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
216
- keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${() => setPreviewOptIn(true)} />
236
+ keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${(commit) => setPreviewOptIn(true, commit)} />
217
237
  </div>
218
238
  <${LightboxHost} />
219
239
  <div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
@@ -232,14 +252,24 @@ function Thread({ name, th }) {
232
252
  setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
233
253
  clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
234
254
  replyButtonBusy: (id) => store.state.rbBusy.has(id),
235
- panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o) }, // #1481
255
+ pollMine: (id) => store.state.pollMine[id], // #1823: this device's own vote
256
+ serverNow: () => Date.now() + store.state.clockSkew, // #1823: the deadline is the server's
257
+ panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims }, // #1481, #1704
236
258
  openImage: openLightbox, // an image, full size, in the page
259
+ emojis: (id) => !!emojiIndex && emojiIndex.known(id), // #1630: the fleet's server emoji
260
+ mentions: (kind, id) => (mentionIndex ? mentionIndex.name(kind, id) : null), // #1682: mention names
237
261
  });
238
262
  th.current = thread;
263
+ // #1630: the server-emoji list is read once a message shows a tag; when it arrives, those messages are drawn again.
264
+ // #1682: the same for mention names — asked for the ids the messages carry; when names arrive, drawn again.
265
+ const wantEmojis = (msgs) => { if (emojiIndex && emojiIndex.wants(msgs)) emojiIndex.ensure(); if (mentionIndex) mentionIndex.ensure(msgs); };
266
+ const offEmojis = emojiIndex ? emojiIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
267
+ const offMentions = mentionIndex ? mentionIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
239
268
  thread.render(store.state.msgs[name] || [], { restore: store.state.scrollMemo[name] ?? null });
269
+ wantEmojis(store.state.msgs[name]);
240
270
  const off = store.subscribe((instance, kind) => {
241
271
  if (instance !== name) return;
242
- if (kind === "msgs") thread.render(store.state.msgs[name] || []);
272
+ if (kind === "msgs") { thread.render(store.state.msgs[name] || []); wantEmojis(store.state.msgs[name]); }
243
273
  else if (kind === "sent") thread.jumpLatest();
244
274
  });
245
275
  const offPv = P() ? P().onChange(() => thread.refreshCards()) : () => {};
@@ -249,7 +279,7 @@ function Thread({ name, th }) {
249
279
  sc.addEventListener("scroll", onScroll, { passive: true });
250
280
  ls.addEventListener("click", thread.onClick);
251
281
  return () => {
252
- off(); offPv(); offPanel();
282
+ off(); offPv(); offPanel(); offEmojis(); offMentions();
253
283
  sc.removeEventListener("scroll", onScroll);
254
284
  ls.removeEventListener("click", thread.onClick);
255
285
  thread.dispose(); // the whole thread goes: every preview in it stops first
@@ -292,6 +322,7 @@ function CommandCard({ name, lease }) {
292
322
  aria-label=${t("chat.cmdDismiss")} onClick=${() => store.dismissCommand(name)}><${Icon} name="close" size=${14} /></button>`}</div>
293
323
  ${c.error ? html`<p class="cmd-text">${c.error}</p>` : null}
294
324
  ${c.text ? html`<pre class="cmd-text">${c.text}</pre>` : null}
325
+ ${c.choices && c.choices.prompt ? html`<pre class="cmd-text">${c.choices.prompt}</pre>` : null}
295
326
  ${c.choices ? html`<div class="cmd-choices" role="group" aria-label=${t("chat.cmdChoose")}>${c.choices.options.map((o) => html`<button key=${o.id} type="button"
296
327
  class=${`btn btn-sm${o.id === c.choices.current ? " current" : ""}`} aria-current=${o.id === c.choices.current ? "true" : undefined} onClick=${() => run(o.id)}>${o.label}</button>`)}</div>` : null}
297
328
  </div>`;
@@ -466,7 +497,7 @@ function DeleteDialog({ name, onClose }) {
466
497
  <button type="button" class="btn btn-danger" disabled=${typed !== want || busy} onClick=${go}>${t("chat.deleteGo")}</button>`}>
467
498
  <p>${t("chat.deleteHelp", name)}</p>
468
499
  <label class="field"><span class="sr-only">${t("chat.deleteHelp", name)}</span>
469
- <input type="text" value=${typed} autocomplete="off" spellcheck="false" placeholder=${want} onInput=${(e) => setTyped(e.target.value)}
500
+ <input type="text" value=${typed} autocomplete="off" spellcheck=${false} placeholder=${want} onInput=${(e) => setTyped(e.target.value)}
470
501
  onKeyDown=${(e) => { if (e.key === "Enter") go(); }} /></label>
471
502
  </${Dialog}>`;
472
503
  }
@@ -73,11 +73,13 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
73
73
  try { const res = await fetch(`/ui/${verb}/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" } }); r = await res.json(); }
74
74
  catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
75
75
  if (r && r.error) toast(r.error, false);
76
+ else if (r && r.held) toast(r.message);
76
77
  else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
77
78
  });
78
79
  const pauseWake = (verb) => act(verb, async () => {
79
80
  const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${verb}`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
80
81
  if (!res.ok) toast((res.body && res.body.error) || t("chat.dActionFailed"), false);
82
+ else if (res.body && res.body.held) toast(res.body.message);
81
83
  else toast(t(verb === "pause" ? "chat.dPaused" : "chat.dWoken", name));
82
84
  });
83
85
  // A ClassicBot room (classicBot.yaml) keeps Classic's split: its own Settings dialog, and only the actions Settings'