@songsid/agend 2.2.1 → 2.2.6

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 (455) hide show
  1. package/dist/agent-cli-instructions.md +5 -2
  2. package/dist/agent-cli.js +284 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.d.ts +2 -0
  5. package/dist/agent-endpoint.js +29 -0
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-file-read.d.ts +38 -0
  8. package/dist/agent-file-read.js +317 -0
  9. package/dist/agent-file-read.js.map +1 -0
  10. package/dist/agent-whats-new.d.ts +11 -0
  11. package/dist/agent-whats-new.js +34 -2
  12. package/dist/agent-whats-new.js.map +1 -1
  13. package/dist/atomic-write.js +9 -1
  14. package/dist/atomic-write.js.map +1 -1
  15. package/dist/auth-api.d.ts +30 -1
  16. package/dist/auth-api.js +61 -12
  17. package/dist/auth-api.js.map +1 -1
  18. package/dist/authority/authority-state.d.ts +38 -0
  19. package/dist/authority/authority-state.js +114 -0
  20. package/dist/authority/authority-state.js.map +1 -0
  21. package/dist/authority/session-authorizations.d.ts +43 -0
  22. package/dist/authority/session-authorizations.js +102 -0
  23. package/dist/authority/session-authorizations.js.map +1 -0
  24. package/dist/authority/web-authority.d.ts +120 -0
  25. package/dist/authority/web-authority.js +157 -0
  26. package/dist/authority/web-authority.js.map +1 -0
  27. package/dist/backend/antigravity.js +3 -3
  28. package/dist/backend/antigravity.js.map +1 -1
  29. package/dist/backend/claude-code.d.ts +2 -2
  30. package/dist/backend/claude-code.js +10 -5
  31. package/dist/backend/claude-code.js.map +1 -1
  32. package/dist/backend/launch-effort.js +1 -1
  33. package/dist/backend/launch-effort.js.map +1 -1
  34. package/dist/backend/muse.js +10 -2
  35. package/dist/backend/muse.js.map +1 -1
  36. package/dist/backend/types.d.ts +14 -0
  37. package/dist/backend/types.js +25 -0
  38. package/dist/backend/types.js.map +1 -1
  39. package/dist/backend-availability.d.ts +120 -0
  40. package/dist/backend-availability.js +396 -0
  41. package/dist/backend-availability.js.map +1 -0
  42. package/dist/backend-command.d.ts +21 -3
  43. package/dist/backend-command.js +23 -4
  44. package/dist/backend-command.js.map +1 -1
  45. package/dist/bundled-skills.d.ts +14 -0
  46. package/dist/bundled-skills.js +29 -0
  47. package/dist/bundled-skills.js.map +1 -0
  48. package/dist/cache-analysis.d.ts +47 -1
  49. package/dist/cache-analysis.js +68 -8
  50. package/dist/cache-analysis.js.map +1 -1
  51. package/dist/cache-ledger.d.ts +16 -3
  52. package/dist/cache-ledger.js +33 -13
  53. package/dist/cache-ledger.js.map +1 -1
  54. package/dist/cache-report-tool.d.ts +17 -0
  55. package/dist/cache-report-tool.js +40 -0
  56. package/dist/cache-report-tool.js.map +1 -0
  57. package/dist/cache-service.d.ts +70 -3
  58. package/dist/cache-service.js +178 -17
  59. package/dist/cache-service.js.map +1 -1
  60. package/dist/channel/adapters/discord-client-retirement.d.ts +5 -0
  61. package/dist/channel/adapters/discord-client-retirement.js +57 -0
  62. package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
  63. package/dist/channel/adapters/discord.d.ts +11 -0
  64. package/dist/channel/adapters/discord.js +66 -6
  65. package/dist/channel/adapters/discord.js.map +1 -1
  66. package/dist/channel/adapters/telegram.d.ts +8 -1
  67. package/dist/channel/adapters/telegram.js +74 -4
  68. package/dist/channel/adapters/telegram.js.map +1 -1
  69. package/dist/channel/mcp-tools.js +48 -2
  70. package/dist/channel/mcp-tools.js.map +1 -1
  71. package/dist/channel/types.d.ts +21 -0
  72. package/dist/classic-channel-manager.d.ts +10 -5
  73. package/dist/classic-channel-manager.js +12 -9
  74. package/dist/classic-channel-manager.js.map +1 -1
  75. package/dist/cli.js +142 -110
  76. package/dist/cli.js.map +1 -1
  77. package/dist/config-validator.js +119 -4
  78. package/dist/config-validator.js.map +1 -1
  79. package/dist/daemon.d.ts +8 -0
  80. package/dist/daemon.js +298 -22
  81. package/dist/daemon.js.map +1 -1
  82. package/dist/decision-list.d.ts +25 -0
  83. package/dist/decision-list.js +30 -0
  84. package/dist/decision-list.js.map +1 -0
  85. package/dist/delivery-outbox.d.ts +10 -0
  86. package/dist/delivery-outbox.js +12 -1
  87. package/dist/delivery-outbox.js.map +1 -1
  88. package/dist/fleet-context.d.ts +15 -0
  89. package/dist/fleet-manager.d.ts +514 -12
  90. package/dist/fleet-manager.js +1885 -163
  91. package/dist/fleet-manager.js.map +1 -1
  92. package/dist/fleet-process-handlers.d.ts +18 -0
  93. package/dist/fleet-process-handlers.js +69 -0
  94. package/dist/fleet-process-handlers.js.map +1 -0
  95. package/dist/fleet-traffic.d.ts +74 -0
  96. package/dist/fleet-traffic.js +196 -0
  97. package/dist/fleet-traffic.js.map +1 -0
  98. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
  99. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
  100. package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
  101. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
  102. package/dist/general-knowledge/skills/web-appearance/SKILL.md +492 -0
  103. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +482 -0
  104. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  105. package/dist/human-delivery-receipts.d.ts +24 -1
  106. package/dist/human-delivery-receipts.js +64 -11
  107. package/dist/human-delivery-receipts.js.map +1 -1
  108. package/dist/instance-avatar.js +3 -0
  109. package/dist/instance-avatar.js.map +1 -1
  110. package/dist/instance-config-impact.js +6 -1
  111. package/dist/instance-config-impact.js.map +1 -1
  112. package/dist/instance-lifecycle.d.ts +2 -0
  113. package/dist/instance-lifecycle.js +20 -10
  114. package/dist/instance-lifecycle.js.map +1 -1
  115. package/dist/instance-names.d.ts +4 -0
  116. package/dist/instance-names.js +12 -0
  117. package/dist/instance-names.js.map +1 -0
  118. package/dist/instructions.d.ts +12 -1
  119. package/dist/instructions.js +13 -2
  120. package/dist/instructions.js.map +1 -1
  121. package/dist/locale.js +118 -10
  122. package/dist/locale.js.map +1 -1
  123. package/dist/login-controller.js +35 -2
  124. package/dist/login-controller.js.map +1 -1
  125. package/dist/ls-attach-resolve.d.ts +95 -0
  126. package/dist/ls-attach-resolve.js +174 -0
  127. package/dist/ls-attach-resolve.js.map +1 -0
  128. package/dist/memory-hogs.d.ts +27 -0
  129. package/dist/memory-hogs.js +124 -0
  130. package/dist/memory-hogs.js.map +1 -0
  131. package/dist/needs-you-hub.d.ts +4 -0
  132. package/dist/needs-you-hub.js +31 -6
  133. package/dist/needs-you-hub.js.map +1 -1
  134. package/dist/needs-you.d.ts +1 -13
  135. package/dist/needs-you.js +3 -37
  136. package/dist/needs-you.js.map +1 -1
  137. package/dist/outbound-handlers.d.ts +15 -0
  138. package/dist/outbound-handlers.js +152 -11
  139. package/dist/outbound-handlers.js.map +1 -1
  140. package/dist/outbound-schemas.d.ts +228 -4
  141. package/dist/outbound-schemas.js +147 -1
  142. package/dist/outbound-schemas.js.map +1 -1
  143. package/dist/public-web-gateway.js +21 -1
  144. package/dist/public-web-gateway.js.map +1 -1
  145. package/dist/public-web-link.d.ts +9 -0
  146. package/dist/public-web-link.js +8 -0
  147. package/dist/public-web-link.js.map +1 -1
  148. package/dist/quickstart-api.d.ts +10 -1
  149. package/dist/quickstart-api.js +12 -4
  150. package/dist/quickstart-api.js.map +1 -1
  151. package/dist/reply-buttons-controller.d.ts +4 -0
  152. package/dist/reply-buttons-controller.js +2 -2
  153. package/dist/reply-buttons-controller.js.map +1 -1
  154. package/dist/repo-worktree-guard.d.ts +36 -0
  155. package/dist/repo-worktree-guard.js +143 -0
  156. package/dist/repo-worktree-guard.js.map +1 -0
  157. package/dist/restart-progress.d.ts +65 -1
  158. package/dist/restart-progress.js +236 -10
  159. package/dist/restart-progress.js.map +1 -1
  160. package/dist/rollout-index.d.ts +5 -0
  161. package/dist/rollout-index.js +26 -5
  162. package/dist/rollout-index.js.map +1 -1
  163. package/dist/scheduler/scheduler.d.ts +70 -1
  164. package/dist/scheduler/scheduler.js +256 -17
  165. package/dist/scheduler/scheduler.js.map +1 -1
  166. package/dist/service-installer.d.ts +2 -1
  167. package/dist/service-installer.js +18 -3
  168. package/dist/service-installer.js.map +1 -1
  169. package/dist/settings-api.d.ts +23 -0
  170. package/dist/settings-api.js +233 -2
  171. package/dist/settings-api.js.map +1 -1
  172. package/dist/settings-change.js +112 -0
  173. package/dist/settings-change.js.map +1 -1
  174. package/dist/settings-effect.js +32 -2
  175. package/dist/settings-effect.js.map +1 -1
  176. package/dist/settings-http-confirmation.js +5 -0
  177. package/dist/settings-http-confirmation.js.map +1 -1
  178. package/dist/tmux-manager.d.ts +5 -0
  179. package/dist/tmux-manager.js +16 -0
  180. package/dist/tmux-manager.js.map +1 -1
  181. package/dist/tool-permissions.d.ts +4 -6
  182. package/dist/tool-permissions.js +100 -2
  183. package/dist/tool-permissions.js.map +1 -1
  184. package/dist/topic-commands.d.ts +17 -7
  185. package/dist/topic-commands.js +39 -21
  186. package/dist/topic-commands.js.map +1 -1
  187. package/dist/topic-link.d.ts +128 -0
  188. package/dist/topic-link.js +269 -0
  189. package/dist/topic-link.js.map +1 -0
  190. package/dist/tunnel/cloudflared.js +37 -2
  191. package/dist/tunnel/cloudflared.js.map +1 -1
  192. package/dist/tunnel/types.d.ts +3 -1
  193. package/dist/tunnel/types.js.map +1 -1
  194. package/dist/types.d.ts +61 -1
  195. package/dist/ui/app-effects.js +245 -0
  196. package/dist/ui/chat-ambient.js +307 -0
  197. package/dist/ui/chat-effect-posters.js +42 -0
  198. package/dist/ui/chat-effects.js +160 -0
  199. package/dist/ui/chat-live-replies.js +58 -0
  200. package/dist/ui/chat-render.js +25 -7
  201. package/dist/ui/chat-store.js +41 -13
  202. package/dist/ui/chat-strings.js +86 -2
  203. package/dist/ui/chat-thread.js +204 -37
  204. package/dist/ui/chat-tour.js +1 -1
  205. package/dist/ui/effect-trial-controller.js +124 -0
  206. package/dist/ui/effect-trial-offer.js +31 -0
  207. package/dist/ui/effect-trial-panel.js +40 -0
  208. package/dist/ui/fleet-cache.js +23 -1
  209. package/dist/ui/fleet-canvas-layout.js +111 -0
  210. package/dist/ui/fleet-canvas.js +525 -0
  211. package/dist/ui/fleet-org.js +27 -12
  212. package/dist/ui/fleet-traffic.js +343 -0
  213. package/dist/ui/fleet-usage.js +401 -0
  214. package/dist/ui/image-lightbox.js +1 -1
  215. package/dist/ui/panel-chat.js +252 -58
  216. package/dist/ui/panel-details.js +20 -6
  217. package/dist/ui/panel-fleet.js +45 -19
  218. package/dist/ui/panel-log.js +6 -6
  219. package/dist/ui/panel-needs.js +3 -3
  220. package/dist/ui/panel-settings.js +58 -13
  221. package/dist/ui/preview-panel.js +25 -9
  222. package/dist/ui/preview.js +3 -2
  223. package/dist/ui/settings-appearance.js +349 -0
  224. package/dist/ui/settings-catalog.js +92 -0
  225. package/dist/ui/settings-dialogs.js +40 -12
  226. package/dist/ui/settings-effects.js +202 -0
  227. package/dist/ui/settings-guests.js +117 -0
  228. package/dist/ui/settings-learn.js +25 -0
  229. package/dist/ui/settings-model.js +15 -6
  230. package/dist/ui/settings-mods.js +271 -0
  231. package/dist/ui/settings-owner-effects.js +80 -0
  232. package/dist/ui/settings-strings.js +335 -10
  233. package/dist/ui/settings-web-authority.js +91 -0
  234. package/dist/ui/shared/agend-auth.js +28 -4
  235. package/dist/ui/shared/agent-widget.js +248 -0
  236. package/dist/ui/shared/app-i18n.js +6 -2
  237. package/dist/ui/shared/app-prefs.js +34 -13
  238. package/dist/ui/shared/app-route.js +42 -2
  239. package/dist/ui/shared/app-session.js +3 -1
  240. package/dist/ui/shared/app-shell.js +119 -41
  241. package/dist/ui/shared/app-store.js +28 -1
  242. package/dist/ui/shared/app-stream.js +14 -7
  243. package/dist/ui/shared/app-themes.js +206 -0
  244. package/dist/ui/shared/app.css +566 -5
  245. package/dist/ui/shared/app.js +23 -10
  246. package/dist/ui/shared/card-strings.js +32 -0
  247. package/dist/ui/shared/effect-app-runtime.js +297 -0
  248. package/dist/ui/shared/effect-baseline.js +64 -0
  249. package/dist/ui/shared/effect-chat-runtime.js +167 -0
  250. package/dist/ui/shared/effect-cursor.js +125 -0
  251. package/dist/ui/shared/effect-frame.js +146 -0
  252. package/dist/ui/shared/effect-geometry.js +137 -0
  253. package/dist/ui/shared/effect-host-style.js +10 -0
  254. package/dist/ui/shared/effect-owner-regions.js +74 -0
  255. package/dist/ui/shared/effect-owner-source.js +122 -0
  256. package/dist/ui/shared/effect-policy.js +151 -0
  257. package/dist/ui/shared/effect-poster-reader.js +98 -0
  258. package/dist/ui/shared/effect-posters.js +117 -0
  259. package/dist/ui/shared/effect-profiler.js +184 -0
  260. package/dist/ui/shared/effect-profiles.js +65 -0
  261. package/dist/ui/shared/effect-registry.js +543 -0
  262. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  263. package/dist/ui/shared/effect-safety.js +153 -0
  264. package/dist/ui/shared/effect-stock-snapshot.js +43 -0
  265. package/dist/ui/shared/effect-stock-trust.js +217 -0
  266. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  267. package/dist/ui/shared/effect-trial-source.js +103 -0
  268. package/dist/ui/shared/effect-trial-stage.js +170 -0
  269. package/dist/ui/shared/effect-trial.css +40 -0
  270. package/dist/ui/shared/fx-motion.js +67 -0
  271. package/dist/ui/shared/header-tools.js +1 -1
  272. package/dist/ui/shared/identity-card.js +273 -0
  273. package/dist/ui/shared/identity-color.js +261 -0
  274. package/dist/ui/shared/instance-nav.js +215 -41
  275. package/dist/ui/shared/owner-css.js +250 -0
  276. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  277. package/dist/ui/shared/panel-view.js +103 -10
  278. package/dist/ui/shared/signin.js +2 -0
  279. package/dist/ui/shared/tokens.css +9 -3
  280. package/dist/ui/shared/ui-dialog.js +1 -1
  281. package/dist/ui/shared/ui-icons.js +3 -0
  282. package/dist/ui/shared/ui-menu.js +1 -1
  283. package/dist/ui/shared/ui-toast.js +1 -1
  284. package/dist/ui/shared/usage-dialog.js +22 -2
  285. package/dist/ui/shared/view-strings.js +98 -2
  286. package/dist/ui/shared/web-authorize.js +151 -0
  287. package/dist/ui/signin.html +1 -0
  288. package/dist/ui/web-terminal/terminal.css +4 -4
  289. package/dist/usage/providers.d.ts +28 -0
  290. package/dist/usage/providers.js +126 -36
  291. package/dist/usage/providers.js.map +1 -1
  292. package/dist/usage-report.d.ts +158 -0
  293. package/dist/usage-report.js +226 -0
  294. package/dist/usage-report.js.map +1 -0
  295. package/dist/usage-rollup.d.ts +51 -0
  296. package/dist/usage-rollup.js +168 -0
  297. package/dist/usage-rollup.js.map +1 -0
  298. package/dist/view-api.d.ts +68 -0
  299. package/dist/view-api.js +249 -8
  300. package/dist/view-api.js.map +1 -1
  301. package/dist/web-ambient.d.ts +72 -0
  302. package/dist/web-ambient.js +161 -0
  303. package/dist/web-ambient.js.map +1 -0
  304. package/dist/web-api.d.ts +117 -7
  305. package/dist/web-api.js +1283 -70
  306. package/dist/web-api.js.map +1 -1
  307. package/dist/web-appearance-limits.d.ts +82 -0
  308. package/dist/web-appearance-limits.js +83 -0
  309. package/dist/web-appearance-limits.js.map +1 -0
  310. package/dist/web-appearance-policy.d.ts +64 -0
  311. package/dist/web-appearance-policy.js +87 -0
  312. package/dist/web-appearance-policy.js.map +1 -0
  313. package/dist/web-appearance-tools.d.ts +53 -0
  314. package/dist/web-appearance-tools.js +124 -0
  315. package/dist/web-appearance-tools.js.map +1 -0
  316. package/dist/web-appearance-view.d.ts +124 -0
  317. package/dist/web-appearance-view.js +278 -0
  318. package/dist/web-appearance-view.js.map +1 -0
  319. package/dist/web-appearance.d.ts +93 -0
  320. package/dist/web-appearance.js +373 -0
  321. package/dist/web-appearance.js.map +1 -0
  322. package/dist/web-asset-parts.d.ts +93 -0
  323. package/dist/web-asset-parts.js +133 -0
  324. package/dist/web-asset-parts.js.map +1 -0
  325. package/dist/web-auth.js +20 -2
  326. package/dist/web-auth.js.map +1 -1
  327. package/dist/web-card.d.ts +110 -0
  328. package/dist/web-card.js +266 -0
  329. package/dist/web-card.js.map +1 -0
  330. package/dist/web-channel-echo.d.ts +7 -1
  331. package/dist/web-channel-echo.js +6 -2
  332. package/dist/web-channel-echo.js.map +1 -1
  333. package/dist/web-chat-history.d.ts +63 -0
  334. package/dist/web-chat-history.js +109 -6
  335. package/dist/web-chat-history.js.map +1 -1
  336. package/dist/web-commands.d.ts +1 -0
  337. package/dist/web-effect-bootstrap.d.ts +12 -0
  338. package/dist/web-effect-bootstrap.js +49 -0
  339. package/dist/web-effect-bootstrap.js.map +1 -0
  340. package/dist/web-effect-bundle.d.ts +28 -0
  341. package/dist/web-effect-bundle.js +171 -0
  342. package/dist/web-effect-bundle.js.map +1 -0
  343. package/dist/web-effect-canonical.d.ts +8 -0
  344. package/dist/web-effect-canonical.js +27 -0
  345. package/dist/web-effect-canonical.js.map +1 -0
  346. package/dist/web-effect-library.d.ts +114 -0
  347. package/dist/web-effect-library.js +530 -0
  348. package/dist/web-effect-library.js.map +1 -0
  349. package/dist/web-effect-playback.d.ts +30 -0
  350. package/dist/web-effect-playback.js +71 -0
  351. package/dist/web-effect-playback.js.map +1 -0
  352. package/dist/web-effect-poster.d.ts +8 -0
  353. package/dist/web-effect-poster.js +52 -0
  354. package/dist/web-effect-poster.js.map +1 -0
  355. package/dist/web-effect-profiles.d.ts +118 -0
  356. package/dist/web-effect-profiles.js +180 -0
  357. package/dist/web-effect-profiles.js.map +1 -0
  358. package/dist/web-effect-service.d.ts +93 -0
  359. package/dist/web-effect-service.js +335 -0
  360. package/dist/web-effect-service.js.map +1 -0
  361. package/dist/web-effect-tools.d.ts +48 -0
  362. package/dist/web-effect-tools.js +129 -0
  363. package/dist/web-effect-tools.js.map +1 -0
  364. package/dist/web-effect-trials.d.ts +86 -0
  365. package/dist/web-effect-trials.js +204 -0
  366. package/dist/web-effect-trials.js.map +1 -0
  367. package/dist/web-effect-upload.d.ts +27 -0
  368. package/dist/web-effect-upload.js +185 -0
  369. package/dist/web-effect-upload.js.map +1 -0
  370. package/dist/web-effect-worker.d.ts +8 -0
  371. package/dist/web-effect-worker.js +170 -0
  372. package/dist/web-effect-worker.js.map +1 -0
  373. package/dist/web-effect.d.ts +103 -0
  374. package/dist/web-effect.js +153 -0
  375. package/dist/web-effect.js.map +1 -0
  376. package/dist/web-guest-access.d.ts +125 -0
  377. package/dist/web-guest-access.js +234 -0
  378. package/dist/web-guest-access.js.map +1 -0
  379. package/dist/web-guest-rooms.d.ts +82 -0
  380. package/dist/web-guest-rooms.js +135 -0
  381. package/dist/web-guest-rooms.js.map +1 -0
  382. package/dist/web-identity-color.d.ts +87 -0
  383. package/dist/web-identity-color.js +209 -0
  384. package/dist/web-identity-color.js.map +1 -0
  385. package/dist/web-identity-palette.d.ts +113 -0
  386. package/dist/web-identity-palette.js +84 -0
  387. package/dist/web-identity-palette.js.map +1 -0
  388. package/dist/web-image-meta.d.ts +38 -0
  389. package/dist/web-image-meta.js +262 -0
  390. package/dist/web-image-meta.js.map +1 -0
  391. package/dist/web-login.d.ts +47 -0
  392. package/dist/web-login.js +115 -0
  393. package/dist/web-login.js.map +1 -1
  394. package/dist/web-mod-agent.d.ts +50 -0
  395. package/dist/web-mod-agent.js +207 -0
  396. package/dist/web-mod-agent.js.map +1 -0
  397. package/dist/web-mod-bundle.d.ts +157 -0
  398. package/dist/web-mod-bundle.js +529 -0
  399. package/dist/web-mod-bundle.js.map +1 -0
  400. package/dist/web-mod-export.d.ts +68 -0
  401. package/dist/web-mod-export.js +100 -0
  402. package/dist/web-mod-export.js.map +1 -0
  403. package/dist/web-mod-store.d.ts +289 -0
  404. package/dist/web-mod-store.js +638 -0
  405. package/dist/web-mod-store.js.map +1 -0
  406. package/dist/web-org.d.ts +3 -3
  407. package/dist/web-org.js +11 -17
  408. package/dist/web-org.js.map +1 -1
  409. package/dist/web-owner-css-store.d.ts +222 -0
  410. package/dist/web-owner-css-store.js +440 -0
  411. package/dist/web-owner-css-store.js.map +1 -0
  412. package/dist/web-owner-css.d.ts +58 -0
  413. package/dist/web-owner-css.js +552 -0
  414. package/dist/web-owner-css.js.map +1 -0
  415. package/dist/web-prefs.d.ts +62 -0
  416. package/dist/web-prefs.js +232 -36
  417. package/dist/web-prefs.js.map +1 -1
  418. package/dist/web-preview.d.ts +15 -3
  419. package/dist/web-preview.js +28 -9
  420. package/dist/web-preview.js.map +1 -1
  421. package/dist/web-returning-device.d.ts +41 -0
  422. package/dist/web-returning-device.js +57 -5
  423. package/dist/web-returning-device.js.map +1 -1
  424. package/dist/web-session-authorization.d.ts +132 -0
  425. package/dist/web-session-authorization.js +193 -0
  426. package/dist/web-session-authorization.js.map +1 -0
  427. package/dist/web-session.d.ts +70 -10
  428. package/dist/web-session.js +137 -22
  429. package/dist/web-session.js.map +1 -1
  430. package/dist/web-settings-patch.d.ts +67 -2
  431. package/dist/web-settings-patch.js +196 -1
  432. package/dist/web-settings-patch.js.map +1 -1
  433. package/dist/web-settings-tools.d.ts +89 -0
  434. package/dist/web-settings-tools.js +273 -0
  435. package/dist/web-settings-tools.js.map +1 -0
  436. package/dist/web-shell-routes.d.ts +2 -2
  437. package/dist/web-shell-routes.js +2 -2
  438. package/dist/web-shell-routes.js.map +1 -1
  439. package/dist/web-terminal-http.js +15 -2
  440. package/dist/web-terminal-http.js.map +1 -1
  441. package/dist/web-theme-store.d.ts +257 -0
  442. package/dist/web-theme-store.js +627 -0
  443. package/dist/web-theme-store.js.map +1 -0
  444. package/dist/web-theme.d.ts +123 -0
  445. package/dist/web-theme.js +416 -0
  446. package/dist/web-theme.js.map +1 -0
  447. package/dist/web-upload.d.ts +13 -0
  448. package/dist/web-upload.js +39 -3
  449. package/dist/web-upload.js.map +1 -1
  450. package/dist/web-widget.d.ts +72 -0
  451. package/dist/web-widget.js +245 -0
  452. package/dist/web-widget.js.map +1 -0
  453. package/dist/workflow-templates/default.md +1 -1
  454. package/package.json +1 -1
  455. package/templates/systemd.service.ejs +8 -0
