@songsid/agend 2.3.1 → 2.3.2-beta.10

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 (338) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +4 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/authority/authority-state.d.ts +4 -0
  15. package/dist/authority/authority-state.js +16 -10
  16. package/dist/authority/authority-state.js.map +1 -1
  17. package/dist/authority/session-authorizations.d.ts +7 -1
  18. package/dist/authority/session-authorizations.js +6 -4
  19. package/dist/authority/session-authorizations.js.map +1 -1
  20. package/dist/authority/web-authority.d.ts +19 -6
  21. package/dist/authority/web-authority.js +22 -8
  22. package/dist/authority/web-authority.js.map +1 -1
  23. package/dist/backend/kiro-identity.d.ts +64 -0
  24. package/dist/backend/kiro-identity.js +101 -0
  25. package/dist/backend/kiro-identity.js.map +1 -1
  26. package/dist/backend/kiro-retired-model.d.ts +62 -0
  27. package/dist/backend/kiro-retired-model.js +160 -0
  28. package/dist/backend/kiro-retired-model.js.map +1 -0
  29. package/dist/backend/kiro.d.ts +71 -2
  30. package/dist/backend/kiro.js +352 -11
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +59 -0
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/backend-signin-guide.d.ts +1 -0
  35. package/dist/backend-signin-guide.js +9 -0
  36. package/dist/backend-signin-guide.js.map +1 -0
  37. package/dist/backend-signin-pending.d.ts +10 -0
  38. package/dist/backend-signin-pending.js +44 -0
  39. package/dist/backend-signin-pending.js.map +1 -0
  40. package/dist/browser-backend-signins.d.ts +56 -0
  41. package/dist/browser-backend-signins.js +189 -0
  42. package/dist/browser-backend-signins.js.map +1 -0
  43. package/dist/cache-ledger.d.ts +38 -2
  44. package/dist/cache-ledger.js +133 -8
  45. package/dist/cache-ledger.js.map +1 -1
  46. package/dist/cache-prices.d.ts +2 -1
  47. package/dist/cache-prices.js +5 -2
  48. package/dist/cache-prices.js.map +1 -1
  49. package/dist/cache-service.d.ts +68 -0
  50. package/dist/cache-service.js +355 -13
  51. package/dist/cache-service.js.map +1 -1
  52. package/dist/channel/adapters/discord.d.ts +13 -0
  53. package/dist/channel/adapters/discord.js +103 -16
  54. package/dist/channel/adapters/discord.js.map +1 -1
  55. package/dist/channel/mcp-tools.js +34 -7
  56. package/dist/channel/mcp-tools.js.map +1 -1
  57. package/dist/channel/tool-router.js +2 -1
  58. package/dist/channel/tool-router.js.map +1 -1
  59. package/dist/channel/types.d.ts +11 -0
  60. package/dist/classic-channel-manager.d.ts +11 -0
  61. package/dist/classic-channel-manager.js +39 -1
  62. package/dist/classic-channel-manager.js.map +1 -1
  63. package/dist/cli.js +21 -2
  64. package/dist/cli.js.map +1 -1
  65. package/dist/config-validator.d.ts +7 -1
  66. package/dist/config-validator.js +92 -12
  67. package/dist/config-validator.js.map +1 -1
  68. package/dist/config.d.ts +19 -0
  69. package/dist/config.js +31 -1
  70. package/dist/config.js.map +1 -1
  71. package/dist/cpu-quota.d.ts +23 -0
  72. package/dist/cpu-quota.js +86 -0
  73. package/dist/cpu-quota.js.map +1 -0
  74. package/dist/daemon.d.ts +57 -3
  75. package/dist/daemon.js +330 -50
  76. package/dist/daemon.js.map +1 -1
  77. package/dist/dashboard-permissions.d.ts +20 -6
  78. package/dist/dashboard-permissions.js +58 -9
  79. package/dist/dashboard-permissions.js.map +1 -1
  80. package/dist/delivery-outbox.d.ts +12 -1
  81. package/dist/delivery-outbox.js +67 -15
  82. package/dist/delivery-outbox.js.map +1 -1
  83. package/dist/doctor.d.ts +3 -0
  84. package/dist/doctor.js +11 -1
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/event-log.d.ts +5 -0
  87. package/dist/event-log.js +13 -0
  88. package/dist/event-log.js.map +1 -1
  89. package/dist/fleet-manager.d.ts +212 -0
  90. package/dist/fleet-manager.js +914 -111
  91. package/dist/fleet-manager.js.map +1 -1
  92. package/dist/fleet-process-handlers.d.ts +1 -0
  93. package/dist/fleet-process-handlers.js +9 -1
  94. package/dist/fleet-process-handlers.js.map +1 -1
  95. package/dist/forged-envelope-scan.d.ts +9 -0
  96. package/dist/forged-envelope-scan.js +30 -5
  97. package/dist/forged-envelope-scan.js.map +1 -1
  98. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  99. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  100. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +28 -5
  101. package/dist/general-knowledge/skills/web-appearance/SKILL.md +31 -8
  102. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +138 -59
  103. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +145 -61
  104. package/dist/guest-seen-people.d.ts +38 -0
  105. package/dist/guest-seen-people.js +78 -0
  106. package/dist/guest-seen-people.js.map +1 -0
  107. package/dist/instance-config-impact.js +14 -6
  108. package/dist/instance-config-impact.js.map +1 -1
  109. package/dist/instance-lifecycle.d.ts +20 -0
  110. package/dist/instance-lifecycle.js +71 -13
  111. package/dist/instance-lifecycle.js.map +1 -1
  112. package/dist/kiro-db-reader.d.ts +5 -0
  113. package/dist/kiro-db-reader.js +18 -0
  114. package/dist/kiro-db-reader.js.map +1 -1
  115. package/dist/kiro-engine-status.d.ts +2 -0
  116. package/dist/kiro-engine-status.js +9 -0
  117. package/dist/kiro-engine-status.js.map +1 -1
  118. package/dist/kiro-migrate.d.ts +49 -0
  119. package/dist/kiro-migrate.js +105 -0
  120. package/dist/kiro-migrate.js.map +1 -0
  121. package/dist/kiro-transcript-lane.d.ts +2 -0
  122. package/dist/kiro-transcript-lane.js.map +1 -1
  123. package/dist/kiro-transcript-worker.js +1 -0
  124. package/dist/kiro-transcript-worker.js.map +1 -1
  125. package/dist/kiro-tui-forensic.d.ts +7 -0
  126. package/dist/kiro-tui-forensic.js +110 -0
  127. package/dist/kiro-tui-forensic.js.map +1 -0
  128. package/dist/kiro-usage-run.d.ts +7 -0
  129. package/dist/kiro-usage-run.js +39 -0
  130. package/dist/kiro-usage-run.js.map +1 -0
  131. package/dist/kiro-usage-scan.d.ts +49 -0
  132. package/dist/kiro-usage-scan.js +223 -0
  133. package/dist/kiro-usage-scan.js.map +1 -0
  134. package/dist/kiro-usage-worker.d.ts +1 -0
  135. package/dist/kiro-usage-worker.js +10 -0
  136. package/dist/kiro-usage-worker.js.map +1 -0
  137. package/dist/kiro-usage.d.ts +58 -0
  138. package/dist/kiro-usage.js +310 -0
  139. package/dist/kiro-usage.js.map +1 -0
  140. package/dist/locale.js +120 -12
  141. package/dist/locale.js.map +1 -1
  142. package/dist/log-service.js +1 -1
  143. package/dist/log-service.js.map +1 -1
  144. package/dist/login-url.d.ts +4 -0
  145. package/dist/login-url.js +22 -0
  146. package/dist/login-url.js.map +1 -0
  147. package/dist/mac-auto-restore.d.ts +117 -0
  148. package/dist/mac-auto-restore.js +294 -0
  149. package/dist/mac-auto-restore.js.map +1 -0
  150. package/dist/needs-you-hub.d.ts +2 -1
  151. package/dist/needs-you-hub.js +1 -1
  152. package/dist/needs-you-hub.js.map +1 -1
  153. package/dist/needs-you.d.ts +8 -2
  154. package/dist/needs-you.js +6 -0
  155. package/dist/needs-you.js.map +1 -1
  156. package/dist/notice-delivery.d.ts +16 -0
  157. package/dist/notice-delivery.js +35 -0
  158. package/dist/notice-delivery.js.map +1 -0
  159. package/dist/office-share-page.d.ts +16 -0
  160. package/dist/office-share-page.js +85 -0
  161. package/dist/office-share-page.js.map +1 -0
  162. package/dist/office-share-service.d.ts +91 -0
  163. package/dist/office-share-service.js +176 -0
  164. package/dist/office-share-service.js.map +1 -0
  165. package/dist/office-share.d.ts +258 -0
  166. package/dist/office-share.js +549 -0
  167. package/dist/office-share.js.map +1 -0
  168. package/dist/opencode-usage-scan.d.ts +28 -0
  169. package/dist/opencode-usage-scan.js +92 -0
  170. package/dist/opencode-usage-scan.js.map +1 -0
  171. package/dist/opencode-usage-worker.d.ts +1 -0
  172. package/dist/opencode-usage-worker.js +10 -0
  173. package/dist/opencode-usage-worker.js.map +1 -0
  174. package/dist/outbound-handlers.js +22 -2
  175. package/dist/outbound-handlers.js.map +1 -1
  176. package/dist/outbound-schemas.d.ts +28 -2
  177. package/dist/outbound-schemas.js +29 -2
  178. package/dist/outbound-schemas.js.map +1 -1
  179. package/dist/public-web-gateway.js +16 -2
  180. package/dist/public-web-gateway.js.map +1 -1
  181. package/dist/quickstart-api.js +13 -2
  182. package/dist/quickstart-api.js.map +1 -1
  183. package/dist/quickstart.js +2 -1
  184. package/dist/quickstart.js.map +1 -1
  185. package/dist/remote-card-answer.d.ts +83 -0
  186. package/dist/remote-card-answer.js +208 -0
  187. package/dist/remote-card-answer.js.map +1 -0
  188. package/dist/reply-triggers.d.ts +2 -0
  189. package/dist/reply-triggers.js +3 -0
  190. package/dist/reply-triggers.js.map +1 -0
  191. package/dist/settings-api.d.ts +28 -0
  192. package/dist/settings-api.js +148 -17
  193. package/dist/settings-api.js.map +1 -1
  194. package/dist/settings-change.js +54 -17
  195. package/dist/settings-change.js.map +1 -1
  196. package/dist/settings-effect.js +12 -3
  197. package/dist/settings-effect.js.map +1 -1
  198. package/dist/settings-instance-patch.js +4 -1
  199. package/dist/settings-instance-patch.js.map +1 -1
  200. package/dist/setup-form.d.ts +1 -1
  201. package/dist/setup-form.js +55 -6
  202. package/dist/setup-form.js.map +1 -1
  203. package/dist/setup-host.d.ts +9 -0
  204. package/dist/setup-host.js +127 -6
  205. package/dist/setup-host.js.map +1 -1
  206. package/dist/setup-signin-assets.d.ts +2 -0
  207. package/dist/setup-signin-assets.js +6 -0
  208. package/dist/setup-signin-assets.js.map +1 -0
  209. package/dist/setup-wizard.js +2 -0
  210. package/dist/setup-wizard.js.map +1 -1
  211. package/dist/start-attempt.d.ts +9 -5
  212. package/dist/start-attempt.js +27 -8
  213. package/dist/start-attempt.js.map +1 -1
  214. package/dist/start-failure-chat.d.ts +4 -0
  215. package/dist/start-failure-chat.js +23 -6
  216. package/dist/start-failure-chat.js.map +1 -1
  217. package/dist/start-failure-notice.d.ts +4 -0
  218. package/dist/start-failure-notice.js +8 -0
  219. package/dist/start-failure-notice.js.map +1 -1
  220. package/dist/start-failure.d.ts +25 -0
  221. package/dist/start-failure.js +75 -2
  222. package/dist/start-failure.js.map +1 -1
  223. package/dist/structured-signin-transport.d.ts +8 -0
  224. package/dist/structured-signin-transport.js +79 -0
  225. package/dist/structured-signin-transport.js.map +1 -0
  226. package/dist/structured-signin.d.ts +131 -0
  227. package/dist/structured-signin.js +388 -0
  228. package/dist/structured-signin.js.map +1 -0
  229. package/dist/tool-permissions.js +12 -0
  230. package/dist/tool-permissions.js.map +1 -1
  231. package/dist/topic-commands.js +6 -2
  232. package/dist/topic-commands.js.map +1 -1
  233. package/dist/transcript-sources.d.ts +26 -2
  234. package/dist/transcript-sources.js +103 -40
  235. package/dist/transcript-sources.js.map +1 -1
  236. package/dist/turn-reply-guard.d.ts +25 -0
  237. package/dist/turn-reply-guard.js +111 -12
  238. package/dist/turn-reply-guard.js.map +1 -1
  239. package/dist/types.d.ts +46 -9
  240. package/dist/ui/backend-signin.js +68 -12
  241. package/dist/ui/chat-ambient.js +1 -1
  242. package/dist/ui/chat-effects.js +4 -4
  243. package/dist/ui/chat-render.js +5 -2
  244. package/dist/ui/chat-strings.js +4 -4
  245. package/dist/ui/chat-thread.js +2 -2
  246. package/dist/ui/first-run.js +2 -2
  247. package/dist/ui/fleet-canvas-view.js +114 -0
  248. package/dist/ui/fleet-canvas.js +15 -77
  249. package/dist/ui/fleet-office-editor.js +94 -14
  250. package/dist/ui/fleet-office-layout.js +235 -0
  251. package/dist/ui/fleet-office-model.js +12 -6
  252. package/dist/ui/fleet-office-overview.js +167 -0
  253. package/dist/ui/fleet-office-pack.d.ts +44 -0
  254. package/dist/ui/fleet-office-pack.js +352 -37
  255. package/dist/ui/fleet-office-skin.js +220 -36
  256. package/dist/ui/fleet-office-store.js +112 -7
  257. package/dist/ui/fleet-office-view.js +155 -0
  258. package/dist/ui/fleet-office-zones.js +264 -42
  259. package/dist/ui/fleet-office.js +957 -160
  260. package/dist/ui/fleet-usage.js +26 -21
  261. package/dist/ui/office-share-dialog.js +145 -0
  262. package/dist/ui/office-share-page.js +164 -0
  263. package/dist/ui/panel-chat.js +9 -2
  264. package/dist/ui/panel-details.js +2 -2
  265. package/dist/ui/panel-fleet.js +11 -77
  266. package/dist/ui/panel-log.js +4 -4
  267. package/dist/ui/panel-needs.js +4 -4
  268. package/dist/ui/panel-settings.js +608 -167
  269. package/dist/ui/search-dialog.js +26 -7
  270. package/dist/ui/settings-appearance.js +82 -29
  271. package/dist/ui/settings-apply.js +42 -7
  272. package/dist/ui/settings-dialogs.js +76 -23
  273. package/dist/ui/settings-draft.js +51 -0
  274. package/dist/ui/settings-guests.js +255 -79
  275. package/dist/ui/settings-learn.js +2 -2
  276. package/dist/ui/settings-model.js +16 -6
  277. package/dist/ui/settings-registry.js +146 -0
  278. package/dist/ui/settings-strings.js +116 -44
  279. package/dist/ui/settings-web-authority.js +34 -23
  280. package/dist/ui/shared/app-i18n.js +18 -2
  281. package/dist/ui/shared/app-nav.js +10 -5
  282. package/dist/ui/shared/app-prefs.js +80 -2
  283. package/dist/ui/shared/app-route.js +39 -5
  284. package/dist/ui/shared/app-shell.js +73 -10
  285. package/dist/ui/shared/app-stream.js +3 -1
  286. package/dist/ui/shared/app.css +307 -17
  287. package/dist/ui/shared/app.js +3 -0
  288. package/dist/ui/shared/effect-stock-trust.js +1 -1
  289. package/dist/ui/shared/identity-color.js +4 -4
  290. package/dist/ui/shared/office/cozy.png +0 -0
  291. package/dist/ui/shared/side-panel.js +95 -0
  292. package/dist/ui/shared/signin.js +2 -2
  293. package/dist/ui/shared/ui-dialog.js +6 -1
  294. package/dist/ui/shared/ui-icons.js +2 -0
  295. package/dist/ui/shared/web-authorize.js +13 -12
  296. package/dist/ui/structured-signin.js +115 -0
  297. package/dist/usage-report.d.ts +20 -0
  298. package/dist/usage-report.js +54 -17
  299. package/dist/usage-report.js.map +1 -1
  300. package/dist/usage-rollup.d.ts +9 -1
  301. package/dist/usage-rollup.js +20 -4
  302. package/dist/usage-rollup.js.map +1 -1
  303. package/dist/web-api.d.ts +26 -2
  304. package/dist/web-api.js +361 -5
  305. package/dist/web-api.js.map +1 -1
  306. package/dist/web-auth.js +8 -2
  307. package/dist/web-auth.js.map +1 -1
  308. package/dist/web-backend-login.d.ts +1 -7
  309. package/dist/web-backend-login.js +2 -24
  310. package/dist/web-backend-login.js.map +1 -1
  311. package/dist/web-experimental.d.ts +1 -1
  312. package/dist/web-experimental.js +1 -1
  313. package/dist/web-experimental.js.map +1 -1
  314. package/dist/web-mod-agent.js +2 -2
  315. package/dist/web-mod-agent.js.map +1 -1
  316. package/dist/web-office-acts.d.ts +173 -0
  317. package/dist/web-office-acts.js +285 -0
  318. package/dist/web-office-acts.js.map +1 -0
  319. package/dist/web-office-skins.d.ts +109 -0
  320. package/dist/web-office-skins.js +299 -0
  321. package/dist/web-office-skins.js.map +1 -0
  322. package/dist/web-office-store.d.ts +8 -2
  323. package/dist/web-office-store.js +77 -14
  324. package/dist/web-office-store.js.map +1 -1
  325. package/dist/web-settings-patch.d.ts +8 -1
  326. package/dist/web-settings-patch.js +65 -5
  327. package/dist/web-settings-patch.js.map +1 -1
  328. package/dist/web-shell-routes.d.ts +4 -2
  329. package/dist/web-shell-routes.js +17 -7
  330. package/dist/web-shell-routes.js.map +1 -1
  331. package/dist/working-recovery.d.ts +104 -0
  332. package/dist/working-recovery.js +298 -0
  333. package/dist/working-recovery.js.map +1 -0
  334. package/dist/working-state-journal.d.ts +5 -1
  335. package/dist/working-state-journal.js +31 -3
  336. package/dist/working-state-journal.js.map +1 -1
  337. package/package.json +3 -2
  338. package/templates/windows-shortcuts.ps1 +155 -140
