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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (502) hide show
  1. package/README.md +7 -1
  2. package/README.zh-TW.md +7 -1
  3. package/dist/agent-cli-instructions.md +9 -2
  4. package/dist/agent-cli.js +63 -2
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +4 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-file-read.d.ts +4 -1
  9. package/dist/agent-file-read.js +189 -16
  10. package/dist/agent-file-read.js.map +1 -1
  11. package/dist/agent-whats-new.d.ts +11 -0
  12. package/dist/agent-whats-new.js +29 -5
  13. package/dist/agent-whats-new.js.map +1 -1
  14. package/dist/auth-api.d.ts +30 -1
  15. package/dist/auth-api.js +45 -13
  16. package/dist/auth-api.js.map +1 -1
  17. package/dist/authority/authority-state.d.ts +38 -0
  18. package/dist/authority/authority-state.js +114 -0
  19. package/dist/authority/authority-state.js.map +1 -0
  20. package/dist/authority/session-authorizations.d.ts +43 -0
  21. package/dist/authority/session-authorizations.js +102 -0
  22. package/dist/authority/session-authorizations.js.map +1 -0
  23. package/dist/authority/web-authority.d.ts +120 -0
  24. package/dist/authority/web-authority.js +157 -0
  25. package/dist/authority/web-authority.js.map +1 -0
  26. package/dist/backend/claude-code.d.ts +2 -2
  27. package/dist/backend/claude-code.js +18 -8
  28. package/dist/backend/claude-code.js.map +1 -1
  29. package/dist/backend/codex-metadata.d.ts +8 -0
  30. package/dist/backend/codex-metadata.js +74 -17
  31. package/dist/backend/codex-metadata.js.map +1 -1
  32. package/dist/backend/codex.js +15 -2
  33. package/dist/backend/codex.js.map +1 -1
  34. package/dist/backend/credential-profile.d.ts +2 -0
  35. package/dist/backend/credential-profile.js +2 -0
  36. package/dist/backend/credential-profile.js.map +1 -1
  37. package/dist/backend/types.d.ts +6 -0
  38. package/dist/backend/types.js +15 -0
  39. package/dist/backend/types.js.map +1 -1
  40. package/dist/backend-availability.d.ts +120 -0
  41. package/dist/backend-availability.js +396 -0
  42. package/dist/backend-availability.js.map +1 -0
  43. package/dist/backend-command.d.ts +23 -3
  44. package/dist/backend-command.js +25 -4
  45. package/dist/backend-command.js.map +1 -1
  46. package/dist/bug-report-cli.d.ts +30 -0
  47. package/dist/bug-report-cli.js +138 -0
  48. package/dist/bug-report-cli.js.map +1 -0
  49. package/dist/bug-report.d.ts +47 -0
  50. package/dist/bug-report.js +136 -0
  51. package/dist/bug-report.js.map +1 -0
  52. package/dist/bundled-skills.d.ts +14 -0
  53. package/dist/bundled-skills.js +29 -0
  54. package/dist/bundled-skills.js.map +1 -0
  55. package/dist/cache-ledger.d.ts +16 -3
  56. package/dist/cache-ledger.js +33 -13
  57. package/dist/cache-ledger.js.map +1 -1
  58. package/dist/cache-service.d.ts +55 -3
  59. package/dist/cache-service.js +152 -16
  60. package/dist/cache-service.js.map +1 -1
  61. package/dist/channel/adapters/discord-client-retirement.d.ts +5 -0
  62. package/dist/channel/adapters/discord-client-retirement.js +57 -0
  63. package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
  64. package/dist/channel/adapters/discord.d.ts +9 -0
  65. package/dist/channel/adapters/discord.js +49 -4
  66. package/dist/channel/adapters/discord.js.map +1 -1
  67. package/dist/channel/mcp-tools.js +22 -11
  68. package/dist/channel/mcp-tools.js.map +1 -1
  69. package/dist/channel/types.d.ts +6 -0
  70. package/dist/classic-channel-manager.d.ts +10 -5
  71. package/dist/classic-channel-manager.js +12 -9
  72. package/dist/classic-channel-manager.js.map +1 -1
  73. package/dist/cli.js +224 -111
  74. package/dist/cli.js.map +1 -1
  75. package/dist/config-validator.d.ts +6 -0
  76. package/dist/config-validator.js +129 -3
  77. package/dist/config-validator.js.map +1 -1
  78. package/dist/config.d.ts +13 -1
  79. package/dist/config.js +14 -5
  80. package/dist/config.js.map +1 -1
  81. package/dist/cross-instance-notice.d.ts +7 -0
  82. package/dist/cross-instance-notice.js +10 -0
  83. package/dist/cross-instance-notice.js.map +1 -1
  84. package/dist/daemon.d.ts +24 -1
  85. package/dist/daemon.js +151 -19
  86. package/dist/daemon.js.map +1 -1
  87. package/dist/decision-list.d.ts +25 -0
  88. package/dist/decision-list.js +30 -0
  89. package/dist/decision-list.js.map +1 -0
  90. package/dist/delivery-outbox.d.ts +10 -0
  91. package/dist/delivery-outbox.js +17 -1
  92. package/dist/delivery-outbox.js.map +1 -1
  93. package/dist/diagnostic-redaction.d.ts +10 -0
  94. package/dist/diagnostic-redaction.js +167 -0
  95. package/dist/diagnostic-redaction.js.map +1 -0
  96. package/dist/fleet-context.d.ts +10 -0
  97. package/dist/fleet-manager.d.ts +505 -15
  98. package/dist/fleet-manager.js +1870 -204
  99. package/dist/fleet-manager.js.map +1 -1
  100. package/dist/fleet-process-handlers.d.ts +18 -0
  101. package/dist/fleet-process-handlers.js +69 -0
  102. package/dist/fleet-process-handlers.js.map +1 -0
  103. package/dist/fleet-traffic.d.ts +74 -0
  104. package/dist/fleet-traffic.js +196 -0
  105. package/dist/fleet-traffic.js.map +1 -0
  106. package/dist/general-knowledge/skills/bug-report/SKILL.md +31 -0
  107. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +17 -6
  108. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
  109. package/dist/general-knowledge/skills/fleet-config/SKILL.md +2 -0
  110. package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
  111. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
  112. package/dist/general-knowledge/skills/scheduling/SKILL.md +4 -0
  113. package/dist/general-knowledge/skills/web-appearance/SKILL.md +190 -47
  114. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +545 -0
  115. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  116. package/dist/human-delivery-receipts.d.ts +24 -1
  117. package/dist/human-delivery-receipts.js +64 -11
  118. package/dist/human-delivery-receipts.js.map +1 -1
  119. package/dist/instance-config-impact.js +7 -1
  120. package/dist/instance-config-impact.js.map +1 -1
  121. package/dist/instance-lifecycle.d.ts +10 -1
  122. package/dist/instance-lifecycle.js +79 -18
  123. package/dist/instance-lifecycle.js.map +1 -1
  124. package/dist/instance-names.d.ts +4 -0
  125. package/dist/instance-names.js +12 -0
  126. package/dist/instance-names.js.map +1 -0
  127. package/dist/instructions.d.ts +12 -1
  128. package/dist/instructions.js +13 -2
  129. package/dist/instructions.js.map +1 -1
  130. package/dist/locale.js +173 -12
  131. package/dist/locale.js.map +1 -1
  132. package/dist/log-steps.d.ts +1 -0
  133. package/dist/log-steps.js +15 -0
  134. package/dist/log-steps.js.map +1 -1
  135. package/dist/login-controller.d.ts +45 -0
  136. package/dist/login-controller.js +101 -5
  137. package/dist/login-controller.js.map +1 -1
  138. package/dist/ls-attach-resolve.d.ts +96 -0
  139. package/dist/ls-attach-resolve.js +180 -0
  140. package/dist/ls-attach-resolve.js.map +1 -0
  141. package/dist/ls-rows.d.ts +12 -0
  142. package/dist/ls-rows.js +82 -3
  143. package/dist/ls-rows.js.map +1 -1
  144. package/dist/memory-hogs.d.ts +27 -0
  145. package/dist/memory-hogs.js +124 -0
  146. package/dist/memory-hogs.js.map +1 -0
  147. package/dist/needs-you.d.ts +10 -0
  148. package/dist/needs-you.js +2 -1
  149. package/dist/needs-you.js.map +1 -1
  150. package/dist/outbound-handlers.d.ts +11 -0
  151. package/dist/outbound-handlers.js +139 -11
  152. package/dist/outbound-handlers.js.map +1 -1
  153. package/dist/outbound-schemas.d.ts +200 -4
  154. package/dist/outbound-schemas.js +124 -11
  155. package/dist/outbound-schemas.js.map +1 -1
  156. package/dist/public-web-gateway.js +17 -4
  157. package/dist/public-web-gateway.js.map +1 -1
  158. package/dist/public-web-link.d.ts +9 -0
  159. package/dist/public-web-link.js +8 -0
  160. package/dist/public-web-link.js.map +1 -1
  161. package/dist/quickstart-api.d.ts +19 -1
  162. package/dist/quickstart-api.js +94 -4
  163. package/dist/quickstart-api.js.map +1 -1
  164. package/dist/reply-buttons-controller.d.ts +4 -0
  165. package/dist/reply-buttons-controller.js +2 -2
  166. package/dist/reply-buttons-controller.js.map +1 -1
  167. package/dist/restart-progress.d.ts +80 -1
  168. package/dist/restart-progress.js +306 -21
  169. package/dist/restart-progress.js.map +1 -1
  170. package/dist/rollout-index.d.ts +5 -0
  171. package/dist/rollout-index.js +26 -5
  172. package/dist/rollout-index.js.map +1 -1
  173. package/dist/scheduler/db.d.ts +40 -13
  174. package/dist/scheduler/db.js +123 -89
  175. package/dist/scheduler/db.js.map +1 -1
  176. package/dist/scheduler/task-board-text.d.ts +26 -0
  177. package/dist/scheduler/task-board-text.js +135 -0
  178. package/dist/scheduler/task-board-text.js.map +1 -0
  179. package/dist/scheduler/task-board.d.ts +650 -0
  180. package/dist/scheduler/task-board.js +1938 -0
  181. package/dist/scheduler/task-board.js.map +1 -0
  182. package/dist/scheduler/types.d.ts +6 -1
  183. package/dist/search/search-docs.d.ts +58 -0
  184. package/dist/search/search-docs.js +63 -0
  185. package/dist/search/search-docs.js.map +1 -0
  186. package/dist/search/search-index.d.ts +141 -0
  187. package/dist/search/search-index.js +440 -0
  188. package/dist/search/search-index.js.map +1 -0
  189. package/dist/search/search-store.d.ts +122 -0
  190. package/dist/search/search-store.js +236 -0
  191. package/dist/search/search-store.js.map +1 -0
  192. package/dist/search/search-worker.d.ts +1 -0
  193. package/dist/search/search-worker.js +163 -0
  194. package/dist/search/search-worker.js.map +1 -0
  195. package/dist/service-installer.js +12 -0
  196. package/dist/service-installer.js.map +1 -1
  197. package/dist/settings-api.d.ts +23 -0
  198. package/dist/settings-api.js +164 -3
  199. package/dist/settings-api.js.map +1 -1
  200. package/dist/settings-change.js +104 -0
  201. package/dist/settings-change.js.map +1 -1
  202. package/dist/settings-confirmation.d.ts +3 -1
  203. package/dist/settings-confirmation.js +2 -0
  204. package/dist/settings-confirmation.js.map +1 -1
  205. package/dist/settings-effect.js +34 -3
  206. package/dist/settings-effect.js.map +1 -1
  207. package/dist/settings-http-confirmation.d.ts +11 -0
  208. package/dist/settings-http-confirmation.js +10 -0
  209. package/dist/settings-http-confirmation.js.map +1 -1
  210. package/dist/setup-form.d.ts +1 -1
  211. package/dist/setup-form.js +23 -7
  212. package/dist/setup-form.js.map +1 -1
  213. package/dist/spawn-gate.d.ts +1 -0
  214. package/dist/spawn-gate.js +13 -3
  215. package/dist/spawn-gate.js.map +1 -1
  216. package/dist/task-board-tool.d.ts +25 -0
  217. package/dist/task-board-tool.js +154 -0
  218. package/dist/task-board-tool.js.map +1 -0
  219. package/dist/task-board-waker.d.ts +44 -0
  220. package/dist/task-board-waker.js +128 -0
  221. package/dist/task-board-waker.js.map +1 -0
  222. package/dist/tmux-exec-helper.d.ts +94 -0
  223. package/dist/tmux-exec-helper.js +407 -0
  224. package/dist/tmux-exec-helper.js.map +1 -0
  225. package/dist/tmux-manager.d.ts +5 -0
  226. package/dist/tmux-manager.js +42 -11
  227. package/dist/tmux-manager.js.map +1 -1
  228. package/dist/tool-permissions.d.ts +0 -7
  229. package/dist/tool-permissions.js +44 -5
  230. package/dist/tool-permissions.js.map +1 -1
  231. package/dist/topic-commands.d.ts +17 -1
  232. package/dist/topic-commands.js +45 -19
  233. package/dist/topic-commands.js.map +1 -1
  234. package/dist/tunnel/cloudflared.js +37 -2
  235. package/dist/tunnel/cloudflared.js.map +1 -1
  236. package/dist/tunnel/types.d.ts +3 -1
  237. package/dist/tunnel/types.js.map +1 -1
  238. package/dist/types.d.ts +63 -1
  239. package/dist/ui/app-effects.js +90 -32
  240. package/dist/ui/backend-signin.js +297 -0
  241. package/dist/ui/chat-ambient.js +307 -0
  242. package/dist/ui/chat-effect-posters.js +8 -8
  243. package/dist/ui/chat-effects.js +110 -15
  244. package/dist/ui/chat-msg-actions.js +176 -0
  245. package/dist/ui/chat-render.js +21 -3
  246. package/dist/ui/chat-store.js +64 -12
  247. package/dist/ui/chat-strings.js +85 -8
  248. package/dist/ui/chat-thread.js +204 -21
  249. package/dist/ui/effect-trial-controller.js +13 -8
  250. package/dist/ui/effect-trial-panel.js +19 -16
  251. package/dist/ui/effects-waker.js +70 -0
  252. package/dist/ui/fleet-canvas-layout.js +117 -0
  253. package/dist/ui/fleet-canvas.js +555 -0
  254. package/dist/ui/fleet-org.js +27 -12
  255. package/dist/ui/fleet-route-scene.js +207 -0
  256. package/dist/ui/fleet-route-worker.js +21 -0
  257. package/dist/ui/fleet-route.js +88 -0
  258. package/dist/ui/fleet-task-card.js +436 -0
  259. package/dist/ui/fleet-task-common.js +448 -0
  260. package/dist/ui/fleet-tasks.js +535 -0
  261. package/dist/ui/fleet-traffic.js +359 -0
  262. package/dist/ui/fleet-usage.js +435 -0
  263. package/dist/ui/panel-chat.js +264 -66
  264. package/dist/ui/panel-details.js +2 -1
  265. package/dist/ui/panel-fleet.js +65 -83
  266. package/dist/ui/panel-needs.js +4 -2
  267. package/dist/ui/panel-settings.js +56 -11
  268. package/dist/ui/preview.js +3 -2
  269. package/dist/ui/search-dialog.js +349 -0
  270. package/dist/ui/settings-appearance.js +44 -19
  271. package/dist/ui/settings-apply.js +3 -3
  272. package/dist/ui/settings-catalog.js +92 -0
  273. package/dist/ui/settings-confirm.js +7 -2
  274. package/dist/ui/settings-dialogs.js +40 -12
  275. package/dist/ui/settings-effects.js +4 -3
  276. package/dist/ui/settings-guests.js +117 -0
  277. package/dist/ui/settings-learn.js +25 -0
  278. package/dist/ui/settings-model.js +15 -6
  279. package/dist/ui/settings-mods.js +23 -9
  280. package/dist/ui/settings-owner-effects.js +15 -13
  281. package/dist/ui/settings-strings.js +122 -42
  282. package/dist/ui/settings-web-authority.js +91 -0
  283. package/dist/ui/shared/agend-auth.js +1 -1
  284. package/dist/ui/shared/app-i18n.js +11 -2
  285. package/dist/ui/shared/app-prefs.js +17 -4
  286. package/dist/ui/shared/app-route.js +19 -1
  287. package/dist/ui/shared/app-session.js +2 -0
  288. package/dist/ui/shared/app-shell.js +174 -30
  289. package/dist/ui/shared/app-store.js +26 -1
  290. package/dist/ui/shared/app-stream.js +5 -1
  291. package/dist/ui/shared/app-themes.js +140 -7
  292. package/dist/ui/shared/app.css +704 -20
  293. package/dist/ui/shared/app.js +26 -15
  294. package/dist/ui/shared/card-strings.js +8 -0
  295. package/dist/ui/shared/effect-app-runtime.js +68 -19
  296. package/dist/ui/shared/effect-chat-runtime.js +16 -3
  297. package/dist/ui/shared/effect-frame.js +26 -7
  298. package/dist/ui/shared/effect-geometry.js +24 -9
  299. package/dist/ui/shared/effect-owner-source.js +8 -1
  300. package/dist/ui/shared/effect-policy.js +21 -7
  301. package/dist/ui/shared/effect-profiler.js +31 -9
  302. package/dist/ui/shared/effect-registry.js +97 -50
  303. package/dist/ui/shared/effect-stock-snapshot.js +9 -0
  304. package/dist/ui/shared/effect-stock-trust.js +62 -10
  305. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  306. package/dist/ui/shared/effect-trial-source.js +1 -1
  307. package/dist/ui/shared/effect-trial-stage.js +54 -31
  308. package/dist/ui/shared/effect-trial.css +40 -0
  309. package/dist/ui/shared/fx-motion.js +67 -0
  310. package/dist/ui/shared/identity-card.js +100 -11
  311. package/dist/ui/shared/identity-color.js +14 -10
  312. package/dist/ui/shared/instance-nav.js +179 -23
  313. package/dist/ui/shared/owner-css.js +30 -1
  314. package/dist/ui/shared/panel-view.js +24 -4
  315. package/dist/ui/shared/tokens.css +9 -3
  316. package/dist/ui/shared/ui-icons.js +2 -0
  317. package/dist/ui/shared/ui-menu.js +20 -7
  318. package/dist/ui/shared/ui-toast.js +16 -3
  319. package/dist/ui/shared/usage-dialog.js +34 -3
  320. package/dist/ui/shared/view-strings.js +72 -0
  321. package/dist/ui/shared/web-authorize.js +198 -0
  322. package/dist/ui/web-terminal/terminal.css +4 -4
  323. package/dist/ui/web-terminal/terminal.html +1 -1
  324. package/dist/update-launch.d.ts +9 -0
  325. package/dist/update-launch.js +38 -12
  326. package/dist/update-launch.js.map +1 -1
  327. package/dist/usage/format-rich.js +6 -3
  328. package/dist/usage/format-rich.js.map +1 -1
  329. package/dist/usage/i18n-keys.d.ts +1 -1
  330. package/dist/usage/i18n-keys.js +2 -1
  331. package/dist/usage/i18n-keys.js.map +1 -1
  332. package/dist/usage/i18n.d.ts +13 -1
  333. package/dist/usage/i18n.js +29 -0
  334. package/dist/usage/i18n.js.map +1 -1
  335. package/dist/usage/providers.d.ts +81 -2
  336. package/dist/usage/providers.js +240 -38
  337. package/dist/usage/providers.js.map +1 -1
  338. package/dist/usage/usage-api.d.ts +5 -1
  339. package/dist/usage/usage-api.js +184 -58
  340. package/dist/usage/usage-api.js.map +1 -1
  341. package/dist/usage-report.d.ts +158 -0
  342. package/dist/usage-report.js +226 -0
  343. package/dist/usage-report.js.map +1 -0
  344. package/dist/usage-rollup.d.ts +51 -0
  345. package/dist/usage-rollup.js +168 -0
  346. package/dist/usage-rollup.js.map +1 -0
  347. package/dist/user-systemd.d.ts +24 -0
  348. package/dist/user-systemd.js +75 -0
  349. package/dist/user-systemd.js.map +1 -0
  350. package/dist/view-api.d.ts +39 -0
  351. package/dist/view-api.js +69 -6
  352. package/dist/view-api.js.map +1 -1
  353. package/dist/web-ambient.d.ts +72 -0
  354. package/dist/web-ambient.js +161 -0
  355. package/dist/web-ambient.js.map +1 -0
  356. package/dist/web-api.d.ts +98 -14
  357. package/dist/web-api.js +1027 -83
  358. package/dist/web-api.js.map +1 -1
  359. package/dist/web-appearance-policy.d.ts +4 -6
  360. package/dist/web-appearance-policy.js +5 -12
  361. package/dist/web-appearance-policy.js.map +1 -1
  362. package/dist/web-appearance-tools.d.ts +3 -0
  363. package/dist/web-appearance-tools.js +22 -3
  364. package/dist/web-appearance-tools.js.map +1 -1
  365. package/dist/web-appearance-view.d.ts +27 -2
  366. package/dist/web-appearance-view.js +129 -14
  367. package/dist/web-appearance-view.js.map +1 -1
  368. package/dist/web-appearance.d.ts +17 -3
  369. package/dist/web-appearance.js +85 -8
  370. package/dist/web-appearance.js.map +1 -1
  371. package/dist/web-asset-parts.d.ts +2 -2
  372. package/dist/web-asset-parts.js +3 -3
  373. package/dist/web-asset-parts.js.map +1 -1
  374. package/dist/web-auth.js +3 -1
  375. package/dist/web-auth.js.map +1 -1
  376. package/dist/web-backend-login.d.ts +130 -0
  377. package/dist/web-backend-login.js +225 -0
  378. package/dist/web-backend-login.js.map +1 -0
  379. package/dist/web-card.d.ts +14 -0
  380. package/dist/web-card.js +40 -6
  381. package/dist/web-card.js.map +1 -1
  382. package/dist/web-channel-echo.d.ts +7 -1
  383. package/dist/web-channel-echo.js +6 -2
  384. package/dist/web-channel-echo.js.map +1 -1
  385. package/dist/web-chat-history.d.ts +83 -0
  386. package/dist/web-chat-history.js +137 -5
  387. package/dist/web-chat-history.js.map +1 -1
  388. package/dist/web-commands.d.ts +6 -1
  389. package/dist/web-commands.js +1 -1
  390. package/dist/web-commands.js.map +1 -1
  391. package/dist/web-effect-bootstrap.d.ts +12 -0
  392. package/dist/web-effect-bootstrap.js +49 -0
  393. package/dist/web-effect-bootstrap.js.map +1 -0
  394. package/dist/web-effect-bundle.js +46 -1
  395. package/dist/web-effect-bundle.js.map +1 -1
  396. package/dist/web-effect-playback.d.ts +30 -0
  397. package/dist/web-effect-playback.js +71 -0
  398. package/dist/web-effect-playback.js.map +1 -0
  399. package/dist/web-effect-profiles.d.ts +4 -4
  400. package/dist/web-effect-service.d.ts +11 -4
  401. package/dist/web-effect-service.js +25 -4
  402. package/dist/web-effect-service.js.map +1 -1
  403. package/dist/web-effect-tools.d.ts +1 -0
  404. package/dist/web-effect-tools.js +4 -4
  405. package/dist/web-effect-tools.js.map +1 -1
  406. package/dist/web-effect-trials.d.ts +2 -2
  407. package/dist/web-effect-trials.js +3 -3
  408. package/dist/web-effect-trials.js.map +1 -1
  409. package/dist/web-effect-upload.js +1 -1
  410. package/dist/web-effect-upload.js.map +1 -1
  411. package/dist/web-effect-worker.d.ts +1 -1
  412. package/dist/web-effect-worker.js +2 -2
  413. package/dist/web-effect-worker.js.map +1 -1
  414. package/dist/web-effect.d.ts +1 -1
  415. package/dist/web-guest-access.d.ts +126 -0
  416. package/dist/web-guest-access.js +238 -0
  417. package/dist/web-guest-access.js.map +1 -0
  418. package/dist/web-guest-rooms.d.ts +82 -0
  419. package/dist/web-guest-rooms.js +135 -0
  420. package/dist/web-guest-rooms.js.map +1 -0
  421. package/dist/web-host-guard.d.ts +11 -0
  422. package/dist/web-host-guard.js +11 -0
  423. package/dist/web-host-guard.js.map +1 -1
  424. package/dist/web-identity-color.d.ts +1 -1
  425. package/dist/web-identity-color.js +2 -2
  426. package/dist/web-identity-color.js.map +1 -1
  427. package/dist/web-identity-palette.d.ts +1 -1
  428. package/dist/web-identity-palette.js +2 -2
  429. package/dist/web-identity-palette.js.map +1 -1
  430. package/dist/web-image-meta.d.ts +13 -0
  431. package/dist/web-image-meta.js +62 -0
  432. package/dist/web-image-meta.js.map +1 -1
  433. package/dist/web-libavoid.d.ts +14 -0
  434. package/dist/web-libavoid.js +46 -0
  435. package/dist/web-libavoid.js.map +1 -0
  436. package/dist/web-login.d.ts +47 -0
  437. package/dist/web-login.js +115 -0
  438. package/dist/web-login.js.map +1 -1
  439. package/dist/web-mod-agent.d.ts +1 -1
  440. package/dist/web-mod-agent.js +7 -5
  441. package/dist/web-mod-agent.js.map +1 -1
  442. package/dist/web-mod-bundle.d.ts +9 -1
  443. package/dist/web-mod-bundle.js +62 -3
  444. package/dist/web-mod-bundle.js.map +1 -1
  445. package/dist/web-mod-export.d.ts +9 -0
  446. package/dist/web-mod-export.js +21 -0
  447. package/dist/web-mod-export.js.map +1 -1
  448. package/dist/web-mod-store.d.ts +7 -1
  449. package/dist/web-mod-store.js +19 -6
  450. package/dist/web-mod-store.js.map +1 -1
  451. package/dist/web-owner-css-store.d.ts +44 -1
  452. package/dist/web-owner-css-store.js +70 -9
  453. package/dist/web-owner-css-store.js.map +1 -1
  454. package/dist/web-prefs.d.ts +14 -0
  455. package/dist/web-prefs.js +37 -4
  456. package/dist/web-prefs.js.map +1 -1
  457. package/dist/web-preview.d.ts +6 -2
  458. package/dist/web-preview.js +4 -2
  459. package/dist/web-preview.js.map +1 -1
  460. package/dist/web-returning-device.d.ts +41 -0
  461. package/dist/web-returning-device.js +57 -5
  462. package/dist/web-returning-device.js.map +1 -1
  463. package/dist/web-session-authorization.d.ts +132 -0
  464. package/dist/web-session-authorization.js +193 -0
  465. package/dist/web-session-authorization.js.map +1 -0
  466. package/dist/web-session.d.ts +70 -10
  467. package/dist/web-session.js +137 -22
  468. package/dist/web-session.js.map +1 -1
  469. package/dist/web-settings-patch.d.ts +49 -2
  470. package/dist/web-settings-patch.js +145 -1
  471. package/dist/web-settings-patch.js.map +1 -1
  472. package/dist/web-settings-tools.d.ts +94 -0
  473. package/dist/web-settings-tools.js +301 -0
  474. package/dist/web-settings-tools.js.map +1 -0
  475. package/dist/web-shell-routes.d.ts +1 -1
  476. package/dist/web-shell-routes.js +1 -1
  477. package/dist/web-shell-routes.js.map +1 -1
  478. package/dist/web-terminal-http.js +15 -2
  479. package/dist/web-terminal-http.js.map +1 -1
  480. package/dist/web-theme-store.d.ts +39 -0
  481. package/dist/web-theme-store.js +122 -11
  482. package/dist/web-theme-store.js.map +1 -1
  483. package/dist/web-theme.d.ts +31 -0
  484. package/dist/web-theme.js +99 -2
  485. package/dist/web-theme.js.map +1 -1
  486. package/dist/web-upload.d.ts +13 -0
  487. package/dist/web-upload.js +39 -3
  488. package/dist/web-upload.js.map +1 -1
  489. package/dist/windows-shortcuts.d.ts +78 -0
  490. package/dist/windows-shortcuts.js +203 -0
  491. package/dist/windows-shortcuts.js.map +1 -0
  492. package/dist/workflow-templates/default.md +4 -1
  493. package/dist/yaml-shared-nodes.d.ts +25 -0
  494. package/dist/yaml-shared-nodes.js +185 -0
  495. package/dist/yaml-shared-nodes.js.map +1 -0
  496. package/launcher/agend-install.cjs +553 -0
  497. package/launcher/install-admission.cjs +4 -39
  498. package/launcher/postinstall.cjs +14 -12
  499. package/package.json +4 -2
  500. package/scripts/preinstall-guard.cjs +2 -5
  501. package/templates/windows-shortcuts.ps1 +181 -0
  502. package/launcher/old-updater-note.cjs +0 -48