@@ -9,13 +9,15 @@
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";
16
16
  import { viewStore } from "./view-roster-store.js";
17
17
  import { onPrefsChanged, prefInForce, prefsWritable, savePref } from "./app-prefs.js";
18
18
  import "./view-strings.js";
19
+ import { themesShown, themesStore } from "./app-themes.js";
20
+ import { identityClasses, identityOf, paintIdentity, useIdentityColors } from "./identity-color.js";
19
21
 
20
22
  /** view.* text with named values. */
21
23
  function tn(key, values = {}) {
@@ -28,12 +30,52 @@ const stored = (k) => { try { return localStorage.getItem(k); } catch { return n
28
30
  const nameOf = (it) => (it && (it.name != null ? it.name : it.instance_name)) || "";
29
31
 
30
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
+ */
31
40
  export function groupOf(it) {
32
41
  const tags = Array.isArray(it.tags) ? it.tags : [];
33
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];
34
45
  if (tags.length) return tags[0];
35
46
  return "Other";
36
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
+ }
37
79
  const BACKEND_LABELS = { "claude-code": "Claude Code", "kiro-cli": "Kiro CLI", codex: "Codex", grok: "Grok Build", antigravity: "Antigravity", opencode: "OpenCode" };
38
80
  export function backendLabel(backend) { return BACKEND_LABELS[backend] || backend || "Unknown"; }
39
81
  export const KNOWN_BACKENDS = new Set(Object.keys(BACKEND_LABELS));
@@ -91,8 +133,8 @@ export function instanceTooltip(it) {
91
133
  if (it.display_name && it.display_name !== name) lines.push(it.display_name, `(${name})`);
92
134
  else lines.push(name);
93
135
  lines.push(line("tooltipBackend", backendLabel(it.backend)));
94
- const src = (s) => (s === "instance" || s === "classic" ? " (configured)" : s === "fleet-default" ? " (fleet default)" : "");
95
- 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)));
96
138
  lines.push(line("tooltipStatus", status));
