agentbox-app 0.1.13 → 0.1.16

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 (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -0,0 +1,575 @@
1
+ // ROOMS ON THIS MAC (w-3d0fa23ffb, docs/team/ROOMS-PLAN.md): the rooms I am in,
2
+ // each room's wall and chat, and the presence row this Mac publishes. Lives
3
+ // inside the team service (main/team/index.mjs) and only runs while Rooms is
4
+ // switched on in Alpha features and someone is signed in.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { wallCards, myPresence, displayNameOf, withoutHidden } from '../../shared/room-cards.mjs';
8
+ import { GENERAL_ROOM } from './rooms-memory.mjs';
9
+ import { ROOM_FILE_NAME, FILE_EXISTS } from './rooms-shape.mjs';
10
+
11
+ const MESSAGES_HELD = 200;
12
+ const MAX_FILE_BYTES = 10 * 1024 * 1024;
13
+ // Played voice notes and opened attachments, so a second play is not a second
14
+ // download; bounded by count and by size, since one file can be 10 MB.
15
+ const FILES_HELD = 40;
16
+ const FILE_CHARS_HELD = 64_000_000;
17
+ const HEARTBEAT_MS = 5 * 60_000;
18
+ const SETTLE_MS = 400;
19
+
20
+ // Pins, mutes, rooms marked unread and the first-room pin are this Mac's,
21
+ // kept per person.
22
+ export function roomPrefsFile(storeRoot) {
23
+ return path.join(storeRoot, '.rooms.json');
24
+ }
25
+
26
+ const PREFS = { pinned: [], muted: [], markedUnread: [], autoPinned: false };
27
+
28
+ function prefsStore(file) {
29
+ const read = () => { try { return JSON.parse(fs.readFileSync(file, 'utf8')) ?? {}; } catch { return {}; } };
30
+ return {
31
+ get: (who) => ({ ...PREFS, ...(read()[who] ?? {}) }),
32
+ set: (who, patch) => {
33
+ const all = read();
34
+ all[who] = { ...PREFS, ...(all[who] ?? {}), ...patch };
35
+ try { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(all, null, 2)); } catch { /* a pin that is not kept is not worth failing over */ }
36
+ return all[who];
37
+ },
38
+ };
39
+ }
40
+
41
+ const EMPTY = { on: false, ready: false, rooms: [], messages: {}, me: null, ranks: {}, invites: [], mentions: [], error: null };
42
+
43
+ const reactionKey = (x) => `${x.messageId}\n${x.by}\n${x.emoji}`;
44
+
45
+ // Only reactions that are on, in the order they were made. Built from Maps so
46
+ // an emoji spelled "__proto__" is an ordinary key.
47
+ function withReactions(held, rows) {
48
+ const on = [...(rows?.values() ?? [])].filter((x) => x.on).sort((a, b) => a.at - b.at);
49
+ const byMessage = new Map();
50
+ for (const x of on) {
51
+ const emojis = byMessage.get(x.messageId) ?? new Map();
52
+ emojis.set(x.emoji, [...(emojis.get(x.emoji) ?? []), x.by]);
53
+ byMessage.set(x.messageId, emojis);
54
+ }
55
+ return held.map((m) => ({ ...m, reactions: m.deleted ? {} : Object.fromEntries(byMessage.get(m.id) ?? []) }));
56
+ }
57
+
58
+ // Padding taken off, so a file of exactly 10 MB is not refused.
59
+ const bytesOf = (b64) => Math.floor((b64.length * 3) / 4) - (b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0);
60
+
61
+ export function createRooms({
62
+ backend, // () => the signed-in backend, or null
63
+ me, // () => { id, name } of who is signed in, or null
64
+ team, // () => { people, cards } from the team service
65
+ enabled, // () => whether Rooms is switched on
66
+ device, // () => this Mac's 12-hex id
67
+ presenceFacts, // () => { running, items, products, summary }
68
+ prefsFile,
69
+ now = Date.now,
70
+ log = () => {},
71
+ onChange = () => {},
72
+ timers = { setTimeout, clearTimeout },
73
+ }) {
74
+ const prefs = prefsStore(prefsFile);
75
+ let state = { ...EMPTY };
76
+ let people = new Map(); // roomId -> [personId]
77
+ let names = new Map(); // roomId -> room_names rows, or null on a cloud without them
78
+ let profiles = new Map(); // personId -> row
79
+ let presence = []; // rows
80
+ let messages = new Map(); // roomId -> [message], oldest first
81
+ let reads = new Map(); // roomId -> ms
82
+ let reactions = new Map(); // roomId -> Map(reactionKey -> reaction), off ones too
83
+ let files = new Map(); // `${roomId}/${name}` -> base64, oldest first
84
+ let mentioned = []; // my newest mentions, which may be older than the messages held
85
+ let unsubscribe = null;
86
+ let started = null; // the backend rooms were started on
87
+ let setUp = false; // my profile made, Coworking joined if I have nowhere else, realtime on
88
+ let lastSent = null;
89
+ let lastSentAt = 0;
90
+ let refreshing = null;
91
+ let again = false;
92
+ let gen = 0; // bumped by every change made here, see refresh
93
+ let settle = null;
94
+
95
+ const meId = () => me()?.id ?? null;
96
+
97
+ function build() {
98
+ if (!started) { state = { ...EMPTY, on: !!enabled() }; return; }
99
+ const who = meId();
100
+ const t = team() ?? {};
101
+ const teammates = (t.people ?? []).map((p) => p.id).filter((id) => id !== who);
102
+ const p = prefs.get(who);
103
+ const pinned = new Set(p.pinned);
104
+ const muted = new Set(p.muted);
105
+ const marked = new Set(p.markedUnread);
106
+ const at = now();
107
+ const mentions = [];
108
+ const mine = ownWork();
109
+ const shown = new Map();
110
+ const rooms = (state.rawRooms ?? []).map((r) => {
111
+ const held = messages.get(r.id) ?? [];
112
+ shown.set(r.id, withReactions(held, reactions.get(r.id)));
113
+ const readAt = reads.get(r.id) ?? 0;
114
+ const unreadMsgs = held.filter((m) => m.at > readAt && m.by !== who && !m.deleted);
115
+ const called = names.get(r.id) ?? null;
116
+ // Everyone in the room by name, invisible or with no card, for the chat.
117
+ const everyone = called?.map((n) => ({ id: n.person_id, name: n.has_card ? n.name : displayNameOf(n.name), avatarUrl: n.avatar_url ?? null })) ?? null;
118
+ const nameOf = (id) => profiles.get(id)?.display_name || everyone?.find((p) => p.id === id)?.name || '';
119
+ const pool = new Map([...mentioned.filter((m) => m.roomId === r.id && m.at > readAt && m.by !== who && !m.deleted), ...unreadMsgs].map((m) => [m.id, m]));
120
+ for (const m of pool.values()) {
121
+ if (m.mentions.includes(who)) mentions.push({ id: `room-mention:${m.id}`, messageId: m.id, roomId: r.id, roomName: r.name, by: m.by, byName: nameOf(m.by), body: m.body, at: m.at });
122
+ }
123
+ const wall = wallCards({ members: people.get(r.id) ?? [], profiles: [...profiles.values()], presence, teammates, teamCards: t.cards ?? [], meId: who, now: at, mine, names: called, listed: !!r.listed });
124
+ return {
125
+ ...r,
126
+ pinned: pinned.has(r.id),
127
+ muted: muted.has(r.id),
128
+ readAt,
129
+ markedUnread: marked.has(r.id),
130
+ // A room marked unread reads as unread until it is read, even with
131
+ // nothing new from anyone else in it.
132
+ unread: Math.max(unreadMsgs.length, marked.has(r.id) ? 1 : 0),
133
+ lastAt: held.at(-1)?.at ?? r.createdAt,
134
+ agents: wall.reduce((n, c) => n + (c.agents ?? 0), 0),
135
+ wall,
136
+ people: everyone,
137
+ };
138
+ });
139
+ state = {
140
+ ...state,
141
+ on: true,
142
+ rooms,
143
+ messages: Object.fromEntries(rooms.map((r) => [r.id, shown.get(r.id)])),
144
+ mentions: mentions.sort((a, b) => b.at - a.at),
145
+ };
146
+ }
147
+
148
+ const changed = () => { build(); onChange(); };
149
+
150
+ // The thread this Mac's newest agent is on, for my own card only.
151
+ function ownWork() {
152
+ let f = {};
153
+ try { f = presenceFacts() ?? {}; } catch { return null; }
154
+ const lead = [...(f.running ?? [])].sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0))[0];
155
+ if (!lead) return { workingOn: null, agents: 0 };
156
+ const item = (f.items ?? []).find((i) => i.id === lead.itemId);
157
+ return { workingOn: (String(item?.label ?? '').trim() || String(item?.title ?? '')).slice(0, 64) || null, agents: f.running.length };
158
+ }
159
+ const local = () => { gen += 1; };
160
+
161
+ // One pass at a time; a call during a pass asks for another, and resolves
162
+ // when the last one has run. A pass that overlapped a change made here
163
+ // (a message heard, a profile saved) is thrown away and run again, so what
164
+ // was read before the change never lands on top of it.
165
+ function refresh() {
166
+ again = true;
167
+ refreshing ??= (async () => {
168
+ while (again) { again = false; await refreshOnce(); }
169
+ })().finally(() => { refreshing = null; });
170
+ return refreshing;
171
+ }
172
+
173
+ async function refreshOnce() {
174
+ const b = backend();
175
+ if (!b || !started) return;
176
+ const g = gen;
177
+ try {
178
+ const rooms = await b.myRooms();
179
+ const ids = new Map();
180
+ const named = new Map();
181
+ await Promise.all(rooms.map(async (r) => {
182
+ const [who, called] = await Promise.all([b.roomPeople(r.id), b.roomNames ? b.roomNames(r.id).catch(() => null) : null]);
183
+ ids.set(r.id, who);
184
+ named.set(r.id, called);
185
+ }));
186
+ const everyone = [...new Set([meId(), ...[...ids.values()].flat()])].filter(Boolean);
187
+ const [profileRows, presenceRows, readRows, mine, invites, ranks, mentionRows] = await Promise.all([
188
+ b.roomProfiles(everyone), b.roomPresence(everyone), b.roomReads(), b.myRoomProfile(),
189
+ b.pendingRoomInvites().catch(() => []), b.agentboxRanks(everyone).catch(() => ({})),
190
+ b.roomMentions({ limit: 50 }).catch(() => []),
191
+ ]);
192
+ const held = new Map();
193
+ const reacted = new Map();
194
+ await Promise.all(rooms.map(async (r) => {
195
+ const [fresh, rows] = await Promise.all([b.roomMessages(r.id, { limit: MESSAGES_HELD }), b.roomReactions(r.id)]);
196
+ held.set(r.id, merge(messages.get(r.id), fresh));
197
+ reacted.set(r.id, new Map(rows.map((x) => [reactionKey(x), x])));
198
+ }));
199
+ if (backend() !== b || started !== b) return;
200
+ if (g !== gen) { again = true; return; }
201
+ people = ids;
202
+ names = named;
203
+ profiles = new Map(profileRows.map((row) => [row.person_id, row]));
204
+ presence = presenceRows;
205
+ messages = held;
206
+ reactions = reacted;
207
+ reads = new Map(readRows.map((r) => [r.roomId, r.readAt]));
208
+ mentioned = mentionRows;
209
+ autoPin(rooms);
210
+ state = { ...state, ready: true, rawRooms: rooms, me: mine, invites, ranks, error: null };
211
+ } catch (err) {
212
+ state = { ...state, error: String(err?.message ?? err) };
213
+ log(`rooms: ${state.error}`);
214
+ }
215
+ changed();
216
+ }
217
+
218
+ function merge(old = [], fresh = []) {
219
+ const byId = new Map(old.map((m) => [m.id, m]));
220
+ for (const m of fresh) byId.set(m.id, m);
221
+ return [...byId.values()].sort((a, b) => a.at - b.at || String(a.id).localeCompare(String(b.id))).slice(-MESSAGES_HELD);
222
+ }
223
+
224
+ // The first room I am ever in is pinned for me, once.
225
+ function autoPin(rooms) {
226
+ const who = meId();
227
+ const p = prefs.get(who);
228
+ if (p.autoPinned || !rooms.length) return;
229
+ const first = rooms.find((r) => r.teamId) ?? rooms[0];
230
+ prefs.set(who, { autoPinned: true, pinned: [...new Set([...p.pinned, first.id])] });
231
+ }
232
+
233
+ function later() {
234
+ if (settle) return;
235
+ settle = timers.setTimeout(() => { settle = null; refresh(); }, SETTLE_MS);
236
+ settle?.unref?.();
237
+ }
238
+
239
+ // An edit or a delete of a message not held is left for when it is read.
240
+ function holdMessage(m) {
241
+ const held = messages.get(m.roomId) ?? [];
242
+ if ((m.editedAt == null && !m.deleted) || held.some((x) => x.id === m.id)) messages.set(m.roomId, merge(held, [m]));
243
+ mentioned = mentioned.map((x) => (x.id === m.id ? m : x));
244
+ if (m.deleted) dropReactions(m.roomId, m.id);
245
+ }
246
+
247
+ function holdReaction(x) {
248
+ const held = reactions.get(x.roomId) ?? new Map();
249
+ held.set(reactionKey(x), x);
250
+ reactions.set(x.roomId, held);
251
+ }
252
+
253
+ function dropReactions(roomId, messageId) {
254
+ const held = reactions.get(roomId);
255
+ if (held) for (const [k, x] of held) if (x.messageId === messageId) held.delete(k);
256
+ }
257
+
258
+ function hear(e) {
259
+ if (e.kind === 'message' || e.kind === 'reaction') {
260
+ const roomId = e.kind === 'message' ? e.message.roomId : e.reaction.roomId;
261
+ if (!messages.has(roomId)) { later(); return; }
262
+ local();
263
+ if (e.kind === 'message') holdMessage(e.message);
264
+ else holdReaction(e.reaction);
265
+ changed();
266
+ return;
267
+ }
268
+ later();
269
+ }
270
+
271
+ function keepFile(key, data) {
272
+ files.delete(key);
273
+ files.set(key, data);
274
+ let total = 0;
275
+ for (const d of files.values()) total += d.length;
276
+ for (const [k, d] of files) {
277
+ if (files.size <= FILES_HELD && total <= FILE_CHARS_HELD) break;
278
+ files.delete(k);
279
+ total -= d.length;
280
+ }
281
+ }
282
+
283
+ // What this Mac sends, when it differs from what it last sent, and every
284
+ // few minutes regardless so others do not read it as gone quiet.
285
+ async function publish({ force = false } = {}) {
286
+ const b = backend();
287
+ if (!b || !started) return false;
288
+ const facts = presenceFacts() ?? {};
289
+ const showWorking = state.me?.show_working ?? 'everyone';
290
+ const row = withoutHidden(myPresence({ ...facts, now: now(), showWorking }), state.me);
291
+ const key = JSON.stringify(row);
292
+ if (!force && key === lastSent && now() - lastSentAt < HEARTBEAT_MS) return false;
293
+ await b.putPresence(device(), { ...row, gone: false });
294
+ lastSent = key;
295
+ lastSentAt = now();
296
+ return true;
297
+ }
298
+
299
+ async function ensureProfile(b) {
300
+ const mine = await b.myRoomProfile();
301
+ if (mine) return mine;
302
+ // The cloud keeps an address of up to 500 characters, not a picture's bytes.
303
+ const face = me()?.avatarUrl ?? null;
304
+ const avatar = typeof face === 'string' && /^https?:\/\//.test(face) && face.length <= 500 ? face : null;
305
+ // Thread names reach strangers only once their owner turns that on. Said
306
+ // here as well as in the cloud's default, for a cloud from before it changed.
307
+ return b.saveRoomProfile({ display_name: displayNameOf(me()?.name ?? '') || 'Someone', avatar_url: avatar, show_working: 'team' });
308
+ }
309
+
310
+ // Tried on every pass until it works: a cloud without the rooms tables yet,
311
+ // or a dropped network, must not leave me nameless and deaf for the session.
312
+ async function setUpOnce(b) {
313
+ try {
314
+ const mine = await ensureProfile(b);
315
+ state = { ...state, me: mine };
316
+ // Someone with no team and no room starts in Coworking, pinned.
317
+ const rooms = await b.myRooms();
318
+ if (!rooms.length && !(team()?.team)) await b.joinListedRoom(GENERAL_ROOM).catch((err) => log(`rooms: Coworking: ${err.message}`));
319
+ unsubscribe ??= b.subscribeRooms(hear);
320
+ setUp = true;
321
+ state = { ...state, error: null };
322
+ } catch (err) {
323
+ state = { ...state, error: String(err?.message ?? err) };
324
+ log(`rooms: starting: ${state.error}`);
325
+ }
326
+ }
327
+
328
+ async function start() {
329
+ const b = backend();
330
+ if (!b || !enabled() || !meId()) return;
331
+ if (started === b) return;
332
+ await stop();
333
+ started = b;
334
+ state = { ...EMPTY, on: true };
335
+ await setUpOnce(b);
336
+ await refresh();
337
+ await publish({ force: true }).catch((err) => log(`rooms: presence: ${err.message}`));
338
+ }
339
+
340
+ // Rooms switched off, or signed out: this Mac says it has gone, and stops.
341
+ async function stop() {
342
+ const b = started;
343
+ unsubscribe?.();
344
+ unsubscribe = null;
345
+ if (settle) timers.clearTimeout(settle);
346
+ settle = null;
347
+ started = null;
348
+ setUp = false;
349
+ lastSent = null;
350
+ people = new Map(); names = new Map(); profiles = new Map(); presence = []; messages = new Map(); reads = new Map(); mentioned = [];
351
+ reactions = new Map(); files = new Map();
352
+ state = { ...EMPTY, on: !!enabled() };
353
+ if (b) await b.putPresence(device(), { gone: true, agents_running: 0, working_on: null, working_private: false, activity: [] }).catch(() => {});
354
+ onChange();
355
+ }
356
+
357
+ // Called on every pass of the team's sync loop.
358
+ async function tick() {
359
+ const want = !!enabled() && !!backend() && !!meId();
360
+ if (want && started !== backend()) return start();
361
+ if (!want && started) return stop();
362
+ if (!want) return;
363
+ if (!setUp) { await setUpOnce(started); if (setUp) await refresh(); }
364
+ await syncName().catch((err) => log(`rooms: name: ${err.message}`));
365
+ await syncStatus().catch((err) => log(`rooms: status: ${err.message}`));
366
+ await publish().catch((err) => log(`rooms: presence: ${err.message}`));
367
+ }
368
+
369
+ // A card made before my name was known reads "Someone" until it is.
370
+ async function syncName() {
371
+ const mine = state.me;
372
+ const name = displayNameOf(me()?.name ?? '');
373
+ if (!mine || !name || (mine.display_name && mine.display_name !== 'Someone')) return;
374
+ const saved = await backend().saveRoomProfile({ display_name: name });
375
+ local();
376
+ state = { ...state, me: saved };
377
+ changed();
378
+ }
379
+
380
+ // The line I wrote for my team (the account menu) is my room card's status.
381
+ async function syncStatus() {
382
+ const mine = state.me;
383
+ if (!mine) return;
384
+ const s = mine.show_status === false ? null : me()?.status ?? null;
385
+ const text = s?.text ?? null;
386
+ const until = text && s?.until ? new Date(s.until).toISOString() : null;
387
+ if ((mine.status_text ?? null) === text && (mine.status_until ? new Date(mine.status_until).toISOString() : null) === until) return;
388
+ const saved = await backend().saveRoomProfile({ status_text: text, status_until: until });
389
+ local();
390
+ state = { ...state, me: saved };
391
+ changed();
392
+ }
393
+
394
+ const need = () => { const b = backend(); if (!b || !started) throw new Error('turn on Rooms and sign in first'); return b; };
395
+ const after = async (p) => { const out = await p; await refresh(); return out; };
396
+
397
+ return {
398
+ state: () => { const { rawRooms, ...shown } = state; return shown; },
399
+ start, stop, tick, refresh, publish,
400
+
401
+ async send(roomId, body, { inReplyTo = null, mentions = [] } = {}) {
402
+ const b = need();
403
+ const m = await b.sendRoomMessage(roomId, { body: String(body ?? '').trim(), inReplyTo, mentions });
404
+ local();
405
+ messages.set(roomId, merge(messages.get(roomId), [m]));
406
+ reads.set(roomId, Math.max(reads.get(roomId) ?? 0, m.at));
407
+ changed();
408
+ return m;
409
+ },
410
+
411
+ async markRead(roomId) {
412
+ const b = need();
413
+ const who = meId();
414
+ const p = prefs.get(who);
415
+ const marked = p.markedUnread.includes(roomId);
416
+ const last = (messages.get(roomId) ?? []).at(-1)?.at;
417
+ const behind = !!last && (reads.get(roomId) ?? 0) < last;
418
+ if (!behind && !marked) return;
419
+ local();
420
+ if (marked) prefs.set(who, { markedUnread: p.markedUnread.filter((x) => x !== roomId) });
421
+ if (behind) reads.set(roomId, last);
422
+ changed();
423
+ if (behind) await b.markRoomRead(roomId, last);
424
+ },
425
+
426
+ // `at` is the time of the first message that should read as unread.
427
+ async markUnread(roomId, at) {
428
+ const b = need();
429
+ const from = Number(at);
430
+ if (!Number.isFinite(from) || from <= 0) throw new Error('Pick the message to mark unread from.');
431
+ const who = meId();
432
+ const p = prefs.get(who);
433
+ local();
434
+ reads.set(roomId, from - 1);
435
+ prefs.set(who, { markedUnread: [...new Set([...p.markedUnread, roomId])] });
436
+ changed();
437
+ await b.markRoomUnread(roomId, from - 1);
438
+ },
439
+
440
+ async react(roomId, messageId, emoji, on = true) {
441
+ const b = need();
442
+ await b.reactRoomMessage(messageId, emoji, !!on);
443
+ const key = reactionKey({ messageId, by: meId(), emoji });
444
+ if (!on && !reactions.get(roomId)?.has(key)) return;
445
+ local();
446
+ holdReaction({ messageId, roomId, by: meId(), emoji, on: !!on, at: now() });
447
+ changed();
448
+ },
449
+
450
+ async edit(roomId, messageId, body, { mentions = [] } = {}) {
451
+ const b = need();
452
+ const m = await b.editRoomMessage(messageId, { body: String(body ?? '').trim(), mentions });
453
+ local();
454
+ holdMessage({ ...m, roomId });
455
+ changed();
456
+ return m;
457
+ },
458
+
459
+ async deleteMessage(roomId, messageId) {
460
+ const b = need();
461
+ await b.deleteRoomMessage(messageId);
462
+ const was = messages.get(roomId)?.find((m) => m.id === messageId);
463
+ local();
464
+ if (was) holdMessage({ ...was, body: '', mentions: [], deleted: true });
465
+ mentioned = mentioned.filter((m) => m.id !== messageId);
466
+ dropReactions(roomId, messageId);
467
+ changed();
468
+ },
469
+
470
+ async putFile(roomId, { name, dataBase64 } = {}) {
471
+ const b = need();
472
+ const n = String(name ?? '');
473
+ if (!ROOM_FILE_NAME.test(n)) throw new Error(`A room cannot keep a file named “${n}”: a name starts with a letter or number and has only letters, numbers, dots, dashes and underscores, up to 160.`);
474
+ const data = String(dataBase64 ?? '');
475
+ if (bytesOf(data) > MAX_FILE_BYTES) throw new Error('That file is over 10 MB, the most a room takes.');
476
+ try {
477
+ await b.putRoomFile(roomId, n, data);
478
+ keepFile(`${roomId}/${n}`, data);
479
+ } catch (err) {
480
+ if (err?.code !== FILE_EXISTS) throw err;
481
+ }
482
+ return n;
483
+ },
484
+
485
+ async file(roomId, name) {
486
+ const b = need();
487
+ const key = `${roomId}/${name}`;
488
+ if (files.has(key)) {
489
+ const data = files.get(key);
490
+ keepFile(key, data);
491
+ return data;
492
+ }
493
+ if (!ROOM_FILE_NAME.test(String(name ?? ''))) return null;
494
+ const data = await b.roomFile(roomId, String(name));
495
+ if (data != null) keepFile(key, data);
496
+ return data;
497
+ },
498
+
499
+ async older(roomId) {
500
+ const b = need();
501
+ const held = messages.get(roomId) ?? [];
502
+ const got = await b.roomMessages(roomId, { before: held[0]?.at ?? now(), limit: 100 });
503
+ local();
504
+ const all = new Map([...got, ...held].map((m) => [m.id, m]));
505
+ messages.set(roomId, [...all.values()].sort((a, b2) => a.at - b2.at));
506
+ changed();
507
+ return got.length;
508
+ },
509
+
510
+ // My card: what it says and who sees what.
511
+ async saveProfile(fields = {}) {
512
+ const b = need();
513
+ const patch = { ...fields };
514
+ if ('fullName' in patch) {
515
+ patch.display_name = displayNameOf(me()?.name ?? '', { full: !!patch.fullName }) || state.me?.display_name || 'Someone';
516
+ delete patch.fullName;
517
+ }
518
+ const saved = await b.saveRoomProfile(patch);
519
+ local();
520
+ state = { ...state, me: saved };
521
+ // My own card is built from `profiles`, which otherwise waits for the next read.
522
+ if (saved?.person_id && profiles.has(saved.person_id)) profiles.set(saved.person_id, saved);
523
+ changed();
524
+ if (['show_working', 'show_agents', 'show_level', 'show_activity'].some((k) => k in patch)) await publish({ force: true }).catch(() => {});
525
+ if ('show_status' in patch) await syncStatus().catch(() => {});
526
+ return saved;
527
+ },
528
+
529
+ // No minutes is until I turn it off, held as a date ten years ahead.
530
+ goInvisible(minutes = null) {
531
+ const m = minutes == null ? 3650 * 24 * 60 : Math.max(1, Math.min(48 * 60, Number(minutes) || 60));
532
+ return this.saveProfile({ invisible_until: new Date(now() + m * 60_000).toISOString() });
533
+ },
534
+ comeBack() { return this.saveProfile({ invisible_until: null }); },
535
+
536
+ link: (roomId) => need().roomLink(roomId),
537
+ revokeLink: (roomId) => need().revokeRoomLink(roomId),
538
+ preview: (token) => need().roomPreview(token),
539
+ listed: () => need().listedRooms(),
540
+ invitesSent: (roomId) => need().roomInvitesSent(roomId),
541
+ joinByLink: (token) => after(need().joinRoomByLink(token)),
542
+ joinListed: (roomId) => after(need().joinListedRoom(roomId)),
543
+ acceptInvite: (roomId) => after(need().acceptRoomInvite(roomId)),
544
+ invite: async (roomId, email) => {
545
+ const clean = String(email ?? '').trim();
546
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(clean)) throw new Error(`${clean || 'that'} is not an email address`);
547
+ await need().inviteToRoom(roomId, clean);
548
+ },
549
+ async create({ name, kind = 'room', listed = false, inviteOnly = false, people: who = [] }) {
550
+ const id = await need().createRoom({ name: String(name ?? '').trim(), kind, listed, inviteOnly, people: who });
551
+ prefs.set(meId(), { pinned: [...new Set([...prefs.get(meId()).pinned, id])] });
552
+ await refresh();
553
+ return id;
554
+ },
555
+ rename: (roomId, name) => after(need().renameRoom(roomId, name)),
556
+ leave: async (roomId) => {
557
+ await need().leaveRoom(roomId);
558
+ const p = prefs.get(meId());
559
+ prefs.set(meId(), { pinned: p.pinned.filter((x) => x !== roomId), muted: p.muted.filter((x) => x !== roomId), markedUnread: p.markedUnread.filter((x) => x !== roomId) });
560
+ await refresh();
561
+ },
562
+ remove: (roomId, personId) => after(need().removeRoomMember(roomId, personId)),
563
+
564
+ pin(roomId, on = true) {
565
+ const p = prefs.get(meId());
566
+ prefs.set(meId(), { pinned: on ? [...new Set([...p.pinned, roomId])] : p.pinned.filter((x) => x !== roomId) });
567
+ changed();
568
+ },
569
+ mute(roomId, on = true) {
570
+ const p = prefs.get(meId());
571
+ prefs.set(meId(), { muted: on ? [...new Set([...p.muted, roomId])] : p.muted.filter((x) => x !== roomId) });
572
+ changed();
573
+ },
574
+ };
575
+ }