@songsid/agend 2.3.2-beta.9 → 2.3.2

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 (243) hide show
  1. package/README.md +6 -0
  2. package/README.zh-TW.md +6 -0
  3. package/dist/agent-cli.js +48 -0
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.js +6 -0
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-whats-new.js +3 -0
  8. package/dist/agent-whats-new.js.map +1 -1
  9. package/dist/auth-api.js +2 -0
  10. package/dist/auth-api.js.map +1 -1
  11. package/dist/backend/claude-code.js +23 -1
  12. package/dist/backend/claude-code.js.map +1 -1
  13. package/dist/backend/codex-session-lookup.d.ts +15 -0
  14. package/dist/backend/codex-session-lookup.js +58 -0
  15. package/dist/backend/codex-session-lookup.js.map +1 -1
  16. package/dist/backend/codex.js +15 -4
  17. package/dist/backend/codex.js.map +1 -1
  18. package/dist/backend/kiro-classic-archive.d.ts +57 -0
  19. package/dist/backend/kiro-classic-archive.js +622 -0
  20. package/dist/backend/kiro-classic-archive.js.map +1 -0
  21. package/dist/backend/kiro-classic-links.d.ts +24 -0
  22. package/dist/backend/kiro-classic-links.js +135 -0
  23. package/dist/backend/kiro-classic-links.js.map +1 -0
  24. package/dist/backend/kiro-identity.d.ts +100 -1
  25. package/dist/backend/kiro-identity.js +262 -31
  26. package/dist/backend/kiro-identity.js.map +1 -1
  27. package/dist/backend/kiro-migration-verify.d.ts +10 -0
  28. package/dist/backend/kiro-migration-verify.js +52 -0
  29. package/dist/backend/kiro-migration-verify.js.map +1 -0
  30. package/dist/backend/kiro-reconcile-store.d.ts +27 -0
  31. package/dist/backend/kiro-reconcile-store.js +344 -0
  32. package/dist/backend/kiro-reconcile-store.js.map +1 -0
  33. package/dist/backend/kiro-v2-store-worker.js +35 -1
  34. package/dist/backend/kiro-v2-store-worker.js.map +1 -1
  35. package/dist/backend/kiro-v2-store.d.ts +31 -1
  36. package/dist/backend/kiro-v2-store.js +22 -12
  37. package/dist/backend/kiro-v2-store.js.map +1 -1
  38. package/dist/backend/kiro.d.ts +124 -1
  39. package/dist/backend/kiro.js +553 -29
  40. package/dist/backend/kiro.js.map +1 -1
  41. package/dist/backend/types.d.ts +102 -0
  42. package/dist/backend/types.js +9 -0
  43. package/dist/backend/types.js.map +1 -1
  44. package/dist/cache-service.d.ts +13 -1
  45. package/dist/cache-service.js +45 -2
  46. package/dist/cache-service.js.map +1 -1
  47. package/dist/channel/ipc-timeouts.js +3 -0
  48. package/dist/channel/ipc-timeouts.js.map +1 -1
  49. package/dist/channel/mcp-tools.js +40 -5
  50. package/dist/channel/mcp-tools.js.map +1 -1
  51. package/dist/channel/tool-router.js +2 -1
  52. package/dist/channel/tool-router.js.map +1 -1
  53. package/dist/classic-channel-manager.d.ts +6 -0
  54. package/dist/classic-channel-manager.js +25 -0
  55. package/dist/classic-channel-manager.js.map +1 -1
  56. package/dist/cli.js +50 -4
  57. package/dist/cli.js.map +1 -1
  58. package/dist/config-validator.d.ts +7 -1
  59. package/dist/config-validator.js +17 -8
  60. package/dist/config-validator.js.map +1 -1
  61. package/dist/config.js +2 -0
  62. package/dist/config.js.map +1 -1
  63. package/dist/daemon.d.ts +66 -0
  64. package/dist/daemon.js +421 -19
  65. package/dist/daemon.js.map +1 -1
  66. package/dist/dashboard-permissions.d.ts +15 -4
  67. package/dist/dashboard-permissions.js +49 -23
  68. package/dist/dashboard-permissions.js.map +1 -1
  69. package/dist/doctor.js +2 -1
  70. package/dist/doctor.js.map +1 -1
  71. package/dist/fleet-control-audit.d.ts +1 -1
  72. package/dist/fleet-control-audit.js +3 -0
  73. package/dist/fleet-control-audit.js.map +1 -1
  74. package/dist/fleet-manager.d.ts +90 -3
  75. package/dist/fleet-manager.js +569 -41
  76. package/dist/fleet-manager.js.map +1 -1
  77. package/dist/forged-envelope-scan.d.ts +9 -0
  78. package/dist/forged-envelope-scan.js +30 -5
  79. package/dist/forged-envelope-scan.js.map +1 -1
  80. package/dist/general-instance.d.ts +24 -0
  81. package/dist/general-instance.js +53 -0
  82. package/dist/general-instance.js.map +1 -1
  83. package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
  84. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
  85. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  86. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
  87. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
  88. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
  89. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
  90. package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
  91. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +57 -17
  92. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +57 -17
  93. package/dist/instance-config-impact.js +3 -2
  94. package/dist/instance-config-impact.js.map +1 -1
  95. package/dist/instance-lifecycle.d.ts +4 -0
  96. package/dist/instance-lifecycle.js +7 -0
  97. package/dist/instance-lifecycle.js.map +1 -1
  98. package/dist/kiro-migrate.d.ts +19 -2
  99. package/dist/kiro-migrate.js +133 -35
  100. package/dist/kiro-migrate.js.map +1 -1
  101. package/dist/kiro-reconcile.d.ts +21 -0
  102. package/dist/kiro-reconcile.js +88 -0
  103. package/dist/kiro-reconcile.js.map +1 -0
  104. package/dist/kiro-session-proof.d.ts +41 -0
  105. package/dist/kiro-session-proof.js +89 -0
  106. package/dist/kiro-session-proof.js.map +1 -0
  107. package/dist/kiro-tui-forensic.d.ts +19 -0
  108. package/dist/kiro-tui-forensic.js +124 -0
  109. package/dist/kiro-tui-forensic.js.map +1 -0
  110. package/dist/locale.js +198 -0
  111. package/dist/locale.js.map +1 -1
  112. package/dist/log-service.js +2 -2
  113. package/dist/log-service.js.map +1 -1
  114. package/dist/needs-you.d.ts +3 -1
  115. package/dist/needs-you.js +4 -2
  116. package/dist/needs-you.js.map +1 -1
  117. package/dist/office-levels.d.ts +64 -0
  118. package/dist/office-levels.js +66 -0
  119. package/dist/office-levels.js.map +1 -0
  120. package/dist/office-share-page.d.ts +3 -1
  121. package/dist/office-share-page.js +8 -3
  122. package/dist/office-share-page.js.map +1 -1
  123. package/dist/office-share-service.d.ts +6 -3
  124. package/dist/office-share-service.js +12 -8
  125. package/dist/office-share-service.js.map +1 -1
  126. package/dist/office-share.d.ts +45 -8
  127. package/dist/office-share.js +75 -15
  128. package/dist/office-share.js.map +1 -1
  129. package/dist/outbound-handlers.js +20 -1
  130. package/dist/outbound-handlers.js.map +1 -1
  131. package/dist/outbound-schemas.d.ts +47 -2
  132. package/dist/outbound-schemas.js +41 -2
  133. package/dist/outbound-schemas.js.map +1 -1
  134. package/dist/public-web-gateway.js +1 -1
  135. package/dist/public-web-gateway.js.map +1 -1
  136. package/dist/quickstart-api.d.ts +39 -1
  137. package/dist/quickstart-api.js +115 -17
  138. package/dist/quickstart-api.js.map +1 -1
  139. package/dist/remote-card-answer.d.ts +83 -0
  140. package/dist/remote-card-answer.js +208 -0
  141. package/dist/remote-card-answer.js.map +1 -0
  142. package/dist/remote-card-prompt.d.ts +38 -0
  143. package/dist/remote-card-prompt.js +86 -0
  144. package/dist/remote-card-prompt.js.map +1 -0
  145. package/dist/scheduler/task-board.d.ts +28 -6
  146. package/dist/scheduler/task-board.js +66 -11
  147. package/dist/scheduler/task-board.js.map +1 -1
  148. package/dist/service-installer.d.ts +13 -0
  149. package/dist/service-installer.js +15 -0
  150. package/dist/service-installer.js.map +1 -1
  151. package/dist/service-owner.d.ts +25 -0
  152. package/dist/service-owner.js +43 -2
  153. package/dist/service-owner.js.map +1 -1
  154. package/dist/settings-api.d.ts +2 -0
  155. package/dist/settings-api.js +18 -12
  156. package/dist/settings-api.js.map +1 -1
  157. package/dist/settings-change.js +10 -5
  158. package/dist/settings-change.js.map +1 -1
  159. package/dist/settings-effect.js +7 -4
  160. package/dist/settings-effect.js.map +1 -1
  161. package/dist/settings-instance-patch.js +1 -1
  162. package/dist/settings-instance-patch.js.map +1 -1
  163. package/dist/setup-form.d.ts +2 -2
  164. package/dist/setup-form.js +139 -93
  165. package/dist/setup-form.js.map +1 -1
  166. package/dist/setup-i18n.d.ts +25 -0
  167. package/dist/setup-i18n.js +226 -0
  168. package/dist/setup-i18n.js.map +1 -0
  169. package/dist/task-board-waker.js +7 -2
  170. package/dist/task-board-waker.js.map +1 -1
  171. package/dist/tool-permissions.js +18 -0
  172. package/dist/tool-permissions.js.map +1 -1
  173. package/dist/transcript-sources.d.ts +1 -0
  174. package/dist/transcript-sources.js +20 -3
  175. package/dist/transcript-sources.js.map +1 -1
  176. package/dist/types.d.ts +10 -0
  177. package/dist/ui/backend-signin.js +4 -3
  178. package/dist/ui/chat-store.js +3 -2
  179. package/dist/ui/chat-strings.js +2 -2
  180. package/dist/ui/fleet-office-card.js +114 -0
  181. package/dist/ui/fleet-office-editor.js +81 -9
  182. package/dist/ui/fleet-office-layout.js +56 -11
  183. package/dist/ui/fleet-office-levels.js +105 -0
  184. package/dist/ui/fleet-office-model.js +9 -1
  185. package/dist/ui/fleet-office-pack.d.ts +15 -3
  186. package/dist/ui/fleet-office-pack.js +177 -23
  187. package/dist/ui/fleet-office-skin.js +39 -3
  188. package/dist/ui/fleet-office-store.js +74 -3
  189. package/dist/ui/fleet-office-view.js +8 -0
  190. package/dist/ui/fleet-office-zones.js +102 -19
  191. package/dist/ui/fleet-office.js +308 -44
  192. package/dist/ui/fleet-org.js +2 -0
  193. package/dist/ui/fleet-task-card.js +2 -2
  194. package/dist/ui/fleet-task-common.js +30 -7
  195. package/dist/ui/fleet-task-work.js +2 -2
  196. package/dist/ui/fleet-tasks.js +10 -11
  197. package/dist/ui/office-share-dialog.js +31 -7
  198. package/dist/ui/office-share-page.js +1 -0
  199. package/dist/ui/panel-chat.js +14 -2
  200. package/dist/ui/panel-log.js +4 -4
  201. package/dist/ui/panel-needs.js +1 -1
  202. package/dist/ui/panel-settings.js +16 -8
  203. package/dist/ui/settings-discord.js +3 -2
  204. package/dist/ui/settings-guests.js +17 -7
  205. package/dist/ui/settings-model.js +17 -9
  206. package/dist/ui/settings-registry.js +1 -0
  207. package/dist/ui/settings-strings.js +16 -4
  208. package/dist/ui/settings-token.js +3 -2
  209. package/dist/ui/settings-wizard.js +25 -9
  210. package/dist/ui/shared/app-i18n.js +2 -2
  211. package/dist/ui/shared/app-stream.js +1 -1
  212. package/dist/ui/shared/app.css +70 -2
  213. package/dist/ui/shared/setup-host-text.js +107 -0
  214. package/dist/ui/structured-signin.js +89 -38
  215. package/dist/usage-rollup.d.ts +34 -3
  216. package/dist/usage-rollup.js +56 -3
  217. package/dist/usage-rollup.js.map +1 -1
  218. package/dist/web-api.d.ts +10 -1
  219. package/dist/web-api.js +207 -17
  220. package/dist/web-api.js.map +1 -1
  221. package/dist/web-auth.js +1 -1
  222. package/dist/web-auth.js.map +1 -1
  223. package/dist/web-office-acts.d.ts +5 -2
  224. package/dist/web-office-acts.js +9 -4
  225. package/dist/web-office-acts.js.map +1 -1
  226. package/dist/web-office-pack-read.d.ts +35 -0
  227. package/dist/web-office-pack-read.js +252 -0
  228. package/dist/web-office-pack-read.js.map +1 -0
  229. package/dist/web-office-pack-tools.d.ts +241 -0
  230. package/dist/web-office-pack-tools.js +785 -0
  231. package/dist/web-office-pack-tools.js.map +1 -0
  232. package/dist/web-office-skins.d.ts +68 -4
  233. package/dist/web-office-skins.js +324 -33
  234. package/dist/web-office-skins.js.map +1 -1
  235. package/dist/web-office-store.d.ts +47 -4
  236. package/dist/web-office-store.js +124 -23
  237. package/dist/web-office-store.js.map +1 -1
  238. package/dist/web-settings-patch.d.ts +3 -1
  239. package/dist/web-settings-patch.js +4 -1
  240. package/dist/web-settings-patch.js.map +1 -1
  241. package/dist/workflow-templates/default.md +1 -1
  242. package/package.json +2 -2
  243. package/templates/windows-shortcuts.ps1 +18 -17