97
139
  lines.push(line("tooltipContext", context));
98
140
  if (it.effort) lines.push(line("tooltipEffort", it.effort + src(it.effort_source)));
@@ -106,6 +148,7 @@ const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order"; // View's key since #999
106
148
  const COLLAPSED_KEY = "agend_instance_collapsed";
107
149
  const FILTER_KEY = "agend_instance_filter";
108
150
  const FAVORITES_KEY = "agend_favorites"; // #1664
151
+ const SORT_KEY = "agend_nav_sort"; // #2179
109
152
  /**
110
153
  * A synced sidebar preference, as the JSON its reader parses: the value in force in memory (app-prefs.js — this
111
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.
@@ -136,6 +179,13 @@ function readFilter() {
136
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) };
137
180
  } catch { return { q: "", status: [], cli: [] }; }
138
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
+ }
139
189
  function readFavorites() {
140
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 []; }
141
191
  }
@@ -149,7 +199,9 @@ export function computeOrder(roster, order) {
149
199
  if (ia !== ib) return ia - ib;
150
200
  if (a === "Other") return 1;
151
201
  if (b === "Other") return -1;
152
- 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));
153
205
  });
154
206
  const instByGroup = new Map();
155
207
  for (const [g, items] of groups) {
@@ -172,12 +224,18 @@ function moveIn(arr, moving, target, before) {
172
224
  }
173
225
 
174
226
  /** The list's state for the page's life: this browser's order, folded groups and filter (each also kept in storage). */