@@ -9,7 +9,7 @@
9
9
  // Public (/assets/): an anonymous View reader gets the same list, fed by View's roster read instead of status frames.
10
10
  import { html, useLayoutEffect, useRef, useState } from "./app-html.js";
11
11
  import { t } from "./app-i18n.js";
12
- import { appStore, createStore, useStore } from "./app-store.js";
12
+ import { appStore, createStore, guestPage, useStore } from "./app-store.js";
13
13
  import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
14
14
  import { Icon } from "./ui-icons.js";
15
15
  import { Skeleton } from "./ui-states.js";
@@ -30,12 +30,52 @@ const stored = (k) => { try { return localStorage.getItem(k); } catch { return n
30
30
  const nameOf = (it) => (it && (it.name != null ? it.name : it.instance_name)) || "";
31
31
 
32
32
  // ── Groups, labels, the filter ──
33
+ /** A team's group key: no tag can be it (a group name is never a control character), so a tag named like a team stays apart. */
34
+ export const TEAM_GROUP = "\u0001team:";
35
+ /**
36
+ * #2172: an agent in a team (fleet.yaml `teams.<name>.members`; `teams` on a status or roster row, in fleet.yaml's
37
+ * order) sits under its first team, whatever its tags say; one in no team under its first tag; a ClassicBot room under
38
+ * "Classic"; the rest under "Other".
39
+ */
33
40
  export function groupOf(it) {
34
41
  const tags = Array.isArray(it.tags) ? it.tags : [];
35
42
  if (tags.includes("classic")) return "Classic";
43
+ const teams = Array.isArray(it.teams) ? it.teams.filter((x) => typeof x === "string" && x) : [];
44
+ if (teams.length) return TEAM_GROUP + teams[0];
36
45
  if (tags.length) return tags[0];
37
46
  return "Other";
38
47
  }
48
+ /** "team" for a team's group, "tag" for a tag's; null for Classic, Other and Favorites. */
49
+ export function groupKind(g) {
50
+ if (typeof g !== "string") return null;
51
+ if (g.startsWith(TEAM_GROUP)) return "team";
52
+ return g === "Other" || g === "Classic" || g.charCodeAt(0) < 32 ? null : "tag";
53
+ }
54
+ /** A group's heading: a team's name; the fallback group ("Other") in the page's language; a tag as it is. */
55
+ export function groupLabel(g) { return g === "Other" ? tn("groupOther") : groupKind(g) === "team" ? g.slice(TEAM_GROUP.length) : g; }
56
+ /** #2172: what a group is, said on its heading — a team's icon, or "#" before a tag — so the grouping can be read. */
57
+ export function GroupMark({ group }) {
58
+ const kind = groupKind(group);
59
+ if (!kind) return null;
60
+ const what = tn(kind === "team" ? "groupTeam" : "groupTag");
61
+ return kind === "team"
62
+ ? html`<span class="v-group-kind team" title=${what}><${Icon} name="team" size=${13} /><span class="sr-only">${what}: </span></span>`
63
+ : html`<span class="v-group-kind tag" title=${what} aria-hidden="true">#</span>`;
64
+ }
65
+ /**
66
+ * #2050: the model as a person reads it. The server's resolver writes English for a model it has not resolved yet
67
+ * ("default (detected when it starts)") and for a CLI's own default ("x (default)"); those are shown in the page's language.
68
+ */
69
+ export function modelText(it, raw = it && it.model) {
70
+ if (!raw || typeof raw !== "string") return raw;
71
+ if (it.model_source === "unresolved") {
72
+ const why = /^default \((.*)\)$/.exec(raw)?.[1];
73
+ const reason = why === "detected when it starts" ? tn("modelWhyStart") : why === "this CLI does not report a default" ? tn("modelWhyCli") : why;
74
+ return reason ? tn("modelDefaultWhy", { reason }) : raw === "default" ? tn("modelDefault") : raw;
75
+ }
76
+ if (it.model_source === "cli-default") { const m = /^(.*) \(default\)$/.exec(raw); if (m) return tn("modelCliDefault", { model: m[1] }); }
77
+ return raw;
78
+ }
39
79
  const BACKEND_LABELS = { "claude-code": "Claude Code", "kiro-cli": "Kiro CLI", codex: "Codex", grok: "Grok Build", antigravity: "Antigravity", opencode: "OpenCode" };
40
80
  export function backendLabel(backend) { return BACKEND_LABELS[backend] || backend || "Unknown"; }
41
81
  export const KNOWN_BACKENDS = new Set(Object.keys(BACKEND_LABELS));
@@ -93,8 +133,8 @@ export function instanceTooltip(it) {
93
133
  if (it.display_name && it.display_name !== name) lines.push(it.display_name, `(${name})`);
94
134
  else lines.push(name);
95
135
  lines.push(line("tooltipBackend", backendLabel(it.backend)));
96
- const src = (s) => (s === "instance" || s === "classic" ? " (configured)" : s === "fleet-default" ? " (fleet default)" : "");
97
- lines.push(line("tooltipModel", (it.model || tn("tooltipUnavailable")) + src(it.model_source)));
136
+ const src = (s) => (s === "instance" || s === "classic" ? tn("srcConfigured") : s === "fleet-default" ? tn("srcFleetDefault") : "");
137
+ lines.push(line("tooltipModel", (modelText(it) || tn("tooltipUnavailable")) + src(it.model_source)));
98
138
  lines.push(line("tooltipStatus", status));
99
139
  lines.push(line("tooltipContext", context));
100
140
  if (it.effort) lines.push(line("tooltipEffort", it.effort + src(it.effort_source)));
@@ -108,6 +148,7 @@ const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order"; // View's key since #999
108
148
  const COLLAPSED_KEY = "agend_instance_collapsed";
109
149
  const FILTER_KEY = "agend_instance_filter";
110
150
  const FAVORITES_KEY = "agend_favorites"; // #1664
151
+ const SORT_KEY = "agend_nav_sort"; // #2179
111
152
  /**
112
153
  * A synced sidebar preference, as the JSON its reader parses: the value in force in memory (app-prefs.js — this
113
154
  * page's own change, else the host's; a full or blocked storage never brings an old one back), else this browser's copy.
@@ -138,6 +179,13 @@ function readFilter() {
138
179
  return { q: f && typeof f.q === "string" ? f.q : "", status: strings(f && f.status).filter((s) => STATUS_FACETS.includes(s)), cli: strings(f && f.cli) };
139
180
  } catch { return { q: "", status: [], cli: [] }; }
140
181
  }
182
+ /** #2179: how the list sorts — { mode: "custom" (the person's order; the default) | "recent", favs: "fixed" | "recent" }. */
183
+ function readSort() {
184
+ try {
185
+ const s = JSON.parse(inForce("nav.sort", SORT_KEY) || "{}");
186
+ return { mode: s && s.mode === "recent" ? "recent" : "custom", favs: s && s.favs === "recent" ? "recent" : "fixed" };
187
+ } catch { return { mode: "custom", favs: "fixed" }; }
188
+ }
141
189
  function readFavorites() {
142
190
  try { const a = JSON.parse(inForce("nav.favorites", FAVORITES_KEY) || "[]"); return Array.isArray(a) ? [...new Set(a.filter((x) => typeof x === "string"))] : []; } catch { return []; }
143
191
  }
@@ -151,7 +199,9 @@ export function computeOrder(roster, order) {
151
199
  if (ia !== ib) return ia - ib;
152
200
  if (a === "Other") return 1;
153
201
  if (b === "Other") return -1;
154
- return a.localeCompare(b);
202
+ const ta = groupKind(a) === "team", tb = groupKind(b) === "team";
203
+ if (ta !== tb) return ta ? -1 : 1; // #2172: teams first, then tags
204
+ return groupLabel(a).localeCompare(groupLabel(b));
155
205
  });
156
206
  const instByGroup = new Map();
157
207
  for (const [g, items] of groups) {
@@ -174,12 +224,18 @@ function moveIn(arr, moving, target, before) {
174
224
  }
175
225
 
176
226
  /** The list's state for the page's life: this browser's order, folded groups and filter (each also kept in storage). */
177
- export const navStore = createStore({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() });
227
+ export const navStore = createStore({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites(), sort: readSort() });
178
228
  /** Read them again from this browser's storage — what a page load does (the store above is made that way). */
179
229
  export function loadNavPrefs() {
180
- navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() });
230
+ navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites(), sort: readSort() });
181
231
  namesStore.set({ primary: readNamesPrimary() });