@@ -1,10 +1,12 @@
1
1
  // The Office view's art (fleet-office.js draws with it on a <canvas>). A skin draws a layout that fleet-office-model.js
2
- // (desks) or fleet-office-zones.js (zones) made. Two skins are built in and draw everything procedurally — little
3
- // pixel people, desks, lamps — so no art ships: "pixel" (the desks layout: a room per team, a desk per instance) and
4
- // "studio" (the zones layout: work desks, a lounge, an alert corner and a nap corner; people walk to the area for their
5
- // state). Every drawing call goes through the OfficeSkin interface below, so a skin pack (pictures from an artist,
2
+ // (desks) or fleet-office-zones.js (zones) made. Three skins are built in. Two draw everything procedurally — little
3
+ // pixel people, desks, lamps: "pixel" (the desks layout: a room per team, a desk per instance) and "studio" (the zones
4
+ // layout: work desks, a lounge, an alert corner and a nap corner; people walk to the area for their state). The third,
5
+ // "cozy" (#2631), is the zones layout on one picture shipped with the page (a warm pixel-art office), the same built-in
6
+ // people in it. Every drawing call goes through the OfficeSkin interface below, so a skin pack (pictures from an artist,
6
7
  // format v1 in fleet-office-pack.js) can replace any part; a part a pack does not supply falls back to the built-in