package/dist/web-api.js CHANGED
@@ -450,7 +450,7 @@ function handleAppIcon(req, res, url, ctx, method) {
450
450
  }
451
451
  /**
452
452
  * #2636: Office share links — GET /ui/office-share (the open ones, and whether this fleet offers them), POST { office,
453
- * minutes?, text? } (a new one; answers its URL once its tunnel is up, or why not), DELETE /ui/office-share/<id|all>.
453
+ * minutes?, text?, theme? } (a new one; #2715 theme "auto" | "light" | "dark"; answers its URL once its tunnel is up, or why not), DELETE /ui/office-share/<id|all>.
454
454
  * An admin session only — on the dashboard reached directly, or (the user, #2636 follow-up) a fleet admin signed in over
455
455
  * the /web public link: the same check either way. Never the CLI token, a read-only session or a guest. The share's own
456
456
  * tunnel and gateway stay apart from /web's. The person is asked again once the body is in.
@@ -515,12 +515,13 @@ function handleOfficeShare(req, res, url, ctx, path, method) {
515
515
  return;
516
516
  }
517
517
  if (!body || typeof body !== "object" || Array.isArray(body) || typeof body.office !== "string"
518
- || (body.minutes !== undefined && typeof body.minutes !== "number") || (body.text !== undefined && typeof body.text !== "boolean")) {
519
- json(res, 400, { error: "office (string), minutes? (number), text? (boolean)" });
518
+ || (body.minutes !== undefined && typeof body.minutes !== "number") || (body.text !== undefined && typeof body.text !== "boolean")
519
+ || (body.theme !== undefined && typeof body.theme !== "string")) {
520
+ json(res, 400, { error: "office (string), minutes? (number), text? (boolean), theme? (auto, light or dark)" });
520
521
  return;
521
522
  }
522
523
  const made = await service.create({ office: body.office, owner: now.person, ...(typeof body.minutes === "number" ? { minutes: body.minutes } : {}),
523
- ...(typeof body.text === "boolean" ? { text: body.text } : {}) });
524
+ ...(typeof body.text === "boolean" ? { text: body.text } : {}), ...(typeof body.theme === "string" ? { theme: body.theme } : {}) });
524
525
  if (!made.ok) {
525
526
  const status = made.error === "disabled" ? 404 : made.error === "tunnel" ? 503 : made.error === "too_many" ? 409 : 400;
526
527
  json(res, status, { error: made.error, ...(made.reason ? { reason: made.reason } : {}) });
@@ -672,6 +673,92 @@ function handleOfficeSkins(req, res, url, ctx, path, method) {
672
673
  });
673
674
  return true;
674
675
  }
676
+ /**
677
+ * #2692: the agents' skin-pack changes, for the Office page (Edit offices → Skin packs) — an admin session only, never
678
+ * the CLI token, a guest or the public link (as uploading a pack by hand). GET { approval, agents, pending, log };
679
+ * POST { approval: "apply" | "approve" } sets the fleet's approval mode; POST /<id> { decision: "approve" | "reject" }
680
+ * decides a pending change (judged again on a yes). Every change is `office_pack_changes` on the stream.
681
+ */
682
+ function handleOfficePackChanges(req, res, url, ctx, path, method) {
683
+ const tools = ctx.officePackTools;
684
+ if (!tools) {
685
+ json(res, 404, { error: "Pack changes are not kept on this fleet" });
686
+ return true;
687
+ }
688
+ const one = /^\/ui\/office-pack-changes\/([0-9a-f]{32})$/.exec(path);
689
+ if (!(method === "GET" && path === "/ui/office-pack-changes") && !(method === "POST" && (path === "/ui/office-pack-changes" || one))) {
690
+ json(res, 404, { error: "not found" });
691
+ return true;
692
+ }
693
+ if (gatewayRequestContext(req)) {
694
+ json(res, 403, { error: "pack_changes_not_over_public_link" });
695
+ return true;
696
+ }
697
+ const who = () => {
698
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
699
+ if (verdict.kind !== "allow")
700
+ return { status: 401 };
701
+ return verdict.via === "session" && verdict.session?.tier === "admin" ? { person: verdict.session.owner ? webPersonKey(verdict.session.owner) : "admin-session" } : { status: 403 };
702
+ };
703
+ const refuse = (status) => json(res, status, { error: status === 401 ? WEB_TOKEN_INVALID_MESSAGE : "pack_changes_need_an_admin_session" });
704
+ const first = who();
705
+ if ("status" in first) {
706
+ refuse(first.status);
707
+ return true;
708
+ }
709
+ if (method === "GET") {
710
+ res.setHeader("Cache-Control", "private, no-store");
711
+ json(res, 200, tools.changes());
712
+ return true;
713
+ }
714
+ (async () => {
715
+ let body;
716
+ try {
717
+ body = await parseBody(req);
718
+ }
719
+ catch {
720
+ json(res, 400, { error: "Invalid JSON" });
721
+ return;
722
+ }
723
+ if (!permitWebContinuation(req, res, ctx))
724
+ return;
725
+ const now = who();
726
+ if ("status" in now) {
727
+ refuse(now.status);
728
+ return;
729
+ }
730
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
731
+ json(res, 400, { error: "an object" });
732
+ return;
733
+ }
734
+ if (!one) {
735
+ if (Object.keys(body).some(k => k !== "approval")) {
736
+ json(res, 400, { error: "pack_changes_invalid", detail: "{ approval }" });
737
+ return;
738
+ }
739
+ const r = tools.setApproval(body.approval, now.person);
740
+ if (!r.ok) {
741
+ json(res, 400, { error: "pack_changes_invalid", detail: r.error });
742
+ return;
743
+ }
744
+ json(res, 200, tools.changes());
745
+ return;
746
+ }
747
+ if (Object.keys(body).some(k => k !== "decision") || (body.decision !== "approve" && body.decision !== "reject")) {
748
+ json(res, 400, { error: "pack_changes_invalid", detail: '{ decision: "approve" | "reject" }' });
749
+ return;
750
+ }
751
+ const r = tools.decide(one[1], body.decision, now.person);
752
+ json(res, r.ok ? 200 : r.status, r.ok ? { text: r.text, ...(r.applied ? { applied: r.applied } : {}) } : { error: r.error, detail: r.text });
753
+ })().catch(err => {
754
+ ctx.logger.error({ err: err.message }, "A pack change failed");
755
+ try {
756
+ json(res, 500, { error: "A pack change failed" });
757
+ }
758
+ catch { /* already answered */ }
759
+ });
760
+ return true;
761
+ }
675
762
  /**
676
763
  * #1629 r4 §3: the owner-CSS routes (see their call site). `who()`, asked first and again after any body: the person a
677
764
  * signed-in human session is, or the status to answer.
@@ -2398,6 +2485,75 @@ export function handleWebRequest(req, res, url, ctx) {
2398
2485
  });
2399
2486
  return true;
2400
2487
  }
2488
+ // #2716: PUT /ui/office-style { skin }: the whole-fleet office's style for every viewer, device and share link (a skin
2489
+ // id, or null for the default; read back as GET /ui/offices's autoSkin). Who may save the offices may set it, as they
2490
+ // would: an admin session, never the CLI token or the public link; asked again once the body is in; counted with the
2491
+ // offices' saves. Every page hears `offices` and reads again.
2492
+ if (method === "PUT" && path === "/ui/office-style") {
2493
+ const store = ctx.officeStore;
2494
+ if (!store) {
2495
+ json(res, 404, { error: "Offices are not kept on this fleet" });
2496
+ return true;
2497
+ }
2498
+ if (gatewayRequestContext(req)) {
2499
+ json(res, 403, { error: "offices_not_over_public_link" });
2500
+ return true;
2501
+ }
2502
+ const who = () => {
2503
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
2504
+ if (verdict.kind !== "allow")
2505
+ return { status: 401 };
2506
+ return verdict.via === "session" && verdict.session?.tier === "admin" ? { person: verdict.session.owner ? webPersonKey(verdict.session.owner) : "admin-session" } : { status: 403 };
2507
+ };
2508
+ const refuse = (status) => json(res, status, { error: status === 401 ? WEB_TOKEN_INVALID_MESSAGE : "offices_need_an_admin_session" });
2509
+ const first = who();
2510
+ if ("status" in first) {
2511
+ refuse(first.status);
2512
+ return true;
2513
+ }
2514
+ (async () => {
2515
+ let body;
2516
+ try {
2517
+ body = await parseBody(req);
2518
+ }
2519
+ catch {
2520
+ json(res, 400, { error: "Invalid JSON" });
2521
+ return;
2522
+ }
2523
+ if (!permitWebContinuation(req, res, ctx))
2524
+ return;
2525
+ const now = who();
2526
+ if ("status" in now) {
2527
+ refuse(now.status);
2528
+ return;
2529
+ }
2530
+ if (!body || typeof body !== "object" || Array.isArray(body) || !("skin" in body) || Object.keys(body).some(k => k !== "skin")) {
2531
+ json(res, 400, { error: "office_style_invalid", detail: "{ skin: a skin id, or null }" });
2532
+ return;
2533
+ }
2534
+ const limiter = ctx.officeWrites, allowed = limiter ? limiter.check(now.person) : { ok: true };
2535
+ if (!allowed.ok) {
2536
+ res.setHeader("Retry-After", String(allowed.retry_after_s));
2537
+ json(res, 429, { error: "offices_rate_limited", retry_after_s: allowed.retry_after_s });
2538
+ return;
2539
+ }
2540
+ const r = store.setAutoSkin(body.skin);
2541
+ if (!r.ok) {
2542
+ json(res, r.status, { error: r.error, ...(r.detail ? { detail: r.detail } : {}) });
2543
+ return;
2544
+ }
2545
+ limiter?.record(now.person);
2546
+ ctx.emitSseEvent("offices", { rev: r.rev });
2547
+ json(res, 200, { rev: r.rev, autoSkin: r.autoSkin });
2548
+ })().catch(err => {
2549
+ ctx.logger.error({ err: err.message }, "Saving the office style failed");
2550
+ try {
2551
+ json(res, 500, { error: "Saving the office style failed" });
2552
+ }
2553
+ catch { /* already answered */ }
2554
+ });
2555
+ return true;
2556
+ }
2401
2557
  // #2578: GET /ui/office-acts { acts }: the agents' live acts (office_set), read as the offices are (the public link