182
232
  }
233
+ /** #2179: change how the list sorts ({ mode?, favs? }): the same on every page and device. */
234
+ export function setNavSort(patch) {
235
+ const sort = { ...navStore.get().sort, ...patch };
236
+ navStore.set({ sort });
237
+ savePref("nav.sort", { mode: sort.mode, favs: sort.favs });
238
+ }
183
239
  /** Change the filter ({ q?, status?, cli? }): the same on every page, and kept by this browser. */
184
240
  export function setNavFilter(patch) {
185
241
  const filter = { ...navStore.get().filter, ...patch };
@@ -188,7 +244,7 @@ export function setNavFilter(patch) {
188
244
  }
189
245
  function setCollapsed(collapsed) { navStore.set({ collapsed }); savePref("nav.collapsed", [...collapsed]); }
190
246
  // Another device rearranged, folded or filtered: this page's list follows (its cache is already updated).
191
- onPrefsChanged((keys) => { if (keys.has("nav.order") || keys.has("nav.collapsed") || keys.has("nav.filter") || keys.has("nav.favorites")) loadNavPrefs(); });
247
+ onPrefsChanged((keys) => { if (keys.has("nav.order") || keys.has("nav.collapsed") || keys.has("nav.filter") || keys.has("nav.favorites") || keys.has("nav.sort")) loadNavPrefs(); });
192
248
 
193
249
  // ── #1664: favorites — starred instances, pinned at the top in the order they were starred (or moved) ──
194
250
  export const MAX_FAVORITES = 50;
@@ -339,6 +395,71 @@ export function InstanceAvatar({ it, state, cli = false }) {
339
395
  const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet === "working" ? t("app.statusWorking") : facet === "idle" ? t("app.statusIdle")
340
396
  : facet === "crashed" ? t("app.statusCrashed") : t("app.statusStopped"));
341
397
 
398
+ // ── #2179: when each instance last replied to a person (the status frame's last_reply) ──
399
+ /** A reply this recent (minutes) is highlighted; the highlight fades to nothing over this time. */
400
+ export const REPLY_FRESH_MIN = 10;
401
+ /** Minutes since the instance last replied to a person, or null (none kept, or no time on this row). */
402
+ export function replyAge(it, now = Date.now()) {
403
+ const at = it && typeof it.last_reply === "string" ? Date.parse(it.last_reply) : NaN;
404
+ return Number.isFinite(at) ? Math.max(0, (now - at) / 60000) : null;
405
+ }
406
+ /** "just now", "2 min ago", "3 h ago", "yesterday", "4 d ago" — in the page's language. */
407
+ export function replyText(mins) {
408
+ if (mins < 1) return tn("replyJustNow");
409
+ if (mins < 60) return tn("replyMinutes", { n: Math.floor(mins) });
410
+ if (mins < 1440) return tn("replyHours", { n: Math.floor(mins / 60) });
411
+ if (mins < 2880) return tn("replyYesterday");
412
+ return tn("replyDays", { n: Math.floor(mins / 1440) });
413
+ }
414
+ /** How strong the highlight is: 1 just after a reply, down to 0 at REPLY_FRESH_MIN minutes. */
415
+ export function replyFade(mins) { return mins == null || mins >= REPLY_FRESH_MIN ? 0 : 1 - mins / REPLY_FRESH_MIN; }
416
+ /** The highlight's strength on the row, through the CSSOM (style-src has no 'unsafe-inline'). */
417
+ function paintReply(el, fade) {
418
+ if (!el) return;
419
+ if (fade > 0) el.style.setProperty("--reply-a", fade.toFixed(3)); else el.style.removeProperty("--reply-a");
420
+ }
421
+ /** Most recent reply first; rows with none keep their order after them (a stable sort). */
422
+ function byRecentReply(age) {
423
+ return (a, b) => {
424
+ const x = age(a), y = age(b);
425
+ if (x == null || y == null) return x == null ? (y == null ? 0 : 1) : -1;
426
+ return x - y;
427
+ };
428
+ }
429
+ /** The group that holds every non-favorite row in "recent" mode: no tag or team can be it, and it is never saved. */
430
+ const RECENT_GROUP = "\u0000recent";
431
+
432
+ /** #2179: the sort button and its menu — how the list sorts, said on the button. */
433
+ function SortMenu({ sort }) {
434
+ const [open, setOpen] = useState(false);
435
+ const box = useRef(null);
436
+ useLayoutEffect(() => {
437
+ if (!open) return undefined;
438
+ const doc = box.current && box.current.ownerDocument;
439
+ if (!doc) return undefined;
440
+ const away = (e) => { if (box.current && !box.current.contains(e.target)) setOpen(false); };
441
+ const esc = (e) => { if (e.key === "Escape") { e.preventDefault(); setOpen(false); } };
442
+ doc.addEventListener("pointerdown", away, true);
443
+ doc.addEventListener("keydown", esc, true);
444
+ return () => { doc.removeEventListener("pointerdown", away, true); doc.removeEventListener("keydown", esc, true); };
445
+ }, [open]);
446
+ const recent = sort.mode === "recent";
447
+ const radio = (group, value, label, hint) => html`<label class="nav-sort-opt"><input type="radio" name=${`nav-sort-${group}`} value=${value}
448
+ checked=${sort[group] === value} onChange=${() => setNavSort({ [group]: value })} /><span>${label}${hint ? html`<span class="nav-sort-hint">${hint}</span>` : null}</span></label>`;
449
+ return html`<div class="nav-sort" ref=${box}>
450
+ <button type="button" class=${`chip-btn nav-sort-btn${recent ? " on" : ""}`} aria-haspopup="dialog" aria-expanded=${open ? "true" : "false"}
451
+ title=${tn("sortTitle")} onClick=${() => setOpen(!open)}><${Icon} name="sort" size=${12} /><span>${tn(recent ? "sortRecent" : "sortCustom")}</span></button>
452
+ ${open ? html`<div class="nav-sort-menu" role="dialog" aria-label=${tn("sortTitle")}>
453
+ <fieldset><legend>${tn("sortTitle")}</legend>
454
+ ${radio("mode", "custom", tn("sortCustom"), tn("sortCustomHint"))}
455
+ ${radio("mode", "recent", tn("sortRecent"), tn("sortRecentHint"))}</fieldset>
456
+ <fieldset><legend>${tn("sortFavs")}</legend>
457
+ ${radio("favs", "fixed", tn("sortFavsFixed"), "")}
458
+ ${radio("favs", "recent", tn("sortFavsRecent"), "")}</fieldset>
459
+ </div>` : null}
460
+ </div>`;
461
+ }
462
+
342
463
  /**
343
464
  * The sidebar's instance list. `route`: the page's (which instance is open, and which view a row opens). `items`: the
344
465
  * rows (status frames' instances, or View's roster for an anonymous reader); `loaded` false shows a skeleton.
@@ -352,6 +473,9 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
352
473
  const filterRef = useRef(null);
353
474
  const list = useRef(null);
354
475
  const drag = useRef(null);
476
+ // #2179: the reply times and their highlight move on by themselves — the list is drawn again every 30 s.
477
+ const [, setTick] = useState(0);
478
+ useLayoutEffect(() => { const id = setInterval(() => setTick((n) => n + 1), 30000); return () => clearInterval(id); }, []);
355
479
  // #1700: which facet groups are open — this device's (screen layout, not a synced preference), read when the list mounts.
356
480
  const [facetOpen, setFacetOpen] = useState(readFacetsOpen);
357
481
  const toggleFacet = (key) => setFacetOpen((cur) => { const next = { ...cur, [key]: !cur[key] }; writeFacetsOpen(next); return next; });
@@ -375,10 +499,30 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
375
499
  const favs = nav.favorites.filter((n) => known.has(n));
376
500
  const favSet = new Set(favs);
377
501
  const canStar = !viewOnly && prefsWritable();
378
- const shownGroups = favs.length ? [FAV_GROUP, ...groupNames] : groupNames;
379
- const shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
380
- if (favs.length) shownInsts.set(FAV_GROUP, favs);
381
- const f = nav.filter;
502
+ // #1920: a guest's list is to pick from — no reordering and no filters (one this device kept would hide rows with no
503
+ // way to clear it).
504
+ const guest = guestPage();
505
+ // #2179: when each row last replied (a guest's list has no times). "recent" mode (never a guest's): the favorites keep
506
+ // their order or are sorted too, and every other row is one list, the last reply first. "custom" (the default) is the
507
+ // person's own order — a reply never moves a row.
508
+ const now = Date.now();
509
+ // The anonymous View reader (viewOnly) has no reply times (/api/profiles carries none) and no way to change the sort:
510
+ // its list is always Custom, whatever this browser kept from a signed-in page (#2181 review).
511
+ const ageOf = (n) => (guest || viewOnly ? null : replyAge(byName.get(n), now));
512
+ const recent = !guest && !viewOnly && nav.sort.mode === "recent";
513
+ const favsSorted = recent && nav.sort.favs === "recent";
514
+ let shownGroups, shownInsts;
515
+ if (recent) {
516
+ const sortRecent = byRecentReply(ageOf);
517
+ const rest = groupNames.flatMap((g) => instByGroup.get(g) || []).filter((n) => !favSet.has(n)).sort(sortRecent);
518
+ shownGroups = [...(favs.length ? [FAV_GROUP] : []), ...(rest.length ? [RECENT_GROUP] : [])];
519
+ shownInsts = new Map([[FAV_GROUP, favsSorted ? [...favs].sort(sortRecent) : favs], [RECENT_GROUP, rest]]);
520
+ } else {
521
+ shownGroups = favs.length ? [FAV_GROUP, ...groupNames] : groupNames;
522
+ shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
523
+ if (favs.length) shownInsts.set(FAV_GROUP, favs);
524
+ }
525
+ const f = guest ? { q: "", status: [], cli: [] } : nav.filter;
382
526
  const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
383
527
  // #1936: while a filter is on, the instance you have open stays on top, pinned and marked "Current", whether or not
384
528
  // it matches; a match is not repeated below. "N / M" still counts the filter's matches (the pinned row counts only
@@ -392,7 +536,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
392
536
  const flip = (key, value) => { const cur = new Set(navStore.get().filter[key]); if (cur.has(value)) cur.delete(value); else cur.add(value); setNavFilter({ [key]: [...cur] }); };
393
537
  // Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
394
538
  // their group.
395
- const dragProps = (item) => shown.active ? {} : {
539
+ const dragProps = (item) => shown.active || guest || recent ? {} : { // #2179: not while sorted by reply
396
540
  draggable: true,
397
541
  onDragStart: (e) => { drag.current = item; e.currentTarget.classList.add("dragging"); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", item.name); } catch { /* Firefox needs data */ } },