7
- // drawing. No pack can be added yet (no upload): spriteSkin() and checkSkinPack() are the seam a later step plugs into.
8
+ // drawing. An admin uploads a pack (#2513, the host keeps it: src/web-office-skins.ts); loadPackSkin() reads it back —
9
+ // its manifest checked here again (checkSkinPack), its pictures by their content ids — and spriteSkin() draws it.
8
10
  //
9
11
  // ── The interface ──
10
12
  //
@@ -22,16 +24,26 @@
22
24
  // @property {(ctx, desk, look, env) => void} drawDeskFront desks: in front — the desk's front, its sign, folders, name tag
23
25
  // @property {(ctx, L, env) => void} [drawBackground] zones: the whole 1280×720 background, areas and their names
24
26
  // @property {(ctx, item, active, ms, env) => void} [drawFurniture] zones: one furniture item (active: activeWhen holds)
27
+ // @property {(ctx, L, env) => void} [drawForeground] zones: a pack's foreground over the whole 1280×720 scene, above
28
+ // the people, furniture and items in the air, below the crowd counts, name tags and bubbles
25
29
  // @property {(ctx, x, y, look, env) => void} [drawTag] zones: a name tag under feet at (x, y)
30
+ // @property {(ctx, area, n, env) => void} [drawCrowd] zones: an area's "+n" (who found no spot in it)
26
31
  // @property {(ctx, desk, look, ms, env) => void} drawCharacter the occupant in its pose at time `ms` (0: still); in
27
- // zones `desk` is the character's box at its current position (its feet at desk.seat)
32
+ // zones `desk` is the character's box at its current position (its feet at desk.seat); walking, `look.heading` is
33
+ // the current step's { dx, dy }
34
+ // @property {(look, at) => {x, y, w, h}} [charBox] zones: the box drawCharacter covers with the feet at `at`
35
+ // ({ x, y, walking, heading }): what hover and click hit (fleet-office-zones.js personAt)
28
36
  // @property {(ctx, prop, ms, env) => void} drawProp a traffic item in the air: { kind, hue, x, y, t }
