@songsid/agend 2.3.0-beta.25 → 2.3.0-beta.26

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 (417) hide show
  1. package/dist/adapter-world.js.map +1 -1
  2. package/dist/agend-home.d.ts +5 -0
  3. package/dist/agend-home.js +7 -0
  4. package/dist/agend-home.js.map +1 -0
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +8 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-file-read.js +1 -1
  9. package/dist/agent-file-read.js.map +1 -1
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/apply-job.js.map +1 -1
  12. package/dist/atomic-write.js.map +1 -1
  13. package/dist/auth-api.d.ts +5 -0
  14. package/dist/auth-api.js +21 -0
  15. package/dist/auth-api.js.map +1 -1
  16. package/dist/authority/authority-state.d.ts +4 -0
  17. package/dist/authority/authority-state.js +17 -8
  18. package/dist/authority/authority-state.js.map +1 -1
  19. package/dist/authority/session-authorizations.js.map +1 -1
  20. package/dist/authority/web-authority.d.ts +14 -6
  21. package/dist/authority/web-authority.js +19 -5
  22. package/dist/authority/web-authority.js.map +1 -1
  23. package/dist/backend/antigravity.d.ts +2 -2
  24. package/dist/backend/antigravity.js +2 -1
  25. package/dist/backend/antigravity.js.map +1 -1
  26. package/dist/backend/claude-code.js.map +1 -1
  27. package/dist/backend/codex-package.js.map +1 -1
  28. package/dist/backend/codex-session-lookup.js.map +1 -1
  29. package/dist/backend/codex.js +1 -1
  30. package/dist/backend/codex.js.map +1 -1
  31. package/dist/backend/effort-metadata.js.map +1 -1
  32. package/dist/backend/grok.js.map +1 -1
  33. package/dist/backend/kiro-engine-ledger.js.map +1 -1
  34. package/dist/backend/kiro-kas-store.js.map +1 -1
  35. package/dist/backend/kiro-v2-store.js.map +1 -1
  36. package/dist/backend/kiro-v3-identity.js.map +1 -1
  37. package/dist/backend/kiro.js.map +1 -1
  38. package/dist/backend/launch-effort.js.map +1 -1
  39. package/dist/backend/mock.js.map +1 -1
  40. package/dist/backend/muse.js.map +1 -1
  41. package/dist/backend/opencode-permission.js.map +1 -1
  42. package/dist/backend/opencode.js.map +1 -1
  43. package/dist/backend/session-signals.js.map +1 -1
  44. package/dist/backend-availability.js.map +1 -1
  45. package/dist/backend-command.js.map +1 -1
  46. package/dist/backend-outage.js.map +1 -1
  47. package/dist/binary-probe.js.map +1 -1
  48. package/dist/bug-report.js.map +1 -1
  49. package/dist/bundled-skills.js.map +1 -1
  50. package/dist/cache-service.js +1 -1
  51. package/dist/cache-service.js.map +1 -1
  52. package/dist/channel/adapters/discord.js +29 -4
  53. package/dist/channel/adapters/discord.js.map +1 -1
  54. package/dist/channel/adapters/telegram.d.ts +6 -0
  55. package/dist/channel/adapters/telegram.js +54 -14
  56. package/dist/channel/adapters/telegram.js.map +1 -1
  57. package/dist/channel/mcp-server.js +10 -1
  58. package/dist/channel/mcp-server.js.map +1 -1
  59. package/dist/channel/mcp-tools.js +2 -2
  60. package/dist/channel/mcp-tools.js.map +1 -1
  61. package/dist/channel/message-queue.js.map +1 -1
  62. package/dist/channel/sticker-download.js.map +1 -1
  63. package/dist/channel/tool-router.js.map +1 -1
  64. package/dist/classic-channel-manager.js +3 -2
  65. package/dist/classic-channel-manager.js.map +1 -1
  66. package/dist/cli-entry.js.map +1 -1
  67. package/dist/cli-flags.js.map +1 -1
  68. package/dist/cli.js +15 -9
  69. package/dist/cli.js.map +1 -1
  70. package/dist/command-table.js.map +1 -1
  71. package/dist/compact-watch.js.map +1 -1
  72. package/dist/completion-install.js.map +1 -1
  73. package/dist/completion.js.map +1 -1
  74. package/dist/config-validator.js +18 -4
  75. package/dist/config-validator.js.map +1 -1
  76. package/dist/config.js +8 -2
  77. package/dist/config.js.map +1 -1
  78. package/dist/connection-secrets.js.map +1 -1
  79. package/dist/context-guardian.js.map +1 -1
  80. package/dist/coredump-filter.js.map +1 -1
  81. package/dist/cpu-profile.js.map +1 -1
  82. package/dist/cross-instance-envelope.js.map +1 -1
  83. package/dist/daemon.d.ts +3 -0
  84. package/dist/daemon.js +73 -3
  85. package/dist/daemon.js.map +1 -1
  86. package/dist/daily-summary.js.map +1 -1
  87. package/dist/darwin-memory.js.map +1 -1
  88. package/dist/dashboard-live.d.ts +8 -0
  89. package/dist/dashboard-live.js +35 -0
  90. package/dist/dashboard-live.js.map +1 -0
  91. package/dist/dashboard-permissions.d.ts +69 -0
  92. package/dist/dashboard-permissions.js +138 -0
  93. package/dist/dashboard-permissions.js.map +1 -0
  94. package/dist/decision-list.js.map +1 -1
  95. package/dist/delivery-consumed-watch.js.map +1 -1
  96. package/dist/delivery-outbox.js.map +1 -1
  97. package/dist/delivery-outbox.test.js.map +1 -1
  98. package/dist/delivery-reconciliation.test.js.map +1 -1
  99. package/dist/diagnostic-redaction.js.map +1 -1
  100. package/dist/doctor.js.map +1 -1
  101. package/dist/e2e-isolation.js.map +1 -1
  102. package/dist/event-log.js.map +1 -1
  103. package/dist/export-import.js +3 -2
  104. package/dist/export-import.js.map +1 -1
  105. package/dist/fleet-control-audit.js.map +1 -1
  106. package/dist/fleet-label.js.map +1 -1
  107. package/dist/fleet-lock.js +3 -2
  108. package/dist/fleet-lock.js.map +1 -1
  109. package/dist/fleet-manager.d.ts +76 -6
  110. package/dist/fleet-manager.js +479 -145
  111. package/dist/fleet-manager.js.map +1 -1
  112. package/dist/fleet-process-handlers.js +3 -2
  113. package/dist/fleet-process-handlers.js.map +1 -1
  114. package/dist/fleet-startup.d.ts +14 -0
  115. package/dist/fleet-startup.js +30 -0
  116. package/dist/fleet-startup.js.map +1 -0
  117. package/dist/fleet-traffic.js.map +1 -1
  118. package/dist/forged-envelope-scan.d.ts +0 -20
  119. package/dist/forged-envelope-scan.js +3 -2
  120. package/dist/forged-envelope-scan.js.map +1 -1
  121. package/dist/full-restart.js.map +1 -1
  122. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +2 -0
  123. package/dist/general-knowledge/skills/web-appearance/SKILL.md +2 -0
  124. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +33 -11
  125. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +42 -13
  126. package/dist/host-memory.js.map +1 -1
  127. package/dist/instance-config-impact.js +6 -0
  128. package/dist/instance-config-impact.js.map +1 -1
  129. package/dist/instance-lifecycle.js +1 -1
  130. package/dist/instance-lifecycle.js.map +1 -1
  131. package/dist/instructions.d.ts +2 -0
  132. package/dist/instructions.js +6 -2
  133. package/dist/instructions.js.map +1 -1
  134. package/dist/kiro-compact-pane.js.map +1 -1
  135. package/dist/kiro-db-reader.js.map +1 -1
  136. package/dist/kiro-engine-status.js.map +1 -1
  137. package/dist/kiro-log-gate.js.map +1 -1
  138. package/dist/kiro-transcript-lane.js.map +1 -1
  139. package/dist/locale.js +22 -0
  140. package/dist/locale.js.map +1 -1
  141. package/dist/log-service.js.map +1 -1
  142. package/dist/log-steps.js.map +1 -1
  143. package/dist/logger.js +3 -2
  144. package/dist/logger.js.map +1 -1
  145. package/dist/login-controller.js.map +1 -1
  146. package/dist/login-manager.js.map +1 -1
  147. package/dist/login-shell.js.map +1 -1
  148. package/dist/memory-pressure.js.map +1 -1
  149. package/dist/muse-usage-relay.js.map +1 -1
  150. package/dist/needs-you-hub.js.map +1 -1
  151. package/dist/needs-you-live.js.map +1 -1
  152. package/dist/network-family.js.map +1 -1
  153. package/dist/operator-control-paths.js +3 -4
  154. package/dist/operator-control-paths.js.map +1 -1
  155. package/dist/outbound-handlers.js.map +1 -1
  156. package/dist/outbound-schemas.d.ts +68 -68
  157. package/dist/outbound-schemas.js +1 -1
  158. package/dist/outbound-schemas.js.map +1 -1
  159. package/dist/outbox-open-error.js.map +1 -1
  160. package/dist/pane-context-cache.js.map +1 -1
  161. package/dist/paths.js.map +1 -1
  162. package/dist/private-dir.js.map +1 -1
  163. package/dist/probe-worker-pool.js.map +1 -1
  164. package/dist/profile-control.js.map +1 -1
  165. package/dist/prompt-file-ref.js.map +1 -1
  166. package/dist/provider-probe.js.map +1 -1
  167. package/dist/provider-secret-registry.js.map +1 -1
  168. package/dist/public-link-auto-open.d.ts +32 -0
  169. package/dist/public-link-auto-open.js +86 -0
  170. package/dist/public-link-auto-open.js.map +1 -0
  171. package/dist/public-link-owner.d.ts +10 -0
  172. package/dist/public-link-owner.js +36 -0
  173. package/dist/public-link-owner.js.map +1 -0
  174. package/dist/public-link-progress.js.map +1 -1
  175. package/dist/public-preview.js.map +1 -1
  176. package/dist/public-web-gateway.js +4 -2
  177. package/dist/public-web-gateway.js.map +1 -1
  178. package/dist/public-web-link.d.ts +35 -1
  179. package/dist/public-web-link.js +142 -43
  180. package/dist/public-web-link.js.map +1 -1
  181. package/dist/quickstart-api.js.map +1 -1
  182. package/dist/quickstart.d.ts +5 -0
  183. package/dist/quickstart.js +25 -16
  184. package/dist/quickstart.js.map +1 -1
  185. package/dist/reply-buttons-controller.js.map +1 -1
  186. package/dist/reply-dedup.js.map +1 -1
  187. package/dist/resource-report.js.map +1 -1
  188. package/dist/restart-guard.js.map +1 -1
  189. package/dist/restart-progress.js.map +1 -1
  190. package/dist/rollout-index.js.map +1 -1
  191. package/dist/runtime-cpu-profile.js.map +1 -1
  192. package/dist/scheduler/db.js.map +1 -1
  193. package/dist/scheduler/scheduler.js.map +1 -1
  194. package/dist/scheduler/task-board-text.js +16 -11
  195. package/dist/scheduler/task-board-text.js.map +1 -1
  196. package/dist/scheduler/task-board.d.ts +39 -11
  197. package/dist/scheduler/task-board.js +123 -45
  198. package/dist/scheduler/task-board.js.map +1 -1
  199. package/dist/scheduler/types.d.ts +1 -1
  200. package/dist/sd-notify.js.map +1 -1
  201. package/dist/search/search-index.js.map +1 -1
  202. package/dist/secret-file.js.map +1 -1
  203. package/dist/secret-store.js.map +1 -1
  204. package/dist/service-installer.d.ts +7 -1
  205. package/dist/service-installer.js +13 -1
  206. package/dist/service-installer.js.map +1 -1
  207. package/dist/service-plan.d.ts +33 -4
  208. package/dist/service-plan.js +177 -8
  209. package/dist/service-plan.js.map +1 -1
  210. package/dist/session-oom.d.ts +21 -0
  211. package/dist/session-oom.js +139 -0
  212. package/dist/session-oom.js.map +1 -0
  213. package/dist/settings-api.js +3 -1
  214. package/dist/settings-api.js.map +1 -1
  215. package/dist/settings-baseline.js.map +1 -1
  216. package/dist/settings-change.d.ts +5 -0
  217. package/dist/settings-change.js +71 -1
  218. package/dist/settings-change.js.map +1 -1
  219. package/dist/settings-confirmation.d.ts +4 -0
  220. package/dist/settings-confirmation.js +2 -0
  221. package/dist/settings-confirmation.js.map +1 -1
  222. package/dist/settings-control.js.map +1 -1
  223. package/dist/settings-effect.js +4 -1
  224. package/dist/settings-effect.js.map +1 -1
  225. package/dist/settings-http-confirmation.js +1 -0
  226. package/dist/settings-http-confirmation.js.map +1 -1
  227. package/dist/settings-instance-patch.js +13 -1
  228. package/dist/settings-instance-patch.js.map +1 -1
  229. package/dist/settings-transaction.js.map +1 -1
  230. package/dist/setup-auth.js.map +1 -1
  231. package/dist/setup-host.d.ts +5 -1
  232. package/dist/setup-host.js +8 -1
  233. package/dist/setup-host.js.map +1 -1
  234. package/dist/setup-wizard.js +5 -2
  235. package/dist/setup-wizard.js.map +1 -1
  236. package/dist/spawn-gate.d.ts +4 -0
  237. package/dist/spawn-gate.js +15 -5
  238. package/dist/spawn-gate.js.map +1 -1
  239. package/dist/startup-memory-deadline.d.ts +23 -0
  240. package/dist/startup-memory-deadline.js +55 -0
  241. package/dist/startup-memory-deadline.js.map +1 -0
  242. package/dist/statusline-watcher.js.map +1 -1
  243. package/dist/storm-window.js.map +1 -1
  244. package/dist/systemd-peer-bus.js.map +1 -1
  245. package/dist/target-queue-worker.js.map +1 -1
  246. package/dist/task-board-summary.js.map +1 -1
  247. package/dist/task-board-tool.js +3 -2
  248. package/dist/task-board-tool.js.map +1 -1
  249. package/dist/task-board-waker.js +8 -4
  250. package/dist/task-board-waker.js.map +1 -1
  251. package/dist/tips.js.map +1 -1
  252. package/dist/tmux-control.js.map +1 -1
  253. package/dist/tmux-exec-helper.js.map +1 -1
  254. package/dist/tmux-manager.d.ts +1 -0
  255. package/dist/tmux-manager.js +2 -1
  256. package/dist/tmux-manager.js.map +1 -1
  257. package/dist/tmux-read.js +1 -1
  258. package/dist/tmux-read.js.map +1 -1
  259. package/dist/tool-permissions.js.map +1 -1
  260. package/dist/tool-progress.js.map +1 -1
  261. package/dist/topic-archiver.js.map +1 -1
  262. package/dist/topic-commands.js.map +1 -1
  263. package/dist/topic-link.js.map +1 -1
  264. package/dist/transcript-delta-reader.js.map +1 -1
  265. package/dist/transcript-jsonl.js.map +1 -1
  266. package/dist/transcript-monitor.js.map +1 -1
  267. package/dist/transcript-sources.js.map +1 -1
  268. package/dist/tunnel/cloudflared-install.js.map +1 -1
  269. package/dist/tunnel/cloudflared.js.map +1 -1
  270. package/dist/tunnel/lease.js +3 -2
  271. package/dist/tunnel/lease.js.map +1 -1
  272. package/dist/tunnel/manager.js.map +1 -1
  273. package/dist/tunnel/public-fetch.js.map +1 -1
  274. package/dist/tunnel/purpose-lane.js.map +1 -1
  275. package/dist/tunnel/types.js.map +1 -1
  276. package/dist/types.d.ts +20 -1
  277. package/dist/ui/chat-render.js +1 -15
  278. package/dist/ui/chat-store.js +171 -61
  279. package/dist/ui/chat-strings.js +7 -2
  280. package/dist/ui/chat-thread.js +48 -1
  281. package/dist/ui/fleet-canvas-layout.js +3 -0
  282. package/dist/ui/fleet-canvas.js +23 -20
  283. package/dist/ui/fleet-office-editor.js +149 -0
  284. package/dist/ui/fleet-office-model.js +358 -0
  285. package/dist/ui/fleet-office-pack.js +199 -0
  286. package/dist/ui/fleet-office-skin.js +474 -0
  287. package/dist/ui/fleet-office-store.js +49 -0
  288. package/dist/ui/fleet-office-zones.js +177 -0
  289. package/dist/ui/fleet-office.js +434 -0
  290. package/dist/ui/fleet-org.js +18 -5
  291. package/dist/ui/guest-refused.html +41 -0
  292. package/dist/ui/image-lightbox.js +9 -3
  293. package/dist/ui/panel-chat.js +14 -13
  294. package/dist/ui/panel-settings.js +24 -7
  295. package/dist/ui/settings-dialogs.js +11 -2
  296. package/dist/ui/settings-model.js +20 -0
  297. package/dist/ui/settings-strings.js +36 -0
  298. package/dist/ui/settings-web-authority.js +7 -2
  299. package/dist/ui/shared/agend-auth.js +2 -0
  300. package/dist/ui/shared/app-i18n.js +2 -2
  301. package/dist/ui/shared/app-prefs.js +2 -0
  302. package/dist/ui/shared/app-shell.js +12 -0
  303. package/dist/ui/shared/app-store.js +1 -0
  304. package/dist/ui/shared/app-stream.js +2 -1
  305. package/dist/ui/shared/app.css +132 -3
  306. package/dist/ui/shared/guest-refused.js +35 -0
  307. package/dist/ui/shared/header-tools.js +2 -2
  308. package/dist/ui/shared/instance-nav.js +25 -8
  309. package/dist/ui/shared/ui-dialog.js +2 -2
  310. package/dist/ui/shared/usage-dialog.js +152 -6
  311. package/dist/ui/shared/view-strings.js +20 -0
  312. package/dist/ui/shared/web-authorize.js +3 -1
  313. package/dist/update-check.js.map +1 -1
  314. package/dist/update-dispatch.js.map +1 -1
  315. package/dist/update-install.js.map +1 -1
  316. package/dist/update-launch.js.map +1 -1
  317. package/dist/update-marker.js.map +1 -1
  318. package/dist/update-preflight.js.map +1 -1
  319. package/dist/usage/format-rich.js.map +1 -1
  320. package/dist/usage/providers.d.ts +8 -0
  321. package/dist/usage/providers.js +6 -2
  322. package/dist/usage/providers.js.map +1 -1
  323. package/dist/usage/statusline-usage.js.map +1 -1
  324. package/dist/usage/usage-api.js +2 -1
  325. package/dist/usage/usage-api.js.map +1 -1
  326. package/dist/usage-report.js.map +1 -1
  327. package/dist/usage-rollup.js +1 -1
  328. package/dist/usage-rollup.js.map +1 -1
  329. package/dist/user-systemd.js.map +1 -1
  330. package/dist/view-api.js +1 -1
  331. package/dist/view-api.js.map +1 -1
  332. package/dist/wake-coordinator.js.map +1 -1
  333. package/dist/web-ambient.js.map +1 -1
  334. package/dist/web-api.d.ts +15 -2
  335. package/dist/web-api.js +174 -25
  336. package/dist/web-api.js.map +1 -1
  337. package/dist/web-app-icon.js.map +1 -1
  338. package/dist/web-appearance-policy.js.map +1 -1
  339. package/dist/web-appearance-tools.js.map +1 -1
  340. package/dist/web-appearance-view.js.map +1 -1
  341. package/dist/web-appearance.js.map +1 -1
  342. package/dist/web-auth.js +5 -4
  343. package/dist/web-auth.js.map +1 -1
  344. package/dist/web-backend-login.js.map +1 -1
  345. package/dist/web-card.js.map +1 -1
  346. package/dist/web-channel-echo.js.map +1 -1
  347. package/dist/web-chat-history.d.ts +41 -11
  348. package/dist/web-chat-history.js +93 -22
  349. package/dist/web-chat-history.js.map +1 -1
  350. package/dist/web-commands.js.map +1 -1
  351. package/dist/web-effect-bundle.d.ts +2 -2
  352. package/dist/web-effect-library.js.map +1 -1
  353. package/dist/web-effect-playback.js.map +1 -1
  354. package/dist/web-effect-profiles.d.ts +16 -16
  355. package/dist/web-effect-profiles.js +3 -2
  356. package/dist/web-effect-profiles.js.map +1 -1
  357. package/dist/web-effect-service.js.map +1 -1
  358. package/dist/web-effect-tools.js.map +1 -1
  359. package/dist/web-effect-trials.js.map +1 -1
  360. package/dist/web-effect-upload.js.map +1 -1
  361. package/dist/web-effect.d.ts +7 -7
  362. package/dist/web-effect.js.map +1 -1
  363. package/dist/web-experimental.js.map +1 -1
  364. package/dist/web-guest-access.js +1 -1
  365. package/dist/web-guest-access.js.map +1 -1
  366. package/dist/web-guest-rooms.js.map +1 -1
  367. package/dist/web-host-guard.js.map +1 -1
  368. package/dist/web-icons.js.map +1 -1
  369. package/dist/web-identity-color.js.map +1 -1
  370. package/dist/web-images.js.map +1 -1
  371. package/dist/web-libavoid.js.map +1 -1
  372. package/dist/web-login.js.map +1 -1
  373. package/dist/web-mentions.js.map +1 -1
  374. package/dist/web-mod-agent.js.map +1 -1
  375. package/dist/web-mod-store.js.map +1 -1
  376. package/dist/web-office-store.d.ts +104 -0
  377. package/dist/web-office-store.js +236 -0
  378. package/dist/web-office-store.js.map +1 -0
  379. package/dist/web-owner-css-store.js.map +1 -1
  380. package/dist/web-owner-css.js.map +1 -1
  381. package/dist/web-png.js.map +1 -1
  382. package/dist/web-prefs.js +2 -0
  383. package/dist/web-prefs.js.map +1 -1
  384. package/dist/web-profile.js.map +1 -1
  385. package/dist/web-reactions.d.ts +2 -1
  386. package/dist/web-request-context.d.ts +5 -0
  387. package/dist/web-request-context.js.map +1 -1
  388. package/dist/web-returning-device.js +1 -1
  389. package/dist/web-returning-device.js.map +1 -1
  390. package/dist/web-schedules.js.map +1 -1
  391. package/dist/web-send-dedupe.d.ts +36 -0
  392. package/dist/web-send-dedupe.js +69 -0
  393. package/dist/web-send-dedupe.js.map +1 -0
  394. package/dist/web-session-authorization.js.map +1 -1
  395. package/dist/web-session.js +1 -1
  396. package/dist/web-session.js.map +1 -1
  397. package/dist/web-settings-patch.d.ts +1 -0
  398. package/dist/web-settings-patch.js +6 -4
  399. package/dist/web-settings-patch.js.map +1 -1
  400. package/dist/web-settings-tools.js.map +1 -1
  401. package/dist/web-terminal-http.js.map +1 -1
  402. package/dist/web-terminal.js.map +1 -1
  403. package/dist/web-theme-store.js.map +1 -1
  404. package/dist/web-upload.d.ts +5 -1
  405. package/dist/web-upload.js +6 -4
  406. package/dist/web-upload.js.map +1 -1
  407. package/dist/web-widget.js.map +1 -1
  408. package/dist/webhook-emitter.js.map +1 -1
  409. package/dist/windows-shortcuts.js +1 -1
  410. package/dist/windows-shortcuts.js.map +1 -1
  411. package/dist/workflow-templates/default.md +1 -1
  412. package/dist/ws-server.js.map +1 -1
  413. package/dist/yaml-shared-nodes.js.map +1 -1
  414. package/launcher/agend-install.cjs +281 -6
  415. package/launcher/private-home.cjs +43 -0
  416. package/package.json +3 -2
  417. package/templates/windows-shortcuts.ps1 +1 -1
