agentbox-app 0.1.12 → 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 (370) 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/acp.mjs +296 -0
  9. package/main/agent-homes.mjs +158 -0
  10. package/main/agent-sessions.mjs +311 -260
  11. package/main/agent-updates.mjs +9 -22
  12. package/main/agents.mjs +179 -466
  13. package/main/analytics.mjs +42 -2
  14. package/main/answer-modes.mjs +103 -37
  15. package/main/approval-prompt-core.mjs +197 -0
  16. package/main/approval-prompt-server.mjs +4 -138
  17. package/main/approvals.mjs +101 -297
  18. package/main/artifact-path.mjs +47 -18
  19. package/main/claude-bin.mjs +45 -311
  20. package/main/claude-input.mjs +57 -7
  21. package/main/claude-launch-env.mjs +54 -0
  22. package/main/claude-models.mjs +39 -7
  23. package/main/claude-plan.mjs +64 -16
  24. package/main/claude-session-open.mjs +28 -0
  25. package/main/claude-usage-process.mjs +40 -0
  26. package/main/claude-usage.mjs +94 -59
  27. package/main/cloud/account.mjs +179 -0
  28. package/main/cloud/aws.mjs +127 -0
  29. package/main/cloud/browser-return.mjs +58 -0
  30. package/main/cloud/clients.mjs +15 -0
  31. package/main/cloud/codex-cloud.mjs +53 -0
  32. package/main/cloud/config.mjs +107 -0
  33. package/main/cloud/docs.mjs +219 -0
  34. package/main/cloud/exec.mjs +27 -0
  35. package/main/cloud/host.mjs +165 -0
  36. package/main/cloud/kit.mjs +80 -0
  37. package/main/cloud/logins.mjs +543 -0
  38. package/main/cloud/managed.mjs +115 -0
  39. package/main/cloud/marker.mjs +104 -0
  40. package/main/cloud/org.mjs +53 -0
  41. package/main/cloud/reuse.mjs +208 -0
  42. package/main/cloud/run-cli.mjs +177 -0
  43. package/main/cloud/run.mjs +1934 -0
  44. package/main/cloud/usage.mjs +76 -0
  45. package/main/cloud-permission.mjs +51 -0
  46. package/main/codex-account.mjs +78 -0
  47. package/main/codex-app-server.mjs +276 -409
  48. package/main/codex-approvals.mjs +148 -433
  49. package/main/codex-bin.mjs +51 -249
  50. package/main/codex-models.mjs +20 -2
  51. package/main/codex-session.mjs +246 -506
  52. package/main/codex-threads-worker.mjs +12 -0
  53. package/main/codex-threads.mjs +112 -11
  54. package/main/codex-usage-file.mjs +5 -3
  55. package/main/codex-usage.mjs +56 -7
  56. package/main/codex-watch.mjs +83 -5
  57. package/main/codex.mjs +95 -363
  58. package/main/config.mjs +37 -5
  59. package/main/corner-tag.mjs +810 -0
  60. package/main/crash-report.mjs +26 -3
  61. package/main/dev-window.mjs +1 -1
  62. package/main/engine-bin.mjs +545 -0
  63. package/main/engine-setup.mjs +25 -24
  64. package/main/feedback.mjs +14 -31
  65. package/main/folder-pictures.mjs +2 -2
  66. package/main/freeze-recorder-thread.mjs +86 -0
  67. package/main/freeze-recorder.mjs +146 -0
  68. package/main/fresh-user.mjs +20 -9
  69. package/main/git-change-offthread.mjs +17 -3
  70. package/main/git-change-thread.mjs +5 -2
  71. package/main/harnesses/claude-stream.mjs +208 -0
  72. package/main/harnesses/claude.mjs +105 -0
  73. package/main/harnesses/codex.mjs +72 -0
  74. package/main/harnesses/common.mjs +78 -0
  75. package/main/harnesses/instructions-file.mjs +116 -0
  76. package/main/harnesses/opencode.mjs +234 -0
  77. package/main/harnesses.mjs +65 -0
  78. package/main/importable-threads.mjs +96 -0
  79. package/main/ipc.mjs +520 -135
  80. package/main/leftovers.mjs +117 -25
  81. package/main/liquid-glass.mjs +26 -0
  82. package/main/live-replies.mjs +10 -4
  83. package/main/local-app-frames.mjs +82 -0
  84. package/main/main-went-red.mjs +330 -0
  85. package/main/main.mjs +248 -37
  86. package/main/memory-gate-history.mjs +85 -20
  87. package/main/memory-gate-server.mjs +129 -11
  88. package/main/memory-gate.mjs +62 -8
  89. package/main/memory-log.mjs +124 -0
  90. package/main/menu.mjs +4 -2
  91. package/main/message-priority.mjs +2 -2
  92. package/main/mic.mjs +38 -0
  93. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  94. package/main/new-chats-watch.mjs +110 -0
  95. package/main/new-user-window.mjs +25 -0
  96. package/main/notify.mjs +64 -5
  97. package/main/opencode-server.mjs +271 -0
  98. package/main/opencode.mjs +287 -0
  99. package/main/phone-alerts.mjs +117 -0
  100. package/main/phone-inbox.mjs +625 -0
  101. package/main/phone-link.mjs +571 -0
  102. package/main/phone-uploads.mjs +476 -0
  103. package/main/picture-shape.mjs +114 -0
  104. package/main/quit-sequence.mjs +143 -0
  105. package/main/refresh-agent-models.mjs +15 -2
  106. package/main/relay-address.mjs +28 -0
  107. package/main/relay-client.mjs +314 -0
  108. package/main/repeats.mjs +10 -4
  109. package/main/row-label.mjs +18 -47
  110. package/main/run-change.mjs +13 -0
  111. package/main/run-ledger.mjs +135 -0
  112. package/main/runs-on.mjs +32 -35
  113. package/main/save-attachments.mjs +41 -0
  114. package/main/serve.mjs +155 -9
  115. package/main/settings.mjs +364 -50
  116. package/main/shell-path.mjs +234 -0
  117. package/main/ship-conflict-brief.mjs +119 -0
  118. package/main/ship-queue.mjs +134 -20
  119. package/main/sign-in-files.mjs +3 -7
  120. package/main/source-updater.mjs +45 -11
  121. package/main/stage-attachment.mjs +68 -0
  122. package/main/stall-watch.mjs +50 -0
  123. package/main/start-hold.mjs +73 -0
  124. package/main/store/home.mjs +87 -232
  125. package/main/store/paths.mjs +11 -0
  126. package/main/store/teammates-files.mjs +99 -0
  127. package/main/store/work-items.mjs +231 -13
  128. package/main/store.mjs +227 -53
  129. package/main/supervisor.mjs +3865 -699
  130. package/main/task-commands.mjs +13 -17
  131. package/main/task-folders-offthread.mjs +6 -1
  132. package/main/task-folders.mjs +897 -105
  133. package/main/task-terminals.mjs +147 -41
  134. package/main/team/index.mjs +114 -10
  135. package/main/team/memory-cloud.mjs +183 -1
  136. package/main/team/room-hours.mjs +48 -0
  137. package/main/team/room-setup.mjs +31 -0
  138. package/main/team/rooms-memory.mjs +454 -0
  139. package/main/team/rooms-shape.mjs +42 -0
  140. package/main/team/rooms-supabase.mjs +324 -0
  141. package/main/team/rooms.mjs +575 -0
  142. package/main/team/session.mjs +96 -26
  143. package/main/team/supabase-backend.mjs +180 -0
  144. package/main/team/sync.mjs +109 -1
  145. package/main/teammates.mjs +580 -0
  146. package/main/terminal-host.mjs +143 -0
  147. package/main/terminal-hosts.mjs +261 -0
  148. package/main/transcript-search.mjs +80 -0
  149. package/main/updater.mjs +71 -9
  150. package/main/webview-guard.mjs +45 -0
  151. package/main/worktree-include.mjs +365 -0
  152. package/mcp/core/documents.mjs +3 -1
  153. package/mcp/core/repeats.mjs +88 -0
  154. package/mcp/core/teammates.mjs +222 -0
  155. package/mcp/core/work.mjs +76 -4
  156. package/mcp/stdio.mjs +24 -11
  157. package/mcp/tools.mjs +116 -8
  158. package/package.json +30 -5
  159. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
  160. package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
  161. package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
  162. package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
  163. package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
  164. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  165. package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
  166. package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
  167. package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
  168. package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
  169. package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
  170. package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
  171. package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
  172. package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
  173. package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
  174. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
  175. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  176. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  177. package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
  178. package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
  179. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
  180. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  181. package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
  182. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  183. package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
  184. package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
  185. package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
  186. package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
  187. package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
  188. package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
  189. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
  190. package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
  191. package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
  192. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
  193. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
  194. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
  195. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  196. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
  197. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  198. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
  199. package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
  200. package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
  201. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  202. package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
  203. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  204. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  205. package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
  206. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
  207. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  208. package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
  209. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
  210. package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
  211. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
  212. package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
  213. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
  214. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
  215. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
  216. package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
  217. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
  218. package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
  219. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  220. package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
  221. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
  222. package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
  223. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
  224. package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
  225. package/renderer/dist/corner-tag.html +13 -0
  226. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  227. package/renderer/dist/emulator/data/compression/README.md +11 -0
  228. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  229. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  230. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  231. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  232. package/renderer/dist/emulator/data/cores/README.md +22 -0
  233. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  234. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  235. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  236. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  237. package/renderer/dist/emulator/data/cores/package.json +65 -0
  238. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  239. package/renderer/dist/emulator/data/emulator.css +1614 -0
  240. package/renderer/dist/emulator/data/loader.js +170 -0
  241. package/renderer/dist/emulator/data/localization/README.md +63 -0
  242. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  243. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  244. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  245. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  246. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  247. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  248. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  249. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  250. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  251. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  252. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  253. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  254. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  255. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  256. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  257. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  258. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  259. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  260. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  261. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  262. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  263. package/renderer/dist/emulator/data/src/compression.js +142 -0
  264. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  265. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  266. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  267. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  268. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  269. package/renderer/dist/emulator/data/src/storage.js +125 -0
  270. package/renderer/dist/emulator/data/version.json +5 -0
  271. package/renderer/dist/emulator/host.js +297 -0
  272. package/renderer/dist/emulator/index.html +28 -0
  273. package/renderer/dist/emulator/provenance.json +6 -0
  274. package/renderer/dist/index.html +16 -3
  275. package/scripts/after-install.mjs +30 -8
  276. package/scripts/approval-server.sh +24 -5
  277. package/scripts/before-publish.mjs +12 -10
  278. package/scripts/cloud-shim.pl +57 -0
  279. package/shared/account-names.d.mts +6 -0
  280. package/shared/account-names.mjs +15 -0
  281. package/shared/agent-hours.mjs +136 -0
  282. package/shared/agent-import.mjs +62 -34
  283. package/shared/agents.d.mts +1 -0
  284. package/shared/agents.mjs +126 -522
  285. package/shared/analytics-events.mjs +12 -0
  286. package/shared/approval-card.mjs +36 -0
  287. package/shared/bridge-map.mjs +45 -1
  288. package/shared/claude-code-shape.mjs +6 -0
  289. package/shared/claude-commands.generated.mjs +6 -4
  290. package/shared/claude-models.generated.mjs +6 -4
  291. package/shared/claude-usage.d.mts +1 -0
  292. package/shared/claude-usage.mjs +42 -1
  293. package/shared/cloud-hand-back.d.mts +7 -0
  294. package/shared/cloud-hand-back.mjs +20 -0
  295. package/shared/cloud-kit.d.mts +219 -0
  296. package/shared/cloud-kit.mjs +341 -0
  297. package/shared/cloud-login-name.mjs +4 -0
  298. package/shared/cloud-start.mjs +17 -0
  299. package/shared/cloud-versions.mjs +23 -0
  300. package/shared/codex-import.mjs +2 -2
  301. package/shared/codex-modes.d.mts +22 -9
  302. package/shared/codex-modes.mjs +67 -11
  303. package/shared/codex-usage.mjs +108 -2
  304. package/shared/corner-tag.d.mts +36 -0
  305. package/shared/corner-tag.mjs +396 -0
  306. package/shared/dead-run-trace.mjs +19 -2
  307. package/shared/engines.mjs +76 -300
  308. package/shared/feedback.d.mts +7 -0
  309. package/shared/feedback.mjs +25 -0
  310. package/shared/first-run-practice.mjs +17 -24
  311. package/shared/harness-definitions.mjs +111 -0
  312. package/shared/instruction-defaults.json +2 -2
  313. package/shared/key-caps.d.mts +14 -0
  314. package/shared/key-caps.mjs +68 -0
  315. package/shared/message-edits.d.mts +7 -0
  316. package/shared/message-edits.mjs +35 -0
  317. package/shared/move-thread.d.mts +24 -0
  318. package/shared/move-thread.mjs +112 -0
  319. package/shared/named-threads.mjs +72 -0
  320. package/shared/not-shipped.d.mts +22 -0
  321. package/shared/not-shipped.mjs +89 -0
  322. package/shared/notify-rules.d.mts +31 -0
  323. package/shared/notify-rules.mjs +55 -8
  324. package/shared/phone-files.mjs +28 -0
  325. package/shared/phone-settings.mjs +118 -0
  326. package/shared/phone-snapshot.d.mts +16 -0
  327. package/shared/phone-snapshot.mjs +348 -0
  328. package/shared/picture-formats.d.mts +1 -0
  329. package/shared/picture-formats.mjs +13 -0
  330. package/shared/provider-commands.mjs +10 -2
  331. package/shared/rank.d.mts +4 -1
  332. package/shared/rank.mjs +82 -19
  333. package/shared/relay-address.mjs +6 -0
  334. package/shared/relay-crypto.mjs +165 -0
  335. package/shared/repeats.d.mts +8 -0
  336. package/shared/repeats.mjs +33 -2
  337. package/shared/room-cards.mjs +167 -0
  338. package/shared/room-links.d.mts +4 -0
  339. package/shared/room-links.mjs +21 -0
  340. package/shared/runs-on.d.mts +3 -13
  341. package/shared/runs-on.mjs +31 -52
  342. package/shared/shipped-note.d.mts +4 -0
  343. package/shared/shipped-note.mjs +6 -0
  344. package/shared/shortcuts.mjs +24 -10
  345. package/shared/spawn-trouble.d.mts +4 -0
  346. package/shared/spawn-trouble.mjs +484 -30
  347. package/shared/team-rules.mjs +9 -2
  348. package/shared/teammates.d.mts +19 -0
  349. package/shared/teammates.mjs +756 -0
  350. package/shared/terminal-focus.d.mts +1 -0
  351. package/shared/terminal-focus.mjs +13 -0
  352. package/shared/terminal-input.d.mts +1 -0
  353. package/shared/terminal-input.mjs +30 -0
  354. package/shared/terminal-keys.d.mts +1 -0
  355. package/shared/terminal-keys.mjs +16 -0
  356. package/shared/terminal-size.d.mts +4 -0
  357. package/shared/terminal-size.mjs +29 -0
  358. package/shared/thread-cards.mjs +4 -1
  359. package/shared/usage.d.mts +12 -0
  360. package/shared/usage.mjs +56 -3
  361. package/shared/websites.d.mts +15 -0
  362. package/shared/websites.mjs +61 -0
  363. package/shared/where-it-runs-from.d.mts +43 -0
  364. package/shared/where-it-runs-from.mjs +155 -0
  365. package/shared/work-items.mjs +150 -29
  366. package/shared/work-lines.mjs +93 -245
  367. package/shared/xterm-invalid-range.d.mts +2 -0
  368. package/shared/xterm-invalid-range.mjs +58 -0
  369. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  370. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -4,11 +4,17 @@ import {randomUUID} from 'node:crypto';