29
37
  // @property {(ctx, x, y, text, env, lift?) => void} drawBubble a speech bubble whose tail points at (x, y), its box `lift` px higher
30
- // The desks layout's back and front layers change only when the fleet does: the renderer keeps them as pictures and
31
- // draws the characters, props and bubbles each frame. In zones the background is kept; furniture, people (walking
32
- // along the pack's waypoints), tags, props and bubbles are drawn each frame, furniture by depth around the people.
38
+ // @property {(env) => string} matte #2540: the colour around the scene (the Office fills the panel; the scene is 16:9)
39
+ // @property {object|null} [backdrop] a pack's loaded background picture (the page samples its edges for the matte)
40
+ // The desks layout's back and front layers change only when the fleet (or the zoom) does: the renderer keeps them as
41
+ // pictures and draws the characters, props and bubbles each frame. In zones the background is kept; furniture, people (walking
42
+ // along the pack's waypoints), tags, props and bubbles are drawn each frame, furniture by depth around the people (depth "y":
43
+ // among them by base y), then the foreground, then the crowd counts, tags and bubbles.
44
+ import { t } from "/assets/app-i18n.js";
33
45
  import { frameAt, nameHash } from "./fleet-office-model.js";
34
- import { CHAR_DEPTH, LOGICAL } from "./fleet-office-pack.js";
46
+ import { CHAR_DEPTH, LOGICAL, checkSkinPack, own, packImages, poseStrip, stripBox, walkStrip } from "./fleet-office-pack.js";
35
47
 
36
48
  export { nameHash };
37
49
 
@@ -212,14 +224,16 @@ function drawDeskFront(ctx, d, look, env) {
212
224
  ctx.fillStyle = pal.text2; ctx.font = "600 8px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
213
225
  ctx.fillText(fitText(ctx, env.labels.closed, 32), d.x + 48, d.y + 63);
214
226
  }
215
- // The name tag.
216
- ctx.fillStyle = pal.surface;
217
- ctx.fillRect(d.x + 6, d.y + 78, d.w - 12, 16);
218
- ctx.strokeStyle = look.match ? pal.focus : pal.border;
219
- ctx.lineWidth = look.match ? 2 : 1;
220
- ctx.strokeRect(d.x + 6.5, d.y + 78.5, d.w - 13, 15);
221
- ctx.fillStyle = pal.text; ctx.font = "600 11px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
222
- ctx.fillText(fitText(ctx, look.label, d.w - 18), d.x + d.w / 2, d.y + 86.5);
227
+ // The name tag (#2554: none when `notag` — a tile too small to read it; the page draws it again on hover).
228
+ if (!look.notag) {
229
+ ctx.fillStyle = pal.surface;
230
+ ctx.fillRect(d.x + 6, d.y + 78, d.w - 12, 16);
231
+ ctx.strokeStyle = look.match ? pal.focus : pal.border;
232
+ ctx.lineWidth = look.match ? 2 : 1;
233
+ ctx.strokeRect(d.x + 6.5, d.y + 78.5, d.w - 13, 15);
234
+ ctx.fillStyle = pal.text; ctx.font = "600 11px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
235
+ ctx.fillText(fitText(ctx, look.label, d.w - 18), d.x + d.w / 2, d.y + 86.5);
236
+ }
223
237
  ctx.restore();
224
238
  if (look.match) { ctx.save(); ctx.strokeStyle = pal.focus; ctx.lineWidth = 2; ctx.strokeRect(d.x + 2, d.y + 2, d.w - 4, d.h - 6); ctx.restore(); }
225
239
  }
@@ -271,21 +285,58 @@ function drawBubble(ctx, x, y, text, env, lift = 0) {
271
285
  ctx.restore();
272
286
  }
273
287
 
288
+ /** A zones name tag's widest text (logical units): longer names are cut with an ellipsis. */
289
+ export const TAG_TEXT_W = 84;
290
+ const TAG_FONT = "600 10px system-ui, sans-serif";
291
+ /** The box a name tag under feet at (x, y) takes (what drawTag draws; bubbles keep clear of it, #2513). */
292
+ export function tagBox(ctx, x, y, label) {
293
+ ctx.save();
294
+ ctx.font = TAG_FONT;
295
+ const t = fitText(ctx, label, TAG_TEXT_W), w = Math.ceil((ctx.measureText ? ctx.measureText(t).width : t.length * 6) + 10);
296
+ ctx.restore();
297
+ return { x: Math.round(x - w / 2), y: y + 3, w, h: 14, text: t };
298
+ }
274
299
  /** Zones: a name tag under someone's feet at (x, y). */
275
300
  function drawTag(ctx, x, y, look, env) {
276
301
  const { pal } = env;
277
302
  ctx.save();
278
303
  if (look.dim) ctx.globalAlpha = 0.35;
279
- ctx.font = "600 10px system-ui, sans-serif";
280
- const t = fitText(ctx, look.label, 84), w = Math.ceil((ctx.measureText ? ctx.measureText(t).width : t.length * 6) + 10);
304
+ ctx.font = TAG_FONT;
305
+ const t = fitText(ctx, look.label, TAG_TEXT_W), w = Math.ceil((ctx.measureText ? ctx.measureText(t).width : t.length * 6) + 10);
281
306
  ctx.fillStyle = pal.surface; ctx.fillRect(Math.round(x - w / 2), y + 3, w, 14);
282
307
  ctx.strokeStyle = look.match ? pal.focus : pal.border; ctx.lineWidth = look.match ? 2 : 1; ctx.strokeRect(Math.round(x - w / 2) + 0.5, y + 3.5, w - 1, 13);
283
308
  ctx.fillStyle = pal.text; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(t, x, y + 10.5);
284
309
  ctx.restore();
285
310
  }
286
311
 
312
+ /**
313
+ * Zones: how many more want an area than it has room for — a chip in its bottom-right corner, inside its rect, so it
314
+ * never covers the area's name (#2513: "+42" over the nap corner's title).
315
+ */
316
+ export function crowdChip(ctx, area, n) {
317
+ ctx.save();
318
+ ctx.font = "700 12px system-ui, sans-serif";
319
+ const text = `+${n}`, w = Math.ceil((ctx.measureText ? ctx.measureText(text).width : text.length * 7) + 12), h = 18, r = area.rect;
320
+ ctx.restore();
321
+ return { x: Math.round(r.x + r.w - w - 6), y: Math.round(r.y + r.h - h - 6), w, h, text };
322
+ }
323
+ function drawCrowd(ctx, area, n, env) {
324
+ if (!n) return;
325
+ const { pal } = env, b = crowdChip(ctx, area, n);
326
+ ctx.save();
327
+ ctx.fillStyle = pal.surface; ctx.fillRect(b.x, b.y, b.w, b.h);
328
+ ctx.strokeStyle = pal.border; ctx.lineWidth = 1; ctx.strokeRect(b.x + 0.5, b.y + 0.5, b.w - 1, b.h - 1);
329
+ ctx.fillStyle = pal.text; ctx.font = "700 12px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
330
+ ctx.fillText(b.text, b.x + b.w / 2, b.y + b.h / 2 + 0.5);
331
+ ctx.restore();
332
+ }
333
+
334
+ /** The built-in character in zones covers this box around its feet (as zonesLayout's desk box). */
335
+ const charBox = (look, at) => ({ x: at.x - 24, y: at.y - 52, w: 48, h: 66 });
336
+ /** #2540: Desks' matte (around the scene): the page's background token, which the desks' floor is too (Studio has its own). */
337
+ const matte = (env) => env.pal.bg;
287
338
  /** @type {OfficeSkin} */
