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
package/main/ipc.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { openSourceFile } from './open-source-file.mjs';
2
2
  import {submitReply} from './live-replies.mjs';
3
+ import { whyItCannotMove } from '../shared/move-thread.mjs';
3
4
  import { createEngineSetup } from './engine-setup.mjs';
4
5
  // IPC: a thin, typed-by-convention surface. The renderer asks; the main
5
6
  // process derives from files and answers. Pushes go one way: "state changed,
@@ -20,13 +21,15 @@ import { createEngineSetup } from './engine-setup.mjs';
20
21
  // process can hand over its own, and the same 78 channels then answer a
21
22
  // browser over http without one line of this file being written twice.
22
23
  import {terminalSenderAllowed} from './terminal-access.mjs';
23
- import {TaskTerminals,terminalPlace} from './task-terminals.mjs';
24
+ import {TaskTerminals,terminalPlace,terminalEnv} from './task-terminals.mjs';
25
+ import {TerminalHosts,taskClosed} from './terminal-hosts.mjs';
24
26
  import {SETTINGS_TERMINAL} from '../shared/settings-terminal.mjs';
25
27
  import {AgentUpdates,installedAgent} from './agent-updates.mjs';
26
28
  import {refreshAgentModels} from './refresh-agent-models.mjs';
27
29
  import {readInstruction,writeInstruction,listVersions,readVersion} from './instruction-settings.mjs';
28
30
  import path from 'node:path';
29
31
  import os from 'node:os';
32
+ import { looksLikeClaudeCode } from '../shared/claude-code-shape.mjs';
30
33
  import fsWatch from 'node:fs';
31
34
  import { listFolders } from './folders.mjs';
32
35
  import { feedbackUrl, sendFeedback } from './feedback.mjs';
@@ -40,16 +43,20 @@ import { readDoc, writeDoc } from './doc-file.mjs';
40
43
  import { readChange } from './code-change.mjs';
41
44
  import { findFile, readCodeFile, writeCodeFile } from './code-file.mjs';
42
45
  import { readNote, writeNote } from './rail-note.mjs';
43
- import { addClaudeAccount, addCodexAccount, outsideAgentsMode, permissionMode, pointAtEngineBin, readSettings, recheckClaude, recheckCodex, searchForEngineBin, setProjectSetting, setWorkspaceSetting } from './settings.mjs';
46
+ import { teamForWindow } from './team/index.mjs';
47
+ import { addClaudeAccount, addCodexAccount, alphaFeatures, cornerTagTurnedOff, countsTurnedOff, lookAgainFor, outsideAgentsMode, permissionMode, pointAtEngineBin, readSettings, searchForEngineBin, setProjectSetting, setWorkspaceSetting } from './settings.mjs';
44
48
  import { ICON_KINDS, clearProjectIcon, setProjectIcon, setProjectName } from './project-identity.mjs';
45
49
  import { ClaudeUsage } from './claude-usage.mjs';
50
+ import { AccountUsage } from './account-usage.mjs';
51
+ import { effectiveProfiles } from './account-discovery.mjs';
46
52
  import { usageEngine } from '../shared/usage.mjs';
47
53
  import { DEFAULT_ENGINE, isEngine } from '../shared/engines.mjs';
48
54
  import * as agents from './agents.mjs';
49
55
  import { findAgentFolders, readAgentFiles, readFolderAgents } from './agent-files.mjs';
50
- import { readCodexThreads } from './codex-threads.mjs';
56
+ import { readCodexThreadsOffThread } from './codex-threads.mjs';
51
57
  import { appSessionFolders } from './agent-homes.mjs';
52
- import { readImportableThreads } from './importable-threads.mjs';
58
+ import { readImportableThreads, readSearchableThreads } from './importable-threads.mjs';
59
+ import { readChatEnds } from './agent-sessions.mjs';
53
60
  import { markImported } from '../shared/agent-import.mjs';
54
61
  import { codexIdOf, importChoice, isCodexImportRow, isNotImportedRow } from '../shared/codex-import.mjs';
55
62
  import { checkProjectFolder } from '../shared/project-folder-check.mjs';
@@ -61,6 +68,9 @@ import { machineryPath } from './store/home.mjs';
61
68
  import { EVENT_NAMES } from '../shared/analytics-events.mjs';
62
69
  import { Name } from '../shared/product-name.mjs';
63
70
  import { handedOnByReply } from '../shared/team-rules.mjs';
71
+ import { closeRefusal, closedAnywayNote } from '../shared/not-shipped.mjs';
72
+ import { saveAttachments } from './save-attachments.mjs';
73
+ import { stageAttachment } from './stage-attachment.mjs';
64
74
 
65
75
  // WHAT A WORKER IS TOLD WHEN HER DECISION COULD NOT BE RECORDED.
66
76
  const SPOOL_REFUSED = `${Name} could not record the founder's decision, so nothing was allowed. Finish what your grants cover and file the rest as a review.`;
@@ -97,6 +107,9 @@ const NO_UPDATER = {
97
107
  move: () => ({ moved: false, error: null }),
98
108
  };
99
109
 
110
+ const TEAMMATE_OPS = new Set(['create', 'describe', 'start', 'update', 'remove', 'post', 'goAhead', 'remember', 'forget']);
111
+ const ROOM_OPS = new Set(['refresh', 'send', 'markRead', 'markUnread', 'react', 'edit', 'deleteMessage', 'putFile', 'file', 'older', 'saveProfile', 'goInvisible', 'comeBack', 'link', 'revokeLink', 'preview', 'listed', 'invitesSent', 'joinByLink', 'joinListed', 'acceptInvite', 'invite', 'create', 'rename', 'leave', 'remove', 'pin', 'mute']);
112
+
100
113
  export function registerIpc({ store, supervisor, config, window, analytics = NO_ANALYTICS, docGrants = null, updater = NO_UPDATER, host, team = null }) {
101
114
  // The three Electron things, or a plain Node stand-in for them. There is no
102
115
  // default: a caller that forgets says so here rather than throwing eighty
@@ -167,8 +180,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
167
180
  // THROW, out of an ipc handler and out of the ⌘Y `before-input-event`
168
181
  // listener, which is worse than either outcome here. A Codex worker is parked
169
182
  // on a promise in this process, so it is told the refusal directly and
170
- // unblocks; a Claude Code worker has only the file, so it denies on its own
171
- // deadline, which is the same answer fifteen minutes later. Nothing on this
183
+ // unblocks; a Claude Code worker has only the file, so it goes on waiting on
184
+ // its own card, which stays on screen and answerable. Nothing on this
172
185
  // path can turn a failure into an allow, and the deny falls out of the SAME
173
186
  // two calls the yes does rather than a second door beside them: `allow` and
174
187
  // `note` stop meaning what she pressed the moment the write fails, and start
@@ -221,7 +234,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
221
234
  try { return store.listProducts().some((p) => p.slug === slug && p.practice === true); } catch { return false; }
222
235
  };
223
236
 