@@ -50,6 +50,7 @@ export function applyStatus(d) {
50
50
  if (i.state === "awaiting_input") awaiting[i.name] = typeof i.interaction_summary === "string" ? i.interaction_summary : "";
51
51
  }
52
52
  appStore.set({ canvasWorker: d.canvas_worker === true, ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
53
+ startup: d.startup?.phase === "starting" ? d.startup : null,
53
54
  connections: typeof d.connections === "number" ? d.connections : null,
54
55
  // #1665: the host preferences' revision (app-prefs.js re-reads them when it moves).
55
56
  prefsRev: typeof d.prefs_rev === "number" ? d.prefs_rev : null,
@@ -34,7 +34,8 @@
34
34
  // org canvas reads /ui/traffic when it has no stream).
35
35
  // #2233: backend_login — a dashboard sign-in started, moved or ended (no details: the page reads its own view).
36
36
  // tasks — the task board changed: the card numbers a write touched (SSE only: the board reads again when it is shown).
37
- const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks"];
37
+ // offices — the stored offices changed ({ rev }; SSE only: the Office view reads /ui/offices again).
38
+ const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks", "offices"];
38
39
  const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
39
40
  const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
40
41
  const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
@@ -470,6 +470,36 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
470
470
  such as 「昨天 下午03:58」, ticks) — and a block body would stretch to it; fit-content keeps a short message short, on
471
471
  the right under its meta, and a long one still wraps at the message's max-width. */
472
472
  .msg.user .body { background: var(--user-bubble); border-radius: var(--r-xl); padding: 10px var(--s-4); width: fit-content; max-width: 100%; margin-left: auto; }
473
+ /* A message this page is sending (chat-thread.js outgoingHtml): shown at once, its text dimmed with "Sending…" until the
474
+ server takes it; failed, a straight red bar with Retry and Delete. Each file: a picture as itself (or a document mark),
475
+ its name, a square-ended bar and its percentage, moved in place. */
476
+ .msg.user.out .meta { align-items: center; gap: var(--s-2); }
477
+ .msg.out.out-queued .body, .msg.out.out-sending .body { color: var(--text-3); }
478
+ .msg.out.out-queued .body .md, .msg.out.out-sending .body .md { opacity: .72; }
479
+ .out-state { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-3); }
480
+ .out-spin { width: 10px; height: 10px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: out-spin .8s linear infinite; }
481
+ @keyframes out-spin { to { transform: rotate(360deg); } }
482
+ .out-files { display: flex; flex-direction: column; gap: var(--s-2); margin-top: var(--s-2); }
483
+ .msg.out .body .out-files:first-child { margin-top: 0; }
484
+ .out-file { display: flex; align-items: center; gap: var(--s-2); min-width: 0; }
485
+ .out-pic { width: 72px; height: 72px; object-fit: cover; border-radius: var(--r-md); flex: none; background: var(--surface-2); }
486
+ .msg.out.out-queued .out-pic, .msg.out.out-sending .out-pic { opacity: .7; }
487
+ .out-doc { width: 32px; height: 40px; flex: none; border: 1px solid var(--border); border-radius: 4px; background: var(--surface-2); }
488
+ .out-file-info { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px var(--s-2); min-width: 0; flex: 1; min-width: 140px; }
489
+ .out-name { font-size: var(--fs-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
490
+ .out-pct { font-size: var(--fs-xs); color: var(--text-3); font-variant-numeric: tabular-nums; }
491
+ .out-bar { grid-column: 1 / -1; height: 3px; background: var(--border); overflow: hidden; }
492
+ .out-fill { display: block; height: 100%; background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform .2s linear; }
493
+ .out-file[data-state="done"] .out-bar, .out-file[data-state="done"] .out-pct { visibility: hidden; }
494
+ .out-file[data-state="failed"] .out-fill { background: var(--danger); }
495
+ .out-file[data-state="failed"] .out-pct { color: var(--danger); }
496
+ .msg.out.out-sent .out-file .out-bar, .msg.out.out-sent .out-file .out-pct { visibility: hidden; }
497
+ /* Hugs its words under the bubble's right edge, as the bubble does: the bar stays beside them, never across the row. */
498
+ .out-note { --lbar-color: var(--danger); display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); width: fit-content; max-width: 100%;
499
+ margin: var(--s-2) 0 0 auto; padding: var(--s-1) 0 var(--s-1) calc(var(--s-2) + 3px); font-size: var(--fs-xs); color: var(--danger); }
500
+ .out-error { overflow-wrap: anywhere; }
501
+ .out-actions { display: inline-flex; gap: var(--s-1); }
502
+ @media (prefers-reduced-motion: reduce) { .out-spin { animation: none; border-right-color: currentColor; opacity: .6; } .out-fill { transition: none; } }
473
503
  /* A person's click on a reply button: the bubble shows 🔘 and the button's label (never the value the agent was given);
474
504
  live, the label is a button that shows the reply its buttons were on. */
475
505
  .click-chip { display: flex; }
@@ -795,6 +825,8 @@ img.amb-p { object-fit: contain; }
795
825
  /* #1939 S2a: Settings → Web → Web authority. */
796
826
  .s-web-authority .s-va-sessions { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
797
827
  .s-web-authority .s-va-sessions li { padding: var(--s-2) var(--s-3); border: 1px solid var(--border); border-radius: var(--r-sm); overflow-wrap: anywhere; }
828
+ /* #2453: "over your named link" stays one phrase (the list item may break anywhere, CJK included). */
829
+ .s-web-authority .s-va-named { white-space: nowrap; }
798
830
  .s-web-authority .s-va-warn { background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn-line); border-radius: var(--r-sm); padding: var(--s-1) var(--s-2); }
799
831
  .s-web-authority .save-row { flex-wrap: wrap; }
800
832
  /* #1920: a guest's note under the composer — on a phone too, where the key hints are hidden. */
@@ -1032,7 +1064,21 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1032
1064
  down, never the count; the count is as tall as a header, so the row does not grow. */
1033
1065
  .nav-facets { display: flex; align-items: flex-start; gap: var(--s-2); padding: var(--s-2) var(--s-1) 0; }
1034
1066
  .nav-facet-groups { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s-1) var(--s-2); flex: 1 1 auto; min-width: 0; }