288
- export const pixelSkin = { id: "pixel", drawFloor, drawRoom, drawDeskBack, drawCharacter, drawDeskFront, drawProp, drawBubble, drawTag };
339
+ export const pixelSkin = { id: "pixel", drawFloor, drawRoom, drawDeskBack, drawCharacter, drawDeskFront, drawProp, drawBubble, drawTag, drawCrowd, charBox, matte };
289
340
 
290
341
  // ── The built-in studio: the zones layout, drawn procedurally ──
291
342
 
@@ -327,7 +378,7 @@ const STUDIO_FURNITURE = [
327
378
  ].sort((a, b) => a.depth - b.depth);
328
379
  /** The studio's pack: the same shape as a checked one (fleet-office-pack.js), furniture by `shape` instead of sprites. */
329
380
  export const STUDIO_PACK = {
330
- format: 1, id: "studio", scale: 1, areas: STUDIO_AREAS, door: { x: HALL_X, y: 704 }, furniture: STUDIO_FURNITURE, ...studioGraph(STUDIO_AREAS),
381
+ format: 1, id: "studio", scale: 1, areas: STUDIO_AREAS, door: { x: HALL_X, y: 704 }, furniture: STUDIO_FURNITURE, ...studioGraph(STUDIO_AREAS), straight: false,
331
382
  stateZones: { working: "desks", idle: "lounge", needs: "alert", crashed: "alert", stuck: "alert", paused: "nap", stopped: "nap", default: "lounge" },
332
383
  characters: {}, byInstance: {}, props: {}, bubbles: {},
333
384
  };
@@ -346,8 +397,6 @@ function drawBackground(ctx, L, env) {
346
397
  ctx.strokeStyle = pal.border; ctx.lineWidth = 2; ctx.strokeRect(r.x + 1, r.y + 1, r.w - 2, r.h - 2);
347
398
  ctx.fillStyle = pal.text; ctx.font = "600 16px system-ui, sans-serif"; ctx.textBaseline = "top"; ctx.textAlign = "left";
348
399
  ctx.fillText(fitText(ctx, env.labels.areas[a.name] || a.name, r.w - 60), r.x + 12, r.y + 10);
349
- const crowd = L.crowd && L.crowd.get(a.name);
350
- if (crowd) { ctx.fillStyle = pal.text2; ctx.textAlign = "right"; ctx.fillText(`+${crowd}`, r.x + r.w - 12, r.y + 10); }
351
400
  ctx.restore();
352
401
  }
353
402
  }
@@ -396,9 +445,11 @@ function drawFurniture(ctx, f, active, ms, env) {
396
445
  }
397
446
  }
398
447
  /** @type {OfficeSkin} */
399
- export const studioSkin = { ...pixelSkin, id: "studio", pack: STUDIO_PACK, drawBackground, drawFurniture };
448
+ /** #2540: the studio's matte is its hall floor (the scene's edge), so the panel around the scene reads as more of it. */
449
+ const studioMatte = (env) => (ZONE_FLOOR[env.mode] || ZONE_FLOOR.dark).hall;
450
+ export const studioSkin = { ...pixelSkin, id: "studio", pack: STUDIO_PACK, drawBackground, drawFurniture, matte: studioMatte };
400
451
 
401
- // ── Skin packs (the seam; nothing registers one yet) ──
452
+ // ── Skin packs ──
402
453
 
403
454
  /**
404
455
  * An OfficeSkin from a checked pack (fleet-office-pack.js checkSkinPack) and its loaded pictures (`images`: src → a
@@ -406,20 +457,29 @@ export const studioSkin = { ...pixelSkin, id: "studio", pack: STUDIO_PACK, drawB
406
457
  */
