@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
@@ -0,0 +1,149 @@
1
+ // Fleet → Org chart → Office: the offices people keep on the host, edited (#2399 carry: the write surface's minimal
2
+ // editor). An admin session adds an office, renames it, chooses its style (Desks / Studio), picks who is in it (whole
3
+ // teams, or one instance at a time; none: everyone) and deletes one — then saves the whole list with the rev it read
4
+ // (PUT /ui/offices; a change made meanwhile is a 409: read again, nothing overwritten). A stored office's layout (seats,
5
+ // paths) is kept as it is; drawing one is for a later editor. Never offered to a guest, the public link or a read-only
6
+ // session (the host refuses them too).
7
+ import { html, useMemo, useState } from "/assets/app-html.js";
8
+ import { t, register } from "/assets/app-i18n.js";
9
+ import { Dialog } from "/assets/ui-dialog.js";
10
+ import { toast } from "/assets/ui-toast.js";
11
+ import { nodeText } from "./fleet-org.js";
12
+
13
+ register("orge", {
14
+ en: {
15
+ edit: "Edit offices", title: "Offices", lead: "Offices everyone on this fleet sees in the Office view. An office with no one picked shows everyone.",
16
+ add: "Add office", newName: "New office", name: "Name", styleLabel: "Style", members: "Who is in it", membersN: "{0} picked", everyone: "Everyone",
17
+ teams: "Teams", instances: "Instances", remove: "Delete", save: "Save", cancel: "Cancel", saved: "Offices saved.",
18
+ none: "No offices yet: the Office view shows the whole fleet.", nameNeeded: "Each office needs a name.", tooMany: "At most {0} offices.",
19
+ changed: "Someone changed the offices meanwhile; your edits were not saved. Keep your edits (Save then replaces their change) or load theirs.",
20
+ changedUnread: "Someone changed the offices meanwhile; your edits were not saved, and the current list could not be read. Try again.",
21
+ keepMine: "Keep my edits", loadTheirs: "Load theirs", keptMine: "Your edits are kept. Save replaces what was saved meanwhile.",
22
+ rateLimited: "Too many saves. Try again in a minute.",
23
+ failed: "The offices were not saved ({0}).", layoutKept: "Its seats and paths are kept.",
24
+ styleUnknown: "{0} (not available here: drawn as Desks)",
25
+ },
26
+ "zh-TW": {
27
+ edit: "編輯辦公室", title: "辦公室", lead: "這個 fleet 的每個人在辦公室檢視中都會看到這些辦公室。沒有選任何成員的辦公室會顯示全部。",
28
+ add: "新增辦公室", newName: "新辦公室", name: "名稱", styleLabel: "風格", members: "成員", membersN: "已選 {0} 個", everyone: "全部",
29
+ teams: "Team", instances: "Instance", remove: "刪除", save: "儲存", cancel: "取消", saved: "辦公室已儲存。",
30
+ none: "還沒有辦公室:辦公室檢視會顯示整個 fleet。", nameNeeded: "每個辦公室都需要名稱。", tooMany: "最多 {0} 個辦公室。",
31
+ changed: "有人在這期間修改了辦公室,你的編輯沒有儲存。你可以保留你的編輯(儲存時會取代對方的修改),或載入對方的版本。",
32
+ changedUnread: "有人在這期間修改了辦公室,你的編輯沒有儲存,也無法讀取目前的清單。請再試一次。",
33
+ keepMine: "保留我的編輯", loadTheirs: "載入對方的版本", keptMine: "已保留你的編輯。儲存時會取代對方在這期間的修改。",
34
+ rateLimited: "儲存太頻繁,請一分鐘後再試。",
35
+ failed: "辦公室沒有儲存({0})。", layoutKept: "它的座位與路徑會保留。",
36
+ styleUnknown: "{0}(這裡沒有:以辦公桌顯示)",
37
+ },
38
+ });
39
+ const te = (k, ...v) => t(`orge.${k}`, ...v);
40
+ export const MAX_OFFICES = 20;
41
+ const STYLES = ["pixel", "studio"];
42
+
43
+ /** A new office's id: unique among `taken`, the store's pattern ([A-Za-z0-9_-], never "auto"). */
44
+ export function newOfficeId(taken, rand = Math.random) {
45
+ for (;;) {
46
+ const id = `office-${Math.floor(rand() * 36 ** 6).toString(36).padStart(6, "0")}`;
47
+ if (!taken.has(id)) return id;
48
+ }
49
+ }
50
+ /** What is sent: the office as the host stores it (no empty layout key). */
51
+ export function officeForSave(o) {
52
+ return { id: o.id, name: o.name.trim(), skin: o.skin, members: [...o.members], ...(o.layout ? { layout: o.layout } : {}) };
53
+ }
54
+
55
+ function Members({ office, view, everyone, onChange }) {
56
+ const picked = new Set(office.members);
57
+ const toggle = (m) => onChange(picked.has(m) ? office.members.filter((x) => x !== m) : [...office.members, m]);
58
+ return html`<div class="oe-members">
59
+ ${view.teams.length ? html`<fieldset class="oe-group"><legend>${te("teams")}</legend>${view.teams.map((tm) => html`<label key=${`t:${tm.name}`} class="check">
60
+ <input type="checkbox" checked=${picked.has(`team:${tm.name}`)} onChange=${() => toggle(`team:${tm.name}`)} /><span>${tm.name}</span></label>`)}</fieldset>` : null}
61
+ <fieldset class="oe-group"><legend>${te("instances")}</legend>${everyone.map((n) => html`<label key=${n.name} class="check">
62
+ <input type="checkbox" checked=${picked.has(n.name)} onChange=${() => toggle(n.name)} /><span>${nodeText(n).primary}</span></label>`)}</fieldset>
63
+ </div>`;
64
+ }
65
+
66
+ /**
67
+ * The editor: `offices`/`rev` as read; `view` the org chart (teams, instances); `store` createOfficeStore's;
68
+ * `ask()` a stamp taken as a request is sent; `onSaved({ rev, offices }, stamp)` after a save or a re-read, returning
69
+ * the snapshot in force (the page keeps a newer one it already shows: #2431 review); `current()` that snapshot now (null:
70
+ * not known), read when the person chooses after a 409 (#2449 review); `onClose`.
71
+ */
72
+ export function OfficeEditor({ offices, rev, view, store, ask = () => 0, current = () => null, onSaved, onClose }) {
73
+ // The draft and the rev it was read at go together (#2424 review): a list the stream re-reads meanwhile never moves
74
+ // the rev under an older draft (a save would then overwrite what it never showed). After a 409 the person chooses
75
+ // (#2431): keep their edits (on the newer rev: Save then replaces what was saved meanwhile, knowingly) or load the
76
+ // newer list — never their draft thrown away unasked.
77
+ const [base, setBase] = useState(() => ({ rev, draft: offices.map((o) => ({ ...o, members: [...o.members] })) }));
78
+ const draft = base.draft;
79
+ const setDraft = (next) => setBase((b) => ({ ...b, draft: typeof next === "function" ? next(b.draft) : next }));
80
+ const [open, setOpen] = useState(null); // the office whose members are shown
81
+ const [busy, setBusy] = useState(false);
82
+ const [msg, setMsg] = useState("");
83
+ const [conflict, setConflict] = useState(null); // after a 409: the newer list, until they choose
84
+ const copy = (list) => list.map((o) => ({ ...o, members: [...o.members] }));
85
+ // The choice takes the snapshot in force when it is made: the stream may have read a newer one since the 409 (#2449).
86
+ const inForce = () => current() || conflict;
87
+ const keepMine = () => { const now = inForce(); setBase((b) => ({ rev: now.rev, draft: b.draft })); setConflict(null); setMsg(te("keptMine")); };
88
+ const loadTheirs = () => { const now = inForce(); setBase({ rev: now.rev, draft: copy(now.offices) }); setOpen(null); setConflict(null); setMsg(""); };
89
+ const everyone = useMemo(() => { const m = new Map(); for (const n of [...view.top, ...view.teams.flatMap((tm) => tm.nodes), ...view.rest.nodes]) if (!m.has(n.name)) m.set(n.name, n); return [...m.values()]; }, [view]);
90
+ const set = (id, patch) => setDraft((d) => d.map((o) => (o.id === id ? { ...o, ...patch } : o)));
91
+ const add = () => {
92
+ if (draft.length >= MAX_OFFICES) return;
93
+ const id = newOfficeId(new Set(draft.map((o) => o.id)));
94
+ setDraft((d) => [...d, { id, name: te("newName"), skin: "pixel", members: [], layout: null }]);
95
+ setOpen(id);
96
+ };
97
+ // Everything is frozen from Save until its answer (and a 409's re-read) is in (#2424 review): no edit made meanwhile
98
+ // is lost when the dialog closes, and no second save starts on a draft about to be replaced.
99
+ const save = async () => {
100
+ if (busy || conflict) return;
101
+ if (draft.some((o) => !o.name.trim())) { setMsg(te("nameNeeded")); return; }
102
+ setBusy(true); setMsg("");
103
+ try {
104
+ const sent = ask();
105
+ const r = await store.saveOffices(draft.map(officeForSave), base.rev);
106
+ if (r.ok) { toast(te("saved")); onSaved({ rev: r.rev, offices: r.offices }, sent); onClose(); return; }
107
+ if (r.status === 409) {
108
+ const reread = ask();
109
+ const fresh = await store.offices();
110
+ // The snapshot in force: this re-read, or a newer one the page read meanwhile. The draft stays as it is.
111
+ const kept = fresh ? onSaved(fresh, reread) || fresh : null;
112
+ if (kept) setConflict({ rev: kept.rev, offices: kept.offices });
113
+ setMsg(kept ? te("changed") : te("changedUnread"));
114
+ return;
115
+ }
116
+ setMsg(r.status === 429 ? te("rateLimited") : te("failed", r.error || r.status));
117
+ } finally { setBusy(false); }
118
+ };
119
+ const actions = html`<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${te("cancel")}</button>
120
+ <button type="button" class="btn btn-primary" id="oeSave" onClick=${save} disabled=${busy || !!conflict}>${te("save")}</button>`;
121
+ return html`<${Dialog} title=${te("title")} onClose=${onClose} actions=${actions} busy=${busy} wide=${true}>
122
+ <p class="note">${te("lead")}</p>
123
+ <fieldset class="oe-form" disabled=${busy}>
124
+ ${draft.length ? null : html`<p class="note" id="oeNone">${te("none")}</p>`}
125
+ <ul class="oe-list">${draft.map((o) => html`<li key=${o.id} class="oe-row" data-office=${o.id}>
126
+ <div class="oe-head">
127
+ <label class="oe-field"><span class="oe-label">${te("name")}</span>
128
+ <input type="text" class="oe-name" maxlength="60" value=${o.name} onInput=${(e) => set(o.id, { name: e.target.value })} /></label>
129
+ <label class="oe-field"><span class="oe-label">${te("styleLabel")}</span>
130
+ <select class="tb-select oe-style" value=${o.skin} onChange=${(e) => set(o.id, { skin: e.target.value })}>
131
+ ${STYLES.map((s) => html`<option key=${s} value=${s}>${t(`orgo.style_${s}`)}</option>`)}
132
+ ${STYLES.includes(o.skin) ? null : html`<option key=${o.skin} value=${o.skin}>${te("styleUnknown", o.skin)}</option>`}</select></label>
133
+ <button type="button" class="btn btn-sm oe-members-btn" aria-expanded=${open === o.id ? "true" : "false"} onClick=${() => setOpen(open === o.id ? null : o.id)}>
134
+ ${te("members")}: ${o.members.length ? te("membersN", o.members.length) : te("everyone")}</button>
135
+ <button type="button" class="btn btn-sm btn-danger oe-remove" onClick=${() => { setDraft((d) => d.filter((x) => x.id !== o.id)); if (open === o.id) setOpen(null); }}>${te("remove")}</button>
136
+ </div>
137
+ ${o.layout ? html`<p class="note">${te("layoutKept")}</p>` : null}
138
+ ${open === o.id ? html`<${Members} office=${o} view=${view} everyone=${everyone} onChange=${(members) => set(o.id, { members })} />` : null}
139
+ </li>`)}</ul>
140
+ <button type="button" class="btn btn-sm" id="oeAdd" onClick=${add} disabled=${draft.length >= MAX_OFFICES}>${te("add")}</button>
141
+ ${draft.length >= MAX_OFFICES ? html`<span class="note">${te("tooMany", MAX_OFFICES)}</span>` : null}
142
+ </fieldset>
143
+ ${msg ? html`<p class="note oe-msg" role="status">${msg}</p>` : null}
144
+ ${conflict ? html`<div class="oe-conflict">
145
+ <button type="button" class="btn btn-sm" id="oeKeepMine" onClick=${keepMine} disabled=${busy}>${te("keepMine")}</button>
146
+ <button type="button" class="btn btn-sm" id="oeLoadTheirs" onClick=${loadTheirs} disabled=${busy}>${te("loadTheirs")}</button>
147
+ </div>` : null}
148
+ </${Dialog}>`;
149
+ }
@@ -0,0 +1,358 @@
1
+ // Fleet → Org chart → Office: the fleet as a small pixel office (fleet-office.js draws it, fleet-office-skin.js holds the
2
+ // art). This module is the pure part — no DOM, no drawing — so the layout, the poses, the filter, the walking paths and
3
+ // the traffic items' lifecycle are tested on their own.
4
+ //
5
+ // - The model: `offices: Office[]`, each { id, name, skin, members, layout?: { seats, waypoints, edges } }. v1 has one
6
+ // office the page makes itself (AUTO_ID, "the whole fleet", built from the teams as the canvas is), whose member
7
+ // filter is the team / people picker; any other office is the host's (GET /ui/offices, fleet-office-store.js),
8
+ // checked again here.
9
+ // TODO(office editor): adding and renaming offices, assigning people, dragging seats and drawing paths is a later
10
+ // step; it writes the same model through the store's saveOffices().
11
+ // - The auto layout: a lobby (General and the instances in no team), then a room per team in fleet.yaml's order, in a
12
+ // grid that wraps at the stage's width (a phone gets narrower rooms); a desk per member; a corridor graph of waypoints
13
+ // (each desk's aisle, a spine down each room's left padding, its door, a corridor under each row of rooms and one down
14
+ // the left margin) that traffic walks along (bfsPath).
15
+ // - Poses come from the org chart's states (fleet-org.js ORG_STATES / orgState); nothing here invents a status.
16
+ import { hue } from "./fleet-traffic.js";
17
+
18
+ // ── Poses: the org chart's states, as a character at a desk ──
19
+
20
+ /** The poses a skin draws (a frame strip each, fleet-office-skin.js); "away" (not in the fleet) is an empty desk. */
21
+ export const POSES = ["working", "idle", "paused", "needs", "walk"];
22
+ /**
23
+ * fleet-org.js ORG_STATES → pose. Needs you, crashed and looks stuck call for the person (a light over the head);
24
+ * starting sits at the desk, queued (waiting its turn to start, #2398) dozes without the closed sign.
25
+ */
26
+ export const POSE_OF = { needs: "needs", crashed: "needs", stuck: "needs", starting: "idle", queued: "paused", working: "working", idle: "idle", paused: "paused", stopped: "paused", missing: "away" };
27
+ export const officePose = (state) => POSE_OF[state] || "idle";
28
+ /** The call light's tone: the state colours the canvas's dots use (crashed is danger; needs you and stuck are warn). */
29
+ export const lightOf = (state) => (state === "crashed" ? "danger" : state === "needs" || state === "stuck" ? "warn" : null);
30
+ /** A closed sign on the desk of an instance that is paused or stopped; the lamp is on while it works. */
31
+ export const signOf = (state) => (state === "paused" || state === "stopped" ? "closed" : null);
32
+ export const lampOf = (state) => state === "working";
33
+
34
+ /** A small stable number from a name (FNV-1a): the same instance always gets the same hair, line, turn. */
35
+ export const nameHash = (s) => { let h = 2166136261; for (const ch of String(s)) { h ^= ch.codePointAt(0); h = Math.imul(h, 16777619); } return h >>> 0; };
36
+ /** The frame of a looping strip at time `ms` (0 when still: the reduced-motion pose). */
37
+ export const frameAt = (ms, frames, fps) => (frames > 1 && ms > 0 ? Math.floor((ms / 1000) * fps) % frames : 0);
38
+
39
+ // ── Coats: the instance's identity colour when someone chose one, else its backend's ──
40
+
41
+ export const COATS = { "claude-code": "#d97757", codex: "#4b8bf4", "kiro-cli": "#9b6bff", grok: "#6b7280", other: "#3fa58c" };
42
+ /** `id`: identity-color.js identityOf(it); the automatic colour is everyone's, so the backend's says more. */
43
+ export function coatOf(it, id, mode = "dark") {
44
+ if (id && !id.off && id.source && id.source !== "auto") return (mode === "light" ? id.light : id.dark) || COATS.other;
45
+ return COATS[it && it.backend] || COATS.other;
46
+ }
47
+
48
+ // ── Who is shown: the auto office's member filter (a team, or the lobby; and/or pinned people) ──
49
+
50
+ export const AUTO_ID = "auto";
51
+ export const LOBBY = "\u0000lobby";
52
+ const MAX_NAMES = 500;
53
+ const nameList = (a) => (Array.isArray(a) ? [...new Set(a.filter((s) => typeof s === "string" && s && s.length <= 200))].slice(0, MAX_NAMES) : []);
54
+ /** The filter as stored, checked: { team: "" (all) | LOBBY | a team's name, pinned: instance names }. */
55
+ export function filterOk(x) {
56
+ const f = x && typeof x === "object" ? x : {};
57
+ return { team: typeof f.team === "string" && f.team.length <= 200 ? f.team : "", pinned: nameList(f.pinned) };
58
+ }
59
+
60
+ /**
61
+ * The rooms of the auto office, from buildOrgView's answer: the lobby (General, then the instances in no team), then
62
+ * each team. `filter`: a team keeps that room only (a team no longer defined: all of them); pinned people keep only
63
+ * them, in whichever rooms they are (a room left with nobody goes). An empty team stays an empty room; an empty lobby goes.
64
+ */
65
+ export function officeRooms(view, filter) {
66
+ const lobby = { key: LOBBY, name: null, lobby: true, nodes: [...view.top, ...view.rest.nodes] };
67
+ const all = [lobby, ...view.teams.map((tm) => ({ key: tm.name, name: tm.name, lobby: false, nodes: tm.nodes }))];
68
+ const f = filterOk(filter);
69
+ const picked = f.team && all.some((r) => r.key === f.team) ? all.filter((r) => r.key === f.team) : all;
70
+ if (!f.pinned.length) return picked.filter((r) => r.nodes.length || !r.lobby);
71
+ const pinned = new Set(f.pinned);
72
+ return picked.map((r) => ({ ...r, nodes: r.nodes.filter((n) => pinned.has(n.name)) })).filter((r) => r.nodes.length);
73
+ }
74
+
75
+ /** A stored office's members: instance names, or `team:<name>` for a team's; none means everyone. */
76
+ export function membersOf(office, view) {
77
+ const every = [...view.top, ...view.teams.flatMap((tm) => tm.nodes), ...view.rest.nodes];
78
+ const byName = new Map(every.map((n) => [n.name, n]));
79
+ if (!office.members.length) return [...byName.values()];
80
+ const out = new Map();
81
+ for (const m of office.members) {
82
+ const team = m.startsWith("team:") ? view.teams.find((tm) => tm.name === m.slice(5)) : null;
83
+ for (const n of team ? team.nodes : byName.has(m) ? [byName.get(m)] : []) if (!out.has(n.name)) out.set(n.name, n);
84
+ }
85
+ return [...out.values()];
86
+ }
87
+
88
+ // ── The model, as stored: checked defensively (it is a browser's localStorage today, a server's later) ──
89
+
90
+ const ID = /^[A-Za-z0-9_-]{1,40}$/;
91
+ const MAX_OFFICES = 20, MAX_SEATS = 500, MAX_WAYPOINTS = 2000, MAX_EDGES = 8000, MAX_COORD = 20000;
92
+ const coordOk = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= MAX_COORD;
93
+ const pointsOk = (list, max, extra) => {
94
+ const out = [], ids = new Set();
95
+ for (const p of Array.isArray(list) ? list.slice(0, max) : []) {
96
+ if (!p || typeof p !== "object" || typeof p.id !== "string" || !p.id || p.id.length > 64 || ids.has(p.id) || !coordOk(p.x) || !coordOk(p.y)) continue;
97
+ ids.add(p.id);
98
+ out.push({ id: p.id, x: p.x, y: p.y, ...extra(p) });
99
+ }
100
+ return out;
101
+ };
102
+ /** A layout: seats (optionally assigned to an instance), waypoints, and edges between known waypoints; null if none. */
103
+ export function layoutOk(x) {
104
+ if (!x || typeof x !== "object") return null;
105
+ const seats = pointsOk(x.seats, MAX_SEATS, (p) => (typeof p.instance === "string" && p.instance && p.instance.length <= 200 ? { instance: p.instance } : {}));
106
+ if (!seats.length) return null;
107
+ const waypoints = pointsOk(x.waypoints, MAX_WAYPOINTS, () => ({}));
108
+ const known = new Set(waypoints.map((w) => w.id));
109
+ const edges = (Array.isArray(x.edges) ? x.edges.slice(0, MAX_EDGES) : [])
110
+ .filter((e) => Array.isArray(e) && e.length === 2 && known.has(e[0]) && known.has(e[1]) && e[0] !== e[1]).map((e) => [e[0], e[1]]);
111
+ return { seats, waypoints, edges };
112
+ }
113
+ /** One office, checked; null for anything that is not one (the auto office's id is the page's own). */
114
+ export function officeOk(x) {
115
+ if (!x || typeof x !== "object" || typeof x.id !== "string" || !ID.test(x.id) || x.id === AUTO_ID) return null;
116
+ const name = typeof x.name === "string" && x.name.trim() ? x.name.trim().slice(0, 60) : x.id;
117
+ const skin = typeof x.skin === "string" && ID.test(x.skin) ? x.skin : "pixel";
118
+ return { id: x.id, name, skin, members: nameList(x.members), layout: layoutOk(x.layout) };
119
+ }
120
+ /** A stored list of offices: the valid ones, ids unique, at most MAX_OFFICES. */
121
+ export function officesOk(list) {
122
+ const out = [], ids = new Set();
123
+ for (const x of Array.isArray(list) ? list : []) {
124
+ const o = officeOk(x);
125
+ if (!o || ids.has(o.id)) continue;
126
+ ids.add(o.id);
127
+ out.push(o);
128
+ if (out.length >= MAX_OFFICES) break;
129
+ }
130
+ return out;
131
+ }
132
+
133
+ // ── Layout ──
134
+
135
+ /**
136
+ * Sizes in CSS px. A desk cell DW×DH (the character, the desk, the name tag, and its aisle at the bottom); a room's
137
+ * padding PAD and header HEAD; GAP between rooms (the corridor runs in it); M the stage's margin. SEAT_Y: where the
138
+ * character sits in its cell; AISLE: the aisle's distance from the cell's bottom.
139
+ */
140
+ export const OF = { DW: 96, DH: 104, PAD: 12, HEAD: 26, GAP: 24, M: 12, SEAT_Y: 40, AISLE: 6, EMPTY_H: 64, MAX_COLS: 6 };
141
+ const colsFor = (n) => (n <= 3 ? Math.max(1, n) : n <= 4 ? 2 : n <= 9 ? 3 : n <= 16 ? 4 : n <= 30 ? 5 : 6);
142
+
143
+ /**
144
+ * The auto office: `rooms` (officeRooms) packed into rows at `width` CSS px. Each room { key, name, lobby, x, y, w, h,
145
+ * desks }; each desk { key, node, x, y, w, h, seat: { x, y }, wp } (wp: the aisle waypoint in front of it). And the
146
+ * corridor graph: `waypoints` [{ id, x, y }], `edges` [[a, b]]. The same rooms and width always give the same picture.
147
+ */
148
+ export function autoLayout(rooms, { width = 960 } = {}) {
149
+ const { DW, DH, PAD, HEAD, GAP, M } = OF;
150
+ const maxCols = Math.max(1, Math.min(OF.MAX_COLS, Math.floor((width - 2 * M - 2 * PAD) / DW)));
151
+ const out = [], waypoints = [], edges = [];
152
+ const wp = (id, x, y) => { waypoints.push({ id, x, y }); return id; };
153
+ let x = M, y = M, rowH = 0, row = 0;
154
+ const rows = [[]];
155
+ rooms.forEach((r, ri) => {
156
+ const n = r.nodes.length, cols = Math.min(colsFor(n), maxCols), lines = Math.ceil(n / cols);
157
+ const w = 2 * PAD + cols * DW, h = n ? HEAD + PAD + lines * DH : OF.EMPTY_H;
158
+ if (rows[row].length && x + w > width - M) { y += rowH + GAP; x = M; rowH = 0; row++; rows.push([]); }
159
+ const room = { key: r.key, name: r.name, lobby: r.lobby, x, y, w, h, cols, desks: [], ri, row };
160
+ const spineX = x + PAD / 2;
161
+ let prevSpine = null;
162
+ for (let line = 0; line < lines; line++) {
163
+ const aisleY = y + HEAD + PAD + (line + 1) * DH - OF.AISLE;
164
+ const spine = wp(`s${ri}.${line}`, spineX, aisleY);
165
+ if (prevSpine) edges.push([prevSpine, spine]);
166
+ prevSpine = spine;
167
+ let prev = spine;
168
+ for (let c = 0; c < cols && line * cols + c < n; c++) {
169
+ const i = line * cols + c, dx = x + PAD + c * DW, dy = y + HEAD + PAD + line * DH;
170
+ const aisle = wp(`a${ri}.${i}`, dx + DW / 2, aisleY);
171
+ edges.push([prev, aisle]);
172
+ prev = aisle;
173
+ room.desks.push({ key: `${r.key}/${r.nodes[i].name}`, node: r.nodes[i], x: dx, y: dy, w: DW, h: DH, seat: { x: dx + DW / 2, y: dy + OF.SEAT_Y }, wp: aisle });
174
+ }
175
+ }
176
+ const door = wp(`d${ri}`, spineX, y + h);
177
+ if (prevSpine) edges.push([prevSpine, door]);
178
+ room.door = door;
179
+ out.push(room);
180
+ rows[row].push(room);
181
+ x += w + GAP;
182
+ rowH = Math.max(rowH, h);
183
+ });
184
+ // The corridors: one under each row of rooms (each door drops to it), joined down the left margin.
185
+ let prevLeft = null;
186
+ rows.forEach((list, k) => {
187
+ if (!list.length) return;
188
+ const cy = Math.max(...list.map((r) => r.y + r.h)) + GAP / 2;
189
+ let prev = wp(`L${k}`, M / 2, cy);
190
+ if (prevLeft) edges.push([prevLeft, prev]);
191
+ prevLeft = prev;
192
+ for (const r of list) {
193
+ const c = wp(`c${k}.${r.ri}`, r.x + PAD / 2, cy);
194
+ edges.push([prev, c], [r.door, c]);
195
+ prev = c;
196
+ }
197
+ });
198
+ const right = out.length ? Math.max(...out.map((r) => r.x + r.w)) : 0, bottom = out.length ? Math.max(...out.map((r) => r.y + r.h)) : 0;
199
+ return { rooms: out, waypoints, edges, width: Math.max(width, right + M), height: bottom + GAP / 2 + M };
200
+ }
201
+
202
+ /**
203
+ * A stored office with its own layout: one room (named for the office) around its seats. A seat assigned to a member
204
+ * holds them; the other members take the free seats in order; `unseated` counts who found none.
205
+ */
206
+ export function customLayout(office, nodes) {
207
+ const { DW, DH, PAD, HEAD, M } = OF;
208
+ const lay = office.layout;
209
+ const left = new Map(nodes.map((n) => [n.name, n]));
210
+ const taken = lay.seats.map((s) => { const n = s.instance ? left.get(s.instance) : undefined; if (n) left.delete(n.name); return n || null; });
211
+ const rest = [...left.values()];
212
+ const desks = lay.seats.map((s, i) => {
213
+ const node = taken[i] || (rest.length ? rest.shift() : null);
214
+ return { key: `${office.id}/${s.id}`, node, x: s.x - DW / 2, y: s.y - OF.SEAT_Y, w: DW, h: DH, seat: { x: s.x, y: s.y }, wp: null };
215
+ });
216
+ const xs = [...desks.flatMap((d) => [d.x, d.x + d.w]), ...lay.waypoints.map((w) => w.x)];
217
+ const ys = [...desks.flatMap((d) => [d.y, d.y + d.h]), ...lay.waypoints.map((w) => w.y)];
218
+ const x0 = Math.max(0, Math.min(...xs) - PAD), y0 = Math.max(0, Math.min(...ys) - PAD - HEAD);
219
+ const room = { key: office.id, name: office.name, lobby: false, x: x0, y: y0, w: Math.max(...xs) + PAD - x0, h: Math.max(...ys) + PAD - y0, desks, ri: 0, row: 0 };
220
+ return { rooms: [room], waypoints: lay.waypoints, edges: lay.edges, width: room.x + room.w + M, height: room.y + room.h + M, unseated: rest.length };
221
+ }
222
+
223
+ /**
224
+ * The backing store's px per CSS px for a W × H (CSS px) canvas at `dpr` (#2431 review): the device's ratio while the
225
+ * bitmap stays one a browser can paint (each side at most 16384, at most 32 Mi px in all — Chromium gives a larger one
226
+ * back blank, without an error); a bigger layout (custom seats far out) is drawn coarser instead of not at all.
227
+ */
228
+ export const CANVAS_MAX = { side: 16384, area: 32 * 1024 * 1024 };
229
+ export function backingScale(w, h, dpr) {
230
+ return Math.min(dpr, CANVAS_MAX.side / Math.max(1, w), CANVAS_MAX.side / Math.max(1, h), Math.sqrt(CANVAS_MAX.area / Math.max(1, w * h)));
231
+ }
232
+
233
+ /** Every desk with someone at it, in reading order. */
234
+ export const desksOf = (L) => L.rooms.flatMap((r) => r.desks).filter((d) => d.node);
235
+ /** The desk at (x, y) in layout px, or null. */
236
+ export const deskAt = (L, x, y) => desksOf(L).find((d) => x >= d.x && x < d.x + d.w && y >= d.y && y < d.y + d.h) || null;
237
+
238
+ // ── Walking: shortest paths on the waypoint graph ──
239
+
240
+ /** The fewest-hops path from waypoint `a` to `b` (both ids, both included); null when there is none. */
241
+ export function bfsPath(edges, a, b) {
242
+ if (a === b) return [a];
243
+ const next = new Map();
244
+ for (const [u, v] of edges) {
245
+ if (!next.has(u)) next.set(u, []);
246
+ if (!next.has(v)) next.set(v, []);
247
+ next.get(u).push(v);
248
+ next.get(v).push(u);
249
+ }
250
+ if (!next.has(a) || !next.has(b)) return null;
251
+ const from = new Map([[a, null]]), queue = [a];
252
+ for (let i = 0; i < queue.length; i++) {
253
+ const u = queue[i];
254
+ for (const v of next.get(u)) {
255
+ if (from.has(v)) continue;
256
+ from.set(v, u);
257
+ if (v === b) {
258
+ const path = [b];
259
+ for (let p = u; p !== null; p = from.get(p)) path.push(p);
260
+ return path.reverse();
261
+ }
262
+ queue.push(v);
263
+ }
264
+ }
265
+ return null;
266
+ }
267
+ /** The waypoint nearest a point; null when there are none. */
268
+ export function nearestWaypoint(waypoints, p) {
269
+ let best = null, d = Infinity;
270
+ for (const w of waypoints) { const e = Math.hypot(w.x - p.x, w.y - p.y); if (e < d) { d = e; best = w; } }
271
+ return best;
272
+ }
273
+ /**
274
+ * The way from one instance's desk to another's (its first desk, if it has two): its seat, the waypoints between the
275
+ * nearest waypoints of the two seats, the other seat. A straight line when the graph has no way; null when either is
276
+ * not drawn.
277
+ */
278
+ export function walkPath(L, from, to) {
279
+ const desks = desksOf(L);
280
+ const a = desks.find((d) => d.node.name === from), z = desks.find((d) => d.node.name === to);
281
+ if (!a || !z || a === z) return null;
282
+ return routeVia(L, a.seat, z.seat, a.wp, z.wp);
283
+ }
284
+ /**
285
+ * The way between two points on a waypoint graph ({ waypoints, edges }): from `a` to its waypoint (`wa`, else the
286
+ * nearest), the fewest hops to `z`'s, then `z`. A straight line when the graph has no way.
287
+ */
288
+ export function routeVia(graph, a, z, wa = null, wz = null) {
289
+ const byId = new Map(graph.waypoints.map((w) => [w.id, w]));
290
+ const pa = (wa && byId.get(wa)) || nearestWaypoint(graph.waypoints, a), pz = (wz && byId.get(wz)) || nearestWaypoint(graph.waypoints, z);
291
+ const ids = pa && pz ? bfsPath(graph.edges, pa.id, pz.id) : null;
292
+ return ids ? [a, ...ids.map((id) => byId.get(id)), z] : [a, z];
293
+ }
294
+ export const pathLength = (pts) => pts.reduce((s, p, i) => (i ? s + Math.hypot(p.x - pts[i - 1].x, p.y - pts[i - 1].y) : 0), 0);
295
+ /** The point a fraction `t` (0–1) of the way along a polyline. */
296
+ export function along(pts, t) {
297
+ const total = pathLength(pts);
298
+ if (!total) return { x: pts[0].x, y: pts[0].y };
299
+ let left = Math.max(0, Math.min(1, t)) * total;
300
+ for (let i = 1; i < pts.length; i++) {
301
+ const l = Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y);
302
+ if (left <= l && l > 0) { const k = left / l; return { x: pts[i - 1].x + (pts[i].x - pts[i - 1].x) * k, y: pts[i - 1].y + (pts[i].y - pts[i - 1].y) * k }; }
303
+ left -= l;
304
+ }
305
+ const last = pts[pts.length - 1];
306
+ return { x: last.x, y: last.y };
307
+ }
308
+
309
+ // ── Traffic: an item carried from desk to desk ──
310
+
311
+ /** px per ms along the path, the shortest and longest trip, the stagger between items that arrive together. */
312
+ export const FLIGHT = { SPEED: 0.35, MIN_MS: 900, MAX_MS: 4000, STAGGER_MS: 150, MAX: 40 };
313
+ const MAX_SEEN = 2000;
314
+ /**
315
+ * The items in the air. `sync(items, now, still)` takes fleet-traffic.js's store items: each new one (a message, a
316
+ * delegation's work, its report coming back) starts one trip from its sender's desk to its receiver's; a delegation with
317
+ * no report (stale) and anything seen before start none; `still` (reduced motion) starts none. `frame(now, L)` the
318
+ * trips under way on layout L: { id, hue, kind, x, y, t }, finished ones dropped; a trip whose desks are not drawn goes.
319
+ */
320
+ export function createFlights({ max = FLIGHT.MAX } = {}) {
321
+ const seen = new Set();
322
+ let trips = [];
323
+ return {
324
+ sync(items, now, still = false) {
325
+ let k = 0, started = 0;
326
+ for (const it of items || []) {
327
+ if (seen.has(it.id)) continue;
328
+ seen.add(it.id);
329
+ if (seen.size > MAX_SEEN) seen.delete(seen.values().next().value);
330
+ if (still || it.phase === "stale") continue;
331
+ trips.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now + k++ * FLIGHT.STAGGER_MS, L: null, pts: null, ms: 0 });
332
+ started++;
333
+ }
334
+ if (trips.length > max) trips = trips.slice(trips.length - max);
335
+ return started;
336
+ },
337
+ frame(now, L) {
338
+ const out = [];
339
+ trips = trips.filter((f) => {
340
+ if (f.L !== L) { f.L = L; f.pts = walkPath(L, f.from, f.to); f.ms = f.pts ? Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(f.pts) / FLIGHT.SPEED)) : 0; }
341
+ if (!f.pts) return false;
342
+ const t = (now - f.start) / f.ms;
343
+ if (t >= 1) return false;
344
+ if (t >= 0) { const p = along(f.pts, t); out.push({ id: f.id, hue: f.hue, kind: f.kind, x: p.x, y: p.y, t }); }
345
+ return true;
346
+ });
347
+ return out;
348
+ },
349
+ size: () => trips.length,
350
+ };
351
+ }
352
+
353
+ /** How many delegations each instance is working on now (the folders on its desk). */
354
+ export function workOn(items) {
355
+ const out = new Map();
356
+ for (const it of items || []) if (it.phase === "work") out.set(it.to, (out.get(it.to) || 0) + 1);
357
+ return out;
358
+ }