4
4
  import {execFileSync} from 'node:child_process';
5
5
  import {createRequire} from 'node:module';
6
6
  import {commandRunning} from '../shared/terminal-state.mjs';
7
+ import {loginShell} from './shell-path.mjs';
7
8
  const require=createRequire(import.meta.url);
8
9
  const LIMIT=262144;
9
10
  export function terminalEnv(source=process.env){
10
11
  const env={...source,TERM:'xterm-256color',COLORTERM:'truecolor'};
11
12
  for(const key of Object.keys(env)) if(/^(ANTHROPIC_|OPENAI_API_KEY$|CLAUDE_CODE_|CLAUDECODE$|CLAUDE_PID$|CLAUDE_EFFORT$|ELECTRON_|NODE_OPTIONS$)/.test(key)) delete env[key];
13
+ // NPM'S STAND-IN EDITOR IS NOBODY'S CHOICE. `npm start` gives everything it
14
+ // runs EDITOR=vi when neither EDITOR nor VISUAL is set, and zsh reads "vi"
15
+ // there as a request for vi keys: Option+Left deleted and Option+Delete
16
+ // moved. A login shell sets the person's own EDITOR from their startup files.
17
+ if(env.EDITOR==='vi'&&!env.VISUAL) delete env.EDITOR;
12
18
  return env;
13
19
  }