398
542
  onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
@@ -447,18 +591,24 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
447
591
  // #1958: its identity colour — the ring, a left bar and/or the name, as resolved; painted through the CSSOM.
448
592
  const id = identityOf(it);
449
593
  const full = !starred && favs.length >= MAX_FAVORITES;
450
- return html`<li key=${name} class="v-li"><a class=${`inst v-inst${on ? " active" : ""} ${identityClasses(id)}`} ref=${(el) => paintIdentity(el, id)} href=${rowPath(name)} title=${instanceTooltip(it)}
594
+ // #2179 (B5, the user's pick): the % (and the ★ beside the row) on the name's line; the time of the last reply at the
595
+ // end of the second line, highlighted for REPLY_FRESH_MIN minutes, fading by itself (a reply under a minute pulses once).
596
+ const mins = ageOf(name), fade = replyFade(mins);
597
+ const two = !!secondary || mins != null;
598
+ return html`<li key=${name} class=${`v-li${two ? " two-line" : ""}`}><a class=${`inst v-inst${on ? " active" : ""}${fade > 0 ? " replied" : ""}${mins != null && mins < 1 ? " just" : ""} ${identityClasses(id)}`}
599
+ ref=${(el) => { paintIdentity(el, id); paintReply(el, fade); }} href=${rowPath(name)} title=${instanceTooltip(it)}
451
600
  aria-current=${on ? "page" : undefined} onClick=${onPick} ...${fav ? {} : dragProps({ type: "instance", name, group: g })}>