407
458
  export function spriteSkin(pack, images, base = pixelSkin) {
408
459
  const img = (src) => (src && images.get(src)) || null;
409
- const strip = (ctx, st, ms, x, y, frame = null) => {
460
+ // A strip's frame at its displayed size (dw × dh, unsmoothed), its anchor on (x, y); mirrored about the anchor.
461
+ const strip = (ctx, st, ms, x, y, frame = null, mirror = false) => {
410
462
  const im = img(st && st.src);
411
463
  if (!im) return false;
412
- const f = frame ?? frameAt(ms, st.frames, st.fps), s = pack.scale;
464
+ const f = frame ?? frameAt(ms, st.frames, st.fps), b = stripBox(st, x, y, mirror);
413
465
  ctx.save();
414
466
  ctx.imageSmoothingEnabled = false;
415
- ctx.drawImage(im, f * st.w, 0, st.w, st.h, Math.round(x - st.ax * s), Math.round(y - st.ay * s), st.w * s, st.h * s);
467
+ if (mirror) { ctx.translate(b.x + b.w, b.y); ctx.scale(-1, 1); ctx.drawImage(im, f * st.w, 0, st.w, st.h, 0, 0, b.w, b.h); }
468
+ else ctx.drawImage(im, f * st.w, 0, st.w, st.h, b.x, b.y, b.w, b.h);
416
469
  ctx.restore();
417
470
  return true;
418
471
  };
472
+ const setOf = (look) => own(pack.characters, own(pack.byInstance, look.name)) || own(pack.characters, "default");
473
+ /** The strip a look shows (its pose; walking, by its heading), when its picture is loaded. */
474
+ const shown = (look, walking = look.pose === "walk") => {
475
+ const ps = poseStrip(setOf(look), walking ? "walk" : look.pose, look.heading);
476
+ return ps && img(ps.strip.src) ? ps : null;
477
+ };
419
478
  return {
420
479
  ...base,
421
480
  id: pack.id,
422
481
  pack,
482
+ backdrop: img(pack.background),
423
483
  drawRoom(ctx, room, env) {
424
484
  const floor = img(pack.room.floor), wall = img(pack.room.wall);
425
485
  if (!floor) { base.drawRoom(ctx, room, env); return; }
@@ -448,27 +508,151 @@ export function spriteSkin(pack, images, base = pixelSkin) {
448
508
  if (!im) { drawBackground(ctx, L, env); return; }
449
509
  ctx.save(); ctx.imageSmoothingEnabled = false; ctx.drawImage(im, 0, 0, LOGICAL.w, LOGICAL.h); ctx.restore();
450
510
  },
511
+ drawForeground(ctx, L, env) {
512
+ const im = img(pack.foreground);
513
+ if (!im) return;
514
+ ctx.save(); ctx.imageSmoothingEnabled = false; ctx.drawImage(im, 0, 0, LOGICAL.w, LOGICAL.h); ctx.restore();
515
+ },
451
516
  drawFurniture(ctx, f, active, ms, env) {
452
517
  if (f.shape) { drawFurniture(ctx, f, active, ms, env); return; }
453
- // Active: its strip (or activeSprite) plays; otherwise its first frame holds.
518
+ // An NPC walking (#2596): its walk strip for its heading (walkSide mirrored going left), when its picture is loaded.
519
+ const ws = walkStrip(f);
520
+ if (ws && strip(ctx, ws.strip, ms, f.x, f.y, null, ws.mirror)) return;
521
+ // Active: its activeSprite (or its strip) plays; otherwise its strip loops ("loop", #2570) or its first frame holds.
454
522
  if (active) strip(ctx, f.activeSprite || f.sprite, ms, f.x, f.y);
523
+ else if (f.loop) strip(ctx, f.sprite, ms, f.x, f.y);
455
524
  else strip(ctx, f.sprite, 0, f.x, f.y, 0);
456
525
  },
457
526
  drawCharacter(ctx, d, look, ms, env) {
458
527
  if (look.pose === "away") return;
459
- const set = pack.characters[pack.byInstance[look.name]] || pack.characters.default;
528
+ const ps = shown(look);
460
529
  ctx.save();
461
530
  if (look.dim) ctx.globalAlpha = 0.35;
462
- const done = !!set && strip(ctx, set[look.pose] || set.idle, ms, d.seat.x, d.seat.y);
531
+ const done = !!ps && strip(ctx, ps.strip, ms, d.seat.x, d.seat.y, null, ps.mirror);
463
532
  ctx.restore();
464
533
  if (!done) base.drawCharacter(ctx, d, look, ms, env);
465
534
  },
466
- drawProp(ctx, it, ms, env) { if (!strip(ctx, pack.props[it.hue], ms, it.x, it.y)) base.drawProp(ctx, it, ms, env); },
535
+ charBox(look, at) {
536
+ const ps = shown({ ...look, heading: at.heading }, !!at.walking);
537
+ return ps ? stripBox(ps.strip, at.x, at.y, ps.mirror) : base.charBox(look, at);
538
+ },
539
+ // An item in the air: its sender's own (their set's "prop", #2562), else the pack's for its kind, else the built-in one.
540
+ drawProp(ctx, it, ms, env) {
541
+ const mine = it.from ? own(setOf({ name: it.from }) || {}, "prop") : null;
542
+ if (!strip(ctx, mine || own(pack.props, it.hue), ms, it.x, it.y)) base.drawProp(ctx, it, ms, env);
543
+ },
467
544
  };
468
545
  }
469
546
 
470
- /** The skins this page has: the built-in ones (a pack would be added here once loaded). */
471
- export const SKINS = new Map([["pixel", pixelSkin], ["studio", studioSkin]]);
472
- export const skinFor = (id) => SKINS.get(id) || pixelSkin;
547
+ // ── The built-in cozy (#2631): the zones layout on a warm pixel-art office drawn as one picture, the built-in people ──
548
+
549
+ /** Its picture, shipped with the page (src/ui/shared/office/cozy.png, auth-api.ts ASSETS: public static, as the font). */
550
+ export const COZY_BACKGROUND = "/assets/office-cozy.png";
551
+ const pts = (...xy) => xy.map(([x, y]) => ({ x, y }));
552
+ // The seats are feet points on the picture: chairs in front of the desks, the sofa, the coffee table and machine, the
553
+ // beanbags (a sleeper's bottom), in front of the whiteboard. The rects put a crowd's first extra spots (fleet-office-zones.js
554
+ // overflowSpots: rows from the rect's top) on the aisles: under the first row of desks, in front of the sofa.
555
+ const COZY_AREAS = {
556
+ desks: { name: "desks", rect: { x: 130, y: 196, w: 430, h: 220 }, seats: pts([193, 238], [336, 238], [481, 238], [193, 354], [336, 354], [481, 354]) },
557
+ lounge: { name: "lounge", rect: { x: 700, y: 196, w: 476, h: 184 }, seats: pts([940, 240], [1025, 310], [1120, 305]) },
558
+ alert: { name: "alert", rect: { x: 900, y: 500, w: 220, h: 110 }, seats: pts([965, 570], [1045, 570]) },
559
+ nap: { name: "nap", rect: { x: 110, y: 470, w: 400, h: 150 }, seats: pts([180, 562], [300, 562], [422, 565]) },
560
+ };
561
+ /** Its walkable graph, on the picture's aisles, every edge across or up/down; each seat's nearest waypoint is in front of it. */
562
+ const COZY_WAYPOINTS = [
563
+ ["door", 642, 640], ["hall", 642, 395], ["top", 642, 175],
564
+ // the desks: the aisle under the first row (y 250), the one under the second (y 395), the column beside them (x 560)
565
+ ["d-east", 560, 395], ["d-up", 560, 250], ["d1-3", 481, 250], ["d1-2", 336, 250], ["d1-1", 193, 250],
566
+ ["d2-3", 481, 395], ["d2-2", 336, 395], ["d2-x", 300, 395], ["d2-1", 193, 395],
567
+ // the beanbags: the aisle above them (y 460)
568
+ ["n-x", 300, 460], ["n-1", 180, 460], ["n-3", 422, 460],
569
+ // the lounge: below the coffee table (y 340), up the sofa's west end; the coffee machine's column (x 1140)
570
+ ["l-hall", 946, 395], ["l-mid", 946, 340], ["l-west", 860, 340], ["l-sofa", 860, 250], ["l-table", 1025, 340],
571
+ ["e-hall", 1140, 395], ["e-coffee", 1140, 305],
572
+ // the whiteboard: in front of it (y 570), from the coffee machine's column
573
+ ["a-east", 1140, 570], ["a-mid", 1005, 570],
574
+ ].map(([id, x, y]) => ({ id, x, y }));
575
+ const COZY_EDGES = [
576
+ ["door", "hall"], ["hall", "top"], ["hall", "d-east"], ["d-east", "d2-3"], ["d2-3", "d2-2"], ["d2-2", "d2-x"], ["d2-x", "d2-1"],
577
+ ["d-east", "d-up"], ["d-up", "d1-3"], ["d1-3", "d1-2"], ["d1-2", "d1-1"], ["d2-x", "n-x"], ["n-x", "n-1"], ["n-x", "n-3"],
578
+ ["hall", "l-hall"], ["l-hall", "l-mid"], ["l-mid", "l-west"], ["l-west", "l-sofa"], ["l-mid", "l-table"],
579
+ ["l-hall", "e-hall"], ["e-hall", "e-coffee"], ["e-hall", "a-east"], ["a-east", "a-mid"],
580
+ ];
581
+ /** The cozy's pack: the picture is the room; the one item is the notice board, its tag saying a pack can restyle it. */
582
+ export const COZY_PACK = {
583
+ format: 1, id: "cozy", scale: 1, background: COZY_BACKGROUND, areas: COZY_AREAS, door: { x: 642, y: 662 },
584
+ waypoints: COZY_WAYPOINTS, edges: COZY_EDGES, straight: true,
585
+ furniture: [{ id: "notice", x: 1211, y: 628, depth: 5, get label() { return t("orgo.cozyNotice"); } }],
586
+ stateZones: { working: "desks", idle: "lounge", needs: "alert", crashed: "alert", stuck: "alert", paused: "nap", stopped: "nap", default: "lounge" },
587
+ characters: {}, byInstance: {}, props: {}, bubbles: {},
588
+ };
589
+ /** Until its picture is in (loadCozySkin): the studio's floors under the cozy's areas. */
590
+ const cozyBare = spriteSkin(COZY_PACK, new Map(), studioSkin);
591
+ let cozyJob = null;
592
+
593
+ /** The skins built into this page; an uploaded pack is loaded by loadPackSkin. */
594
+ export const SKINS = new Map([["pixel", pixelSkin], ["studio", studioSkin], ["cozy", cozyBare]]);
595
+ export const BUILT_IN_SKINS = ["pixel", "studio", "cozy"];
596
+ /**
597
+ * #2631: the cozy's picture, loaded once; the skin drawn from it then takes the cozy's place in SKINS (skinFor). The
598
+ * skin, or null while it cannot load (the bare one stays; tried again next time).
599
+ */
600
+ export function loadCozySkin({ image = defaultImage } = {}) {
601
+ if (SKINS.get("cozy") !== cozyBare) return Promise.resolve(SKINS.get("cozy"));
602
+ cozyJob ??= image(COZY_BACKGROUND).then((im) => {
603
+ cozyJob = null;
604
+ if (!im) return null;
605
+ const sk = spriteSkin(COZY_PACK, new Map([[COZY_BACKGROUND, im]]), studioSkin);
606
+ SKINS.set("cozy", sk);
607
+ return sk;
608
+ }, () => { cozyJob = null; return null; });
609
+ return cozyJob;
610
+ }
611
+ /** A skin by id: a built-in one, else a pack loaded here (`packs`: id → skin), else the default (pixel). */
612
+ export const skinFor = (id, packs = null) => SKINS.get(id) || (packs && packs.get(id)) || pixelSkin;
613
+
614
+ const loaded = new Map(); // `${id}@${sha}` → Promise<OfficeSkin | null>
615
+ // Room for every pack the host keeps (web-office-skins.ts SKIN_LIMITS.packs) and a few replaced ones (each a new sha).
616
+ const MAX_LOADED = 24;
617
+ const defaultImage = (url) => new Promise((resolve) => {
618
+ if (typeof Image !== "function") { resolve(null); return; }
619
+ const im = new Image();
620
+ im.onload = () => resolve(im);
621
+ im.onerror = () => resolve(null);
622
+ im.src = url;
623
+ });
624
+ /**
625
+ * An uploaded pack (GET /ui/office-skins/<id>?v=<sha>) as an OfficeSkin: its manifest checked (null when it is not a
626
+ * pack: the page then draws a built-in style and says so), its pictures loaded from their content ids (one that does
627
+ * not load is drawn by the built-in skin). Kept per id and sha, so a pack is fetched once. `url`: where a picture's path
628
+ * is read (a share link's page reads its own address, #2636).
629
+ */
630
+ export function loadPackSkin(id, sha, { fetch: get = globalThis.fetch, image = defaultImage, url = (p) => p } = {}) {
631
+ const key = `${id}@${sha}`;
632
+ if (loaded.has(key)) return loaded.get(key);
633
+ const base = `/ui/office-skins/${encodeURIComponent(id)}`;
634
+ const job = (async () => {
635
+ try {
636
+ const r = await get(`${base}?v=${encodeURIComponent(sha)}`, { method: "GET", headers: { "Content-Type": "application/json" } });
637
+ if (!r || !r.ok) return null;
638
+ const d = await r.json();
639
+ const c = checkSkinPack(d && d.manifest, { kept: true });
640
+ if (!c.ok || c.pack.id !== id) return null;
641
+ const files = d.files && typeof d.files === "object" ? d.files : {};
642
+ const images = new Map();
643
+ await Promise.all(packImages(c.pack).map(async (src) => {
644
+ const fid = own(files, src);
645
+ if (typeof fid !== "string" || !/^[0-9a-f]{32}$/.test(fid)) return;
646
+ const im = await image(url(`${base}/img/${fid}.png`));
647
+ if (im) images.set(src, im);
648
+ }));
649
+ return spriteSkin(c.pack, images);
650
+ } catch { return null; }
651
+ })();
652
+ loaded.set(key, job);
653
+ if (loaded.size > MAX_LOADED) loaded.delete(loaded.keys().next().value);
654
+ job.then((skin) => { if (!skin) loaded.delete(key); }); // a failed read is tried again next time
655
+ return job;
656
+ }
473
657
  /** The people's depth in zones (fleet-office-pack.js). */
474
658
  export { CHAR_DEPTH };
@@ -2,14 +2,48 @@
2
2
  // - the offices people made: the host's (GET / PUT /ui/offices, src/web-office-store.ts — one list for every viewer);
3
3
  // what comes back is checked again here (fleet-office-model.js officesOk), never trusted;
4
4
  // - this browser's own choices (localStorage; blocked storage: this page only, as the canvas's Canvas/List): which
5
- // office is shown, the auto office's member filter, and its style (skin).
6
- // TODO(office editor): adding and renaming offices, assigning people, dragging seats and drawing paths call
7
- // saveOffices() with the whole list and the rev they read (a change in between answers 409).
5
+ // office is shown, the auto office's member filter, and its style (skin); #2540: the view (zoom and centre) and the
6
+ // fit mode (Fit or Fill) per office; #2554: whether All offices shows the lobby tile.
7
+ // - the skin packs an admin uploaded (#2513: GET /ui/office-skins, POST to upload one, DELETE to remove one;
8
+ // src/web-office-skins.ts). The office editor and the layout editor call saveOffices() with the whole list and the rev
9
+ // they read (a change in between answers 409).
8
10
  import { AUTO_ID, filterOk, officesOk } from "./fleet-office-model.js";
11
+ import { fitModeOk, savedOk } from "./fleet-office-view.js";
9
12
 
10
- const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin";
13
+ const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin", ASLEEP_KEY = "agend_org_office_hide_asleep", NIGHT_KEY = "agend_org_office_night";
14
+ /** #2554: "0" when All offices leaves the lobby tile out (default: shown). */
15
+ const LOBBY_KEY = "agend_org_office_lobby";
16
+ /** #2540: { "<office>@<style>": [k, cx, cy] (and a 1 after them: a fit) } — the last VIEWS_MAX views, the most recent last. */
17
+ const VIEW_KEY = "agend_org_office_view", VIEWS_MAX = 32;
18
+ /** #2540: { "<office>@<style>": "cover" } — the offices whose fit fills the panel (the rest: "contain"), the last VIEWS_MAX. */
19
+ const FIT_KEY = "agend_org_office_fit";
20
+ const PACK_ID = /^[A-Za-z0-9_-]{1,40}$/;
21
+ /** The host's list of skin packs, checked: [{ id, name, sha }] (anything odd dropped). */
22
+ export function skinListOk(d) {
23
+ const out = [], ids = new Set();
24
+ for (const p of Array.isArray(d && d.packs) ? d.packs.slice(0, 64) : []) {
25
+ if (!p || typeof p.id !== "string" || !PACK_ID.test(p.id) || ids.has(p.id) || typeof p.sha !== "string" || !/^[0-9a-f]{32}$/.test(p.sha)) continue;
26
+ ids.add(p.id);
27
+ out.push({ id: p.id, name: typeof p.name === "string" && p.name.trim() ? p.name.trim().slice(0, 60) : p.id, sha: p.sha });
28
+ }
29
+ return out;
30
+ }
11
31
 
12
32
  /** `fetch`: the caller's (a lease's, so an answer after the person moved on is dropped); `storage`: default localStorage. */
33
+ /**
34
+ * Agents' acts as the host sends them (GET /ui/office-acts, the `office_acts` event), checked: the well-formed ones.
35
+ * `now`: the host's clock when it sent them — each end is moved onto this page's clock (they may differ).
36
+ */
37
+ export function actsOk(list, now) {
38
+ const skew = typeof now === "number" && Number.isFinite(now) ? now - Date.now() : 0;
39
+ const str = (v) => typeof v === "string" && v.length > 0 && v.length <= 200, num = (v) => typeof v === "number" && Number.isFinite(v);
40
+ return (Array.isArray(list) ? list : []).filter((a) => a && typeof a === "object" && str(a.instance) && str(a.office) && num(a.until)
41
+ && (a.x === undefined || num(a.x)) && (a.y === undefined || num(a.y)) && (a.person === undefined || str(a.person))
42
+ && (a.say === undefined || (typeof a.say === "string" && a.say.length <= 40)) && (a.pose === undefined || ["idle", "working", "needs", "paused"].includes(a.pose)))
43
+ .map((a) => ({ instance: a.instance, office: a.office, until: a.until - skew, ...(num(a.since) ? { since: a.since } : {}), ...(str(a.state) ? { state: a.state } : {}),
44
+ ...(num(a.x) && num(a.y) ? { x: a.x, y: a.y } : {}),
45
+ ...(a.person ? { person: a.person } : {}), ...(a.say ? { say: a.say } : {}), ...(a.pose ? { pose: a.pose } : {}) }));
46
+ }
13
47
  export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {}) {
14
48
  const st = () => { try { return storage || globalThis.localStorage || null; } catch { return null; } };
15
49
  const read = (k) => { try { const s = st(); return s ? s.getItem(k) : null; } catch { return null; } };
@@ -37,13 +71,84 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
37
71
  return r.ok ? { ok: true, rev: d.rev, offices: officesOk(d.offices) } : { ok: false, status: r.status, error: d.error || "offices_failed" };
38
72
  } catch { return { ok: false, status: 0, error: "offices_failed" }; }
39
73
  },