2402
2558
  // too, which has no stream); every change also comes as `office_acts` on the stream. Nothing here writes one.
2403
2559
  if (method === "GET" && path === "/ui/office-acts") {
@@ -2409,8 +2565,41 @@ export function handleWebRequest(req, res, url, ctx) {
2409
2565
  json(res, 200, { acts: acts.live(), now: Date.now() }); // now: the host's clock, for the page's offset
2410
2566
  return true;
2411
2567
  }
2568
+ // #2661: GET /ui/office-levels { enabled, unit, detail, levels }: each instance's Office level, read as the offices
2569
+ // are (a guest's routes do not include it). The level for whoever may open the Office; the numbers behind it (work
2570
+ // tokens, today, first seen) only where the usage report would be served (`detail`): not with web.usage_panel off,
2571
+ // not over the public link unless web.usage_report_on_public_link. Off (web.office_levels false): no levels at all.
2572
+ if (method === "GET" && path === "/ui/office-levels") {
2573
+ if (!ctx.officeLevels) {
2574
+ json(res, 404, { error: "Office levels are not kept on this fleet" });
2575
+ return true;
2576
+ }
2577
+ const admitted = admissionOf(req, url, ctx);
2578
+ if (!admitted) {
2579
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
2580
+ return true;
2581
+ }
2582
+ const detail = () => ctx.fleetConfig?.web?.usage_panel !== false
2583
+ && (!gatewayRequestContext(req) || ctx.fleetConfig?.web?.usage_report_on_public_link === true);
2584
+ void import("./office-levels.js").then(async ({ LEVEL, levelsFor }) => {
2585
+ const levels = await ctx.officeLevels();
2586
+ // Asked again before it is sent (it was awaited): a session signed out or a switch turned off meanwhile.
2587
+ if (admissionOf(req, url, ctx) !== admitted)
2588
+ return json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
2589
+ if (!levels || ctx.fleetConfig?.web?.office_levels === false)
2590
+ return json(res, 200, { enabled: false, unit: LEVEL.UNIT, detail: false, levels: {} });
2591
+ const d = detail();
2592
+ json(res, 200, { enabled: true, unit: LEVEL.UNIT, detail: d, levels: levelsFor(levels, d) });
2593
+ }).catch((err) => { ctx.logger.error({ err }, "office levels failed"); try {
2594
+ json(res, 500, { error: "Office levels failed" });
2595
+ }
2596
+ catch { /* already answered */ } });
2597
+ return true;
2598
+ }
2412
2599
  if (path === "/ui/office-skins" || path.startsWith("/ui/office-skins/"))
2413
2600
  return handleOfficeSkins(req, res, url, ctx, path, method);
2601
+ if (path === "/ui/office-pack-changes" || path.startsWith("/ui/office-pack-changes/"))
2602
+ return handleOfficePackChanges(req, res, url, ctx, path, method);
2414
2603
  if (path === "/ui/office-share" || path.startsWith("/ui/office-share/"))
2415
2604
  return handleOfficeShare(req, res, url, ctx, path, method);
2416
2605
  // ── #1630: avatars and Discord server emoji, same-origin (the page never loads a platform's CDN; img-src stays 'self') ──
@@ -2500,12 +2689,12 @@ export function handleWebRequest(req, res, url, ctx) {
2500
2689
  json(res, 400, { error: "instance, nonce and action required" });
2501
2690
  return;
2502
2691
  }
2503
- const { instance, nonce, action } = body;
2504
- if (typeof instance !== "string" || typeof nonce !== "string" || typeof action !== "string") {
2692
+ const { instance, nonce, action, text } = body;
2693
+ if (typeof instance !== "string" || typeof nonce !== "string" || typeof action !== "string" || (text !== undefined && typeof text !== "string")) {
2505
2694
  json(res, 400, { error: "instance, nonce and action required" });
2506
2695
  return;
2507
2696
  }
2508
- const r = await click(instance, nonce, action);
2697
+ const r = text === undefined ? await click(instance, nonce, action) : await click(instance, nonce, action, text);
2509
2698
  // 409: answered elsewhere first, expired, or never open — the page drops the buttons on `gone`.
2510
2699
  json(res, r.status, r.status === 200 ? { answered: true } : { error: r.error ?? "Refused", ...(r.status === 409 ? { gone: true } : {}) });
2511
2700
  })().catch(err => {
@@ -3231,7 +3420,8 @@ export function handleWebRequest(req, res, url, ctx) {
3231
3420
  // #2296: every open Tasks tab reads the board again after each change; on the dashboard they all get the same
3232
3421
  // board for a revision, so it is built once per (revision, done=all, minute) — the minute for the 14-day
3233
3422
  // window. The public link's reads are filtered per request (hidden instances) and are built each time.
3234
- const key = gateway ? null : `${rev}:${allDone ? 1 : 0}:${Math.floor(Date.now() / 60_000)}`;
3423
+ // #2757 review: and by the dispatchers gone (a deleted one makes its cards the person's, the revision unchanged).
3424
+ const key = gateway ? null : `${rev}:${allDone ? 1 : 0}:${Math.floor(Date.now() / 60_000)}:${board.goneDispatchers(ctx.dispatcherGone)}`;
3235
3425
  const kept = key ? boardBodies.get(board) : undefined;
3236
3426
  if (kept && kept.key === key) {
3237
3427
  jsonText(res, 200, kept.body);
@@ -3243,7 +3433,7 @@ export function handleWebRequest(req, res, url, ctx) {
3243
3433
  // #2355 review: the names a project's lead may be — the fleet's instances (a Classic-only room is not one); over the
3244
3434
  // public link, not a hidden one.
3245
3435
  const leads = Object.keys(ctx.fleetConfig?.instances ?? {}).filter((n) => !hiddenName(n)).sort();
3246
- const body = JSON.stringify({ rev, cards: board.views(cards, scope), projects, leads, stats: board.projectStats() });
3436
+ const body = JSON.stringify({ rev, cards: board.views(cards, scope, ctx.dispatcherGone), projects, leads, stats: board.projectStats(ctx.dispatcherGone) });
3247
3437
  if (key)
3248
3438
  boardBodies.set(board, { key, body });
3249
3439
  jsonText(res, 200, body);
@@ -3258,7 +3448,7 @@ export function handleWebRequest(req, res, url, ctx) {
3258
3448
  // by every open drawer (the public link's is filtered per request, as the board's).
3259
3449
  const only = url.searchParams.get("instance");
3260
3450
  const names = (only ? [only] : [...new Set([...Object.keys(ctx.fleetConfig?.instances ?? {}), ...board.liveAssignees()])]).filter((n) => !hiddenName(n));
3261
- const key = gateway ? null : `${board.revision()}:${only === null ? "*" : `=${only}`}:${names.join("\u0000")}`;
3451
+ const key = gateway ? null : `${board.revision()}:${only === null ? "*" : `=${only}`}:${names.join("\u0000")}:${board.goneDispatchers(ctx.dispatcherGone)}`;
3262
3452
  const kept = key ? workBodies.get(board) : undefined;
3263
3453
  if (kept && kept.key === key) {
3264
3454
  jsonText(res, 200, kept.body);
@@ -3271,7 +3461,7 @@ export function handleWebRequest(req, res, url, ctx) {
3271
3461
  for (const c of [...w.doing, ...w.blocked, ...w.review, ...w.ready, ...w.waiting.map((x) => x.card), ...w.mains.map((x) => x.card)])
3272
3462
  if (!hidden(c))
3273
3463
  all.set(c.uuid, c);
3274
- const viewOf = new Map(board.views([...all.values()], scope).map((v) => [v.uuid, v]));
3464
+ const viewOf = new Map(board.views([...all.values()], scope, ctx.dispatcherGone).map((v) => [v.uuid, v]));
3275
3465
  const drawn = (cards) => cards.flatMap((c) => (viewOf.has(c.uuid) ? [viewOf.get(c.uuid)] : []));
3276
3466
  const codes = (cards) => cards.filter((x) => all.has(x.card.uuid)).map((x) => x.card.code);
3277
3467
  const body = JSON.stringify({ instances: works.map(({ name, w }) => ({
@@ -3288,7 +3478,7 @@ export function handleWebRequest(req, res, url, ctx) {
3288
3478
  if (method === "GET" && path === "/ui/tasks/search") {
3289
3479
  const names = new Map(board.listProjects(true).map((p) => [p.id, p.name]));
3290
3480
  const hits = board.search(url.searchParams.get("q") ?? "", Number(url.searchParams.get("limit")) || 20, scope).filter((h) => !hidden(h.card));
3291
- const views = board.views(hits.map((h) => h.card), scope);
3481
+ const views = board.views(hits.map((h) => h.card), scope, ctx.dispatcherGone);
3292
3482
  const results = hits.map(({ card: c, match }, i) => ({
3293
3483
  card: views[i],
3294
3484
  match,
@@ -3304,13 +3494,13 @@ export function handleWebRequest(req, res, url, ctx) {
3304
3494
  json(res, 404, { error: "no such card", code: "not_found" });
3305
3495
  return true;
3306
3496
  }
3307
- json(res, 200, shown(board.detail(ref, scope)));
3497
+ json(res, 200, shown(board.detail(ref, scope, ctx.dispatcherGone)));
3308
3498
  return true;
3309
3499
  }
3310
3500
  if (method === "POST" && path === "/ui/tasks/cards") {
3311
3501
  boardWrite(req, res, url, ctx, board, CardCreateSchema, (spec, person) => {
3312
3502
  const { cards, said } = board.createCards(person, { cards: [spec] });
3313
- return { card: board.view(cards[0]), notes: said };
3503
+ return { card: board.view(cards[0], {}, ctx.dispatcherGone), notes: said };
3314
3504
  });
3315
3505
  return true;
3316
3506
  }
@@ -3318,16 +3508,16 @@ export function handleWebRequest(req, res, url, ctx) {
3318
3508
  if (card[2] === "move") {
3319
3509
  boardWrite(req, res, url, ctx, board, CardMoveSchema, ({ to, ...move }, person) => {
3320
3510
  const { card: moved, said } = board.moveCard(person, ref, to, move);
3321
- return { card: board.view(moved), notes: said };
3511
+ return { card: board.view(moved, {}, ctx.dispatcherGone), notes: said };
3322
3512
  });
3323
3513
  }
3324
3514
  else if (card[2] === "reorder") {
3325
- boardWrite(req, res, url, ctx, board, CardReorderSchema, ({ after, before }, person) => ({ card: board.view(board.reorder(person, ref, after ?? null, before ?? null)) }));
3515
+ boardWrite(req, res, url, ctx, board, CardReorderSchema, ({ after, before }, person) => ({ card: board.view(board.reorder(person, ref, after ?? null, before ?? null), {}, ctx.dispatcherGone) }));
3326
3516
  }
3327
3517
  else {
3328
3518
  boardWrite(req, res, url, ctx, board, CardPatchSchema, (patch, person) => {
3329
3519
  const { card: updated, said } = board.updateCard(person, ref, patch);
3330
- return { card: board.view(updated), notes: said };
3520
+ return { card: board.view(updated, {}, ctx.dispatcherGone), notes: said };
3331
3521
  });
3332
3522
  }
3333
3523
  return true;