452
601
  <${InstanceAvatar} it=${it} state=${dotState(it, own(exec, name) ? exec[name] : null, waiting)} cli=${true} />
453
- <span class="inst-text"><span class="inst-name" title=${label}>${label}</span>
454
- ${secondary ? html`<span class="inst-alt" title=${secondary}>${secondary}</span>` : null}
602
+ <span class="inst-text"><span class="inst-line"><span class="inst-name" title=${label}>${label}</span>
603
+ <span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span></span>
604
+ ${two ? html`<span class="inst-line inst-line2"><span class="inst-alt" title=${secondary || undefined}>${secondary || ""}</span>
605
+ ${mins != null ? html`<span class=${`inst-when${fade > 0 ? " fresh" : ""}`} title=${tn("replyTitle", { time: new Date(Date.parse(it.last_reply)).toLocaleString() })}>${replyText(mins)}</span>` : null}</span>` : null}
455
606
  ${facet === "needs" ? html`<span class="inst-sub"><span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span></span>` : null}
456
607
  <span class="sr-only">${tn("tooltipBackend", { value: backendLabel(it.backend) })}</span></span>
457
- <span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
458
608
  </a>${fav && editing && canStar ? html`<button type="button" class="icon-btn inst-move" aria-label=${tn("favUp", { name: label })}
459
- title=${shown.active ? tn("favFiltered") : tn("favUp", { name: label })} disabled=${shown.active || i === 0} onClick=${() => { if (!shown.active) moveFavorite(name, -1, known); }}><${Icon} name="up" size=${14} /></button><button type="button" class="icon-btn inst-move"
460
- aria-label=${tn("favDown", { name: label })} title=${shown.active ? tn("favFiltered") : tn("favDown", { name: label })} disabled=${shown.active || i === count - 1}
461
- onClick=${() => { if (!shown.active) moveFavorite(name, 1, known); }}><${Icon} name="down" size=${14} /></button>` : null}${canStar
609
+ title=${shown.active ? tn("favFiltered") : favsSorted ? tn("favSorted") : tn("favUp", { name: label })} disabled=${shown.active || favsSorted || i === 0} onClick=${() => { if (!shown.active && !favsSorted) moveFavorite(name, -1, known); }}><${Icon} name="up" size=${14} /></button><button type="button" class="icon-btn inst-move"
610
+ aria-label=${tn("favDown", { name: label })} title=${shown.active ? tn("favFiltered") : favsSorted ? tn("favSorted") : tn("favDown", { name: label })} disabled=${shown.active || favsSorted || i === count - 1}
611
+ onClick=${() => { if (!shown.active && !favsSorted) moveFavorite(name, 1, known); }}><${Icon} name="down" size=${14} /></button>` : null}${canStar
462
612
  ? html`<button type="button" class=${`icon-btn inst-star${starred ? " on" : ""}`} aria-pressed=${starred ? "true" : "false"} disabled=${full}
463
613
  aria-label=${tn(starred ? "favRemove" : "favAdd", { name: label })} title=${full ? tn("favFull", { max: MAX_FAVORITES }) : tn(starred ? "favRemove" : "favAdd", { name: label })}
464
614
  onClick=${() => toggleFavorite(name, known)}><${Icon} name="star" size=${14} /></button>` : null}</li>`;
@@ -467,6 +617,8 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
467
617
  return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
468
618
  <div data-trust="navigation" class="side-section" id="instanceList" ref=${list}>
469
619
  <div class="inst-head"><h2 class="side-label">${tn("instancesLabel")}</h2>
620
+ ${canStar && items.length && !guest ? html`<${SortMenu} sort=${nav.sort} />`
621
+ : recent ? html`<span class="chip-btn nav-sort-btn on nav-sort-static"><${Icon} name="sort" size=${12} /><span>${tn("sortRecent")}</span></span>` : null}
470
622
  ${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
471
623
  ${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
472
624
  ${!loaded ? html`<${Skeleton} lines=${4} />` : !items.length ? html`<p class="side-empty">${t("app.noInstances")}</p>` : null}
@@ -474,6 +626,10 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
474
626
  <div class="v-group-head pinned-head"><span class="grow">${tn("pinnedCurrent")}</span></div>
475
627
  <ul class="inst-list">${row(pinned, PINNED_GROUP, 0, 1)}</ul></div>` : null}
476
628
  ${listed.map(({ group: g, names }) => {
629
+ // #2179: in "recent" mode every other row is one list — open, not dragged, not saved as a group.
630
+ if (g === RECENT_GROUP) return html`<div key=${g} class="v-group v-recent" role="group" aria-label=${tn("recentOthers")}>
631
+ <div class="v-group-head recent-head"><${Icon} name="clock" size=${14} /><span class="grow">${tn("recentOthers")}</span><span class="count">${names.length}</span></div>
632
+ <ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul></div>`;
477
633
  // #1664: Favorites — always open, not dragged (↑ ↓ in Edit), named in the page's language.
478
634
  if (g === FAV_GROUP) return html`<div key=${g} class="v-group v-fav" role="group" aria-label=${tn("favorites")}>
479
635
  <div class="v-group-head fav-head"><${Icon} name="star" size=${14} cls="fav-mark" /><span class="grow">${tn("favorites")}</span><span class="count">${names.length}</span></div>
@@ -481,14 +637,14 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
481
637
  const folded = nav.collapsed.has(g);
482
638
  return html`<div key=${g} class="v-group">
483
639
  <button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
484
- title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
485
- <${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
640
+ title=${tn(folded ? "expandGroup" : "collapseGroup", { group: groupLabel(g) })} ...${dragProps({ type: "group", name: g })}>
641
+ <${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><${GroupMark} group=${g} /><span class="grow">${groupLabel(g)}</span><span class="count">${names.length}</span></button>
486
642
  ${folded ? null : html`<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul>`}
487
643
  </div>`;
488
644
  })}