1035
- .nav-facets .nav-shown { flex: none; margin: 0 0 0 auto; padding: 2px 0; white-space: nowrap; }
1067
+ /* #2401: the count, "shown" under it and Clear under that. The word's width is held by ::before (its text from
1068
+ data-reserve) and idle Clear's by the button itself, both at no height, unseen and out of reach (visibility): the column —
1069
+ and the headers' box beside it — is as wide with a facet set as without. */
1070
+ .nav-facet-end { flex: none; margin: 0 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; }
1071
+ .nav-facets .nav-shown { margin: 0; padding: 2px 0; white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
1072
+ .nav-shown-word { display: block; }
1073
+ .nav-facet-end::before { content: attr(data-reserve); display: block; height: 0; overflow: hidden; visibility: hidden; font-size: var(--fs-xs); white-space: nowrap; }
1074
+ .nav-facet-end .is-idle { visibility: hidden; height: 0; min-height: 0; margin: 0; padding-top: 0; padding-bottom: 0; border-top-width: 0; border-bottom-width: 0; overflow: hidden; }
1075
+ /* No open list beside it (.compact): one cell of one line, as wide as the count or Clear; a facet set, Clear in the count's
1076
+ place (the count transparent, still read; the word for a screen reader only, so its width is not held here). */
1077
+ .nav-facets.compact .nav-facet-end { display: grid; justify-items: end; align-items: center; }
1078
+ .nav-facets.compact .nav-facet-end > * { grid-area: 1 / 1; margin: 0; }
1079
+ .nav-facets.compact .nav-facet-end::before { display: none; }
1080
+ .nav-facets.compact .nav-shown.is-set { opacity: 0; pointer-events: none; }
1081
+ .nav-facets.compact .nav-shown-word { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
1036
1082
  .nav-facet { font-size: var(--fs-xs); color: var(--text-2); min-width: 0; max-width: 100%; }
1037
1083
  /* #1700: a facet group's header — the whole row toggles its list; folded with a filter set, a short summary. */
1038
1084
  .nav-facet-head { display: inline-flex; align-items: center; gap: var(--s-1); max-width: 100%; padding: 2px var(--s-2); border: 0; border-radius: var(--r-sm);
@@ -1041,10 +1087,15 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1041
1087
  .nav-facet-head:focus-visible { outline: 2px solid var(--accent); }
1042
1088
  .nav-facet-head .caret { color: var(--text-3); flex: none; }
1043
1089
  .nav-facet-sum { min-width: 0; max-width: 12em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--accent); }
1090
+ /* #2401: the summary takes only the room its line has left — never counted when the headers wrap (contain: inline-size,
1091
+ its gap taken back), so a facet set never sends a header to a line of its own; the header holding it grows into the rest. */
1092
+ .nav-facet.has-sum { flex-grow: 1; }
1093
+ .nav-facet.has-sum .nav-facet-head { width: 100%; }
1094
+ .nav-facet-sum { contain: inline-size; flex: 1 1 0; margin-left: calc(-1 * var(--s-1)); padding-left: var(--s-1); }
1044
1095
  .nav-facet-n { display: inline-block; min-width: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: var(--on-accent, #fff); text-align: center; font-variant-numeric: tabular-nums; }
1045
1096
  .nav-facet-list { display: flex; flex-direction: column; gap: 2px; padding: var(--s-1) var(--s-2); }
1046
1097
  .nav-chip { display: flex; align-items: center; gap: var(--s-2); cursor: pointer; color: var(--text); }
1047
- .nav-facet-reset { font-size: var(--fs-xs); }
1098
+ .nav-facet-reset { font-size: var(--fs-xs); flex: none; white-space: nowrap; margin-top: 2px; padding: 2px var(--s-1); }
1048
1099
 
1049
1100
  /* One gap on every page (View's, #1448): the header's controls are the same size on every tab. */
1050
1101
  .panel-actions .btn { gap: var(--s-1); }
@@ -1102,7 +1153,42 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1102
1153
  /* #1673: the usage rail beside the page on a wide screen (header-tools.js UsageRail; shown only at min-width 1200px). */
1103
1154
  .u-rail { width: 320px; flex-shrink: 0; display: flex; flex-direction: column; min-height: 0; border-left: 1px solid var(--border); background: var(--bg); }
1104
1155
  .u-rail-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); padding: var(--s-3) var(--s-3) var(--s-2) var(--s-4); }
1105
- .u-rail-head h2 { margin: 0; font-size: var(--fs-md); font-weight: var(--fw-semibold); }
1156
+ .u-rail-head h2 { margin: 0; flex: 1; min-width: 0; font-size: var(--fs-md); font-weight: var(--fw-semibold); }
1157
+ /* The usage view's compact mode (usage-dialog.js CompactGroup / CompactRow), the user's pick (2026-10-22): the logins
1158
+ grouped by provider (its name once, in small capitals), each in three short lines — its name and plan with the
1159
+ soonest reset on the right, then each window as a labelled full-width bar and the percent. */
1160
+ .dlg-tools { display: inline-flex; align-items: center; flex: none; }
1161
+ .u-compact-toggle { flex: none; }
1162
+ /* flex: none — a group never shrinks to fit the rail (it would clip its accounts): the rail scrolls instead. */
1163
+ .u-cgroup { flex: none; display: flex; flex-direction: column; border-radius: var(--r-md); background: var(--surface); overflow: hidden; }
1164
+ .u-cg-head { display: flex; align-items: baseline; gap: 4px; padding: 7px var(--s-3) 3px; font-size: 11px; font-weight: var(--fw-semibold);
1165
+ letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-3); }
1166
+ .u-cg-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1167
+ .u-cg-count { flex: none; }
1168
+ .u-crow { display: flex; flex-direction: column; gap: 3px; width: 100%; min-width: 0; padding: 6px var(--s-3); border: 0; border-top: 1px solid var(--border);
1169
+ background: transparent; color: inherit; font: inherit; line-height: 1.3; text-align: left; cursor: pointer; }
1170
+ .u-cg-head + .u-crow { border-top: 0; }
1171
+ .u-crow:hover { background: var(--surface-2); }
1172
+ .u-crow:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--focus); }
1173
+ /* The name never gives way to the plan (#2415); the reset keeps its room on the right. */
1174
+ .u-crow-head { display: flex; align-items: center; gap: 6px; min-width: 0; font-size: var(--fs-sm); }
1175
+ .u-crow-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--fw-semibold); color: var(--text); }
1176
+ .u-crow .u-plan { flex: 0 100 auto; min-width: 22px; max-width: max-content; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1177
+ padding-block: 0; line-height: 16px; font-size: 10.5px; }
1178
+ .u-crow-reset { margin-left: auto; flex: none; padding-left: 6px; font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
1179
+ .u-cm { display: grid; grid-template-columns: 2.6em minmax(0, 1fr) 3.4em; align-items: center; gap: 7px; min-width: 0; }
1180
+ .u-cm-tag { font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1181
+ /* #2240: an account of several pools (agy) names each pool's line: a wider label, ending in an ellipsis if it must. */
1182
+ .u-crow.is-pooled .u-cm { grid-template-columns: minmax(0, 7.5em) minmax(0, 1fr) 3.4em; }
1183
+ .u-cm-bar { height: 6px; border-radius: var(--r-full); background: var(--surface-2); overflow: hidden; }
1184
+ .u-cm-fill { display: block; height: 100%; width: 0; min-width: 2px; border-radius: var(--r-full); background: var(--accent); }
1185
+ .u-cm-fill.warn { background: var(--warn); } .u-cm-fill.crit { background: var(--danger); }
1186
+ .u-cm-pct { text-align: right; font-size: 13px; font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; color: var(--text); }
1187
+ .u-cm-pct.warn { color: var(--warn); } .u-cm-pct.crit { color: var(--danger); }
1188
+ .u-crow-note { font-size: var(--fs-xs); color: var(--text-3); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1189
+ .u-crow-note.is-alert::before { content: "●"; margin-right: 5px; color: var(--danger); }
1190
+ .u-crow.is-stale .u-crow-note.is-alert::before { color: var(--warn); }
1191
+ @media (prefers-reduced-motion: no-preference) { .u-crow { transition: background-color 0.15s var(--ease); } }
1106
1192
  .u-rail-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--s-3) var(--s-4); display: flex; flex-direction: column; gap: var(--s-3); }
1107
1193
  @media (max-width: 1199px) { .u-rail { display: none; } }
1108
1194
  @media (max-width: 899px) {
@@ -1460,6 +1546,49 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1460
1546
  .oc-tools { margin-left: 0; width: 100%; flex-wrap: wrap; row-gap: var(--s-2); }
1461
1547
  .oc-mini { display: none; }
1462
1548
  }
1549
+ /* ── Fleet → Org chart → Office (fleet-office.js): the fleet as a pixel office on one <canvas>. Its size is set through
1550
+ the CSSOM (never a style attribute, #1300); the stage scrolls, the page around it too. ── */
1551
+ .oo { display: flex; flex-direction: column; gap: var(--s-2); }
1552
+ .oo-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
1553
+ .oo-pick { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-2); }
1554
+ .oo-pick .tb-select { max-width: 200px; }
1555
+ .oo-people { position: relative; }
1556
+ /* #2399: the offices' minimal editor (fleet-office-editor.js) — one row per office, its members unfolded under it. */
1557
+ .oe-form { border: 0; margin: 0; padding: 0; min-width: 0; }
1558
+ /* #2431: after a 409, the two ways on (keep my edits, load theirs), side by side; they wrap on a phone. */
1559
+ .oe-conflict { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
1560
+ .oe-list { list-style: none; margin: var(--s-3) 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-3); }
1561
+ .oe-row { padding: var(--s-3); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); display: flex; flex-direction: column; gap: var(--s-2); }
1562
+ .oe-head { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s-2); }
1563
+ .oe-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
1564
+ .oe-label { font-size: var(--fs-xs); color: var(--text-2); }
1565
+ .oe-name { min-width: 0; width: 16em; max-width: 100%; }
1566
+ .oe-remove { margin-left: auto; }
1567
+ .oe-members { display: flex; flex-wrap: wrap; gap: var(--s-3); }
1568
+ .oe-group { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; min-width: 12em; max-height: 240px; overflow-y: auto; }
1569
+ .oe-group legend { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--text-2); padding: 0; margin-bottom: 4px; }
1570
+ .oo-people > .btn.on { border-color: var(--accent); }
1571
+ .oo-people-pop { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; width: min(280px, calc(100vw - 32px)); max-height: 340px; overflow-y: auto;
1572
+ display: flex; flex-direction: column; gap: var(--s-2); padding: var(--s-2); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-2); }
1573
+ .oo-people-pop input[type="search"] { width: 100%; height: 32px; }
1574
+ .oo-people-list { list-style: none; margin: 0; padding: 0; }
1575
+ .oo-people-list label { display: flex; align-items: center; gap: 8px; padding: 3px 2px; font-size: var(--fs-xs); color: var(--text); cursor: pointer; overflow-wrap: anywhere; }
1576
+ .oo-note { font-size: var(--fs-xs); }
1577
+ .oo-stage { position: relative; height: clamp(420px, calc(100dvh - 280px), 960px); overflow: auto; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--bg); }
1578
+ .oo-canvas { display: block; margin: 0 auto; image-rendering: pixelated; }
1579
+ .oo-canvas.hot { cursor: pointer; }
1580
+ .oo-tip { position: absolute; left: 0; top: 0; z-index: 5; max-width: 240px; pointer-events: none; padding: 6px 10px; font-size: var(--fs-xs); line-height: 1.4;
1581
+ color: var(--text); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-2); overflow-wrap: anywhere; }
1582
+ /* The desks as a list: for a screen reader, and shown (over the stage's corner) while the keyboard is in it. */
1583
+ .oo-desks { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; list-style: none; }
1584
+ .oo-desks:focus-within { position: sticky; left: var(--s-2); bottom: var(--s-2); width: auto; height: auto; max-height: 40%; margin: 0; padding: var(--s-1); clip: auto; overflow-y: auto;
1585
+ display: inline-flex; flex-wrap: wrap; gap: 4px; max-width: calc(100% - 2 * var(--s-2)); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); white-space: normal; }
1586
+ .oo-desk { font-size: var(--fs-xs); padding: 2px 6px; border-radius: var(--r-sm); background: var(--bg); color: var(--text); border: 1px solid var(--border); }
1587
+ .oo-desk:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
1588
+ @media (max-width: 640px) {
1589
+ .oo-stage { height: calc(100dvh - 300px); min-height: 340px; }
1590
+ .oo-pick .tb-select { max-width: 150px; }
1591
+ }
1463
1592
  /* A phone: Fleet's five tabs fit when only the open one shows its label (the others keep theirs for screen readers). */