224
- ipcMain.handle('zero:approve', (_e, { id, allow, note }) => {
237
+ // From her phone the answer names the card it drew (main/phone-inbox.mjs),
238
+ // and a request rewritten since gets nothing.
239
+ ipcMain.handle('zero:approve', (_e, { id, allow, note, card }) => {
240
+ if (card !== undefined && !approvals.cardStillShown(config.storeRoot, id, card)) return { ok: false, error: 'the card changed after your phone showed it, so nothing was allowed' };
225
241
  return { ok: answerApproval(id, allow, note) };
226
242
  });
227
243
 
@@ -265,15 +281,42 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
265
281
  bin: () => config.claudeBin,
266
282
  // The account marked In use, asked each time so a switch is read at once.
267
283
  account: () => supervisor.usageAccount(),
268
- onChange: push,
284
+ onChange: () => {
285
+ const reading = usage.peek();
286
+ if (reading) supervisor.noteUsageReading(reading.profile, reading);
287
+ push();
288
+ },
269
289
  });
270
290
 
271
- ipcMain.handle('zero:refresh-usage', async event => {
291
+ ipcMain.handle('zero:refresh-usage', async (event, { engine = DEFAULT_ENGINE } = {}) => {
272
292
  if (event.senderFrame?.parent || (event.sender && event.sender !== window.webContents)) throw Error('Usage refresh is only available in the main app window.');
293
+ // Codex is asked of this computer and not of OpenAI; main/codex-usage.mjs
294
+ // says why there is no explicit read.
295
+ if (engine !== DEFAULT_ENGINE) {
296
+ const reading = supervisor.refreshCodexUsage();
297
+ return reading ? { ok: true, reading: { engine, ...reading } } : { ok: false, error: 'Codex has reported no usage on this computer yet.' };
298
+ }
273
299
  const result = await usage.refreshNow();
274
300
  return result.reading ? { ...result, reading: { engine: DEFAULT_ENGINE, ...result.reading } } : result;
275
301
  });
276
302
 
303
+ // Every Claude login's own reading, asked for only by the Settings page.
304
+ const accountUsage = new AccountUsage({
305
+ bin: () => config.claudeBin,
306
+ env: () => supervisor._workerEnv(DEFAULT_ENGINE),
307
+ onReading: (profile, reading) => supervisor.noteUsageReading(profile, reading),
308
+ });
309
+ ipcMain.handle('zero:account-usage', async (event, { refresh = false, engine = DEFAULT_ENGINE } = {}) => {
310
+ if (event.senderFrame?.parent || (event.sender && event.sender !== window.webContents)) throw Error('Usage is only available in the main app window.');
311
+ // Codex's are files on this computer, read on every ask.
312
+ if (engine === 'codex') return { readings: supervisor.codexAccountUsage() };
313
+ const profiles = effectiveProfiles(config.authProfiles, { home: config.home });
314
+ // The cloud's logins too; a look takes a sandbox session, so the page is
315
+ // not held for it and redraws off the snapshot when it lands.
316
+ void supervisor.lookAtCloudLogins({ force: !!refresh });
317
+ return { readings: await accountUsage.readAll(profiles.length ? profiles : ['default'], { refresh: !!refresh }) };
318
+ });
319
+
277
320
  // Agents this run has already counted. In memory on purpose: a fresh launch
278
321
  // seeing the same agent again is a fresh fact about the app being used.
279
322
  const seenAgents = new Set();
@@ -334,12 +377,25 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
334
377
  if(product===SETTINGS_TERMINAL.product&&id===SETTINGS_TERMINAL.id){
335
378
  return pendingLogin ?? config.home ?? os.homedir();
336
379
  }
380
+ return taskPlace(product,id);
381
+ }});
382
+ const taskPlace=(product,id)=>{
337
383
  const agent=id.startsWith('agent:')?liveAgents().find(a=>'agent:'+a.pid===id && (a.product??'')===product):null;
338
384
  return terminalPlace({store,supervisor,product,id,agent});
339
- }});
385
+ };
386
+ /*
387
+ * A TASK'S TERMINAL LIVES AS LONG AS THE TASK IS OPEN, in a process that
388
+ outlives the app (main/terminal-host.mjs). The sign-in terminal on
389
+ Settings belongs to no task and stays in the app. */
390
+ const taskTerminals=new TerminalHosts({local:terminals,logFile:path.join(supervisor.userDir,'terminal-host.log'),log:line=>console.warn(`zero: ${line}`)});
391
+ // A task's folder is not deleted while its terminal is open (supervisor.mjs, `_folderInUse`).
392
+ supervisor.terminalOpen=itemId=>taskTerminals.hasTerminal(itemId);
393
+ const lookAtTerminals=()=>taskTerminals.endClosed(taskClosed(store)).catch(e=>console.warn('zero: terminals:',e.message));
394
+ void taskTerminals.ready().then(lookAtTerminals);
395
+ setInterval(lookAtTerminals,60_000).unref?.();
340
396
  const agentUpdates=new AgentUpdates({
341
- installation:async engine=>{if(engine==='claude')recheckClaude(config);else recheckCodex(config);return installedAgent(engine,config);},
342
- refresh:async engine=>{if(engine==='claude')recheckClaude(config);else recheckCodex(config);await refreshAgentModels(engine,config,supervisor._codexHome());if(engine==='codex')for(const profile of supervisor._codexProfiles())supervisor._releaseIdleCodex(profile);},
397
+ installation:async engine=>{await lookAgainFor(engine,config);return installedAgent(engine,config);},
398
+ refresh:async engine=>{await lookAgainFor(engine,config);await refreshAgentModels(engine,config,supervisor._codexOfferHomes());if(engine==='codex')for(const profile of supervisor._codexProfiles())supervisor._releaseIdleCodex(profile);},
343
399
  });
344
400
  ipcMain.handle('zero:agent-update',async(event,{engine,action}={})=>{
345
401
  if(event.senderFrame?.parent||(event.sender&&event.sender!==window.webContents))throw Error('Updates are only available in the main app window.');
@@ -348,7 +404,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
348
404
  if(action==='recheck')return agentUpdates.check(engine,true);
349
405
  if(action==='start')return agentUpdates.start(engine);
350
406
  if(action==='status')return agentUpdates.status(engine);
351
- if(action==='refresh'){await refreshAgentModels(engine,config,supervisor._codexHome());return true;}
407
+ if(action==='refresh'){await refreshAgentModels(engine,config,supervisor._codexOfferHomes());return true;}
352
408
  throw Error('Unknown update action.');
353
409
  });
354
410
  let terminalQuitPending=false,terminalQuitReady=false;
@@ -357,13 +413,13 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
357
413
  event.preventDefault();
358
414
  if(!terminalQuitPending){terminalQuitPending=true;void Promise.all([terminals.shutdown(),agentUpdates.shutdown()]).finally(()=>{terminalQuitReady=true;app.quit();});}
359
415
  });