489
645
  ${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
490
646
  </div>
491
- <div class="v-filter">
647
+ ${guest ? null : html`<div class="v-filter">
492
648
  <label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
493
649
  <input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
494
650
  onInput=${(e) => setNavFilter({ q: e.target.value })}
@@ -504,7 +660,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
504
660
  </div>
505
661
  <p class="note nav-shown" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
506
662
  </div>
507
- </div>
663
+ </div>`}
508
664
  </div>`;
509
665
  }
510
666
 
@@ -80,6 +80,7 @@ function sync() {
80
80
  let valid = false;
81
81
  try { valid = link.disabled !== true && !!link.sheet && link.sheet.ownerNode === link && link.sheet.disabled !== true && link.sheet.href === new URL(href, location.href).href; } catch { /* unknown stylesheet */ }
82
82
  linkState = valid ? "on" : "unknown"; life(linkState, valid ? "loaded" : "load_unverified");
83
+ if (valid) shownHere(sha); // #2198: an agent's trial is on this screen now
83
84
  });
84
85
  link.addEventListener("error", () => { if (!current()) return; linkState = "unknown"; life("unknown", "load_failed"); });
85
86
  life("loading", "loading");
@@ -189,12 +190,40 @@ async function reread() {
189
190
  if (body && mine === gen) reconcile(body); // nothing newer happened since this was asked
190
191
  } catch { /* the next change tries again */ }
191
192
  }
193
+ /**
194
+ * #2198: an agent's trial (General saved it) waits on the host until a person's page shows it. This page shows it once
195
+ * its stylesheet has loaded on screen, and says so: the host starts the keep window then (the dead-man switch, from when
196
+ * a person can see the Keep button). If the host cannot be told, this page reverts it itself at KEEP_SECONDS, as it does
197
+ * for a version it saved.
198
+ */
199
+ let waitingShown = null; // { id, sha } of an agent's trial this page is showing and has not reported yet
200
+ function shownHere(sha) {
201
+ const w = waitingShown;
202
+ if (!w || w.sha !== sha) return;
203
+ waitingShown = null;
204
+ const trial = ownerCssTrial.get();
205
+ if (!trial || trial.id !== w.id) return;
206
+ post("/ui/owner-css/shown", { trial: w.id }).then((r) => {
207
+ if (ownerCssTrial.get() !== trial) return;
208
+ const ms = r.ok && r.body && Number.isFinite(r.body.remaining_ms) ? r.body.remaining_ms : null;
209
+ if (ms === null) { startOwnerCssTrial(w.id, w.sha, trial.previous, KEEP_SECONDS, true); return; }
210
+ startOwnerCssTrial(w.id, w.sha, trial.previous, ms / 1000 + 1, false);
211
+ }, () => { if (ownerCssTrial.get() === trial) startOwnerCssTrial(w.id, w.sha, trial.previous, KEEP_SECONDS, true); });
212
+ }
192
213
  /** Match the host: the version in force, and whether one is on trial (another device's included) — or none any more. */
193
214
  function reconcile(state) {
194
215
  const pending = state.pending && state.pending.sha === state.active && typeof state.pending.id === "string" ? state.pending : null;
195
216
  const trial = ownerCssTrial.get();
196
- if (!pending) { if (trial) dropTrial(); showActive(state.active, state.effectState); return; }
217
+ if (!pending) { waitingShown = null; if (trial) dropTrial(); showActive(state.active, state.effectState); return; }
197
218
  if (trial && trial.id === pending.id) return; // already asking about this one
219
+ if (pending.waiting === true) {
220
+ // Shown here: its stylesheet goes in, and once it has loaded this page reports it (shownHere). Until then the bar
221
+ // waits on the host's word, as for another device's trial.
222
+ waitingShown = { id: pending.id, sha: pending.sha };
223
+ startOwnerCssTrial(pending.id, pending.sha, pending.previous, HOST_TRIAL_SECONDS, false);
224
+ if (linkState === "on" && ownedLink && ownedLink.getAttribute("href") === `/ui/owner-css.css?v=${encodeURIComponent(pending.sha)}`) shownHere(pending.sha);
225
+ return;
226
+ }
198
227
  // The host's own remaining time and a second (never this device's clock): it has settled the trial by then.
199
228
  const remaining = typeof pending.remaining_ms === "number" && Number.isFinite(pending.remaining_ms) ? pending.remaining_ms : 0;
200
229
  startOwnerCssTrial(pending.id, pending.sha, pending.previous, remaining / 1000 + 1, false);
@@ -16,7 +16,7 @@ import { HeaderTools, VIEW_SCALE, textSizeStore } from "./header-tools.js";
16
16
  import { readStream } from "./read-stream.js";
17
17
  // The usage dialog's metric row lives with the dialog now (usage-dialog.js); re-exported for what imported it here.
18
18
  export { UsageMetric } from "./usage-dialog.js";
19
- import { backendLabel, STATUS_KEYS } from "./instance-nav.js";
19
+ import { backendLabel, modelText, STATUS_KEYS } from "./instance-nav.js";
20
20
  import { viewStore } from "./view-roster-store.js";
21
21
 
22
22
  /** #1580: the app's stream says the fleet is not answering (a restart): the View's polls skip their turn until it is. */
@@ -27,7 +27,7 @@ import { Dialog } from "./ui-dialog.js";
27
27
  import { Empty, ErrorState, Skeleton } from "./ui-states.js";
28
28
  import { Icon } from "./ui-icons.js";
29
29
  import "./view-strings.js";
30
- import { checkCard, fitPicture, IdentityCard, useCard } from "./identity-card.js";
30
+ import { BannerFocusPreview, checkCard, fitPicture, IdentityCard, useBannerPreview, useCard } from "./identity-card.js";
31
31
  import { confirmDialog } from "./ui-confirm.js";
32
32
  import { openColorDialog } from "./identity-color.js";
33
33
 
@@ -305,7 +305,7 @@ function Card({ it, viewOnly, onEdit }) {
305
305
  <div class="v-avatar">${it.has_avatar ? html`<img ref=${avatar} alt="" />` : html`<span class="v-initial" aria-hidden="true">${initial}</span>`}</div>
306
306
  <div class="v-card-body">
307
307
  <div class="v-card-line"><strong>${it.display_name || it.instance_name}</strong>${it.role ? html`<span class="v-role">${it.role}</span>` : null}</div>
308
- <div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${it.model}` : ""}${ctx}</div>
308
+ <div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${modelText(it)}` : ""}${ctx}</div>
309
309
  ${open && it.description ? html`<p class="v-card-desc">${it.description}</p>` : null}
310
310
  </div>
311
311
  <button type="button" class="icon-btn" onClick=${toggle} aria-expanded=${open ? "true" : "false"} aria-label=${tn(open ? "collapseProfile" : "expandProfile")} title=${tn(open ? "collapseProfile" : "expandProfile")}>
@@ -332,6 +332,11 @@ function EditProfile({ it, onClose }) {
332
332
  const [removeCard, setRemoveCard] = useState({ portrait: false, banner: false });
333
333
  const [clearCard, setClearCard] = useState(false);
334
334
  const pics = { portrait: useRef(null), banner: useRef(null) };
335
+ // #2086: the banner's focus — this person's (null: follow the agent's, else the centre), and the picture to preview:
336
+ // a newly chosen file, else the banner drawn now.
337
+ // `agent`: the agent's own focus (what "Follow the agent's" shows; else the centre).
338
+ const [focus, setFocus] = useState({ value: 50, mine: null, start: null, agent: null });
339
+ const [bannerPreview, preview] = useBannerPreview();
335
340
  useEffect(() => {
336
341
  let live = true;
337
342
  // The card's fields stay disabled until this read is in (#1889 review: a draft typed meanwhile was overwritten);
@@ -343,6 +348,11 @@ function EditProfile({ it, onClose }) {
343
348
  const resolved = checkCard(it.instance_name, body);
344
349
  const mine = (k) => (resolved && resolved[k] && resolved[k].by === "human" ? resolved[k] : null);
345
350
  setCard({ mine: { tagline: mine("tagline"), bio: mine("bio"), portrait: mine("portrait"), banner: mine("banner") }, limits: body.limits || null });
351
+ const bf = resolved && resolved.banner_focus;
352
+ const agent = Number.isInteger(body.banner_focus_agent) && body.banner_focus_agent >= 0 && body.banner_focus_agent <= 100 ? body.banner_focus_agent : null;
353
+ const mineFocus = bf && bf.by === "human" ? bf.value : null;
354
+ setFocus({ value: mineFocus ?? agent ?? 50, mine: mineFocus, start: mineFocus, agent });
355
+ if (resolved && resolved.banner) preview.fromSrc(resolved.banner.src);
346
356
  setCardText({ tagline: mine("tagline") ? mine("tagline").text : "", bio: mine("bio") ? mine("bio").text : "" });
347
357
  }).catch(() => { if (live && lease.current()) setCard(none); });
348
358
  return () => { live = false; };
@@ -360,6 +370,7 @@ function EditProfile({ it, onClose }) {
360
370
  const clearing = clearCard;
361
371
  const before = card && card.mine ? { tagline: card.mine.tagline ? card.mine.tagline.text : "", bio: card.mine.bio ? card.mine.bio.text : "" } : { tagline: "", bio: "" };
362
372
  if (!clearing) for (const k of ["tagline", "bio"]) if (cardText[k] !== before[k]) draft[`card_${k}`] = cardText[k];
373
+ if (!clearing && focus.mine !== focus.start) draft.card_banner_focus = focus.mine; // #2086
363
374
  const cardPics = {};
364
375
  try {
365
376
  // A picture too large is fitted first (after asking) — before anything is written, so declining writes nothing.
@@ -422,10 +433,19 @@ function EditProfile({ it, onClose }) {
422
433
  <label class="field"><span>${tn("cardTagline")}</span><input value=${cardText.tagline} onInput=${(e) => setCardText({ ...cardText, tagline: e.target.value })} /></label>
423
434
  <label class="field"><span>${tn("cardBio")}</span><textarea rows="4" value=${cardText.bio} onInput=${(e) => setCardText({ ...cardText, bio: e.target.value })}></textarea></label>
424
435
  ${["portrait", "banner"].map((slot) => html`<div key=${slot} class="form">