175
- 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() });
176
228
  /** Read them again from this browser's storage — what a page load does (the store above is made that way). */
177
229
  export function loadNavPrefs() {
178
- navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() });
230
+ navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites(), sort: readSort() });
179
231
  namesStore.set({ primary: readNamesPrimary() });
180
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
+ }
181
239
  /** Change the filter ({ q?, status?, cli? }): the same on every page, and kept by this browser. */
182
240
  export function setNavFilter(patch) {
183
241
  const filter = { ...navStore.get().filter, ...patch };
@@ -186,7 +244,7 @@ export function setNavFilter(patch) {
186
244
  }
187
245
  function setCollapsed(collapsed) { navStore.set({ collapsed }); savePref("nav.collapsed", [...collapsed]); }
188
246
  // Another device rearranged, folded or filtered: this page's list follows (its cache is already updated).
189
- 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(); });
190
248
 
191
249
  // ── #1664: favorites — starred instances, pinned at the top in the order they were starred (or moved) ──
192
250
  export const MAX_FAVORITES = 50;
@@ -222,6 +280,8 @@ function writeFacetsOpen(open) { try { localStorage.setItem(FACETS_KEY, JSON.str
222
280
 
223
281
  /** The Favorites group's key among the tag groups: no tag can be it (a group name is never a control character). */
224
282
  const FAV_GROUP = "\u0000favorites";
283
+ /** #1936: the open instance, pinned on top while a filter is on. */
284
+ const PINNED_GROUP = "\u0000pinned";
225
285
  function persistOrder(groupNames, instByGroup) {
226
286
  const rows = [];
227
287
  groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
@@ -280,9 +340,16 @@ const CLI_MONOGRAM = { "claude-code": "C", codex: "X", "kiro-cli": "K", grok: "G
280
340
  // One path segment under /ui/img/<kind>/ — no further slash or backslash, and not a dot segment ("." / ".." or their
281
341
  // %2e spellings, which a browser resolves too): the picture can only ever be one of those routes (#1642 review).
282
342
  const sameOrigin = (src) => typeof src === "string"
283
- && /^\/ui\/img\/(bot|avatar|emoji)\/(?!(?:\.|%2[eE]){1,2}(?:\?|$))[^/\\?#\s]+(\?v=[0-9A-Za-z%._-]*)?$/.test(src);
284
- /** The avatar to draw: the server's, if it is one the page may load; else the first character. */
343
+ && (/^\/ui\/img\/(bot|avatar|emoji)\/(?!(?:\.|%2[eE]){1,2}(?:\?|$))[^/\\?#\s]+(\?v=[0-9A-Za-z%._-]*)?$/.test(src)
344
+ // #1629: a theme's picture — one name segment, then its 32-hex content id.
345
+ || /^\/ui\/img\/theme\/(?!(?:\.|%2[eE]){1,2}\/)[^/\\?#\s]+\/[0-9a-f]{32}$/.test(src));
346
+ /**
347
+ * The avatar to draw: the server's, if it is one the page may load; else the first character. #1629: the agent's theme
348
+ * picture first, while this viewer shows agents' themes (fx.show, not safe mode).
349
+ */
285
350
  export function avatarOf(it) {
351
+ const th = it && themesShown() ? it.theme_avatar : null;
352
+ if (th && th.kind === "image" && sameOrigin(th.src)) return { kind: "image", src: th.src };
286
353
  const a = it && it.avatar;
287
354
  if (a && a.kind === "image" && sameOrigin(a.src)) return { kind: "image", src: a.src };
288
355
  if (a && a.kind === "emoji" && sameOrigin(a.src)) return { kind: "emoji", src: a.src, alt: typeof a.alt === "string" ? a.alt : "" };
@@ -290,7 +357,6 @@ export function avatarOf(it) {
290
357
  // #1722: the initial is the leading name's ("Names"), not the server's (which is always the nickname's).
291
358
  return { kind: "initial", text: firstCharacter(namesOf(it).primary) || (a && a.kind === "initial" && typeof a.text === "string" ? a.text : "") };
292
359
  }
293
- const ACCENT = /^#[0-9a-f]{6}$/i;
294
360
  /** The status dot on the avatar: needs you, crashed, paused, stopped, looks stuck, working, idle — colour AND a label. */
295
361
  export function dotState(it, exec, awaiting) {
296
362
  if (awaiting != null) return "needs";
@@ -306,22 +372,19 @@ const DOT = {
306
372
  };
307
373
  export function dotLabel(state) { return (DOT[state] || DOT.stopped)[1](); }
308
374
 
309
- /** An instance's avatar with its status dot. The ring (its bot's colour) is a CSS custom property set through the CSSOM. */
375
+ /**
376
+ * An instance's avatar with its status dot. #1958: its ring is the identity colour, painted by the row it sits in
377
+ * (identity-color.js: `.idc.id-ring`), for every face.
378
+ */
310
379
  export function InstanceAvatar({ it, state, cli = false }) {
380
+ useStore(themesStore); // #1629: redraw when the viewer turns agents' themes on or off
311
381
  const a = avatarOf(it);
312
382
  const [broken, setBroken] = useState("");
313
- const el = useRef(null);
314
- const accent = typeof it.accent === "string" && ACCENT.test(it.accent) && a.kind === "image" ? it.accent : "";
315
- useLayoutEffect(() => {
316
- const node = el.current;
317
- if (!node || !node.style) return;
318
- if (accent) node.style.setProperty("--ring", accent); else node.style.removeProperty("--ring");
319
- }, [accent]);
320
383
  const src = a.kind !== "initial" && a.src && broken !== a.src ? a.src : "";
321
384
  const text = a.kind === "emoji" && a.text ? a.text : a.kind === "initial" ? a.text : firstCharacter(namesOf(it).primary);
322
385
  const [cls] = DOT[state] || DOT.stopped;
323
386
  const label = dotLabel(state);
324
- return html`<span ref=${el} class=${`av${accent ? " ring" : ""}${a.kind === "emoji" ? " av-emoji" : ""}`}>
387
+ return html`<span class=${`av${a.kind === "emoji" ? " av-emoji" : ""}`}>
325
388
  ${src ? html`<img src=${src} alt="" decoding="async" onError=${() => setBroken(src)} />` : html`<span class="av-text" aria-hidden="true">${text}</span>`}
326
389
  <span class=${`av-dot dot ${cls}`} role="img" aria-label=${label} title=${label}></span>${cli
327
390
  ? html`<span class="av-cli" aria-hidden="true" title=${backendLabel(it.backend)}>${CLI_MONOGRAM[it.backend] || "·"}</span>` : null}</span>`;
@@ -332,6 +395,71 @@ export function InstanceAvatar({ it, state, cli = false }) {
332
395
  const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet === "working" ? t("app.statusWorking") : facet === "idle" ? t("app.statusIdle")
333
396
  : facet === "crashed" ? t("app.statusCrashed") : t("app.statusStopped"));
334
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
+
335
463
  /**
336
464
  * The sidebar's instance list. `route`: the page's (which instance is open, and which view a row opens). `items`: the
337
465
  * rows (status frames' instances, or View's roster for an anonymous reader); `loaded` false shows a skeleton.
@@ -341,22 +469,25 @@ const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet ===
341
469
  export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting = {}, viewOnly = false, onPick }) {
342
470
  const nav = useStore(navStore);
343
471
  const names = useStore(namesStore); // #1722: which name leads
472
+ useIdentityColors(); // #1958: repaint when the host's colour settings or the themes switch change
344
473
  const filterRef = useRef(null);
345
474
  const list = useRef(null);
346
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); }, []);
347
479
  // #1700: which facet groups are open — this device's (screen layout, not a synced preference), read when the list mounts.
348
480
  const [facetOpen, setFacetOpen] = useState(readFacetsOpen);
349
481
  const toggleFacet = (key) => setFacetOpen((cur) => { const next = { ...cur, [key]: !cur[key] }; writeFacetsOpen(next); return next; });
350
482
  // #1664: "☆ Edit" — the stars on every row (on a phone the only way to them) and ↑ ↓ on the favorites — only on the
351
483
  // full list, as drag: in a filtered one the row above may not be the one above in the list (#1677 review).
352
484
  const [editing, setEditing] = useState(false);
353
- // Where a row goes: the view the page is in (#1523 §3.1). View → View; Details, or a Fleet tab (Q4 = A), → Details;
354
- // everywhere else → Chat. The anonymous reader only ever has View.
485
+ // Where a row goes: the view the page is in (#1523 §3.1). View → View; Details → Details; everywhere else, a Fleet tab
486
+ // included (#1917: the user's call, replacing Q4 = A), → Chat. The anonymous reader only ever has View.
355
487
  const rowPath = viewOnly || (route && route.panel === "view") ? viewPath
356
488
  : route && route.panel === "log" ? logPath // #1218: Log → Log
357
- : route && (route.panel === "details" || route.panel === "fleet") ? detailsPath : chatPath;
489
+ : route && route.panel === "details" ? detailsPath : chatPath;
358
490
  const active = route && (route.panel === "chat" || route.panel === "view" || route.panel === "log" || route.panel === "details") ? route.instance : null;
359
- useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded]);
360
491
  const own = (map, name) => Object.prototype.hasOwnProperty.call(map, name);
361
492
  const byName = new Map(items.map((it) => [nameOf(it), it]));
362
493
  const facetOf = (name) => statusFacet(byName.get(name), own(exec, name) ? exec[name] : null, own(awaiting, name) ? awaiting[name] : null);
@@ -368,18 +499,44 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
368
499
  const favs = nav.favorites.filter((n) => known.has(n));
369
500
  const favSet = new Set(favs);
370
501
  const canStar = !viewOnly && prefsWritable();
371
- const shownGroups = favs.length ? [FAV_GROUP, ...groupNames] : groupNames;
372
- const shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
373
- if (favs.length) shownInsts.set(FAV_GROUP, favs);
374
- 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;
375
526
  const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
527
+ // #1936: while a filter is on, the instance you have open stays on top, pinned and marked "Current", whether or not
528
+ // it matches; a match is not repeated below. "N / M" still counts the filter's matches (the pinned row counts only
529
+ // when it is one). Clearing the filter puts it back in its place, and the list scrolls to it again.
530
+ const pinned = shown.active && active && byName.has(active) ? active : null;
531
+ const listed = pinned ? shown.groups.map((x) => ({ ...x, names: x.names.filter((n) => n !== pinned) })).filter((x) => x.names.length) : shown.groups;
532
+ useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded, shown.active]);
376
533
  const backends = [...new Set(items.map((it) => it.backend).filter((b) => typeof b === "string" && b))].sort((a, b) => backendLabel(a).localeCompare(backendLabel(b)));
377
534
  const toggle = (g) => { const c = new Set(navStore.get().collapsed); if (c.has(g)) c.delete(g); else c.add(g); setCollapsed(c); };
378
535
  // From the store, not this render's copy: two clicks before the next render must both count.
379
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] }); };
380
537
  // Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
381
538
  // their group.
382
- const dragProps = (item) => shown.active ? {} : {
539
+ const dragProps = (item) => shown.active || guest || recent ? {} : { // #2179: not while sorted by reply
383
540
  draggable: true,
384
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 */ } },
385
542
  onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
@@ -431,31 +588,48 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
431
588
  const waiting = own(awaiting, name) ? awaiting[name] : null;
432
589
  const on = name === active;
433
590
  const fav = g === FAV_GROUP, starred = favSet.has(name);
591
+ // #1958: its identity colour — the ring, a left bar and/or the name, as resolved; painted through the CSSOM.
592
+ const id = identityOf(it);
434
593
  const full = !starred && favs.length >= MAX_FAVORITES;
435
- return html`<li key=${name} class="v-li"><a class=${`inst v-inst${on ? " active" : ""}`} 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)}
436
600
  aria-current=${on ? "page" : undefined} onClick=${onPick} ...${fav ? {} : dragProps({ type: "instance", name, group: g })}>
437
601
  <${InstanceAvatar} it=${it} state=${dotState(it, own(exec, name) ? exec[name] : null, waiting)} cli=${true} />
438
- <span class="inst-text"><span class="inst-name" title=${label}>${label}</span>
439
- ${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}
440
606
  ${facet === "needs" ? html`<span class="inst-sub"><span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span></span>` : null}
441
607
  <span class="sr-only">${tn("tooltipBackend", { value: backendLabel(it.backend) })}</span></span>
442
- <span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
443
608
  </a>${fav && editing && canStar ? html`<button type="button" class="icon-btn inst-move" aria-label=${tn("favUp", { name: label })}
444
- 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"
445
- aria-label=${tn("favDown", { name: label })} title=${shown.active ? tn("favFiltered") : tn("favDown", { name: label })} disabled=${shown.active || i === count - 1}
446
- 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
447
612
  ? html`<button type="button" class=${`icon-btn inst-star${starred ? " on" : ""}`} aria-pressed=${starred ? "true" : "false"} disabled=${full}
448
613
  aria-label=${tn(starred ? "favRemove" : "favAdd", { name: label })} title=${full ? tn("favFull", { max: MAX_FAVORITES }) : tn(starred ? "favRemove" : "favAdd", { name: label })}
449
614
  onClick=${() => toggleFavorite(name, known)}><${Icon} name="star" size=${14} /></button>` : null}</li>`;
450
615
  };
451
616
  // The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
452
617
  return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
453
- <div class="side-section" id="instanceList" ref=${list}>
618
+ <div data-trust="navigation" class="side-section" id="instanceList" ref=${list}>
454
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}
455
622
  ${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
456
623
  ${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
457
624
  ${!loaded ? html`<${Skeleton} lines=${4} />` : !items.length ? html`<p class="side-empty">${t("app.noInstances")}</p>` : null}
458
- ${shown.groups.map(({ group: g, names }) => {
625
+ ${pinned ? html`<div key=${PINNED_GROUP} class="v-group v-pinned" role="group" aria-label=${tn("pinnedLabel")}>
626
+ <div class="v-group-head pinned-head"><span class="grow">${tn("pinnedCurrent")}</span></div>
627
+ <ul class="inst-list">${row(pinned, PINNED_GROUP, 0, 1)}</ul></div>` : null}
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>`;
459
633
  // #1664: Favorites — always open, not dragged (↑ ↓ in Edit), named in the page's language.
460
634
  if (g === FAV_GROUP) return html`<div key=${g} class="v-group v-fav" role="group" aria-label=${tn("favorites")}>
461
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>
@@ -463,14 +637,14 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
463
637
  const folded = nav.collapsed.has(g);
464
638
  return html`<div key=${g} class="v-group">
465
639
  <button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
466
- title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
467
- <${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>
468
642
  ${folded ? null : html`<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul>`}
469
643
  </div>`;
470
644
  })}
471
645
  ${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
472
646
  </div>
473
- <div class="v-filter">
647
+ ${guest ? null : html`<div class="v-filter">
474
648
  <label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
475
649
  <input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
476
650
  onInput=${(e) => setNavFilter({ q: e.target.value })}
@@ -486,7 +660,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
486
660
  </div>
487
661
  <p class="note nav-shown" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
488
662
  </div>
489
- </div>
663
+ </div>`}
490
664
  </div>`;
491
665
  }
492
666