360
- ipcMain.handle('zero:terminal',(_event,payload={})=>{
416
+ ipcMain.handle('zero:terminal',async(_event,payload={})=>{
361
417
  if(_event.senderFrame?.parent || (_event.sender && _event.sender!==window.webContents)) throw Error('Terminal is only available in the main app window.');
362
418
  const {product,id,action,data,cols,rows,offset,wait}=payload;
363
419
  if(typeof product!=='string'||typeof id!=='string'||!id||id.length>300)throw Error('Invalid task.');
364
420
  const key=JSON.stringify({product,id});
365
421
  if(product==='@agent-update')return agentUpdates.terminal(id,payload);
366
- switch(action){
422
+ if(product===SETTINGS_TERMINAL.product&&id===SETTINGS_TERMINAL.id)switch(action){
367
423
  case 'open':return terminals.open(key);
368
424
  case 'read':return terminals.read(key,offset,wait);
369
425
  case 'write':terminals.write(key,data);return true;
@@ -371,6 +427,14 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
371
427
  case 'close':terminals.close(key);return true;
372
428
  default:throw Error('Unknown terminal action.');
373
429
  }
430
+ switch(action){
431
+ case 'open':return taskTerminals.open(key,{...taskPlace(product,id),env:terminalEnv()});
432
+ case 'read':return taskTerminals.read(key,offset,wait);
433
+ case 'write':await taskTerminals.write(key,data);return true;
434
+ case 'resize':await taskTerminals.resize(key,cols,rows);return true;
435
+ case 'close':await taskTerminals.close(key);return true;
436
+ default:throw Error('Unknown terminal action.');
437
+ }
374
438
  });
375
439
 
376
440
  // The Done and All tabs' next page of finished threads the snapshot's 8 MB
@@ -410,6 +474,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
410
474
  // draw -- which is what a Codex-only workflow paid every five minutes until
411
475
  // now. The Codex side never starts anything at all (main/codex-usage.mjs).
412
476
  const reading = usageFor === DEFAULT_ENGINE ? usage.read() : supervisor.codexUsage();
477
+ const alpha = alphaFeatures(config);
413
478
  return {
414
479
  products: store.listProducts(),
415
480
  // Each with the place she dragged it to, if she did (w-6e5b532a95).
@@ -417,7 +482,11 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
417
482
  // THE TEAM: who is signed in, their team and its people, and the
418
483
  // title-free activity of their private work. Null on a build with no
419
484
  // team cloud, which is the single-person app.
420
- team: team ? team.state() : null,
485
+ team: team ? teamForWindow(team.state(), alpha.multiplayer) : null,
486
+ // Which alpha features are on (Settings, Alpha features).
487
+ alpha,
488
+ // TEAMMATES: who they are, what each is on, and the one team chat.
489
+ teammates: alpha.teammates ? (() => { try { return store.teammatesView(items, now); } catch (err) { return { list: [], chat: [], runs: [], error: err.message }; } })() : null,
421
490
  agents: liveAgents(),
422
491
  approvals: approvals.listPending(config.storeRoot),
423
492
  supervisor: running,
@@ -451,6 +520,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
451
520
  // while fixes landed underneath it, and a boot-time check cannot see
452
521
  // that. See main/staleness.mjs.
453
522
  restartNeeded: restartNeeded(supervisor.appDir),
523
+ // WHERE THREADS RUN, for the cloud button beside the filters
524
+ // (renderer/src/cloud-menu.ts). `setUp` false draws no button at all.
525
+ cloud: supervisor.cloudStatus(),
454
526
  // HER RATE LIMIT, WHICH IS WHAT ACTUALLY CAPS THE FLEET. Null until the
455
527
  // first reading lands, and the corner draws nothing at all until then
456
528
  // rather than a pill full of dashes.
@@ -590,8 +662,19 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
590
662
  // answered at once (supervisor.sendNow).
591
663
  ipcMain.handle('zero:send-now', (_e, { product, id }) => supervisor.sendNow(product, id));
592
664
 
593
- ipcMain.handle('zero:answer', async (_e, { product, id, answer, status, priority, permissionMode, model, effort, now, inReplyTo }) => {
665
+ ipcMain.handle('zero:answer', async (_e, { product, id, answer, status, priority, permissionMode, engine, model, effort, now, inReplyTo, fromPhone, closeUnshipped }) => {
594
666
  if (isAgentRow(id)) return { ok: false };
667
+ // WORK THAT NEVER REACHED MAIN IS NOT CLOSED UNASKED (shared/not-shipped.mjs).
668
+ // The window asks first and says so with `closeUnshipped`; the phone and
669
+ // anything else are turned down with the reason.
670
+ if (status === 'done' && product && id) {
671
+ let row = null;
672
+ try { row = store.readItem(product, id); } catch { row = null; }
673
+ const refused = closeRefusal(row, { closeUnshipped });
674
+ if (refused) return refused;
675
+ const said = closeUnshipped ? closedAnywayNote(row) : null;
676
+ if (said) store.noteItem(product, id, said);
677
+ }
595
678
  // A level picked from ⌘K arrives here with no words, and gives way the
596
679
  // same as one picked on the summary (zero:thread-edit). A reply that
597
680
  // happens to carry its row's level does not.
@@ -618,7 +701,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
618
701
  if (choice === 'yes') {
619
702
  try {
620
703
  const { codexDirs } = appSessionFolders({ config, supervisor });
621
- thread = readCodexThreads({ codexDirs }).threads.find((t) => t.id === codexIdOf(row)) ?? null;
704
+ thread = (await readCodexThreadsOffThread({ codexDirs })).threads.find((t) => t.id === codexIdOf(row)) ?? null;
622
705
  } catch { thread = null; }
623
706
  }
624
707
  const out = store.answerCodexImport(product, id, choice, { thread });
@@ -637,7 +720,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
637
720
  // handing them over in one call and writing the model afterwards would be a
638
721
  // race the run usually wins, so she would pick a model and watch the old one
639
722
  // take the task. `answerItem` writes both, and it writes the model first.
640
- const out = await submitReply(supervisor,{product,id,answer,status,permissionMode,now:!!now},()=>store.answerItem(product, id, { answer, status, priority, permissionMode, model, effort, inReplyTo: thread }));
723
+ const out = await submitReply(supervisor,{product,id,answer,status,permissionMode,now:!!now},()=>store.answerItem(product, id, { answer, status, priority, permissionMode, engine, model, effort, inReplyTo: thread, fromPhone }));
641
724
  // A REPLY ON A ROW A TEAMMATE GAVE YOU HANDS IT BACK TO THEM, so the
642
725
  // conversation moves to their inbox instead of sitting in both
643
726
  // (shared/team-rules.mjs handedOnByReply). Archiving hands nothing on.
@@ -673,6 +756,13 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
673
756
  return { ok: true };
674
757
  });
675
758
 
759
+ // THREADS KEPT ON THIS MAC whatever the cloud is set to: id to true, or null.
760
+ ipcMain.handle('zero:set-thread-on-mac', (_e, { onMac }) => {
761
+ supervisor.setThreadOnMac(onMac);
762
+ push();
763
+ return { ok: true };
764
+ });
765
+
676
766
  // PINNED THREADS (w-20323283e6): id to true, or null to unpin.
677
767
  ipcMain.handle('zero:set-thread-pins', (_e, { pins }) => {
678
768
  supervisor.setThreadPins(pins);
@@ -712,14 +802,18 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
712
802
  // THE TEAM'S DOORS. Each answers { ok, team } or { ok: false, error } with
713
803
  // the error in words, so the window can say what went wrong where it
714
804
  // happened instead of a row silently not appearing.
805
+ const teamNow = () => teamForWindow(team.state(), alphaFeatures(config).multiplayer);
715
806
  const teamCall = (fn) => async (_e, arg) => {
716
807
  if (!team) return { ok: false, error: 'This build has no team cloud set up.' };
717
808
  try {
718
- await fn(arg ?? {});
809
+ const got = await fn(arg ?? {});
719
810
  push();
720
- return { ok: true, team: team.state() };
811
+ // A call that wrote a row (a message, into its conversation) says which,
812
+ // so the toast after it can open it with O (w-5513afd09a).
813
+ const wrote = typeof got?.id === 'string' && typeof got?.product === 'string';
814
+ return { ok: true, team: teamNow(), ...(wrote ? { made: { product: got.product, id: got.id } } : {}) };
721
815
  } catch (err) {
722
- return { ok: false, error: String(err?.message ?? err), team: team.state() };
816
+ return { ok: false, error: String(err?.message ?? err), team: teamNow() };
723
817
  }
724
818
  };
725
819
  // Signing in finishes in the browser, so the app comes back to the front
@@ -739,9 +833,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
739
833
  try {
740
834
  const out = await team.signUp(email, password, name);
741
835
  push();
742
- return { ok: true, team: team.state(), confirm: !!out?.confirm };
836
+ return { ok: true, team: teamNow(), confirm: !!out?.confirm };
743
837
  } catch (err) {
744
- return { ok: false, error: String(err?.message ?? err), team: team.state() };
838
+ return { ok: false, error: String(err?.message ?? err), team: teamNow() };
745
839
  }
746
840
  });
747
841
  ipcMain.handle('zero:team-create', teamCall(({ name }) => team.createTeam(name)));
@@ -759,7 +853,46 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
759
853
  ipcMain.handle('zero:team-sync', teamCall(() => team.syncNow()));
760
854
  // A MESSAGE TO A PERSON (approved 2026-10-01: people get messages, never
761
855
  // tasks). main/team/index.mjs keeps it between the two of them.
762
- ipcMain.handle('zero:team-message', teamCall(({ to, body, priority }) => team.message(to, body, priority)));
856
+ ipcMain.handle('zero:team-message', teamCall(({ to, body, priority, files }) => team.message(to, body, priority, files)));
857
+ // ROOMS (alpha): one door, and only these calls through it (main/team/rooms.mjs).
858
+ ipcMain.handle('zero:teammates', async (_e, { op, args = {} } = {}) => {
859
+ const mates = store.teammates;
860
+ if (!mates) return { ok: false, error: 'Teammates need a store.' };
861
+ if (!TEAMMATE_OPS.has(op)) return { ok: false, error: `no teammates call named ${op}` };
862
+ if (!alphaFeatures(config).teammates) return { ok: false, error: 'Teammates are an alpha feature. Turn them on in Settings, Alpha features.' };
863
+ try {
864
+ const a = args && typeof args === 'object' ? args : {};
865
+ let value;
866
+ if (op === 'create') {
867
+ value = mates.create({ ...a, engine: supervisor.engineOffered(a.engine) ?? null });
868
+ if (a.start !== false) value = mates.start(value.id);
869
+ } else if (op === 'describe') value = mates.describe({ ...a, engine: supervisor.engineOffered(a.engine) ?? null });
870
+ else if (op === 'start') value = mates.start(a.id);
871
+ else if (op === 'update') value = mates.update(a.id, { ...a.patch, ...(a.patch && 'engine' in a.patch ? { engine: supervisor.engineOffered(a.patch.engine) ?? null } : {}) });
872
+ else if (op === 'remove') value = mates.remove(a.id);
873
+ else if (op === 'post') value = mates.post({ text: a.text, to: a.to, fresh: a.fresh === true });
874
+ else if (op === 'goAhead') value = mates.goAhead(a.id, a.ref);
875
+ else if (op === 'remember') value = mates.remember(a.id, a.text, { from: 'you' });
876
+ else if (op === 'forget') value = mates.forget(a.id, a.memory);
877
+ supervisor.wake?.();
878
+ push();
879
+ return { ok: true, value: value ?? null };
880
+ } catch (err) {
881
+ return { ok: false, error: String(err?.message ?? err) };
882
+ }
883
+ });
884
+ ipcMain.handle('zero:rooms', async (_e, { op, args = [] } = {}) => {
885
+ const rooms = team?.rooms;
886
+ if (!rooms) return { ok: false, error: 'This build has no team cloud set up.' };
887
+ if (!ROOM_OPS.has(op)) return { ok: false, error: `no room call named ${op}` };
888
+ try {
889
+ const value = await rooms[op](...(Array.isArray(args) ? args : [args]));
890
+ push();
891
+ return { ok: true, value: value ?? null, rooms: rooms.state() };
892
+ } catch (err) {
893
+ return { ok: false, error: String(err?.message ?? err), rooms: rooms.state() };
894
+ }
895
+ });
763
896
  // AN EDIT TO A THREAD, MADE IN PLACE from its summary: the summary's lines,
764
897
  // who sees it, its priority and what it is linked to. Works with or without a
765
898
  // team, since every thread has a summary.
@@ -775,6 +908,26 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
775
908
  return { ok: false, error: String(err?.message ?? err) };
776
909
  }
777
910
  });
911
+ // MOVE A THREAD TO ANOTHER PROJECT (w-c9da359466). Only a thread nothing has
912
+ // ever run on: shared/move-thread.mjs says why, and the store asks again.
913
+ // The one fact only this side has is whether a worker is inside it right
914
+ // now, which no field on the row can stand in for.
915
+ ipcMain.handle('zero:move-item', (_e, { product, id, to }) => {
916
+ try {
917
+ const live = supervisor.status().running.some((r) => r.itemId === id);
918
+ if (live) {
919
+ const why = whyItCannotMove(store.readItem(product, id), { live: true });
920
+ return { ok: false, error: why };
921
+ }
922
+ const out = store.moveItem(product, id, to);
923
+ push();
924
+ supervisor.wake();
925
+ return { ok: true, ...out };
926
+ } catch (err) {
927
+ return { ok: false, error: String(err?.message ?? err) };
928
+ }
929
+ });
930
+
778
931
  // A TASK A TEAMMATE GAVE YOU, ROUTED WITH ONE KEY. To an agent: it runs on
779
932
  // your Mac (you become its runner). Keep it: it stays yours and the routing
780
933
  // choices go away. Hand it back: it returns to whoever gave it to you.
@@ -810,7 +963,24 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
810
963
  await team.syncNow();
811
964
  }));