425
- <label class="field"><span>${tn(slot === "portrait" ? "cardPortrait" : "cardBanner")}</span><input ref=${pics[slot]} type="file" accept="image/png,image/jpeg,image/webp" /></label>
436
+ <label class="field"><span>${tn(slot === "portrait" ? "cardPortrait" : "cardBanner")}</span><input ref=${pics[slot]} type="file" accept="image/png,image/jpeg,image/webp"
437
+ onChange=${slot === "banner" ? (e) => { const fl = e.currentTarget.files && e.currentTarget.files[0]; if (fl) preview.fromFile(fl); } : undefined} /></label>
426
438
  ${card && card.mine && card.mine[slot] ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removeCard[slot] ? "true" : "false"}
427
439
  onClick=${() => setRemoveCard({ ...removeCard, [slot]: !removeCard[slot] })}>${tn(removeCard[slot] ? "cardKeep" : "cardRemove")}</button></div>` : null}
428
440
  </div>`)}
441
+ ${bannerPreview ? html`<div class="field bf-field">
442
+ <span>${tn("cardBannerFocus")}</span>
443
+ <${BannerFocusPreview} src=${bannerPreview.src} focus=${focus.value} />
444
+ <input type="range" min="0" max="100" step="1" value=${focus.value} aria-label=${tn("cardBannerFocus")}
445
+ onInput=${(e) => { const v = Number(e.currentTarget.value); setFocus({ ...focus, value: v, mine: v }); }} />
446
+ <span class="note">${tn("cardBannerFocusNote")}</span>
447
+ ${focus.mine !== null ? html`<div class="field-row"><button type="button" class="btn btn-sm" onClick=${() => setFocus({ ...focus, value: focus.agent ?? 50, mine: null })}>${tn("cardFocusFollow")}</button></div>` : null}
448
+ </div>` : null}
429
449
  </fieldset>
430
450
  <div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${clearCard ? "true" : "false"} onClick=${() => setClearCard(!clearCard)}>${tn(clearCard ? "keepCard" : "clearCard")}</button>
431
451
  ${clearCard ? html`<span class="muted">${tn("clearCardNote")}</span>` : null}</div>
@@ -9,7 +9,12 @@
9
9
  * surfaces it is used on, and focus rings and strong borders meet 3:1 (tests/web-tokens-1408.test.ts).
10
10
  *
11
11
  * Inter is bundled (/assets/inter.woff2, SIL OFL 1.1 — see fonts/README.md): the CSP allows fonts from 'self' only,