14
20
  /*
@@ -32,17 +38,6 @@ export function terminalPlace({store,supervisor,product,id,agent}){
32
38
  return {cwd,notice};
33
39
  }
34
40
  export function terminalFolder(args){return terminalPlace(args).cwd;}
35
- // The shell a person actually has. `$SHELL` when that file exists, then
36
- // zsh, bash, and sh. A machine with no zsh used to spawn `/bin/zsh` and
37
- // the terminal never came up.
38
- export function loginShell(env=process.env,exists=fs.existsSync){
39
- const preferred=env.SHELL;
40
- if(preferred&&exists(preferred))return preferred;
41
- for(const candidate of ['/bin/zsh','/bin/bash','/bin/sh']){
42
- if(exists(candidate))return candidate;
43
- }
44
- return preferred||'/bin/sh';
45
- }
46
41
  // Enumerate only descendants of our own shell, including background process
47
42
  // groups. Kill children before shell; closing a pane never calls this.
48
43
  export function disposeTerminalProcess(pty){
@@ -68,21 +63,68 @@ export function disposeTerminalProcess(pty){
68
63
  * it is the same call the toolbar makes to decide whether to draw Stop command,
69
64
  * deliberately, so a shell she can see is busy can never be one this reaps.
70
65
  *
71
- * WHICH SHELLS NOBODY IS WATCHING: the panel asks for output every 150ms while
72
- * it is on screen and stops the instant it is hidden or she leaves the task
66
+ * WHICH SHELLS NOBODY IS WATCHING: the panel asks for output at least once a
67
+ * second while it is on screen (each ask waits up to READ_WAIT_MS for some)
68
+ * and stops the instant it is hidden or she leaves the task
73
69
  * (TaskTerminal.tsx). So `lastReadAt` is not a guess about attention, it is the
74
70
  * last time the app was actually asked what this shell had to say.
75
71
  *
76
- * Idle AND unwatched for ten minutes is a shell that costs something and is
77
- * worth nothing, and it goes. Everything else stays until she or the app quits.
72
+ * Idle AND unwatched for an hour is a shell that costs something and is
73
+ * worth nothing, and it goes. Everything else stays until its task is closed:
74
+ * task terminals run in ./terminal-host.mjs, which outlives the app.
75
+ *
76
+ * IT WAS TEN MINUTES, AND THAT WAS TOO SHORT (w-6841800f17): "I come back after
77
+ * maybe 15 minutes and it's gone ... I don't see the error message." A deploy
78
+ * that fails leaves an idle shell with the error on it, and ten minutes away
79
+ * from the task took the error with the shell. So the wait is an hour, and when
80
+ * a shell does go, the end of what it showed comes back with the next one.
81
+ *
82
+ * AND A SHELL THAT PRINTED SOMETHING SHE HAS NOT SEEN IS NOT WORTH NOTHING
83
+ * (w-f147e487a1). She started the AWS tools install, left the thread while it
84
+ * ran, and came back to a new shell: the install finished while she was away,
85
+ * and since the idle clock ran from her last look, the shell went on the next
86
+ * sweep with its output unread. "I was running it." So the hour starts only
87
+ * once she has read everything it printed, and at the cap a shell she has read
88
+ * goes before one she has not; when all twenty hold unread output, the least
89
+ * recently watched still makes room.
78
90
  */