40
- /** The office shown here: an id (one the host no longer has falls back to the auto office where it is used). */
74
+ /** #2578: what agents are doing in the offices now (office_set): their acts, or null when they could not be read. */
75
+ async acts() {
76
+ try {
77
+ const r = await get("/ui/office-acts", { method: "GET", headers });
78
+ if (!r || !r.ok) return null;
79
+ const d = await r.json();
80
+ return actsOk(d && d.acts, d && d.now);
81
+ } catch { return null; }
82
+ },
83
+ /** The skin packs the host keeps: { rev, packs: [{ id, name, sha }] }; null when they could not be read. */
84
+ async skins() {
85
+ try {
86
+ const r = await get("/ui/office-skins", { method: "GET", headers });
87
+ if (!r || !r.ok) return null;
88
+ const d = await r.json();
89
+ return { rev: typeof (d && d.rev) === "string" ? d.rev : "", packs: skinListOk(d) };
90
+ } catch { return null; }
91
+ },
92
+ /** Upload a pack (an admin session): { manifest, files: { name: base64 }, name? } → { ok, pack } or { ok: false, status, error, detail }. */
93
+ async uploadSkin(body) {
94
+ try {
95
+ const r = await get("/ui/office-skins", { method: "POST", headers, body: JSON.stringify(body) });
96
+ const d = await r.json().catch(() => ({}));
97
+ return r.ok ? { ok: true, pack: d.pack, rev: d.rev } : { ok: false, status: r.status, error: d.error || "skin_failed", detail: d.detail || "" };
98
+ } catch { return { ok: false, status: 0, error: "skin_failed", detail: "" }; }
99
+ },
100
+ /** Remove a pack (an admin session). */
101
+ async deleteSkin(id) {
102
+ try {
103
+ const r = await get(`/ui/office-skins/${encodeURIComponent(id)}`, { method: "DELETE", headers });
104
+ const d = await r.json().catch(() => ({}));
105
+ return r.ok ? { ok: true } : { ok: false, status: r.status, error: d.error || "skin_failed" };
106
+ } catch { return { ok: false, status: 0, error: "skin_failed" }; }
107
+ },
108
+ /**
109
+ * The office shown here: an id (one the host no longer has falls back to the auto office where it is used), or
110
+ * #2554's All offices (fleet-office-overview.js ALL_ID; with fewer than two offices stored: the auto office).
111
+ */
41
112
  choice: () => { const v = read(CHOICE_KEY); return typeof v === "string" && v && v.length <= 40 ? v : AUTO_ID; },