12
- * and nothing is loaded from another site. Chinese falls back to the system's CJK fonts, in the order below. */
12
+ * and nothing is loaded from another site. Chinese falls back to the system's CJK fonts, in the order below.
13
+ *
14
+ * Emoji (#1918 follow-up, the user's tofu boxes): every stack ends with the platforms' colour emoji fonts and the
15
+ * `emoji` generic (--font-emoji), so an emoji the text fonts lack is drawn by Windows' Segoe UI Emoji, macOS's Apple
16
+ * Color Emoji or Linux's Noto Color Emoji rather than as a box — for the stock stacks and each theme font
17
+ * (app.css: .th-font-system, .th-font-mono). A browser tries the families in order for each character. */
13
18
 
14
19
  @font-face {
15
20
  font-family: "Inter";
@@ -22,8 +27,9 @@
22
27
 
23
28
  :root {
24
29
  /* ── Type ── */
25
- --font-ui: Inter, "PingFang TC", "PingFang SC", "Noto Sans TC", "Noto Sans SC", "Noto Sans CJK TC", "Noto Sans CJK SC", "Microsoft JhengHei", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
26
- --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
30
+ --font-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji;
31
+ --font-ui: Inter, "PingFang TC", "PingFang SC", "Noto Sans TC", "Noto Sans SC", "Noto Sans CJK TC", "Noto Sans CJK SC", "Microsoft JhengHei", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif, var(--font-emoji);
32
+ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, var(--font-emoji);
27
33
  --fs-xs: 12px; --lh-xs: 16px;
28
34
  --fs-sm: 13px; --lh-sm: 18px;
29
35
  --fs-md: 15px; --lh-md: 24px; /* message text */
@@ -37,8 +37,10 @@ const P = {
37
37
  type: () => html`<path d="M4 7V5h16v2M9 19h6M12 5v14"/>`,
38
38
  chart: () => html`<path d="M3 20h18M6 16v-4M11 16V8M16 16v-6"/>`,
39
39
  up: () => html`<path d="M12 19V5M5 12l7-7 7 7"/>`,
40
+ sort: () => html`<path d="M7 4v16M3 16l4 4 4-4M17 20V4M13 8l4-4 4 4"/>`, // #2179: the sidebar's sort
40
41
  star: () => html`<path d="M12 2.5l2.94 5.96 6.56.95-4.75 4.63 1.12 6.54L12 17.5l-5.87 3.08 1.12-6.54L2.5 9.41l6.56-.95L12 2.5z"/>`,
41
42
  eye: () => html`<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>`,
43
+ idcard: () => html`<rect x="2" y="5" width="20" height="14" rx="2"/><circle cx="8" cy="11" r="2"/><path d="M5 16a3 3 0 0 1 6 0M14 10h5M14 14h3"/>`,
42
44
  check: () => html`<path d="M20 6L9 17l-5-5"/>`,
43
45
  inbox: () => html`<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>`,
44
46
  bell: () => html`<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>`,
@@ -4,21 +4,25 @@ import { html, useEffect, useRef, useState } from "./app-html.js";
4
4
  import { Icon } from "./ui-icons.js";
5
5
  import { t } from "./app-i18n.js";
6
6
 
7
- /** items: [{ key, label, icon?, onSelect, danger?, disabled? }] (falsy entries are skipped). */
8
- export function Menu({ items, label, icon = "more", cls = "" }) {
7
+ /**
8
+ * items: [{ key, label, icon?, onSelect, danger?, disabled? }] (falsy entries are skipped). `checked` makes a radio item;
9
+ * `checkbox: true` with `checked` a checkbox item (#2217 hotfix: the chat's card menu), which toggles and keeps the menu
10
+ * open. `active`: the button shows a pressed look (what it controls is on), without changing its role.
11
+ */
12
+ export function Menu({ items, label, icon = "more", cls = "", active = false }) {
9
13
  const [open, setOpen] = useState(false);
10
14
  const btn = useRef(null), list = useRef(null);
11
15
  const shown = (items || []).filter(Boolean);
12
16
  useEffect(() => {
13
17
  if (!open) return undefined;
14
- const first = list.current && list.current.querySelector("[role=menuitem]:not([disabled])");
18
+ const first = list.current && list.current.querySelector("[role=menuitem]:not([disabled]), [role=menuitemradio]:not([disabled]), [role=menuitemcheckbox]:not([disabled])");
15
19
  if (first) first.focus();
16
20
  const outside = (e) => { if (list.current && !list.current.contains(e.target) && btn.current && !btn.current.contains(e.target)) setOpen(false); };
17
21
  document.addEventListener("pointerdown", outside, true);
18
22
  return () => document.removeEventListener("pointerdown", outside, true);
19
23
  }, [open]);
20
24
  function onKey(e) {
21
- const all = [...list.current.querySelectorAll("[role=menuitem]:not([disabled])")];
25
+ const all = [...list.current.querySelectorAll("[role=menuitem]:not([disabled]), [role=menuitemradio]:not([disabled]), [role=menuitemcheckbox]:not([disabled])")];
22
26
  const i = all.indexOf(document.activeElement);
23
27
  if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); setOpen(false); btn.current && btn.current.focus(); }
24
28
  else if (e.key === "ArrowDown") { e.preventDefault(); (all[(i + 1) % all.length] || all[0])?.focus(); }
@@ -29,11 +33,20 @@ export function Menu({ items, label, icon = "more", cls = "" }) {
29
33
  }
30
34
  function choose(item) { setOpen(false); btn.current && btn.current.focus(); item.onSelect(); }
31
35
  return html`<div class=${`menu ${cls}`}>
32
- <button ref=${btn} type="button" class="icon-btn" aria-haspopup="menu" aria-expanded=${open ? "true" : "false"}
36
+ <button ref=${btn} type="button" class=${`icon-btn${active ? " active" : ""}`} aria-haspopup="menu" aria-expanded=${open ? "true" : "false"}
33
37
  aria-label=${label || t("app.more")} title=${label || t("app.more")} onClick=${() => setOpen(!open)}><${Icon} name=${icon} /></button>
34
38
  ${open ? html`<div data-trust="menu" ref=${list} class="menu-list" role="menu" aria-label=${label || t("app.more")} onKeyDown=${onKey}>
35
- ${shown.map(item => html`<button key=${item.key} type="button" role="menuitem" class=${`menu-item${item.danger ? " danger" : ""}`}
36
- disabled=${!!item.disabled} onClick=${() => choose(item)}>${item.icon ? html`<${Icon} name=${item.icon} size=${16} />` : null}<span>${item.label}</span></button>`)}
39
+ ${shown.map(item => item.heading
40
+ ? html`<div key=${item.key} class="menu-heading" role="presentation">${item.label}</div>`
41
+ : item.checkbox
42
+ ? html`<button key=${item.key} type="button" role="menuitemcheckbox" aria-checked=${item.checked ? "true" : "false"} class="menu-item menu-radio"
43
+ disabled=${!!item.disabled} onClick=${() => item.onSelect()}><span class="menu-tick" aria-hidden="true">${item.checked ? html`<${Icon} name="check" size=${16} />` : null}</span><span>${item.label}</span></button>`
44
+ : item.checked !== undefined
45
+ // #2196: one of a group of choices (Page colours): a radio item, the chosen one ticked.
46
+ ? html`<button key=${item.key} type="button" role="menuitemradio" aria-checked=${item.checked ? "true" : "false"} class="menu-item menu-radio"
47
+ disabled=${!!item.disabled} onClick=${() => choose(item)}><span class="menu-tick" aria-hidden="true">${item.checked ? html`<${Icon} name="check" size=${16} />` : null}</span><span>${item.label}</span></button>`
48
+ : html`<button key=${item.key} type="button" role="menuitem" class=${`menu-item${item.danger ? " danger" : ""}`}
49
+ disabled=${!!item.disabled} onClick=${() => choose(item)}>${item.icon ? html`<${Icon} name=${item.icon} size=${16} />` : null}<span>${item.label}</span></button>`)}
37
50
  </div>` : null}
38
51
  </div>`;
39
52
  }
@@ -1,5 +1,6 @@
1
- // Toasts: one stack in a corner (under the header on a phone), each gone after a few seconds. Text only.
2
- import { html, useEffect, useState } from "./app-html.js";
1
+ // Toasts: one stack in a corner (under the header on a phone; above the tabs on the task board), each gone after a few
2
+ // seconds. Text only. A manual popover, so it stands in the top layer above an open dialog (#2299).
3
+ import { html, useEffect, useRef, useState } from "./app-html.js";
3
4
  import { createStore } from "./app-store.js";
4
5
 
5
6
  const toasts = createStore([]);
@@ -14,5 +15,17 @@ export function toast(text, ok = true) {
14
15
  export function Toasts() {
15
16
  const [list, setList] = useState(toasts.get());
16
17
  useEffect(() => toasts.subscribe(setList), []);
17
- return html`<div data-trust="toast" class="toasts" role="status" aria-live="polite">${list.map(x => html`<div key=${x.id} class=${`toast ${x.ok ? "ok" : "err"}`}>${x.text}</div>`)}</div>`;
18
+ // #2299: a manual popover — in the top layer, so an open modal dialog's backdrop does not dim a toast (it did: grey on
19
+ // grey in the dark theme). Shown again on every new toast, so it is above a dialog opened since. Without popover
20
+ // support it stays an ordinary fixed box, as before.
21
+ const box = useRef(null);
22
+ useEffect(() => {
23
+ const el = box.current;
24
+ if (!el || typeof el.showPopover !== "function") return;
25
+ try {
26
+ if (el.matches(":popover-open")) el.hidePopover();
27
+ if (list.length) el.showPopover();
28
+ } catch { /* a popover that cannot be shown stays where it is */ }
29
+ }, [list]);
30
+ return html`<div ref=${box} popover="manual" data-trust="toast" class="toasts" role="status" aria-live="polite">${list.map(x => html`<div key=${x.id} class=${`toast ${x.ok ? "ok" : "err"}`}>${x.text}</div>`)}</div>`;
18
31
  }