1464
1593
  @media (max-width: 479px) {
1465
1594
  .p-fleet .seg-item:not(.active) > span, .p-settings .seg-item:not(.active) > span { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
@@ -0,0 +1,35 @@
1
+ // What a guest's browser is shown for a page it cannot open. Loaded from /assets/guest-refused.js.
2
+ //
3
+ // The gate answers a guest's navigation with this page instead of its JSON (#2399 smoke): "members" for a page that is
4
+ // not a guest's (Fleet, Settings…), "ended" when no chat is shared with the guest any more. The page's words are in
5
+ // the browser's language, as the sign-in page's are; the HTML carries the English for a browser without scripts.
6
+ (() => {
7
+ "use strict";
8
+ const T = {
9
+ en: {
10
+ members_title: "This page is not open to guests",
11
+ members_lead: "This page is for the fleet's members. As a guest you can use the chats shared with you.",
12
+ members_go: "Open your chats",
13
+ ended_title: "No chats are shared with you",
14
+ ended_lead: "Your guest access on this fleet has no chat now — you may have left the room it came from. Ask whoever invited you to share a chat with you again.",
15
+ },
16
+ "zh-TW": {
17
+ members_title: "訪客無法開啟這個頁面",
18
+ members_lead: "這個頁面只給 fleet 的成員使用。身為訪客,你可以使用分享給你的聊天。",
19
+ members_go: "打開你的聊天",
20
+ ended_title: "目前沒有分享給你的聊天",
21
+ ended_lead: "你在這個 fleet 的訪客權限目前沒有任何聊天——你可能已經離開當初的頻道。請聯絡邀請你的人,再分享聊天給你。",
22
+ },
23
+ };
24
+ const lang = /^zh/i.test(navigator.language || "") ? "zh-TW" : "en";
25
+ const t = (k) => T[lang][k] || T.en[k] || k;
26
+ document.documentElement.lang = lang === "zh-TW" ? "zh-Hant" : "en";
27
+ const main = document.getElementById("refused");
28
+ const why = main && main.dataset.why === "ended" ? "ended" : "members";
29
+ document.getElementById("title").textContent = t(`${why}_title`);
30
+ document.getElementById("lead").textContent = t(`${why}_lead`);
31
+ // Nothing to open when no chat is shared (the chats would answer this page again): no button then.
32
+ const go = document.getElementById("go");
33
+ if (go) { if (why === "ended") go.remove(); else go.textContent = t("members_go"); }
34
+ document.title = `AgEnD — ${t(`${why}_title`)}`;
35
+ })();
@@ -9,7 +9,7 @@ import { html, useEffect, useState } from "./app-html.js";
9
9
  import { t } from "./app-i18n.js";
10
10
  import { createStore, useStore } from "./app-store.js";
11
11
  import { Icon } from "./ui-icons.js";
12
- import { UsageBody, UsageDialog } from "./usage-dialog.js";
12
+ import { UsageBody, UsageCompactToggle, UsageDialog } from "./usage-dialog.js";
13
13
 
14
14
  export const TEXT_SIZE_KEY = "agend_text_size";
15
15
  export const VIEW_FIT_KEY = "agend_view_fit";
@@ -126,7 +126,7 @@ export function UsageRail({ route }) {
126
126
  const { open } = useStore(usageRailStore);
127
127
  if (!wide || !open || !route || !RAIL_PANELS.includes(route.panel) || !usageOffered()) return null;
128
128
  return html`<aside data-trust="usage" class="u-rail" aria-label=${t("view.usage.title_plain")}>
129
- <div class="u-rail-head"><h2>${t("view.usage.title")}</h2>
129
+ <div class="u-rail-head"><h2>${t("view.usage.title")}</h2><${UsageCompactToggle} />
130
130
  <button type="button" class="icon-btn" onClick=${() => setUsageRail(false)} aria-label=${t("app.usageHide")} title=${t("app.usageHide")}><${Icon} name="close" size=${16} /></button></div>
131
131
  <div class="u-rail-body"><${UsageBody} owner="usage-rail" /></div>
132
132
  </aside>`;
@@ -82,11 +82,12 @@ export function backendLabel(backend) { return BACKEND_LABELS[backend] || backen
82
82
  export const KNOWN_BACKENDS = new Set(Object.keys(BACKEND_LABELS));
83
83
  export const STATUS_KEYS = { running: "statusRunning", paused: "statusPaused", stopped: "statusStopped", crashed: "statusCrashed" };
84
84
 
85
- /** The status chips, in order (design Q5): what the row's dot says, in five words. */
86
- export const STATUS_FACETS = ["working", "needs", "idle", "stopped", "crashed"];
85
+ /** The status chips, in order: what the row's dot says, including agents queued for this boot. */
86
+ export const STATUS_FACETS = ["working", "needs", "starting", "idle", "stopped", "crashed"];
87
87
  /** Which status chip a row falls under: needs you first, then crashed, not running, working (or stuck), idle. */
88
88
  export function statusFacet(it, exec, awaiting) {
89
89
  if (awaiting != null) return "needs";
90
+ if (it?.startup_phase === "queued" || it?.startup_phase === "starting") return "starting";
90
91
  if (!it || it.status === "crashed") return "crashed";
91
92
  if (it.status !== "running") return "stopped";
92
93
  return exec === "working" || exec === "stuck" ? "working" : "idle";
@@ -127,7 +128,8 @@ export function filterSidebar(groupNames, instByGroup, rosterByName, query, labe
127
128
  }
128
129
  export function instanceTooltip(it) {
129
130
  const context = it.context_pct != null && Number.isFinite(it.context_pct) ? `${Math.round(Math.min(100, Math.max(0, it.context_pct)))}%` : tn("tooltipUnavailable");
130
- const status = STATUS_KEYS[it.status] ? tn(STATUS_KEYS[it.status]) : it.status || tn("statusUnknown");
131
+ const status = it.startup_phase === "queued" ? t("app.statusQueued") : it.startup_phase === "starting" ? t("app.statusStarting")
132
+ : STATUS_KEYS[it.status] ? tn(STATUS_KEYS[it.status]) : it.status || tn("statusUnknown");
131
133
  const line = (key, value) => tn(key, { value });
132
134
  const lines = [];
133
135
  const name = nameOf(it);
@@ -361,12 +363,15 @@ export function avatarOf(it) {
361
363
  /** The status dot on the avatar: needs you, crashed, paused, stopped, looks stuck, working, idle — colour AND a label. */
362
364
  export function dotState(it, exec, awaiting) {
363
365
  if (awaiting != null) return "needs";
366
+ if (it?.startup_phase === "queued") return "queued";
367
+ if (it?.startup_phase === "starting") return "starting";
364
368
  if (!it || it.status === "crashed") return "crashed";
365
369
  if (it.status === "paused") return "paused";
366
370
  if (it.status !== "running") return "stopped";
367
371
  return exec === "stuck" ? "stuck" : exec === "working" ? "working" : "idle";
368
372
  }
369
373
  const DOT = {
374
+ queued: ["off", () => t("app.statusQueued")], starting: ["busy", () => t("app.statusStarting")],
370
375
  needs: ["warn", () => t("app.needsYou")], crashed: ["bad", () => t("app.statusCrashed")], paused: ["off hollow", () => t("app.statusPaused")],
371
376
  stopped: ["off", () => t("app.statusStopped")], stuck: ["warn", () => t("app.statusStuck")], working: ["busy", () => t("app.statusWorking")],
372
377
  idle: ["ok", () => t("app.statusIdle")],
@@ -393,7 +398,7 @@ export function InstanceAvatar({ it, state, cli = false }) {
393
398
 
394
399
  // ── The list ──
395
400
 
396
- const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet === "working" ? t("app.statusWorking") : facet === "idle" ? t("app.statusIdle")
401
+ const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet === "starting" ? t("app.statusStarting") : facet === "working" ? t("app.statusWorking") : facet === "idle" ? t("app.statusIdle")
397
402
  : facet === "crashed" ? t("app.statusCrashed") : t("app.statusStopped"));
398
403
 
399
404
  // ── #2179: when each instance last replied to a person (the status frame's last_reply) ──
@@ -571,7 +576,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
571
576
  const sel = f[key];
572
577
  const summary = !open && sel.length ? (sel.length <= 2 ? sel.map(text).join(", ") : tn("facetSelected", { n: sel.length })) : "";
573
578
  const hint = tn(open ? "facetHide" : "facetShow", { group: label });
574
- return html`<div class=${`nav-facet${open ? " open" : ""}`}>
579
+ return html`<div class=${`nav-facet${open ? " open" : ""}${summary ? " has-sum" : ""}`}>
575
580
  <button type="button" class="nav-facet-head" aria-expanded=${open ? "true" : "false"} aria-controls=${`nav-facet-${key}`}
576
581
  title=${summary ? `${hint} (${label} · ${summary})` : hint} onClick=${() => toggleFacet(key)}>
577
582
  <${Icon} name="chevron" size=${12} cls=${open ? "caret" : "caret folded"} /><span class="nav-facet-label">${label}</span>
@@ -614,6 +619,9 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
614
619
  aria-label=${tn(starred ? "favRemove" : "favAdd", { name: label })} title=${full ? tn("favFull", { max: MAX_FAVORITES }) : tn(starred ? "favRemove" : "favAdd", { name: label })}
615
620
  onClick=${() => toggleFavorite(name, known)}><${Icon} name="star" size=${14} /></button>` : null}</li>`;
616
621
  };
622
+ // #2401: an open list of two or more lines leaves room under the count for "shown" and Clear (stacked); otherwise compact.
623
+ const facetSet = f.status.length > 0 || f.cli.length > 0;
624
+ const stacked = (!viewOnly && facetOpen.status) || (facetOpen.cli && backends.length > 1);
617
625
  // The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
618
626
  return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
619
627
  <div data-trust="navigation" class="side-section" id="instanceList" ref=${list}>
@@ -653,13 +661,22 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
653
661
  ${f.q ? html`<button type="button" class="icon-btn v-filter-clear" aria-label=${tn("filterClear")} title=${tn("filterClear")}
654
662
  onClick=${() => { recordEdit(filterRef.current); setNavFilter({ q: "" }); filterRef.current && filterRef.current.focus(); }}><${Icon} name="close" size=${14} /></button>` : null}
655
663
  </label>
656
- <div class="nav-facets">
664
+ <div class=${`nav-facets${stacked ? "" : " compact"}`}>
657
665
  <div class="nav-facet-groups">
658
666
  ${viewOnly ? null : chips("status", STATUS_FACETS, tn("filterStatus"), statusText)}
659
667
  ${backends.length > 1 || f.cli.length ? chips("cli", backends, tn("filterCli"), backendLabel) : null}
660
- ${f.status.length || f.cli.length ? html`<button type="button" class="chip-btn nav-facet-reset" onClick=${() => setNavFilter({ status: [], cli: [] })}>${tn("filterFacetsReset")}</button>` : null}
661
668
  </div>
662
- <p class="note nav-shown" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
669
+ ${/* #2401: the count, its "shown" and Clear stack at the row's right end — beside an open list there is room under
670
+ the count, so setting a facet adds no row (a word beside the count, or Clear beside it, wrapped CLI under the
671
+ list). With no such list (.compact) they share one cell of one line: a facet set, Clear takes the count's place
672
+ (the count kept for a screen reader and in Clear's title). Either way the column keeps Clear's width (idle, there
673
+ unseen at no height: .is-idle, a class — never a style attribute, #1300), and stacked the word's too
674
+ (data-reserve, drawn unseen at no height), so the headers' box beside it never narrows. */ ""}
675
+ <div class="nav-facet-end" data-reserve=${tn("filterShown")}>
676
+ <p class=${`note nav-shown${facetSet ? " is-set" : ""}`} aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? html`<span class="nav-shown-word"> ${tn("filterShown")}</span>` : ""}</p>
677
+ <button type="button" class=${`chip-btn nav-facet-reset${facetSet ? "" : " is-idle"}`} title=${facetSet ? `${shown.shown} / ${shown.total} ${tn("filterShown")}` : undefined}
678
+ onClick=${() => setNavFilter({ status: [], cli: [] })}>${tn("filterFacetsReset")}</button>
679
+ </div>
663
680
  </div>
664
681
  </div>`}
665
682
  </div>`;
@@ -10,7 +10,7 @@ import { t } from "./app-i18n.js";
10
10
  * <${Dialog} title="Create task" onClose=${…} actions=${html`…`}>…body…</${Dialog}>
11
11
  * Rendered = open; the parent unmounts it to close. `busy` keeps Esc and the backdrop from closing it mid-request.
12
12
  */
13
- export function Dialog({ title, onClose, actions, children, busy = false, wide = false, drawer = false, label }) {
13
+ export function Dialog({ title, onClose, actions, children, busy = false, wide = false, drawer = false, label, tools = null }) {
14
14
  const ref = useRef(null);
15
15
  const opener = useRef(null);
16
16
  const busyRef = useRef(busy);
@@ -38,7 +38,7 @@ export function Dialog({ title, onClose, actions, children, busy = false, wide =
38
38
  }, [onClose]);
39
39
  return html`<dialog data-trust="dialog" ref=${ref} class=${`dlg${wide ? " dlg-wide" : ""}${drawer ? " dlg-drawer" : ""}`} aria-label=${label || title}>
40
40
  <div class="dlg-box">
41
- <div class="dlg-head"><h2 class="dlg-title">${title}</h2>
41
+ <div class="dlg-head"><h2 class="dlg-title">${title}</h2>${tools ? html`<span class="dlg-tools">${tools}</span>` : null}
42
42
  <button type="button" class="icon-btn dlg-x" aria-label=${t("app.close")} title=${t("app.close")} onClick=${() => { if (!busyRef.current) onClose(); }}><${Icon} name="close" /></button></div>
43
43
  <div class="dlg-body">${children}</div>
44
44
  ${actions ? html`<div class="dlg-actions">${actions}</div>` : null}
@@ -4,7 +4,8 @@
4
4
  import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "./app-html.js";
5
5
  import { t } from "./app-i18n.js";
6
6
  import { useLease } from "./app-ctx.js";
7
- import { guestPage } from "./app-store.js";
7
+ import { createStore, guestPage, useStore } from "./app-store.js";
8
+ import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
8
9
  import { readStream } from "./read-stream.js";
9
10
  import { Dialog } from "./ui-dialog.js";
10
11
  import { ErrorState, Skeleton } from "./ui-states.js";
@@ -129,6 +130,142 @@ export function updatedAgo(fetchedAt, now = Date.now()) {
129
130
  return tn("usageUpdatedAgo", { age: min < 60 ? tn("usageMinutes", { n: min }) : tn("usageHours", { n: Math.floor(min / 60) }) });
130
131
  }
131
132
 
133
+ // ── Compact: one line per login (a synced preference, "usage.compact"; the dialog and the rail alike) ──
134
+ const COMPACT_KEY = "agend_usage_compact";
135
+ function readCompact() {
136
+ const p = prefInForce("usage.compact");
137
+ return (p ? p.value : stored(COMPACT_KEY) === "on" ? true : undefined) === true;
138
+ }
139
+ export const usageViewStore = createStore({ compact: readCompact() });
140
+ export function setUsageCompact(on) {
141
+ usageViewStore.set({ compact: !!on });
142
+ savePref("usage.compact", on ? true : null); // the full cards are the default: switching back clears it
143
+ }
144
+ onPrefsChanged((keys) => { if (keys.has("usage.compact")) usageViewStore.set({ compact: readCompact() }); });
145
+ /** The header's switch between one line per login and the full cards (beside the title: the dialog's and the rail's). */
146
+ export function UsageCompactToggle() {
147
+ const { compact } = useStore(usageViewStore);
148
+ const [label, title] = compact ? [tn("usageFull"), tn("usageFullTitle")] : [tn("usageCompact"), tn("usageCompactTitle")];
149
+ return html`<button type="button" class="btn btn-ghost btn-sm u-compact-toggle" aria-pressed=${compact ? "true" : "false"} title=${title}
150
+ onClick=${() => setUsageCompact(!compact)}>${label}</button>`;
151
+ }
152
+ const TWO_DAYS = 48 * 3_600_000;
153
+ /**
154
+ * A login's two columns on its compact line: `short`, its shortest percent window under two days (the session), and
155
+ * `long`, its longest of two days or more (the week, the month) — null when it has none (a dash in that column). A window
156
+ * that reset since the numbers were read is left out (its full card shows no number); one of unknown length is a long one.
157
+ */
158
+ export function compactSlots(p) {
159
+ const pct = ((p && p.metrics) || []).filter((m) => m && m.type === "percent" && !m.resetPassed);
160
+ const isShort = (m) => Number(m.windowMs) > 0 && m.windowMs < TWO_DAYS;
161
+ const short = pct.filter(isShort).sort((a, b) => a.windowMs - b.windowMs)[0] || null;
162
+ const long = pct.filter((m) => !isShort(m)).map((m, i) => ({ m, i }))
163
+ .sort((a, b) => ((Number(b.m.windowMs) || -1) - (Number(a.m.windowMs) || -1)) || a.i - b.i)[0]?.m || null;
164
+ return { short, long };
165
+ }
166
+ /** The windows a compact line shows, short first. */
167
+ export const compactMetrics = (p) => { const { short, long } = compactSlots(p); return [short, long].filter(Boolean); };
168
+ /** A window as a few characters ("5h", "7d"); one whose length is not known says its label. */
169
+ export function windowTag(m) {
170
+ const ms = Number(m && m.windowMs);
171
+ if (!(ms > 0)) return usageText(m.label, m.labelI18n);
172
+ const h = ms / 3_600_000;
173
+ return h < 48 ? tn("usageWindowHours", { n: Math.round(h) }) : tn("usageWindowDays", { n: Math.round(h / 24) });
174
+ }
175
+ /** A time ahead in one unit, for the narrow column: "45m", "3h", "4d" (rounded down past an hour; the tooltip is exact). */
176
+ function coarseDuration(ms) {
177
+ const min = Math.max(1, Math.ceil(ms / 60_000));
178
+ if (min < 60) return tn("usageWindowMinutes", { n: min });
179
+ const h = Math.floor(min / 60);
180
+ return h < 48 ? tn("usageWindowHours", { n: h }) : tn("usageWindowDays", { n: Math.floor(h / 24) });
181
+ }
182
+ /** The soonest reset still ahead among `metrics`: { short: "↻ 2h", full: "resets in 2h 5m" }, or null when none is known. */
183
+ export function soonestReset(metrics, now = Date.now()) {
184
+ const at = metrics.map((m) => new Date(m.resetsAt || "").getTime()).filter((t) => Number.isFinite(t) && t > now).sort((a, b) => a - b)[0];
185
+ return at === undefined ? null : { short: `↻ ${coarseDuration(at - now)}`, full: resetText(new Date(at).toISOString()) };
186
+ }
187
+ /**
188
+ * The logins grouped by provider for the compact view (nine logins, five of them Claude's: the provider once, then a
189
+ * line per login), in the order the person set (↑↓): a group where its first login is, its logins in their order.
190
+ */
191
+ export function compactGroups(providers) {
192
+ const groups = new Map();
193
+ providers.forEach((p, i) => {
194
+ const base = providerTitle(p).base;
195
+ if (!groups.has(base)) groups.set(base, { base, items: [] });
196
+ groups.get(base).items.push({ p, i });
197
+ });
198
+ return [...groups.values()];
199
+ }
200
+ const level = (pct) => (pct >= 90 ? "crit" : pct >= 70 ? "warn" : "");
201
+ /**
202
+ * #2240: an account whose percent windows come from several pools (agy: Gemini, Claude & others, Gemini 4 Argon) — each
203
+ * pool with its windows and the tighter one (the most used); null for an account of one pool (its 5h / 7d lines).
204
+ */
205
+ export function compactPools(p) {
206
+ const pct = ((p && p.metrics) || []).filter((m) => m && m.type === "percent" && !m.resetPassed);
207
+ const pools = new Map();
208
+ for (const m of pct) {
209
+ if (!m.pool || typeof m.pool.label !== "string") return null;
210
+ if (!pools.has(m.pool.label)) pools.set(m.pool.label, { pool: m.pool, metrics: [] });
211
+ pools.get(m.pool.label).metrics.push(m);
212
+ }
213
+ if (pools.size < 2) return null;
214
+ return [...pools.values()].map((x) => ({ ...x, worst: x.metrics.reduce((a, b) => ((b.used ?? 0) > (a.used ?? 0) ? b : a)) }));
215
+ }
216
+ const pctOf = (m) => Math.min(100, Math.max(0, m.used ?? 0));
217
+ /**
218
+ * One window on its own line: its few characters ("5h"), a full-width bar and the percent, coloured as the full card.
219
+ * A pool's line (#2240): the pool's name in place of the window, its every window in the tooltip.
220
+ */
221
+ function CompactMeter({ m, tag, title }) {
222
+ const bar = useRef(null);
223
+ const pct = Math.min(100, Math.max(0, m.used ?? 0)), cls = level(pct);
224
+ useLayoutEffect(() => { if (bar.current) bar.current.style.width = `${pct}%`; }, [pct]); // CSSOM (#1300)
225
+ const word = cls === "crit" ? tn("usageNear") : cls === "warn" ? tn("usageHigh") : "";
226
+ return html`<span class="u-cm" title=${title || `${usageText(m.label, m.labelI18n)}: ${pct.toFixed(0)}%${word ? ` (${word})` : ""}`}>
227
+ <span class="u-cm-tag">${tag || windowTag(m)}</span><span class="u-cm-bar"><span ref=${bar} class=${`u-cm-fill ${cls}`}></span></span>
228
+ <span class=${`u-cm-pct ${cls}`}>${pct.toFixed(0)}%</span></span>`;
229
+ }
230
+ /** What a login says instead of numbers (not logged in, an error), or "" when it has numbers. */
231
+ function rowNote(p) {
232
+ if (p.status === "error") return usageText(p.error || tn("usage.error_fallback"), p.errorI18n);
233
+ if (p.status === "no-credentials") return tn("usage.not_logged_in");
234
+ return "";
235
+ }
236
+ /**
237
+ * A login in three short lines, under its provider's name (the user's pick, 2026-10-22): its name and plan with the
238
+ * soonest reset on the right; then its short window and its long one, each a labelled full-width bar and the percent.
239
+ * Numbers not current, a failed read or no login: one muted line in place of the bars, what is wrong as its tooltip.
240
+ * Pressed, it opens its full card.
241
+ */
242
+ export function CompactRow({ p, onExpand }) {
243
+ const title = providerTitle(p), failed = p.status === "error" || p.status === "no-credentials";
244
+ const pools = failed || p.stale ? null : compactPools(p);
245
+ const metrics = failed || p.stale ? [] : pools ? pools.flatMap((x) => x.metrics) : compactMetrics(p);
246
+ const note = rowNote(p), stale = p.stale ? staleNote(p) : null;
247
+ const say = stale ? stale.mark : note || tn("usage.no_data");
248
+ const detail = stale ? [stale.mark, stale.detail].filter(Boolean).join(" — ") : say;
249
+ const reset = soonestReset(metrics);
250
+ const name = title.profile || title.base;
251
+ return html`<button type="button" class=${`u-crow${p.stale ? " is-stale" : ""}${pools ? " is-pooled" : ""}`} aria-expanded="false" title=${`${p.name} — ${tn("usageExpand")}`} onClick=${onExpand}>
252
+ <span class="u-crow-head"><span class="u-crow-name">${name}</span>
253
+ ${p.plan ? html`<span class="pill u-plan" title=${p.plan}><span aria-hidden="true">${planBadge(p.plan)}</span><span class="sr-only">${p.plan}</span></span>` : null}
254
+ ${reset ? html`<span class="u-crow-reset" title=${reset.full}>${reset.short}<span class="sr-only"> (${reset.full})</span></span>` : null}</span>
255
+ ${pools ? pools.map((x) => html`<${CompactMeter} key=${x.pool.label} m=${x.worst} tag=${usageText(x.pool.label, x.pool.labelI18n)}
256
+ title=${x.metrics.map((m) => `${usageText(m.label, m.labelI18n)}: ${pctOf(m).toFixed(0)}%`).join(" · ")} />`)
257
+ : metrics.length ? metrics.map((m, j) => html`<${CompactMeter} key=${j} m=${m} />`)
258
+ : html`<span class=${`u-crow-note${(stale || (p.status === "error" && !p.transient)) ? " is-alert" : ""}`} title=${detail}>${say}</span>`}
259
+ </button>`;
260
+ }
261
+ /** A provider's logins: its name once (×N), in small capitals, then each login (or the full card of one opened). */
262
+ export function CompactGroup({ group, line }) {
263
+ return html`<section class="u-cgroup">
264
+ <div class="u-cg-head"><span class="u-cg-name" title=${group.base}>${group.base}</span>${group.items.length > 1 ? html`<span class="u-cg-count">×${group.items.length}</span>` : null}</div>
265
+ ${group.items.map(line)}
266
+ </section>`;
267
+ }
268
+
132
269
  /**
133
270
  * The usage read and its rows: the dialog's body and the rail's (#1673). `owner`: its lease's key — the reads and the
134
271
  * minute's timer live and end with it.
@@ -141,6 +278,11 @@ export function UsageBody({ owner }) {
141
278
  const [failed, setFailed] = useState(false);
142
279
  const [order, setOrder] = useState(() => { try { const a = JSON.parse(stored(USAGE_ORDER_KEY) || "[]"); return Array.isArray(a) ? a.filter((x) => typeof x === "string") : []; } catch { return []; } });
143
280
  const reads = useMemo(() => readStream(lease), [lease]);
281
+ // Compact (one line per login): a line pressed opens its full card here, until folded or the mode changes.
282
+ const { compact } = useStore(usageViewStore);
283
+ const [expanded, setExpanded] = useState(() => new Set());
284
+ useEffect(() => { setExpanded(new Set()); }, [compact]);
285
+ const toggleOne = (id) => setExpanded((was) => { const next = new Set(was); if (next.has(id)) next.delete(id); else next.add(id); return next; });
144
286
  // Refresh (force) supersedes a read on its way; the minute's tick waits for it.
145
287
  const load = async (force) => {
146
288
  const token = reads.begin(force);
@@ -188,10 +330,10 @@ export function UsageBody({ owner }) {
188
330
  else if (!data || !Array.isArray(data.providers)) content = html`<${ErrorState} message=${tn("usageFail")} onRetry=${() => load(true)} />`;
189
331
  else {
190
332
  const providers = ordered(data.providers);
191
- content = html`${!providers.length ? html`<p class="note">${tn("usage.empty_active")}</p>` : null}
192
- ${providers.map((p, i) => html`<section key=${key(p)} class=${p.stale ? "u-provider is-stale" : "u-provider"}>
333
+ const card = (p, i) => html`<section key=${key(p)} class=${p.stale ? "u-provider is-stale" : "u-provider"}>
193
334
  <div class="u-head"><strong title=${p.name}>${providerTitle(p).base}</strong>${p.plan ? html`<span class="pill u-plan" title=${p.plan}><span aria-hidden="true">${planBadge(p.plan)}</span><span class="sr-only">${p.plan}</span></span>` : null}
194
- <span class="u-order">
335
+ <span class="u-order">${compact ? html`<button type="button" class="icon-btn u-collapse" aria-expanded="true" aria-label=${tn("usageCollapse")} title=${tn("usageCollapse")}
336
+ onClick=${() => toggleOne(key(p))}><${Icon} name="minus" size=${14} /></button>` : null}
195
337
  <button type="button" class="icon-btn" disabled=${i === 0} aria-label=${tn("usageMoveUp")} title=${tn("usageMoveUp")} onClick=${() => move(key(p), -1, data.providers)}><${Icon} name="up" size=${14} /></button>
196
338
  <button type="button" class="icon-btn" disabled=${i === providers.length - 1} aria-label=${tn("usageMoveDown")} title=${tn("usageMoveDown")} onClick=${() => move(key(p), 1, data.providers)}><${Icon} name="down" size=${14} /></button>
197
339
  </span></div>
@@ -202,7 +344,11 @@ export function UsageBody({ owner }) {
202
344
  : p.status === "no-credentials" ? html`<p class="note">${tn("usage.not_logged_in")}</p>${p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : null}`
203
345
  : html`${(p.metrics || []).map((m, j) => html`<${UsageMetric} key=${j} m=${m} />`)}
204
346
  ${!p.stale && p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : !(p.metrics || []).length ? html`<p class="note">${tn("usage.no_data")}</p>` : null}`}
205
- </section>`)}
347
+ </section>`;
348
+ content = html`${!providers.length ? html`<p class="note">${tn("usage.empty_active")}</p>` : null}
349
+ ${compact ? compactGroups(providers).map((g) => html`<${CompactGroup} key=${g.base} group=${g}
350
+ line=${({ p, i }) => (expanded.has(key(p)) ? card(p, i) : html`<${CompactRow} key=${key(p)} p=${p} onExpand=${() => toggleOne(key(p))} />`)} />`)
351
+ : providers.map((p, i) => card(p, i))}
206
352
  <div class="u-foot"><span class="note" title=${new Date(data.fetchedAt).toLocaleString()}>${updatedAgo(data.fetchedAt)}${failed ? html` · <span class="u-refresh-failed" role="status">${tn("usageRefreshFailed")}</span>` : data.refreshing ? html` · <span class="u-refreshing">${tn("usageRefreshing")}</span>` : null}</span>
207
353
  ${byAgent() ? html`<a class="btn btn-ghost btn-sm u-by-agent" href="/ui/fleet/usage" title=${tn("usageByAgentTitle")}>${tn("usageByAgent")}</a>` : null}
208
354
  <button type="button" class="btn btn-sm" onClick=${() => load(true)}>${tn("usageRefresh")}</button></div>`;
@@ -218,5 +364,5 @@ const byAgent = () => !guestPage() && typeof location !== "undefined" && locatio
218
364
 
219
365
  /** `owner`: the navigation that opened it — its reads and the minute's timer live and end with it. */
220
366
  export function UsageDialog({ onClose, owner = "" }) {
221
- return html`<${Dialog} title=${tn("usage.title")} onClose=${onClose} wide=${true}><${UsageBody} owner=${`usage-dialog:${owner}`} /></${Dialog}>`;
367
+ return html`<${Dialog} title=${tn("usage.title")} tools=${html`<${UsageCompactToggle} />`} onClose=${onClose} wide=${true}><${UsageBody} owner=${`usage-dialog:${owner}`} /></${Dialog}>`;
222
368
  }
@@ -47,6 +47,16 @@ register("view", {
47
47
  "usageNear": "near limit",
48
48
  "usageMoveUp": "Move up",
49
49
  "usageMoveDown": "Move down",
50
+ "usageCompact": "Compact",
51
+ "usageCompactTitle": "One line per login",
52
+ "usageFull": "Full",
53
+ "usageFullTitle": "The full card for each login",
54
+ "usageExpand": "Show the full card",
55
+ "usageCollapse": "Back to one line",
56
+ "usageWindowHours": "{n}h",
57
+ "usageWindowDays": "{n}d",
58
+ "usageWindowMinutes": "{n}m",
59
+ "usageStaleMark": "Not current",
50
60
  "tooltipBackend": "Backend: {value}",
51
61
  "tooltipStatus": "Status: {value}",
52
62
  "tooltipContext": "Context: {value}",
@@ -279,6 +289,16 @@ register("view", {
279
289
  "usageNear": "接近上限",
280
290
  "usageMoveUp": "上移",
281
291
  "usageMoveDown": "下移",
292
+ "usageCompact": "精簡",
293
+ "usageCompactTitle": "每個登入一行",
294
+ "usageFull": "完整",
295
+ "usageFullTitle": "每個登入顯示完整卡片",
296
+ "usageExpand": "顯示完整卡片",
297
+ "usageCollapse": "收回成一行",
298
+ "usageWindowHours": "{n}小時",
299
+ "usageWindowDays": "{n}天",
300
+ "usageWindowMinutes": "{n}分",
301
+ "usageStaleMark": "不是最新",
282
302
  "tooltipBackend": "後端:{value}",
283
303
  "tooltipStatus": "狀態:{value}",
284
304
  "tooltipContext": "上下文:{value}",