42
113
  saveChoice: (id) => write(CHOICE_KEY, String(id)),
43
114
  filter: () => { const s = read(FILTER_KEY); try { return filterOk(s ? JSON.parse(s) : null); } catch { return filterOk(null); } },
44
115
  saveFilter: (f) => write(FILTER_KEY, JSON.stringify(filterOk(f))),
45
- /** The whole fleet's style here: a skin id ("pixel": desks, "studio": zones); an unknown one is the default where used. */
46
- skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "pixel"; },
116
+ /** The whole fleet's style here: a skin id ("pixel": desks, "studio" and "cozy": zones); none chosen yet: cozy (#2631); an unknown one is the default where used. */
117
+ skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "cozy"; },
47
118
  saveSkin: (id) => write(SKIN_KEY, String(id)),
119
+ /** Whether paused and stopped people are left out of the Office here (default: shown). */
120
+ hideAsleep: () => read(ASLEEP_KEY) === "1",
121
+ saveHideAsleep: (v) => write(ASLEEP_KEY, v ? "1" : "0"),
122
+ /** #2568: whether the idle go to bed at night (00:00–06:00 on this device's clock) here (default: off). */
123
+ nightOn: () => read(NIGHT_KEY) === "1",
124
+ saveNightOn: (v) => write(NIGHT_KEY, v ? "1" : "0"),
125
+ /** #2554: whether All offices shows the lobby tile (the instances in no stored office) here (default: shown). */
126
+ lobbyTile: () => read(LOBBY_KEY) !== "0",
127
+ saveLobbyTile: (v) => write(LOBBY_KEY, v ? "1" : "0"),
128
+ /** The view kept for `key` (an office and its style): { k, cx, cy } (fleet-office-view.js savedOf), or null. */
129
+ view: (key) => {
130
+ try { const m = JSON.parse(read(VIEW_KEY) || "{}"); const a = m && Object.prototype.hasOwnProperty.call(m, key) ? m[key] : null;
131
+ return Array.isArray(a) ? savedOk({ k: a[0], cx: a[1], cy: a[2], fit: a[3] === 1 }) : null; } catch { return null; }
132
+ },
133
+ saveView: (key, v) => {
134
+ const ok = savedOk(v);
135
+ if (!ok || typeof key !== "string" || key.length > 100) return;
136
+ let m = {};
137
+ try { const o = JSON.parse(read(VIEW_KEY) || "{}"); if (o && typeof o === "object" && !Array.isArray(o)) m = o; } catch { m = {}; }
138
+ const r = (n) => Math.round(n * 1000) / 1000;
139
+ const next = Object.entries(m).filter(([k2, a]) => k2 !== key && Array.isArray(a)).slice(-(VIEWS_MAX - 1));
140
+ write(VIEW_KEY, JSON.stringify(Object.fromEntries([...next, [key, [r(ok.k), r(ok.cx), r(ok.cy), ...(ok.fit ? [1] : [])]]])));
141
+ },
142
+ /** The fit mode kept for `key`: "contain" (the default) or "cover" (Fill). */
143
+ fitMode: (key) => {
144
+ try { const m = JSON.parse(read(FIT_KEY) || "{}"); return fitModeOk(m && Object.prototype.hasOwnProperty.call(m, key) ? m[key] : null); } catch { return "contain"; }
145
+ },
146
+ saveFitMode: (key, mode) => {
147
+ if (typeof key !== "string" || key.length > 100) return;
148
+ let m = {};
149
+ try { const o = JSON.parse(read(FIT_KEY) || "{}"); if (o && typeof o === "object" && !Array.isArray(o)) m = o; } catch { m = {}; }
150
+ const next = Object.entries(m).filter(([k2, v]) => k2 !== key && v === "cover").slice(-(VIEWS_MAX - 1));
151
+ write(FIT_KEY, JSON.stringify(Object.fromEntries(fitModeOk(mode) === "cover" ? [...next, [key, "cover"]] : next)));
152
+ },
48
153
  };
49
154
  }