79
- const IDLE_MS=600000;
91
+ const IDLE_MS=3600000;
92
+ // How much of a closed shell's screen comes back. The end is where the error
93
+ // is; 64K characters is several hundred lines of it.
94
+ const KEPT_SCREEN=65536;
95
+ // Closed shells whose screen is still held for her. Twice the cap is every
96
+ // terminal open now plus as many again that the app has tidied away.
97
+ const KEPT_SHELLS=40;
98
+ /*
99
+ * THE END OF A SCREEN, CUT AT A LINE. Cutting mid-line would start the replay
100
+ halfway through a word or an escape sequence. */
101
+ function keptScreen(buffer){
102
+ if(buffer.length<=KEPT_SCREEN)return buffer;
103
+ const tail=buffer.slice(-KEPT_SCREEN);
104
+ const line=tail.indexOf('\n');
105
+ return line<0?tail:tail.slice(line+1);
106
+ }
107
+ // The longest a read waits for output before answering with none, so the panel
108
+ // still hears about a command that finished quietly and the sweep still sees
109
+ // it watching.
110
+ const READ_WAIT_MS=1000;
80
111
  const SWEEP_MS=60000;
81
112
  const MAX_TERMINALS=20;
113
+ /*
114
+ * A TERMINAL LIVES AS LONG AS ITS TASK IS OPEN. Closing the task ends it; so,
115
+ * while memory is critical, does going unwatched. Each says why on the next
116
+ * terminal, in these words.
117
+ */
118
+ export const ENDED={
119
+ closed:'This task was closed, so its terminal was ended.',
120
+ memory:'This terminal was ended because this computer was running out of memory.',
121
+ };
122
+ // While memory is critical, a terminal looked at this recently is still in use.
123
+ const WATCHED_MS=600000;
82
124
  export class TaskTerminals {
83
- constructor({spawn,resolve,disposeProcess=disposeTerminalProcess,env=terminalEnv(),shell=loginShell(),idleMs=IDLE_MS,sweepMs=SWEEP_MS,now=()=>Date.now()}={}){
125
+ constructor({spawn,resolve,disposeProcess=disposeTerminalProcess,env=terminalEnv(),shell=loginShell(),idleMs=IDLE_MS,sweepMs=SWEEP_MS,now=()=>Date.now(),readPressure=null,watchedMs=WATCHED_MS}={}){
84
126
  this.spawn=spawn??((...args)=>require('node-pty').spawn(...args));this.resolve=resolve;this.disposeProcess=disposeProcess;this.env=env;this.shell=shell;this.sessions=new Map();
85
- this.idleMs=idleMs;this.now=now;
127
+ this.idleMs=idleMs;this.now=now;this.readPressure=readPressure;this.watchedMs=watchedMs;
86
128
  // Keys whose shell this class ended on its own, and WHY. The next open on
87
129
  // one says so rather than handing her a blank screen where her scrollback
88
130
  // was, and the two reasons are different sentences because they are
@@ -92,7 +134,7 @@ export class TaskTerminals {
92
134
  // `unref` so a sweep that is merely pending never holds the app open, and
93
135
  // never in a test: a suite that leaves an interval behind hangs the runner.
94
136
  if(sweepMs>0&&typeof setInterval==='function'){
95
- this.timer=setInterval(()=>{try{this.sweepIdle();}catch{}},sweepMs);
137
+ this.timer=setInterval(()=>{try{this.sweepIdle();}catch{}this.relieveMemory().catch(()=>{});},sweepMs);
96
138
  this.timer?.unref?.();
97
139
  }
98
140
  }
@@ -105,29 +147,69 @@ export class TaskTerminals {
105
147
  if(session.exited)return false;
106
148
  try{return commandRunning(session.pty.process||'Shell',false);}catch{return false;}
107
149
  }
150
+ // Output past the last byte a read handed to the panel.
151
+ unread(session){return session.offset+session.buffer.length>(session.seenTo??0);}
108
152
  /*
109
- * THE ONE THE CAP TAKES: least recently watched first, and never a busy one.
110
- Null means every slot is genuinely working, which is the only case left
111
- where she is told no. */
153
+ * THE ONE THE CAP TAKES: one she has read everything in before one she has
154
+ not, then least recently watched, and never a busy one. Null means every
155
+ slot is genuinely working, which is the only case left where she is told
156
+ no. */
112
157
  oldestIdle(){
113
- let pick=null,at=Infinity;
158
+ let pick=null,pickUnread=true,at=Infinity;
114
159
  for(const [key,s] of this.sessions){
115
160
  if(this.busy(s))continue;
116
- const seen=s.lastReadAt??0;
117
- if(seen<at){at=seen;pick=key;}
161
+ const unread=this.unread(s),seen=s.lastReadAt??0;
162
+ if(pick===null||(pickUnread&&!unread)||(unread===pickUnread&&seen<at)){pick=key;pickUnread=unread;at=seen;}
118
163
  }
119
164
  return pick;
120
165
  }
166
+ /*
167
+ * THE APP ENDS A SHELL, AND KEEPS WHAT IT SAID. Only these two paths keep the
168
+ screen: when she presses End shell herself, `close` is called directly and
169
+ nothing comes back, because she chose to throw it away. */
170
+ retire(key,why){
171
+ const s=this.sessions.get(key);
172
+ const screen=s?keptScreen(s.buffer):'';
173
+ this.close(key);
174
+ this.reaped.delete(key);
175
+ this.reaped.set(key,{why,screen});
176
+ while(this.reaped.size>KEPT_SHELLS)this.reaped.delete(this.reaped.keys().next().value);
177
+ }
121
178
  sweepIdle(){
122
179
  const cutoff=this.now()-this.idleMs;
123
180
  for(const [key,s] of [...this.sessions]){
124
- if(this.busy(s))continue;
181
+ if(this.busy(s)||this.unread(s))continue;
125
182
  if((s.lastReadAt??0)>cutoff)continue;
126
- this.close(key);
127
- this.reaped.set(key,'The previous shell here was closed after ten minutes idle.');
183
+ this.retire(key,'The previous shell here was closed after an hour idle.');
128
184
  }
129
185
  }
130
- open(key){
186
+ end(key,why){if(this.sessions.has(key))this.retire(key,why);}
187
+ list(){
188
+ return [...this.sessions].map(([key,s])=>({key,busy:this.busy(s),exited:s.exited,unread:this.unread(s),lastReadAt:s.lastReadAt??0,cwd:s.cwd,pid:s.pty.pid}));
189
+ }
190
+ /*
191
+ * TOO MUCH FOR THIS COMPUTER. Only while memory is critical, checked once a
192
+ minute: every shell at its prompt that nobody has looked at for ten
193
+ minutes, and one terminal running something, the one looked at least
194
+ recently. Never one looked at inside those ten minutes. */
195
+ async relieveMemory(){
196
+ if(!this.readPressure||!this.sessions.size)return [];
197
+ let pressure='normal';
198
+ try{pressure=await this.readPressure();}catch{}
199
+ if(pressure!=='critical')return [];
200
+ const cutoff=this.now()-this.watchedMs,idle=[];let busy=null,at=Infinity;
201
+ for(const [key,s] of this.sessions){
202
+ if(s.exited||(s.lastReadAt??0)>cutoff)continue;
203
+ if(!this.busy(s)){idle.push(key);continue;}
204
+ if((s.lastReadAt??0)<at){busy=key;at=s.lastReadAt??0;}
205
+ }
206
+ const ended=busy?[...idle,busy]:idle;
207
+ for(const key of ended)this.retire(key,ENDED.memory);
208
+ return ended;
209
+ }
210
+ // `given` is where to open it when the caller has already worked that out:
211
+ // the terminal host (./terminal-host.mjs) cannot read the store.
212
+ open(key,given){
131
213
  if(this.sessions.has(key))return this.read(key,0);
132
214
  if(this.sessions.size>=MAX_TERMINALS){
133
215
  /*
@@ -139,34 +221,58 @@ export class TaskTerminals {
139
221
  and says so in those words. */
140
222
  const victim=this.oldestIdle();
141
223
  if(victim===null)throw Error('20 terminals are open and every one of them is running something. Stop one of those commands before opening another terminal.');
142
- this.close(victim);
143
- this.reaped.set(victim,'This shell was closed to make room for a newer terminal.');
224
+ this.retire(victim,'This shell was closed to make room for a newer terminal.');
144
225
  }
145
- const place=this.resolve(key);
226
+ const place=given??this.resolve(key);
146
227
  const cwd=typeof place==='string'?place:place.cwd;
147
228
  const command=place?.command;
148
- const pty=this.spawn(command?.file??this.shell,command?.args??['-l'],{cwd,env:{...this.env,...command?.env},cols:80,rows:24,name:'xterm-256color'});
229
+ const pty=this.spawn(command?.file??place?.shell??this.shell,command?.args??['-l'],{cwd,env:{...(place?.env??this.env),...command?.env},cols:80,rows:24,name:'xterm-256color'});
149
230
  let resolveExit;const exitPromise=new Promise(resolve=>{resolveExit=resolve;});
150
- const session={token:randomUUID(),pty,cwd,buffer:'',offset:0,exitCode:null,exited:false,exitPromise,lastReadAt:this.now()};this.sessions.set(key,session);
231
+ const session={token:randomUUID(),pty,cwd,buffer:'',offset:0,exitCode:null,exited:false,exitPromise,lastReadAt:this.now(),waiters:new Set()};this.sessions.set(key,session);
151
232
  /*
152
233
  * AND IT SAYS WHAT HAPPENED TO THE LAST ONE. Without this line, reopening a
153
234
  terminal the app tidied away looks exactly like the app losing her
154
235
  scrollback, which is the same event read as a bug. Dim, one line, above a
155
- real prompt. */
156
- const why=this.reaped.get(key);
157
- if(why!==undefined){this.reaped.delete(key);session.buffer=`\x1b[2m[${why} This is a new one.]\x1b[0m\r\n`;}
236
+ real prompt, under whatever the old shell last showed. */
237
+ const gone=this.reaped.get(key);
238
+ if(gone!==undefined){
239
+ this.reaped.delete(key);
240
+ const old=gone.screen?`${gone.screen}\x1b[0m${gone.screen.endsWith('\n')?'':'\r\n'}`:'';
241
+ session.buffer=`${old}\x1b[2m[${gone.why}${old?' Its output is above.':''} This is a new one.]\x1b[0m\r\n`;
242
+ }
158
243
  if(place?.notice)session.buffer+=`\x1b[2m[${place.notice}]\x1b[0m\r\n`;
159
- pty.onData(data=>{session.buffer+=data;const trim=Math.max(0,session.buffer.length-LIMIT);session.buffer=session.buffer.slice(trim);session.offset+=trim;});
160
- pty.onExit(({exitCode})=>{session.exited=true;session.exitCode=exitCode;resolveExit();});
244
+ pty.onData(data=>{session.buffer+=data;const trim=Math.max(0,session.buffer.length-LIMIT);session.buffer=session.buffer.slice(trim);session.offset+=trim;this.wake(session);});
245
+ pty.onExit(({exitCode})=>{session.exited=true;session.exitCode=exitCode;resolveExit();this.wake(session);});
161
246
  return this.read(key,0);
162
247
  }
163
248
  get(key){const s=this.sessions.get(key);if(!s)throw Error('Terminal session ended. Open a new terminal to continue.');return s;}
164
249
  // EVERY READ IS THE PANEL SAYING IT IS STILL THERE. This one line is the whole
165
250
  // attention signal the sweep runs on; see the note on the class.
166
- read(key,offset=0){const s=this.get(key);if(!Number.isSafeInteger(offset)||offset<0)throw Error('Invalid terminal cursor.');s.lastReadAt=this.now();const end=s.offset+s.buffer.length;return {token:s.token,cwd:s.cwd,data:s.buffer.slice(Math.max(0,offset-s.offset)),offset:end,truncated:offset<s.offset,exited:s.exited,exitCode:s.exitCode,process:s.exited?'Exited':s.pty.process||'Shell'};}
251
+ read(key,offset=0,wait=0){
252
+ const s=this.get(key);if(!Number.isSafeInteger(offset)||offset<0)throw Error('Invalid terminal cursor.');s.lastReadAt=this.now();
253
+ const end=s.offset+s.buffer.length;
254
+ /*
255
+ * A READ MAY WAIT FOR THE SHELL'S NEXT OUTPUT, and comes back the moment
256
+ there is some. The panel used to ask every 150ms, so the echo of a key
257
+ sat in this buffer until the next ask: deleted characters stayed on her
258
+ screen after she deleted them (w-16e47d0836). `live` tells the panel this
259
+ main can wait, so it asks again at once instead of in 150ms; a main
260
+ older than its renderer (⌘R ahead of a restart) answers without it. */
261
+ if(wait>0&&!s.exited&&offset>=end)return new Promise((resolve,reject)=>{
262
+ const done=()=>{clearTimeout(timer);s.waiters.delete(done);try{if(this.sessions.get(key)!==s)throw Error('Terminal session ended. Open a new terminal to continue.');resolve({...this.read(key,offset),live:true});}catch(e){reject(e);}};
263
+ const timer=setTimeout(done,Math.min(wait,READ_WAIT_MS));
264
+ s.waiters.add(done);
265
+ });
266
+ s.seenTo=end;
267
+ const state={token:s.token,cwd:s.cwd,data:s.buffer.slice(Math.max(0,offset-s.offset)),offset:end,truncated:offset<s.offset,exited:s.exited,exitCode:s.exitCode,process:s.exited?'Exited':s.pty.process||'Shell'};
268
+ return wait>0?{...state,live:true}:state;
269
+ }
270
+ // Answers every waiting read. Deferred a turn so the pieces of one echo the
271
+ // pty hands over back to back go out as one answer.
272
+ wake(s){if(!s.waiters.size||s.waking)return;s.waking=true;setImmediate(()=>{s.waking=false;for(const done of [...s.waiters])done();});}
167
273
  write(key,data){const s=this.get(key);if(s.exited)throw Error('This shell has exited. Start a new session.');if(typeof data!=='string'||data.length>65536)throw Error('Terminal input is too large. Paste at most 64 KB at a time.');s.pty.write(data);}
168
274
  resize(key,cols,rows){if(!Number.isInteger(cols)||!Number.isInteger(rows)||cols<2||cols>500||rows<2||rows>300)throw Error('Invalid terminal size.');const s=this.get(key);if(!s.exited)s.pty.resize(cols,rows);}
169
- close(key){const s=this.sessions.get(key);if(s){if(!s.exited)this.disposeProcess(s.pty);this.sessions.delete(key);}}
275
+ close(key){const s=this.sessions.get(key);if(s){if(!s.exited)this.disposeProcess(s.pty);this.sessions.delete(key);for(const done of [...s.waiters])done();}}
170
276
  async shutdown(){
171
277
  const exits=[...this.sessions.values()].map(s=>s.exitPromise);
172
278
  this.dispose();
@@ -17,28 +17,58 @@ import { listSharedProjects, joinSharedProject, markShared, makeDirect } from '.
17
17
  import { firstSentence } from '../../shared/thread-cards.mjs';
18
18
  import { cardsFor } from '../../shared/thread-cards.mjs';
19
19
  import fs from 'node:fs';
20
+ import { saveAttachments, attachmentMarkdown } from '../save-attachments.mjs';
20
21
  import { hasLapsed, statusEnds } from '../../shared/team-status.mjs';
22
+ import QRCode from 'qrcode';
23
+ import { createPhoneInbox } from '../phone-inbox.mjs';
24
+ import { SNAPSHOT_LIMITS, phoneProjectsOf, phoneEligible } from '../../shared/phone-snapshot.mjs';
25
+ import { createRooms } from './rooms.mjs';
21
26
 
22
27
  // `signingIn` is { url } while a Google sign-in waits on the browser, so the
23
28
  // page can offer that link again rather than a button that does nothing.
24
- const EMPTY = { configured: false, started: false, signedIn: false, signingIn: null, me: null, team: null, invites: [], sent: [], people: [], cards: [], lastSyncAt: null, error: null };
29
+ const EMPTY = { configured: false, canSignIn: false, started: false, settling: false, signedIn: false, signingIn: null, me: null, team: null, invites: [], sent: [], people: [], cards: [], lastSyncAt: null, error: null };
30
+
31
+ // With Alpha features › Multiplayer off the window gets the sign-in the phone
32
+ // needs and no team, so it draws the single-person app.
33
+ export function teamForWindow(state, multiplayer) {
34
+ if (multiplayer || !state) return state;
35
+ const { rooms, ...rest } = state;
36
+ return { ...rest, configured: false, team: null, invites: [], sent: [], people: [], cards: [] };
37
+ }
25
38
 
26
39
  export function createTeamService({
27
40
  session, store, disk, accountRoot, stateFile, onChange = () => {}, log = () => {}, intervalMs = 5000, startRetryMs = 1500,
28
41
  // The clock, so a test can move past the moment a status runs out.
29
42
  now = Date.now,
43
+ // HER PHONE WITH THE MAC SHUT (docs/cloud/PHONE-PLAN.md, step 1): which
44
+ // projects she put on it (slugs), how a command from it is done, and where
45
+ // this Mac remembers what it sent. Needs no team, only a sign-in.
46
+ phone = null,
47
+ // ROOMS (alpha, docs/team/ROOMS-PLAN.md): { enabled, device, presenceFacts, prefsFile }.
48
+ rooms: roomsSetup = null,
30
49
  }) {
31
- let state = { ...EMPTY, configured: !!session?.configured };
50
+ // The phone site's address, only an https one, and its code for Settings.
51
+ const site = typeof phone?.site === 'string' && /^https:\/\/[^\s/]+(\/[^\s]*)?$/.test(phone.site) ? phone.site : null;
52
+ let siteQr = null;
53
+ // `settling` holds the window's boot screen until the team is known, so it
54
+ // never draws the single-person app first and then redraws with the team.
55
+ // `canSignIn` survives teamForWindow, so a build with Multiplayer off still asks.
56
+ let state = { ...EMPTY, configured: !!session?.configured, canSignIn: !!session?.configured, settling: !!session?.configured, phoneSite: site, phoneSiteQr: null };
32
57
  let backend = null;
33
58
  let sync = null;
34
59
  let timer = null;
35
60
  let unsubscribe = null;
36
61
  let pending = null;
62
+ let phoneInbox = null;
37
63
  // The sync cursors and which team this Mac is in, in one file.
38
64
  let local = null;
39
65
  const kept = () => (local ??= fileSyncState(stateFile, fs));
40
66
 
41
67
  const set = (patch) => { state = { ...state, ...patch }; onChange(); };
68
+ const rooms = roomsSetup ? createRooms({
69
+ backend: () => backend, me: () => state.me, team: () => state, now, log, onChange, ...roomsSetup,
70
+ }) : null;
71
+ if (site) QRCode.toString(site, { type: 'svg', margin: 1, errorCorrectionLevel: 'M' }).then((qr) => { siteQr = qr; set({ phoneSiteQr: qr }); }).catch(() => {});
42
72
 
43
73
  function products() {
44
74
  try { return store.listProducts(); } catch { return []; }
@@ -139,12 +169,71 @@ export function createTeamService({
139
169
  set({ error: String(err?.message ?? err) });
140
170
  return null;
141
171
  } finally {
172
+ // On its own, so a team sync that failed does not hold up her phone,
173
+ // nor her phone the team.
174
+ await phoneOnce();
175
+ await rooms?.tick().catch((err) => log(`rooms: ${err.message}`));
142
176
  pending = null;
143
177
  }
144
178
  });
145
179
  return pending;
146
180
  }
147
181
 
182
+ async function phoneOnce() {
183
+ // Not before the app's handlers exist: a command taken then would be
184
+ // marked refused for no reason of hers.
185
+ if (!phoneInbox || phone.ready?.() === false) return;
186
+ try {
187
+ await phoneInbox.syncOnce();
188
+ if (state.phoneError) set({ phoneError: null });
189
+ } catch (err) {
190
+ set({ phoneError: String(err?.message ?? err) });
191
+ }
192
+ }
193
+
194
+ function makePhoneInbox() {
195
+ if (!phone) return null;
196
+ const productOf = (slug) => products().find((p) => p.slug === slug) ?? null;
197
+ // WHO IT SENDS AS, fixed now, and whether that is still who is signed in
198
+ // (Codex's second review, finding 1).
199
+ const owner = state.me?.id;
200
+ const signedInAs = backend;
201
+ if (!owner) return null;
202
+ let inbox = null;
203
+ inbox = createPhoneInbox({
204
+ backend,
205
+ owner,
206
+ alive: () => phoneInbox === inbox && backend === signedInAs && state.me?.id === owner,
207
+ device: phone.device?.() ?? null,
208
+ projects: () => {
209
+ const on = new Set(phoneProjectsOf(phone.projects(), owner, phoneEligible(products())));
210
+ return products().filter((p) => on.has(p.slug)).map((p) => ({ slug: p.slug, name: p.name || p.slug, dir: p.dir || null, repoPath: p.repoPath || null }));
211
+ },
212
+ listItems: (slug) => { const p = productOf(slug); try { return p ? disk.readWorkItems(p.dir) : []; } catch { return []; } },
213
+ historyOf: (slug, id, { all = false } = {}) => { try { return store.readHistory(slug, id, { max: all ? Number.MAX_SAFE_INTEGER : SNAPSHOT_LIMITS.lines * 4 }); } catch { return []; } },
214
+ apply: phone.apply,
215
+ liveOf: (slug, id) => phone.live?.(slug, id) ?? null,
216
+ approvalsOf: () => phone.approvals?.() ?? [],
217
+ readSettings: phone.readSettings ?? null,
218
+ version: phone.version ?? '',
219
+ idleMs: phone.idleMs ?? null,
220
+ // The lid shut: projects whose threads carry on in the cloud, and runs it started (shared/cloud-kit.mjs).
221
+ carrying: phone.carrying ?? null,
222
+ kitsFor: phone.kitsFor ?? null,
223
+ adopt: phone.adopt ?? null,
224
+ cloudReplies: phone.cloudReplies ?? null,
225
+ runHome: phone.runHome ?? null,
226
+ newThreadKits: phone.newThreadKits ?? null,
227
+ adoptNew: phone.adoptNew ?? null,
228
+ // One file per person: what this Mac sent up for one account is not
229
+ // what the next one to sign in here has.
230
+ stateFile: phone.stateFile.replace(/(\.json)?$/, `.${owner}.json`),
231
+ now,
232
+ log,
233
+ });
234
+ return inbox;
235
+ }
236
+
148
237
  function stopLoop() {
149
238
  if (timer) clearInterval(timer);
150
239
  timer = null;
@@ -165,9 +254,12 @@ export function createTeamService({
165
254
  // no team cloud. So the single-person app opened on no projects at all.
166
255
  // Everybody gets one at boot now (main/main.mjs), signed in or not.
167
256
  await refreshTeam();
257
+ if (state.settling) set({ settling: false });
258
+ phoneInbox = makePhoneInbox();
168
259
  sync = createTeamSync({
169
260
  backend,
170
261
  disk,
262
+ log,
171
263
  state: kept(),
172
264
  // ARCHIVED ONES COUNT AS HERE. Asked of the visible list, a shared
173
265
  // project you archived read as missing and was joined again beside it.
@@ -184,7 +276,10 @@ export function createTeamService({
184
276
  }
185
277
 
186
278
  return {
187
- state: () => state,
279
+ state: () => (rooms ? { ...state, rooms: rooms.state() } : state),
280
+ rooms,
281
+ /** What the desktop's banner was told arrived, for her phone's alerts (main/phone-alerts.mjs). */
282
+ phoneArrivals: (list, needsYou) => phoneInbox?.arrivals(list, needsYou),
188
283
 
189
284
  // THREE TRIES AT START, because one dropped request left the app signed
190
285
  // out until it was restarted (measured 2026-10-01 on a loaded Mac: the
@@ -199,10 +294,10 @@ export function createTeamService({
199
294
  try {
200
295
  const b = await session.restore();
201
296
  if (b) await signedIn(b);
202
- set({ started: true });
297
+ set({ started: true, settling: false });
203
298
  return state;
204
299
  } catch (err) {
205
- set({ error: String(err?.message ?? err) });
300
+ set({ error: String(err?.message ?? err), settling: false });
206
301
  if (attempt < 2) await new Promise((r) => setTimeout(r, startRetryMs * (attempt + 1)));
207
302
  }
208
303
  }
@@ -251,13 +346,16 @@ export function createTeamService({
251
346
 
252
347
  async signOut() {
253
348
  stopLoop();
349
+ // While still signed in, so this Mac can say it has gone.
350
+ await rooms?.stop().catch(() => {});
254
351
  await session.signOut?.();
255
352
  disk.setLineAuthor(null);
256
353
  delete process.env.AGENTBOX_PERSON_ID;
257
354
  backend = null;
258
355
  sync = null;
356
+ phoneInbox = null;
259
357
  // Signed out is a known answer, so the window shows the sign-in page at once.
260
- state = { ...EMPTY, configured: !!session?.configured, started: true };
358
+ state = { ...EMPTY, configured: !!session?.configured, started: true, phoneSite: site, phoneSiteQr: siteQr };
261
359
  onChange();
262
360
  return state;
263
361
  },
@@ -372,7 +470,7 @@ export function createTeamService({
372
470
  * later is not a chance to lift it. Nothing picked means nothing sent, so
373
471
  * the row keeps composeItem's 0, which the inbox reads as Medium.
374
472
  */
375
- async message(toIn, body, priority) {
473
+ async message(toIn, body, priority, files = []) {
376
474
  if (!backend || !state.team || !state.me) throw new Error('start or join a team first');
377
475
  const me = state.me.id;
378
476
  // One person or a few: the same conversation for the same people.
@@ -381,8 +479,9 @@ export function createTeamService({
381
479
  if (others.length > 11) throw new Error('a message goes to at most eleven people');
382
480
  if (others.some((p) => !state.people.some((q) => q.id === p))) throw new Error('that person is not on your team');
383
481
  const to = others.length === 1 ? others[0] : others;
384
- const text = String(body ?? '').trim();
385
- if (!text) throw new Error('a message needs some words');
482
+ const words = String(body ?? '').trim();
483
+ const carried = (Array.isArray(files) ? files : []).filter((f) => f && (f.dataBase64 || f.srcPath));
484
+ if (!words && !carried.length) throw new Error('a message needs some words');
386
485
  // THE RECORD IS REUSED ONLY IF IT IS EXACTLY THE TWO OF YOU AND ONE OF
387
486
  // YOU MADE IT. Found by review 2026-10-01: a teammate could make a
388
487
  // "direct" record holding you and somebody else, and the next message
@@ -404,6 +503,10 @@ export function createTeamService({
404
503
  }
405
504
  product = products().find((p) => p.dir === made.dir) ?? { slug: made.slug, dir: made.dir };
406
505
  }
506
+ // ITS FILES, saved into the conversation now that there is one, and
507
+ // linked under the words the way the reply box links them. The sync
508
+ // sends each one on before the line that names it (./sync.mjs).
509
+ const text = [words, attachmentMarkdown(saveAttachments(product.dir, carried))].filter(Boolean).join('\n\n');
407
510
  // ONE CONVERSATION PER PERSON (decided 2026-10-01 from user research:
408
511
  // people expect to click a name and see what was said before, and a
409
512
  // subject line on every message between two coworkers makes talking
@@ -411,7 +514,8 @@ export function createTeamService({
411
514
  // conversation the two of you already have, reopened if it was put away,
412
515
  // and only the first message ever starts one. Work stays in threads; this
413
516
  // is talk.
414
- const title = firstSentence(text, 90) || text.slice(0, 90);
517
+ // Named by its words; a message that is only files is named by them.
518
+ const title = firstSentence(words, 90) || words.slice(0, 90) || (carried.length === 1 ? String(carried[0].name ?? 'A file') : `${carried.length} files`);
415
519
  const convo = conversationIn(product.slug);
416
520
  if (convo) {
417
521
  store.answerItem(product.slug, convo.id, { answer: text });