812
965
 
813
- ipcMain.handle('zero:compose', (_e, { product, title, body, kind, priority, runAt, start, labels, engine, model, effort, assignee, due, visibility, visibleTo }) => {
966
+ // 'granted', or 'settings' once macOS's Microphone page has been opened instead.
967
+ ipcMain.handle('zero:mic-access', async () => {
968
+ try { return host.micAccess ? await host.micAccess() : 'granted'; } catch { return 'settings'; }
969
+ });
970
+
971
+ // EDIT OR DELETE A MESSAGE YOU SENT TO A TEAMMATE, in a conversation only,
972
+ // for the same reason as a reaction above.
973
+ ipcMain.handle('zero:team-edit', teamCall(async ({ product, id, on, text, gone }) => {
974
+ if (!teamMe()) throw new Error('sign in first');
975
+ if (!store.readItem(product, id)) throw new Error('that conversation is gone');
976
+ const here = store.listProducts().find((p) => p.slug === product);
977
+ if (!here?.team?.direct) throw new Error('only a message to a teammate can be edited');
978
+ if (!gone && !String(text ?? '').trim()) throw new Error('a message needs words; delete it instead');
979
+ store.editMessage(product, id, { on, text, gone: gone === true });
980
+ await team.syncNow();
981
+ }));
982
+
983
+ ipcMain.handle('zero:compose', (_e, { product, title, body, kind, priority, runAt, start, labels, engine, model, effort, assignee, due, visibility, visibleTo, fromPhone }) => {
814
984
  const out = store.composeItem(product, {
815
985
  // HOW HARD IT THINKS rides through unjudged: the store keeps any word
816
986
  // shaped like a level, and the spawn is the gate that knows each
@@ -824,6 +994,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
824
994
  // AND WHO MAY SEE IT, when she chose people rather than the team
825
995
  // (w-41ff964775). The store keeps the list only beside 'people'.
826
996
  visibleTo,
997
+ // The phone command that asked for it (main/phone-inbox.mjs).
998
+ fromPhone,
827
999
  people: assignee && teamMe() ? [teamMe(), assignee] : undefined,
828
1000
  // WHICH CODING AGENT SHE CHOSE, AND ONLY IF SHE COULD HAVE. `engineOffered`
829
1001
  // is the supervisor's own test and the door makes no judgement of its own:
@@ -1041,7 +1213,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1041
1213
  // is the sentence, which the screens show as it stands.
1042
1214
  ipcMain.handle('zero:choose-folder', async (_e, { startIn } = {}) => {
1043
1215
  const r = await dialog.showOpenDialog(window, {
1044
- title: 'Where is its code?',
1216
+ // The title is what the screen behind it asks. On Linux the chooser
1217
+ // draws one, and it was still asking "Where is its code?", the heading
1218
+ // that screen gave up in w-ec62ab6b38.
1219
+ title: 'Pick a project folder',
1045
1220
  buttonLabel: 'Use this folder',
1046
1221
  properties: ['openDirectory', 'createDirectory'],
1047
1222
  ...(startIn ? { defaultPath: startIn } : {}),
@@ -1165,9 +1340,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1165
1340
  // EVERY FOLDER EITHER TOOL KEEPS SESSIONS IN (main/agent-homes.mjs), not only
1166
1341
  // ~/.claude and ~/.codex, which is where the import used to look and nothing
1167
1342
  // else (w-e46f2e9cea).
1168
- const readAllThreads = () => readImportableThreads({ config, supervisor });
1169
- ipcMain.handle('zero:agent-threads', () => {
1170
- const threads = readAllThreads();
1343
+ const readAllThreads = () => readImportableThreads({ config, supervisor, readCodex: readCodexThreadsOffThread });
1344
+ ipcMain.handle('zero:agent-threads', async () => {
1345
+ const threads = await readAllThreads();
1171
1346
  lastThreads = threads;
1172
1347
  // Each one already in the inbox says so, and the card leaves it out
1173
1348
  // (w-db6f5e331e). The walk's folder suggestions still read all of them.
@@ -1176,12 +1351,48 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1176
1351
  return { threads: markImported(threads, items) };
1177
1352
  });
1178
1353
 
1354
+ /**
1355
+ * EVERY CHAT THE NAME FIELD CAN SEARCH, read once and kept (w-6418318cd5).
1356
+ *
1357
+ * The card's own list is ten days. Typing a name has to reach past that, and
1358
+ * a name lives inside a transcript, so finding one means opening files —
1359
+ * 29,703 of them on the Mac this was written on. It is taken ONCE, the first
1360
+ * time somebody types, and held for the life of the window: every keystroke
1361
+ * after the first is a filter in the renderer, not a disk walk. `Look again`
1362
+ * on the card drops it, which is the one way back to the disk.
1363
+ *
1364
+ * `reached` is the moment the read gave up at, zero when it read the whole
1365
+ * year. The card prints it, because "no chat with that name" over a read
1366
+ * that stopped in August would be a lie nobody could see.
1367
+ *
1368
+ * What it searches is the card's latest ten-day list joined with that kept
1369
+ * read, so a chat newer than the first search is found too, and the join is
1370
+ * `lastThreads`, because importing resolves an id against it. A chat the
1371
+ * list holds keeps the list's copy: the search reads no title, answer or
1372
+ * ending, and its copy would bring the chat in without them.
1373
+ */
1374
+ let searchable = null;
1375
+ ipcMain.handle('zero:search-agent-threads', async (_e, { again = false } = {}) => {
1376
+ if (again) searchable = null;
1377
+ if (!searchable) {
1378
+ let got = { threads: [], reached: 0 };
1379
+ try { got = await readSearchableThreads({ config, supervisor, readCodex: readCodexThreadsOffThread }); } catch { got = { threads: [], reached: 0 }; }
1380
+ searchable = got;
1381
+ }
1382
+ const byId = new Map(lastThreads.map((t) => [String(t?.id ?? ''), t]));
1383
+ for (const t of searchable.threads) if (!byId.has(String(t?.id ?? ''))) byId.set(String(t?.id ?? ''), t);
1384
+ lastThreads = [...byId.values()];
1385
+ let items = [];
1386
+ try { items = store.listItems(); } catch { items = []; }
1387
+ return { threads: markImported(lastThreads, items), reached: searchable.reached };
1388
+ });
1389
+
1179
1390
  // So this is `zero:import-agents` with the agent taken out of it. No ticks are
1180
1391
  // saved, because there is nothing for the card to remember: an imported thread
1181
1392
  // is a row, and a row is what stops it being imported twice
1182
- // (`threadsNeedingRows`). It does NOT resume the conversation; she was offered
1183
- // that and picked this instead.
1184
- ipcMain.handle('zero:import-threads', (_e, { product, threads } = {}) => {
1393
+ // (`threadsNeedingRows`). The row's first reply resumes the conversation as a
1394
+ // copy (`Supervisor#importedThreadFor`, GitHub #44).
1395
+ ipcMain.handle('zero:import-threads', async (_e, { product, threads, land } = {}) => {
1185
1396
  if (!product) return { ok: false, added: 0, already: 0, ids: [] };
1186
1397
  const want = new Set(
1187
1398
  (Array.isArray(threads) ? threads : [])
@@ -1191,15 +1402,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1191
1402
  if (!want.size) { return { ok: true, added: 0, already: 0, ids: [] }; }
1192
1403
  // The read is taken again only if nothing has been handed out yet, which is
1193
1404
  // the case a test or a fresh window can reach and a person cannot.
1194
- if (!lastThreads.length) lastThreads = readAllThreads();
1405
+ if (!lastThreads.length) lastThreads = await readAllThreads();
1195
1406
  // Two kinds, two rows.
1196
- const chosen = lastThreads.filter((t) => want.has(String(t?.id ?? '')));
1197
- let out = { ids: [], added: 0, already: 0 };
1407
+ const chosen = readChatEnds(lastThreads.filter((t) => want.has(String(t?.id ?? ''))));
1408
+ let out = { ids: [], added: 0, already: 0, done: 0 };
1198
1409
  try {
1199
1410
  // Each store method takes its own kind off the list and leaves the other.
1200
- const claude = store.importThreadRows(product, chosen);
1201
- const codex = store.importCodexThreads(product, chosen);
1202
- out = { ids: [...claude.ids, ...codex.ids], added: claude.added + codex.added, already: claude.already + codex.already };
1411
+ // Setup's last screen sorts what it brings in: finished chats go to Done.
1412
+ const how = { land: land === 'sorted' ? 'sorted' : 'inbox' };
1413
+ const claude = store.importThreadRows(product, chosen, how);
1414
+ const codex = store.importCodexThreads(product, chosen, how);
1415
+ out = { ids: [...claude.ids, ...codex.ids], added: claude.added + codex.added, already: claude.already + codex.already, done: claude.done + codex.done };
1203
1416
  } catch (err) {
1204
1417
  push();
1205
1418
  return { ok: false, error: String(err.message), added: 0, already: 0, ids: [] };
@@ -1278,6 +1491,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1278
1491
  if (isAgentRow(id)) return { ok: false };
1279
1492
  const out = store.answerItem(product, id, { status: 'open' });
1280
1493
  supervisor.liftBrakeForHer?.();
1494
+ supervisor.forgetDeadStarts?.({ product, id });
1281
1495
  supervisor.wake();
1282
1496
  return out;
1283
1497
  });
@@ -1358,9 +1572,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1358
1572
  // it. Every handler answers `{ ok, error }` rather than throwing across the
1359
1573
  // bridge, because the one thing this screen must never do is leave her
1360
1574
  // believing a setting took when it did not.
1361
- ipcMain.handle('zero:settings-read', () => {
1575
+ ipcMain.handle('zero:settings-read', (_e, how) => {
1362
1576
  try {
1363
- return { ok: true, ...readSettings({ config, supervisor, store }) };
1577
+ return { ok: true, ...readSettings({ config, supervisor, store }, { search: how?.search !== false }) };
1364
1578
  } catch (err) {
1365
1579
  return { ok: false, error: String(err.message) };
1366
1580
  }
@@ -1368,12 +1582,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1368
1582
 
1369
1583
  // CHECK AGAIN, from the last card of the walk. The card will not open the
1370
1584
  // inbox on a Mac with no Claude Code on it, so the way through it is to
1371
- // install Claude Code and press this. It throws the remembered shell answer
1372
- // away first, because a button that says it looked again and reads a thirty
1373
- // second old memory is the same broken promise as a key that does nothing.
1374
- ipcMain.handle('zero:claude-recheck', () => {
1585
+ // install Claude Code and press this. It waits for a fresh shell answer,
1586
+ // asked off the window's thread, because a button that says it looked again
1587
+ // and reads an old memory is the same broken promise as a key that does nothing.
1588
+ ipcMain.handle('zero:claude-recheck', async () => {
1375
1589
  try {
1376
- return { ok: true, workspace: recheckClaude(config) };
1590
+ return { ok: true, workspace: await lookAgainFor('claude', config) };
1377
1591
  } catch (err) {
1378
1592
  return { ok: false, error: String(err.message) };
1379
1593
  }
@@ -1384,9 +1598,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1384
1598
  // search finds lands on `config.codexBin`, which is the fact `engineChoices`
1385
1599
  // reads, so a press that finds Codex is also what makes the Coding agent row
1386
1600
  // appear -- without the restart a main-process fact usually costs.
1387
- ipcMain.handle('zero:codex-recheck', () => {
1601
+ ipcMain.handle('zero:codex-recheck', async () => {
1388
1602
  try {
1389
- return { ok: true, workspace: recheckCodex(config) };
1603
+ return { ok: true, workspace: await lookAgainFor('codex', config) };
1390
1604
  } catch (err) {
1391
1605
  return { ok: false, error: String(err.message) };
1392
1606
  }
@@ -1446,8 +1660,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1446
1660
  const engineSetup = createEngineSetup({
1447
1661
  find: (engine) => {
1448
1662
  const finders = {
1449
- claude: () => { const s = recheckClaude(config); return { found: s.claudeFound, path: s.claudeBin }; },
1450
- codex: () => { const s = recheckCodex(config); return { found: s.found, path: s.bin }; },
1663
+ claude: async () => { const s = await lookAgainFor('claude', config); return { found: s.claudeFound, path: s.claudeBin }; },
1664
+ codex: async () => { const s = await lookAgainFor('codex', config); return { found: s.found, path: s.bin }; },
1451
1665
  };
1452
1666
  return finders[engine]?.() ?? { found: false, path: null };
1453
1667
  },
@@ -1515,7 +1729,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1515
1729
 
1516
1730
  ipcMain.handle('zero:settings-set-project', (_e, payload) => {
1517
1731
  try {
1518
- setProjectSetting({ config, supervisor }, payload ?? {});
1732
+ setProjectSetting({ config, supervisor, store }, payload ?? {});
1519
1733
  push();
1520
1734
  return { ok: true, ...readSettings({ config, supervisor, store }) };
1521
1735
  } catch (err) {
@@ -1523,9 +1737,90 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1523
1737
  }
1524
1738
  });
1525
1739
 
1740
+ // SIGNING IN TO AGENTBOX CLOUD ITSELF, on the app's own sign-in page
1741
+ // (main/cloud/account.mjs): Google, or an email and a password, traded for
1742
+ // this Mac's cloud key. One session per page, made when the page first asks.
1743
+ let cloudAccount = null;
1744
+ const cloudAccountSessionNow = async () => {
1745
+ const home = supervisor.config.home || os.homedir();
1746
+ const { cloudAccountConfig, cloudAccountSession } = await import('./cloud/account.mjs');
1747
+ const config = cloudAccountConfig(supervisor.appDir, { home });
1748
+ if (!config) return null;
1749
+ if (!cloudAccount || cloudAccount.config !== JSON.stringify(config)) {
1750
+ const { shell } = await import('electron');
1751
+ cloudAccount = { config: JSON.stringify(config), value: config, session: cloudAccountSession({ config, openExternal: (u) => shell.openExternal(u), log: (l) => console.log(`cloud account: ${l}`) }) };
1752
+ }
1753
+ return cloudAccount;
1754
+ };
1755
+ ipcMain.handle('zero:cloud-account-sign-in', async (_e, { method, email, password, name, acceptTerms } = {}) => {
1756
+ const acct = await cloudAccountSessionNow();
1757
+ if (!acct) return { ok: false, error: 'This copy of the app has no cloud to sign in to.' };
1758
+ const { cloudSignIn } = await import('./cloud/account.mjs');
1759
+ const out = await cloudSignIn({ config: acct.value, session: acct.session, method, email, password, name, acceptTerms: acceptTerms === true, home: supervisor.config.home || os.homedir() });
1760
+ if (method === 'google') { try { if (window?.isMinimized?.()) window.restore(); window?.show?.(); } catch { /* no window to raise */ } }
1761
+ push();
1762
+ return out;
1763
+ });
1764
+ ipcMain.handle('zero:cloud-account-cancel', async () => { (await cloudAccountSessionNow())?.session.cancel(); return { ok: true }; });
1765
+ ipcMain.handle('zero:cloud-account-reopen', async () => { await (await cloudAccountSessionNow())?.session.reopen(); return { ok: true }; });
1766
+ ipcMain.handle('zero:cloud-account-sign-out', async () => {
1767
+ const { signOutOfCloud } = await import('./cloud/account.mjs');
1768
+ const out = await signOutOfCloud({ home: supervisor.config.home || os.homedir() });
1769
+ push();
1770
+ return out;
1771
+ });
1772
+ // PAYING FOR A CLOUD PLAN: Stripe's own pages, in the browser, as other
1773
+ // desktop apps do. An app window drew them badly and never offers Apple Pay.
1774
+ const cloudCall = async (name, args = {}) => {
1775
+ try {
1776
+ const account = await import('./cloud/account.mjs');
1777
+ const { shell } = await import('electron');
1778
+ return await account[name]({ ...args, openExternal: (u) => shell.openExternal(u), home: supervisor.config.home || os.homedir() });
1779
+ } catch (err) {
1780
+ return { ok: false, error: 'unreachable', message: `The cloud did not answer: ${err?.message || err}` };
1781
+ }
1782
+ };
1783
+ ipcMain.handle('zero:cloud-account-plan', async () => {
1784
+ const plan = await cloudCall('cloudPlan');
1785
+ supervisor.cloudPlanSeen?.(plan);
1786
+ return plan;
1787
+ });
1788
+ ipcMain.handle('zero:cloud-account-pay', (_e, { plan } = {}) => cloudCall('payForCloud', { plan }));
1789
+ ipcMain.handle('zero:cloud-account-billing', () => cloudCall('openCloudBilling'));
1790
+
1791
+ // THE CLOUD SETUP'S SIGN-IN for one account, made in the cloud
1792
+ // (main/cloud/logins.mjs). The Mac relays the link and the code only.
1793
+ ipcMain.handle('zero:cloud-signin', async (_e, { action, engine, key, code } = {}) => {
1794
+ try {
1795
+ const out = await supervisor.cloudSignIn(action, engine, key ?? null, code);
1796
+ push();
1797
+ return { ok: true, key: out?.key ?? null };
1798
+ } catch (err) {
1799
+ return { ok: false, error: String(err.message) };
1800
+ }
1801
+ });
1802
+ // CLAUDE'S CODE, FROM THE CLIPBOARD, WHEN SHE COMES BACK (2026-10-14: she
1803
+ // approved on Claude's page and waited, not seeing that the code it shows
1804
+ // has to come back). Answered only while a Claude sign-in is waiting for
1805
+ // its code, and only with text shaped like one: anything else she copied
1806
+ // stays where it is.
1807
+ ipcMain.handle('zero:cloud-clipboard-code', () => {
1808
+ try {
1809
+ const waiting = (supervisor.cloudStatus?.().signIns || []).some((s) => s.engine === 'claude' && s.state === 'needs-code');
1810
+ if (!waiting) return { code: null };
1811
+ const text = String(host?.readClipboardText?.() ?? '').trim();
1812
+ return { code: looksLikeClaudeCode(text) ? text : null };
1813
+ } catch {
1814
+ return { code: null };
1815
+ }
1816
+ });
1526
1817
  ipcMain.handle('zero:settings-set-workspace', (_e, payload) => {
1527
1818
  try {
1528
- setWorkspaceSetting({ config, supervisor }, payload ?? {});
1819
+ // Before the save: once the switch is off, nothing leaves, not even this.
1820
+ if (countsTurnedOff(payload)) { analytics.track('counts_turned_off'); analytics.flush(); }
1821
+ setWorkspaceSetting({ config, supervisor, store }, payload ?? {});
1822
+ if (payload?.key === 'alphaRooms') team?.rooms?.tick().catch(() => {});
1823
+ if (cornerTagTurnedOff(payload)) analytics.track('tag_turned_off');
1529
1824
  push();
1530
1825
  return { ok: true, ...readSettings({ config, supervisor, store }) };
1531
1826
  } catch (err) {
@@ -1649,29 +1944,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1649
1944
  // in the product's attachments/ dir and come back as relative paths the
1650
1945
  // caller embeds in the body as markdown. Relative paths keep them readable
1651
1946
  // by workers through the MCP and renderable in the message view.
1652
- ipcMain.handle('zero:save-attachments', async (_e, { product, files }) => {
1653
- const fs = await import('node:fs');
1654
- const dir = store.productDir(product);
1655
- const outDir = path.join(dir, 'attachments');
1656
- fs.mkdirSync(outDir, { recursive: true });
1657
- const saved = [];
1658
- for (const file of files ?? []) {
1659
- const safe = String(file.name ?? 'file').replace(/[^\w.-]+/g, '-').slice(0, 80);
1660
- // A RANDOM SUFFIX, THE SAME ONE THE STAGING PATH ALREADY USES. The name
1661
- // was the millisecond plus the filename, so two files saved in the same
1662
- // millisecond with the same name overwrote each other and the message
1663
- // ended up with two links to one picture. Screenshots are often sent in
1664
- // pairs, which is exactly the shape that collides. It has never been observed
1665
- // happening; it is one line to make impossible.
1666
- const rel = `attachments/${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`;
1667
- const abs = path.join(dir, rel);
1668
- if (file.dataBase64) fs.writeFileSync(abs, Buffer.from(file.dataBase64, 'base64'));
1669
- else if (file.srcPath) fs.copyFileSync(file.srcPath, abs);
1670
- else continue;
1671
- saved.push({ rel, name: safe, image: /\.(png|jpe?g|gif|webp)$/i.test(safe) });
1672
- }
1673
- return saved;
1674
- });
1947
+ // The saving itself is main/save-attachments.mjs, shared with a message to a
1948
+ // person, which saves its own once it knows which conversation it is in.
1949
+ ipcMain.handle('zero:save-attachments', async (_e, { product, files }) => saveAttachments(store.productDir(product), files));
1675
1950
 
1676
1951
  // A PASTED SCREENSHOT GOES TO DISK THE MOMENT IT IS PASTED.
1677
1952
  //
@@ -1712,32 +1987,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1712
1987
  // costs nothing at send time because `zero:save-attachments` above already
1713
1988
  // copies from `srcPath`. Bytes are copied on disk rather than carried through
1714
1989
  // this bridge as base64, which a file picked off disk should never need.
1715
- ipcMain.handle('zero:stage-attachment', async (_e, { name, dataBase64, srcPath }) => {
1716
- const fs = await import('node:fs');
1717
- const dir = path.join(config.accountRoot, '.staging');
1718
- fs.mkdirSync(dir, { recursive: true });
1719
- // Sweep on the way in rather than on a timer: an abandoned paste is only
1720
- // ever discovered by the next one, and a week is longer than any draft she
1721
- // has ever left open. Failure to sweep must never fail the paste.
1722
- try {
1723
- const week = Date.now() - 7 * 24 * 60 * 60 * 1000;
1724
- for (const f of fs.readdirSync(dir)) {
1725
- const p = path.join(dir, f);
1726
- try { if (fs.statSync(p).mtimeMs < week) fs.unlinkSync(p); } catch { /* leave it */ }
1727
- }
1728
- } catch { /* a sweep that cannot run is not a paste that failed */ }
1729
- const safe = String(name ?? 'pasted.png').replace(/[^\w.-]+/g, '-').slice(0, 80);
1730
- const abs = path.join(dir, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`);
1731
- // A path is a drag, bytes are a paste. Already inside the folder we serve
1732
- // from is left alone: copying it would only make a second file to sweep.
1733
- if (typeof srcPath === 'string' && srcPath) {
1734
- if (path.resolve(srcPath) === path.resolve(path.join(dir, path.basename(srcPath)))) return { path: srcPath };
1735
- fs.copyFileSync(srcPath, abs);
1736
- return { path: abs };
1737
- }
1738
- fs.writeFileSync(abs, Buffer.from(String(dataBase64 ?? ''), 'base64'));
1739
- return { path: abs };
1740
- });
1990
+ ipcMain.handle('zero:stage-attachment', async (_e, file) => stageAttachment(path.join(config.accountRoot, '.staging'), file));
1741
1991
 
1742
1992
  // Unstick ONE stalled item: forget its answer's delivery mark and tick, so
1743
1993
  // a fresh worker picks the answer up now.
@@ -1746,6 +1996,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1746
1996
  const item = store.listItems().find((i) => i.product === product && i.id === id);
1747
1997
  if (!item?.answer || item.answer === '(withdrawn)') return { ok: false, error: 'no live answer to redeliver' };
1748
1998
  supervisor.liftBrakeForHer?.();
1999
+ supervisor.forgetDeadStarts?.(item);
1749
2000
  supervisor.redeliverAnswer(item, item.answer);
1750
2001
  supervisor.tick?.().catch(() => {});
1751
2002
  return { ok: true };
@@ -1787,7 +2038,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1787
2038
  const { shell } = await import('electron');
1788
2039
  if (typeof src !== 'string' || !src) return { ok: false, error: 'no path' };
1789
2040
  if (/^https?:/.test(src)) { if (mode !== 'resolve') shell.openExternal(src); return { ok: true, opened: src } }
1790
- const found = resolveArtifact({
2041
+ const found = await resolveArtifact({
1791
2042
  src,
1792
2043
  product,
1793
2044
  products: store.listProducts(),
@@ -1833,7 +2084,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1833
2084
  // text rather than the path. What may be written is decided in
1834
2085
  // main/doc-file.mjs: markdown only, inside this product's own folders only.
1835
2086
  ipcMain.handle('zero:read-doc', async (_e, { product, src }) => {
1836
- const found = resolveArtifact({
2087
+ const found = await resolveArtifact({
1837
2088
  src,
1838
2089
  product,
1839
2090
  products: store.listProducts(),
@@ -1847,7 +2098,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1847
2098
  // THE CHANGE A RUN MADE. Same finder as every other artifact, so a click on
1848
2099
  // the chip and a click on a path in her message open the same file.
1849
2100
  ipcMain.handle('zero:code-change', async (_e, { product, src }) => {
1850
- const found = resolveArtifact({
2101
+ const found = await resolveArtifact({
1851
2102
  src,
1852
2103
  product,
1853
2104
  products: store.listProducts(),
@@ -1868,8 +2119,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1868
2119
  // same pair the pane already has on screen. The change is re-read here rather
1869
2120
  // than trusted from the renderer, because it is what says which folders a
1870
2121
  // write may land in and that decision may not come from the window.
1871
- const openChange = (product, src) => {
1872
- const found = resolveArtifact({
2122
+ const openChange = async (product, src) => {
2123
+ const found = await resolveArtifact({
1873
2124
  src,
1874
2125
  product,
1875
2126
  products: store.listProducts(),
@@ -1885,7 +2136,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1885
2136
  };
1886
2137
 
1887
2138
  ipcMain.handle('zero:code-file', async (_e, { product, src, path: filePath }) => {
1888
- const opened = openChange(product, src);
2139
+ const opened = await openChange(product, src);
1889
2140
  if (!opened.ok) return opened;
1890
2141
  const found = findFile({ change: opened.change, product: opened.prod, filePath });
1891
2142
  if (!found.ok) return found;
@@ -1893,7 +2144,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1893
2144
  });
1894
2145
 
1895
2146
  ipcMain.handle('zero:save-code-file', async (_e, { product, src, path: filePath, text, mtime }) => {
1896
- const opened = openChange(product, src);
2147
+ const opened = await openChange(product, src);
1897
2148
  if (!opened.ok) return opened;
1898
2149
  const found = findFile({ change: opened.change, product: opened.prod, filePath });
1899
2150
  if (!found.ok) return found;
@@ -1902,7 +2153,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1902
2153
 
1903
2154
  ipcMain.handle('zero:write-doc', async (_e, { product, src, text, mtime }) => {
1904
2155
  const prod = store.listProducts().find((p) => p.slug === product) ?? null;
1905
- const found = resolveArtifact({
2156
+ const found = await resolveArtifact({
1906
2157
  src,
1907
2158
  product,
1908
2159
  products: store.listProducts(),
@@ -1954,9 +2205,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1954
2205
 
1955
2206
  // The thread history behind the time under a task's title. Raw ledger lines,
1956
2207
  // read fresh: the renderer owns what they mean, this owns getting them across.
1957
- ipcMain.handle('zero:item-history', async (_e, { product, id }) => {
2208
+ ipcMain.handle('zero:item-history', async (_e, { product, id, before }) => {
1958
2209
  try {
1959
- return { ok: true, lines: store.readHistory(product, id) };
2210
+ return { ok: true, lines: store.readHistory(product, id, { before }) };
1960
2211
  } catch (err) {
1961
2212
  return { ok: false, error: err.message, lines: [] };
1962
2213
  }