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/main.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  // The app: the agent inbox. Electron shell around a keyboard-first renderer, a
2
2
  // file-derived store, and a supervisor of headless sessions.
3
3
 
4
- import { app, BrowserWindow, Menu, Notification, clipboard, crashReporter, dialog, ipcMain, nativeImage, net, powerMonitor, protocol, screen as electronScreen, shell, session, safeStorage } from 'electron';
4
+ import { app, BrowserWindow, Menu, Notification, clipboard, crashReporter, dialog, ipcMain, nativeImage, net, powerMonitor, protocol, screen as electronScreen, shell, session, safeStorage, systemPreferences } from 'electron';
5
5
  import * as workItemsDisk from './store/work-items.mjs';
6
6
  import { createTeamService, teamStateFile } from './team/index.mjs';
7
+ import { roomsSetup } from './team/room-setup.mjs';
7
8
  import { loadCloudConfig, supabaseSession } from './team/session.mjs';
8
9
  // MY WORKSPACE IS NOT A TEAM FEATURE ANY MORE (w-f8d123be62), even though it
9
10
  // still lives in the team folder next to the sharing rules it has to obey.
@@ -18,37 +19,45 @@ import { copySelection } from './copy-selection.mjs';
18
19
  import { screenDetailFor } from './screen-detail.mjs';
19
20
  import { Store } from './store.mjs';
20
21
  import { storeRootEnv } from './store/home.mjs';
22
+ import { keepAnswerModesIn } from './answer-modes.mjs';
21
23
  import { Supervisor } from './supervisor.mjs';
22
24
  import { startCodexWatch } from './codex-watch.mjs';
23
- import { readCodexThreads } from './codex-threads.mjs';
25
+ import { watchNewChatsFor } from './new-chats-watch.mjs';
26
+ import { readCodexThreadsOffThread } from './codex-threads.mjs';
24
27
  import { appSessionFolders } from './agent-homes.mjs';
25
28
  import { registerIpc } from './ipc.mjs';
26
- import { createPhoneLink } from './phone-link.mjs';
27
- import { relayAddress } from './relay-address.mjs';
28
- import { atTheApp } from '../shared/notify-rules.mjs';
29
+ import { stallWatch } from './stall-watch.mjs';
30
+ import { startFreezeRecorder } from './freeze-recorder.mjs';
31
+ import { applyThrough, phoneDevice } from './phone-inbox.mjs';
29
32
  import { answerWhatOnlyTheWindowAnswered } from './serve.mjs';
30
33
  import { carryHerBriefsAcross, joinMessageRules, setAsideShippedMessageRules, unifyAgentInstructions } from './instruction-settings.mjs';
31
34
  import * as approvals from './approvals.mjs';
32
35
  import { recoveryToast } from '../shared/recovery.mjs';
33
36
  import { dataFolderName, isNewUserBuild } from '../shared/side-build.mjs';
34
37
  import { runningAsAFreshUser } from '../shared/fresh-user-home.mjs';
38
+ import { newUserWindow } from './new-user-window.mjs';
35
39
  import { openFreshUser } from './fresh-user.mjs';
36
40
  import { installCrashReports, reportFromRenderer, pending as pendingCrashes, setTransport } from './crash-report.mjs';
37
41
  import { createAnalytics, createDailyCount } from './analytics.mjs';
38
42
  import { createUpdater } from './updater.mjs';
43
+ import { quitStep, flushBeforeQuit, quitThenExit } from './quit-sequence.mjs';
39
44
  import { createPlace } from '../shared/where-it-runs-from.mjs';
40
45
  import { createSourceUpdater } from './source-updater.mjs';
41
46
  import { learnShellDataDirs, recoverShellPath } from './shell-path.mjs';
42
47
  import { installNotifier } from './notify.mjs';
43
48
  import { createCornerTag } from './corner-tag.mjs';
49
+ import { loadLiquidGlass } from './liquid-glass.mjs';
44
50
  import { DOC_SCHEMES, DocGrants, docPath } from './doc-scheme.mjs';
45
51
  import { quietTheFramesScrollbars } from './frame-scrollbars.mjs';
46
52
  import { letLocalAppsShowInThePane } from './local-app-frames.mjs';
53
+ import { hardenAttachedWebview } from './webview-guard.mjs';
47
54
  import { IMG_SCHEMES, imgPath, mediaResponse, mediaType, servable } from './img-scheme.mjs';
48
55
  import { hotWindowVerdict, storeHasWork } from './dev-window.mjs';
49
56
  import { writeHeldThenReload } from './write-before-reload.mjs';
50
57
  import { IS_SHE_TYPING_IN_THE_PAGE, theAppKeepsThisInput, theAppKeepsThisLetterInAPage } from '../shared/artifact-keys.mjs';
51
58
  import { NAME, Name, WAS } from '../shared/product-name.mjs';
59
+ import { JOIN_SCHEME, ourLink, tokenFromLink } from '../shared/room-links.mjs';
60
+ import { getMic } from './mic.mjs';
52
61
 
53
62
  // HOW LONG TO WAIT AFTER THE LID OPENS, and the wait is for the network rather
54
63
  // than for the wake. `powerMonitor`'s resume event fires while wifi is still
@@ -238,6 +247,24 @@ installCrashReports({
238
247
  });
239
248
 
240
249
  let window = null;
250
+ // A ROOM INVITE LINK THAT OPENED THE APP (Rooms, alpha): agentbox://join/<token>.
251
+ // Held until the window takes it ('zero:take-join'); a window that is up is
252
+ // told to come and take it, and one still starting takes it as it mounts.
253
+ let pendingJoin = tokenFromLink(process.argv.find((a) => a.startsWith(`${JOIN_SCHEME}://`)) ?? '');
254
+ function showWindow() {
255
+ if (!window || window.isDestroyed()) return;
256
+ if (window.isMinimized()) window.restore();
257
+ window.focus();
258
+ }
259
+ function openJoinLink(url) {
260
+ const token = tokenFromLink(url);
261
+ if (!token) return;
262
+ pendingJoin = token;
263
+ if (window && !window.isDestroyed()) {
264
+ window.webContents.send('zero:join-room');
265
+ showWindow();
266
+ }
267
+ }
241
268
  // Set by ⌘R and read once by the renderer as it comes back up, so a reload can
242
269
  // say so. A reload that lands on an identical screen and a chord that did
243
270
  // nothing at all look exactly alike, and telling them apart is the whole
@@ -285,19 +312,23 @@ function reloadRenderer() {
285
312
  }
286
313
 
287
314
  async function createWindow() {
288
- const shellPath = await recoverShellPath();
315
+ // Where the shell says Claude Code and Codex keep their logins and sessions,
316
+ // never put into this environment (main/shell-path.mjs). WAITED FOR, beside
317
+ // the PATH read so it costs no extra time: the folders it names are where
318
+ // workers sign in now (w-6d8c543a43), and a task resumed at launch before
319
+ // the answer landed ran on ~/.claude signed out.
320
+ const [shellPath] = await Promise.all([recoverShellPath(), learnShellDataDirs()]);
289
321
  process.env.PATH = shellPath.path;
290
322
  if (shellPath.error) console.warn('agentbox: could not load your shell PATH; using the inherited PATH:', shellPath.error);
291
- // Where the shell says Claude Code and Codex keep their sessions, for the
292
- // import only and never into this environment (main/shell-path.mjs). Not
293
- // waited for: it lands long before setup reaches its last card.
294
- void learnShellDataDirs();
295
323
  const config = loadConfig(dataDir);
296
324
  // The store modules read the app's own home variable at call time; setting it
297
325
  // here points auth session files, sync, and every store path at its root.
298
326
  // Written under every name this app has had, because a store server or a
299
327
  // script from before a rename is still out there reading the old one.
300
328
  Object.assign(process.env, storeRootEnv(config.storeRoot));
329
+ // What a reply may do is kept in her own folder, not that home, which every
330
+ // worker is granted (main/answer-modes.mjs).
331
+ keepAnswerModesIn({ userDir, storeRoot: config.storeRoot });
301
332
 
302
333
  // Menu needs the config (approval hotkeys answer the oldest pending card),
303
334
  // so it installs here rather than at whenReady. The spool watcher in ipc
@@ -311,11 +342,11 @@ async function createWindow() {
311
342
  // themselves with `approvals.answer`, and on a Codex card a file is not an
312
343
  // answer at all: the thing waiting is Agentbox itself, in this process, so
313
344
  // nothing on that path ever reads one. Her click on Allow landed as a
314
- // FORGERY -- the file appeared for a request still waiting, `sweepForgery`
315
- // deleted it inside two seconds, the log said "an answer file appeared for
316
- // approval <id> that the founder did not write", her card came back and the
317
- // worker was denied fifteen minutes later by the deadline. It also spent the
318
- // one warning that is supposed to mean a worker attacked the spool.
345
+ // FORGERY -- the file appeared for a request still waiting, the card's own
346
+ // poll deleted it inside two seconds, the log said "an answer file appeared
347
+ // for approval <id> that the founder did not write", and her card came back
348
+ // with the worker still parked on it. It also spent the one warning that is
349
+ // supposed to mean a worker attacked the spool.
319
350
  //
320
351
  // The variable is filled in the moment `registerIpc` returns, a few hundred
321
352
  // lines down. Until then there is nothing to answer with and nothing that
@@ -354,7 +385,7 @@ async function createWindow() {
354
385
  analytics.track('app_opened');
355
386
  usedToday(Date.now());
356
387
  } else {
357
- console.log(`zero: diagnostics is ${analytics.reason}, nothing is sent`);
388
+ console.log(`zero: diagnostics ${analytics.reason}, nothing is sent`);
358
389
  }
359
390
 
360
391
  // EVERYBODY HAS SOMEWHERE TO SEND A THREAD (w-f8d123be62, approved
@@ -379,7 +410,16 @@ async function createWindow() {
379
410
  const supervisor = new Supervisor(config, store, appDir, dataDir, userDir);
380
411
  // THAT agents start and stop work (main/analytics.mjs checks the switch).
381
412
  supervisor.count = (name, props) => analytics.track(name, props);
382
- app.on('before-quit', () => supervisor.killAll());
413
+ const stalls = stallWatch();
414
+ supervisor.stalls = stalls;
415
+ // AND WHICH OF OUR FUNCTIONS HELD THE THREAD, each time it froze.
416
+ const freezes = startFreezeRecorder({
417
+ dir: path.join(userDir, 'memory-log'),
418
+ appDir,
419
+ onFreeze: (f) => { stalls.noteFreeze(f); console.warn(`zero: the app froze for ${f.ms} ms in ${f.where}`); },
420
+ });
421
+ quitStep(app, 'before-quit', 'freezes.stop', () => freezes.stop());
422
+ quitStep(app, 'before-quit', 'supervisor.killAll', () => supervisor.killAll());
383
423
  // THE TEAM. Off entirely on a build with no team cloud (the single-person
384
424
  // app). Otherwise it restores whoever was signed in, and from then on every
385
425
  // line this Mac writes says who wrote it and shared projects stay in sync.
@@ -390,13 +430,16 @@ async function createWindow() {
390
430
  // single-person app, which has no sign-in to keep
391
431
  // (tests/the-installed-app-is-the-single-person-app.test.mjs).
392
432
  const cloudConfig = loadCloudConfig(appDir, { packaged: app.isPackaged });
433
+ let phoneApply = null;
434
+ let phoneSettings = null;
435
+ // Every installed build has a cloud now, so the keychain is asked only once
436
+ // a sign-in is read or kept, not at launch.
393
437
  const team = cloudConfig ? createTeamService({
394
438
  session: supabaseSession({
395
439
  cloudConfig,
396
440
  sessionFile: path.join(config.storeRoot, '.team-session'),
397
- ...(safeStorage.isEncryptionAvailable()
398
- ? { encrypt: (text) => safeStorage.encryptString(text), decrypt: (buf) => safeStorage.decryptString(buf) }
399
- : { encrypt: null, decrypt: null }),
441
+ encrypt: (text) => (safeStorage.isEncryptionAvailable() ? safeStorage.encryptString(text) : Buffer.from(text, 'utf8')),
442
+ decrypt: (buf) => (safeStorage.isEncryptionAvailable() ? safeStorage.decryptString(buf) : buf.toString('utf8')),
400
443
  packaged: app.isPackaged,
401
444
  openExternal: (url) => shell.openExternal(url),
402
445
  }),
@@ -408,10 +451,42 @@ async function createWindow() {
408
451
  // exists; until then a change has nobody to tell.
409
452
  onChange: () => { try { pushUpdate(); } catch { /* no window yet */ } },
410
453
  log: (line) => console.log(line),
454
+ // HER PHONE WITH THE MAC SHUT (main/phone-inbox.mjs). What it left is done
455
+ // through the window's own handlers, filed below once the window exists.
456
+ phone: {
457
+ // Her yes, with her id on it (shared/phone-snapshot.mjs phoneProjectsOf).
458
+ projects: () => config.phoneProducts ?? null,
459
+ ready: () => !!phoneApply,
460
+ apply: (cmd) => phoneApply(cmd),
461
+ // The Settings pages' model, read through the window's own handler.
462
+ readSettings: () => phoneSettings?.() ?? null,
463
+ version: app.getVersion(),
464
+ live: (slug, id) => supervisor.liveFor(slug, id),
465
+ // The cards waiting on her, each with the fingerprint her answer must carry back.
466
+ approvals: () => approvals.listPending(config.storeRoot).map((r) => ({ ...r, card: approvals.cardShown(config.storeRoot, r.id) })),
467
+ stateFile: path.join(config.storeRoot, '.phone-inbox.json'),
468
+ // This installation and this store together (main/phone-inbox.mjs).
469
+ device: () => phoneDevice(userDir, config.storeRoot),
470
+ // Where her phone reads it, once the site is up (cloud/team.config.json).
471
+ site: typeof cloudConfig.phoneSite === 'string' ? cloudConfig.phoneSite : null,
472
+ // No alert on her phone while she is at this Mac (main/phone-alerts.mjs).
473
+ idleMs: () => powerMonitor.getSystemIdleTime() * 1000,
474
+ // A reply she sends with the lid shut carries on in the cloud (shared/cloud-kit.mjs).
475
+ carrying: (c) => supervisor.phoneCarrying(c),
476
+ kitsFor: (slug) => supervisor.leavesKitsFor(slug),
477
+ adopt: ({ project, item, runId, replies }) => supervisor.adoptCloudRun({ product: project, itemId: item, runId, replies }),
478
+ cloudReplies: ({ project, item, replies }) => supervisor.cloudRunReplies({ product: project, itemId: item, replies }),
479
+ runHome: ({ project, item, runId }) => supervisor.cloudRunHome({ product: project, itemId: item, runId }),
480
+ // And a new thread she starts with the lid shut.
481
+ newThreadKits: (c) => supervisor.leaveNewThreadKits(c),
482
+ adoptNew: ({ project, item, ...rest }) => supervisor.adoptCloudNewThread({ product: project, itemId: item, ...rest }),
483
+ },
484
+ // ROOMS, while switched on in Alpha features (docs/team/ROOMS-PLAN.md).
485
+ rooms: roomsSetup({ config, store, supervisor, userDir }),
411
486
  }) : null;
412
487
  if (team) {
413
488
  team.start().then(() => supervisor.wake?.()).catch((err) => console.warn(`team: ${err.message}`));
414
- app.on('before-quit', () => team.stop());
489
+ quitStep(app, 'before-quit', 'team.stop', () => team.stop());
415
490
  }
416
491
  // HER CODEX CONVERSATIONS ASK TO COME IN. Once a minute: a new one in a
417
492
  // folder a project points at gets a row asking yes or no, and the ones she
@@ -424,11 +499,20 @@ async function createWindow() {
424
499
  const codexWatch = startCodexWatch({
425
500
  store,
426
501
  count: (name, props) => analytics.track(name, props),
427
- readThreads: (opts) => readCodexThreads({ ...opts, codexDirs: appSessionFolders({ config, supervisor }).codexDirs }),
502
+ readThreads: (opts) => readCodexThreadsOffThread({ ...opts, codexDirs: appSessionFolders({ config, supervisor }).codexDirs }),
428
503
  });
429
- app.on('before-quit', () => codexWatch.stop());
430
- // Whatever is still queued goes with the app rather than dying in memory.
431
- app.on('will-quit', () => { analytics.flush(); });
504
+ quitStep(app, 'before-quit', 'codexWatch.stop', () => codexWatch.stop());
505
+ // New chats from Claude Code and Codex, once asked for on setup's last
506
+ // screen or in Settings. The watch checks the switch every pass.
507
+ const newChatsWatch = watchNewChatsFor({ store, config, supervisor });
508
+ quitStep(app, 'before-quit', 'newChatsWatch.stop', () => newChatsWatch.stop());
509
+ // Whatever is still queued goes with the app rather than dying in memory,
510
+ // and the quit WAITS for it (main/quit-sequence.mjs). It did not: the
511
+ // process went as soon as this returned, so the last batch was a POST that
512
+ // never left, which on an install that opens the app once is everything we
513
+ // would ever have heard from it (w-60c8ddbab6).
514
+ quitStep(app, 'before-quit', 'count the quit', () => analytics.track('app_quit', { seconds: Math.round(process.uptime()) }));
515
+ flushBeforeQuit(app, () => analytics.flush());
432
516
 
433
517
  // Right-click. Electron ships no context menu at all, so links could not be
434
518
  // copied or opened by hand anywhere in the app. Built per click from what is
@@ -619,9 +703,18 @@ async function createWindow() {
619
703
  window = new BrowserWindow({
620
704
  width: 1440,
621
705
  height: 900,
622
- minWidth: 980,
706
+ // Half of a 1440-point screen, so the window tiles; under 700 is the phone layout.
707
+ minWidth: 720,
623
708
  minHeight: 600,
624
- title: NAME,
709
+ // A throwaway copy says which one it is and stands a shoulder clear;
710
+ // your own window is centred and titled as it was.
711
+ ...newUserWindow({
712
+ fresh: runningAsAFreshUser(),
713
+ name: NAME,
714
+ screen: electronScreen.getPrimaryDisplay()?.workArea,
715
+ width: 1440,
716
+ height: 900,
717
+ }),
625
718
  backgroundColor: '#1a1a1c',
626
719
  titleBarStyle: 'hiddenInset',
627
720
  webPreferences: {
@@ -632,6 +725,11 @@ async function createWindow() {
632
725
  },
633
726
  });
634
727
 
728
+ // Electron hands the window's title to the page's the moment one loads, and
729
+ // renderer/index.html carries the app's name, so a throwaway copy went back
730
+ // to saying "Agentbox" as soon as it drew anything.
731
+ window.on('page-title-updated', (e) => e.preventDefault());
732
+
635
733
  // THE MENU IS A BAR INSIDE THE WINDOW on Linux and Windows. On macOS it is
636
734
  // the system menu, so it stays. The menu itself stays everywhere: the Ctrl
637
735
  // chords are bound to it, and taking the menu out would take them with it.
@@ -706,21 +804,20 @@ async function createWindow() {
706
804
 
707
805
  // The desktop door. `main/serve.mjs` is the other one and hands over plain
708
806
  // Node stand-ins for these three; everything else about the call is the same.
709
- // THE PHONE DOOR (main/phone-link.mjs) answers with these same handlers, so
710
- // each one is filed in a Map as Electron gets it. And every push the window
711
- // gets is handed to the phones' event streams too.
807
+ // What the phone leaves in the cloud is done through these same handlers
808
+ // (main/phone-inbox.mjs), so each one is filed in a Map as Electron gets it.
712
809
  const phoneChannels = new Map();
713
- const phoneListeners = new Set();
714
810
  const filingIpcMain = {
715
- handle(channel, fn) { ipcMain.handle(channel, fn); phoneChannels.set(channel, fn); },
811
+ handle(channel, fn) { ipcMain.handle(channel, stalls.time(channel, fn)); phoneChannels.set(channel, fn); },
716
812
  };
717
- const filingHost = { ipcMain: filingIpcMain, app, dialog };
718
- const sendToWindow = window.webContents.send.bind(window.webContents);
719
- window.webContents.send = (channel, ...args) => {
720
- sendToWindow(channel, ...args);
721
- for (const send of phoneListeners) {
722
- try { send({ channel, args }); } catch { /* a closed phone is not an error */ }
723
- }
813
+ // `readClipboardText` is read only by the cloud setup, and only for a
814
+ // Claude sign-in code (main/ipc.mjs `zero:cloud-clipboard-code`).
815
+ const filingHost = {
816
+ ipcMain: filingIpcMain, app, dialog, readClipboardText: () => clipboard.readText(),
817
+ // A voice note asks for the microphone on the press of its button.
818
+ micAccess: () => (process.platform === 'darwin'
819
+ ? getMic({ systemPreferences, openExternal: (url) => shell.openExternal(url) })
820
+ : 'granted'),
724
821
  };
725
822
  const ipc = registerIpc({
726
823
  store, supervisor, config, window, analytics, docGrants, updater,
@@ -732,38 +829,17 @@ async function createWindow() {
732
829
  // See the `installMenu` call above for what writing the file instead did to a
733
830
  // Codex card.
734
831
  answerApproval = ipc.answerApproval;
735
- // What the window answers itself (badge, boot info) a phone gets the plain
736
- // stand-ins for, as a browser tab does.
832
+ // What the window answers itself (badge, boot info) gets the plain stand-ins
833
+ // a browser tab gets.
737
834
  answerWhatOnlyTheWindowAnswered({ handle: (channel, fn) => { if (!phoneChannels.has(channel)) phoneChannels.set(channel, fn); } });
738
- const phone = createPhoneLink({
739
- file: path.join(userDir, 'phone-link.json'),
740
- channels: phoneChannels,
741
- listeners: phoneListeners,
742
- dist: path.join(appDir, 'renderer', 'dist'),
743
- // Pictures and design pages, under the same two rules the window's own
744
- // schemes use below: a page the pane was granted, and a picture inside a
745
- // root the app owns (main/serve.mjs answerFile).
746
- files: { allows: (kind, f) => (kind === 'doc' ? docGrants.allows(f) : servable(f, imageRoots())) },
747
- // "Anywhere": the relay a phone reaches this Mac through from any network
748
- // (main/relay-client.mjs). None until it is deployed, and then no choice.
749
- relayUrl: relayAddress({ appDir }),
750
- onChange: () => { if (!window.isDestroyed()) sendToWindow('zero:phone-changed'); },
751
- // The phone stays quiet while she is at the desktop app, by the same rule
752
- // the desktop's own notifications use (shared/notify-rules.mjs).
753
- atDesk: () => {
754
- try { return atTheApp({ focused: !window.isDestroyed() && window.isFocused(), idleMs: powerMonitor.getSystemIdleTime() * 1000 }); } catch { return false; }
755
- },
756
- });
757
- window.on('focus', () => phone.seen());
758
- // Settings' own channel, on the window only: a phone does not get to turn
759
- // the phone door off or hand out its key.
760
- ipcMain.handle('zero:phone-link', (_e, payload) => phone.act(payload ?? {}));
761
- app.on('before-quit', () => phone.close());
835
+ phoneApply = applyThrough(phoneChannels);
836
+ // Every minute, so no engine search: that is a login shell on this thread.
837
+ phoneSettings = () => phoneChannels.get('zero:settings-read')?.({ sender: null }, { search: false }) ?? null;
762
838
  updater.start();
763
839
  // A task the app just shipped moved main: look now rather than in half an
764
840
  // hour, so the restart is offered while she still remembers the task.
765
841
  supervisor.onShipped = () => { try { updater.check?.(); } catch {} };
766
- app.on('before-quit', () => updater.stop());
842
+ quitStep(app, 'before-quit', 'updater.stop', () => updater.stop());
767
843
 
768
844
  // Cmd+Y / Cmd+N answer the oldest approval card from ANYWHERE, including
769
845
  // while a child web contents owns the keyboard (menu accelerators turned out
@@ -931,7 +1007,6 @@ async function createWindow() {
931
1007
  // Unread badge + notification on new inbox arrivals are driven by the
932
1008
  // renderer (it owns the definition of "inbox"), through the badge API.
933
1009
  ipcMain.handle('zero:badge', (_e, count) => {
934
- phone.count(count);
935
1010
  if (process.platform === 'darwin') app.dock.setBadge(count > 0 ? String(count) : '');
936
1011
  });
937
1012
  // WHETHER to speak, and what to say, is not the renderer's call: only this
@@ -940,13 +1015,23 @@ async function createWindow() {
940
1015
  // a locked screen all read the same from in there). The renderer says what
941
1016
  // arrived; main/notify.mjs decides. tell me when I am in another app, never
942
1017
  // while I am in Agentbox.
943
- installNotifier({ app, window, Notification, powerMonitor, ipcMain, nativeImage, onArrivals: (arrivals) => phone.arrivals(arrivals) });
1018
+ // A banner and the tray never disagree: whatever a banner told you about
1019
+ // while you were away is in the tray until you are back (w-93b3a1a796).
1020
+ let cornerTag = null;
1021
+ const notifier = installNotifier({
1022
+ app, window, Notification, powerMonitor, ipcMain, nativeImage,
1023
+ onArrivals: (list, needsYou) => team?.phoneArrivals(list, needsYou),
1024
+ onChange: () => cornerTag?.refresh(),
1025
+ });
944
1026
  // THE CORNER TAG (w-dafae58a23): what is ready for her, floating over
945
1027
  // whatever app she is in. Its page is built beside the app's own, and comes
946
1028
  // from the dev server only when the app's own page does.
947
- createCornerTag({
1029
+ cornerTag = createCornerTag({
948
1030
  BrowserWindow, screen: electronScreen, ipcMain, Menu, powerMonitor, config, saveConfig, mainWindow: window,
949
1031
  preload: path.join(appDir, 'main', 'corner-tag-preload.cjs'),
1032
+ glass: loadLiquidGlass(),
1033
+ told: () => notifier.told(),
1034
+ count: (name, props) => analytics.track(name, props),
950
1035
  // `part` is the tag itself or the list beside it, one page drawn twice.
951
1036
  load: (tagWindow, part) => {
952
1037
  const devTag = process.env.ZERO_DEV_URL;
@@ -971,6 +1056,7 @@ async function createWindow() {
971
1056
  // itself (it has no disk and should not have one), and 'render-process-gone'
972
1057
  // only fires when the process DIES, which a thrown React error does not.
973
1058
  ipcMain.handle('zero:crash', (_e, payload) => { reportFromRenderer(payload ?? {}); });
1059
+ ipcMain.handle('zero:take-join', () => { const token = pendingJoin; pendingJoin = null; return token; });
974
1060
  // External links from markdown open in the real browser; local artifacts
975
1061
  // (a demo page, a screenshot a worker saved) open in their default app, so
976
1062
  // "see it working" is one click from the message.
@@ -980,18 +1066,14 @@ async function createWindow() {
980
1066
  return { action: 'deny' };
981
1067
  });
982
1068
 
983
- // NO PAGE IN AN EMBEDDED BROWSER STARTS SOUND BEFORE SHE HAS CLICKED IN IT.
1069
+ // WHAT AN EMBEDDED PAGE MAY DO IS DECIDED HERE, NOT ON THE TAG THAT ASKED.
984
1070
  //
985
- // Electron's default is `no-user-gesture-required`, so any page may start
986
- // media the moment it loads, and the rest browser reloads her last tab at
987
- // every launch, out of sight. Measured 2026-09-10 on Electron 43 in a hidden
988
- // webview on a YouTube watch page: default, the video ran from 3s to 15s;
989
- // with this rule it sat paused at 0 for all 16s. A click anywhere in the page
990
- // is activation, so pressing play, or picking the next video inside YouTube,
991
- // works as before. Every webview this window attaches gets it, the junk
992
- // browser included, which is what "never" asks for.
993
- window.webContents.on('will-attach-webview', (_e, webPreferences) => {
994
- webPreferences.autoplayPolicy = 'document-user-activation-required';
1071
+ // The pane draws a website now, which is the first code in this window that
1072
+ // nobody here wrote, so every webview is pinned below the tag: no preload,
1073
+ // sandboxed, context-isolated, node off, web security on, and no sound
1074
+ // before a click. main/webview-guard.mjs is the rule and the reasons.
1075
+ window.webContents.on('will-attach-webview', (_e, webPreferences, params) => {
1076
+ hardenAttachedWebview(webPreferences, params);
995
1077
  });
996
1078
 
997
1079
  // A page in the pane hides its scrollbar until you scroll, like the app's own
@@ -1007,10 +1089,34 @@ async function createWindow() {
1007
1089
  // focus and would otherwise swallow the key. Intercept it below the page.
1008
1090
  app.on('web-contents-created', (_event, contents) => {
1009
1091
  if (contents.getType() !== 'webview') return;
1092
+
1093
+ // A LINK IN A WEBSITE THAT WANTS A NEW WINDOW GOES TO YOUR OWN BROWSER.
1094
+ // The pane is one page at a time; a second Electron window would be a
1095
+ // browser we have not designed, and `allowpopups` is off anyway
1096
+ // (webview-guard.mjs), so without this such a link does nothing at all.
1097
+ contents.setWindowOpenHandler(({ url }) => {
1098
+ if (/^https?:/.test(url)) shell.openExternal(url);
1099
+ return { action: 'deny' };
1100
+ });
1101
+
1010
1102
  contents.on('before-input-event', (e, input) => {
1103
+ // ESCAPE HAS TO REACH THE APP FROM INSIDE A PAGE, and a webview is not
1104
+ // a frame of this window, so the handler above never hears it. It comes
1105
+ // up the same channel a frame's does, because the renderer answers that
1106
+ // one and decides there what escape means (whatTheFileSentUp). It used
1107
+ // to be sent on `zero:escape-browser`, the junk browser's own channel,
1108
+ // which no renderer code has listened to since that pane went: the key
1109
+ // landed nowhere and the pane took two presses to close.
1011
1110
  if (input.type === 'keyDown' && input.key === 'Escape' && window && !window.isDestroyed()) {
1012
- window.webContents.send('zero:escape-browser');
1013
- window.webContents.focus();
1111
+ // The keyboard is left where it is, exactly as it is for a frame: the
1112
+ // renderer blurs the page before the pane goes. Taking focus back
1113
+ // here first would move `document.activeElement` off the webview, and
1114
+ // the renderer's own check of the same fact would then refuse the key
1115
+ // that this just sent.
1116
+ window.webContents.send('zero:key-in-the-file', {
1117
+ key: input.key, meta: !!input.meta, ctrl: !!input.control, alt: !!input.alt, shift: !!input.shift,
1118
+ fromGuest: true,
1119
+ });
1014
1120
  return;
1015
1121
  }
1016
1122
  // The guest page owns the keyboard while she is in the junk browser, so
@@ -1093,7 +1199,9 @@ async function createWindow() {
1093
1199
  } catch (err) {
1094
1200
  console.error('screenshot failed:', err.message);
1095
1201
  }
1096
- app.quit();
1202
+ // The picture is taken, so the run is over whether the quit works or
1203
+ // not. On Linux it did not, and the shell's `timeout` ended these runs.
1204
+ quitThenExit(app, { what: 'the screenshot run' });
1097
1205
  }, Number(process.env.ZERO_SCREENSHOT_DELAY_MS ?? 2500));
1098
1206
  }
1099
1207
 
@@ -1111,12 +1219,16 @@ async function createWindow() {
1111
1219
  if (!app.requestSingleInstanceLock()) {
1112
1220
  app.quit();
1113
1221
  } else {
1114
- app.on('second-instance', () => {
1222
+ app.on('second-instance', (_e, argv = []) => {
1115
1223
  if (window) {
1116
1224
  if (window.isMinimized()) window.restore();
1117
1225
  window.focus();
1118
1226
  }
1227
+ const link = argv.find((a) => String(a).startsWith(`${JOIN_SCHEME}://`));
1228
+ if (link) openJoinLink(link);
1119
1229
  });
1230
+ // Stripe's last page links back on the app's scheme, which only needs the window.
1231
+ app.on('open-url', (e, url) => { e.preventDefault(); if (ourLink(url)) showWindow(); openJoinLink(url); });
1120
1232
  app.whenReady().then(() => {
1121
1233
  // A from-source Linux launch keeps the launcher command installed. The
1122
1234
  // entry runs that command; this only refreshes where it points. A packaged
@@ -1132,6 +1244,9 @@ if (!app.requestSingleInstanceLock()) {
1132
1244
  // THE ABOUT BOX SAYS AGENTBOX AND ITS OWN VERSION (w-db6f5e331e). Run from
1133
1245
  // source it read the bundle's, which is Electron's: "Electron 43.0.0".
1134
1246
  app.setAboutPanelOptions({ applicationName: NAME, applicationVersion: app.getVersion(), version: '' });
1247
+ // Only the installed app takes the invite scheme: run from source it would
1248
+ // point every link on the Mac at this checkout's Electron.
1249
+ if (app.isPackaged) app.setAsDefaultProtocolClient(JOIN_SCHEME);
1135
1250
  // Serve a granted file, and nothing else. A page under this scheme is one
1136
1251
  // of ours, but it is written by an agent and read by her, so the check is
1137
1252
  // on every request rather than only on the one the pane asked for.
@@ -177,9 +177,20 @@ const programOf = (key) => key.split(' ')[0];
177
177
  */
178
178
  const measured = (list) => (list ?? []).filter((o) => o !== '?');
179
179
 
180
+ /**
181
+ * Light, however it was learned: `l` measured on its own, `c` only as part of
182
+ * a chain. Read the same everywhere, so `c` changes no verdict; it is there to
183
+ * be counted. Pinned by
184
+ * tests/the-record-says-whether-it-learned-light-from-a-chain.test.mjs.
185
+ */
186
+ const isLight = (o) => o === 'l' || o === 'c';
187
+
180
188
  /**
181
189
  * The learned record. Each key and each program keeps its last ten
182
- * observations as letters: l light, m in between, h heavy, ? not measured.
190
+ * observations as letters: l light, c light but learned only inside a chain,
191
+ * m in between, h heavy, ? not measured. A build that predates `c` drops it on
192
+ * load, which costs those runs as evidence and holds a little more than it
193
+ * needs to; it cannot call anything light that is not.
183
194
  * Both maps are kept in least-recently-seen-first order so the oldest falls off
184
195
  * first when either passes `maxKeys`.
185
196
  */
@@ -221,6 +232,15 @@ export class CommandHistory {
221
232
  if (keys.length > 1 || opaque) {
222
233
  this._push(this.keys, chainKey(keys, opaque), obs);
223
234
  if (obs !== 'l') return true;
235
+ // And only as a chain: the early return above means these records only
236
+ // ever hear about the chains that came out light, so they drift light.
237
+ // `c` marks that weaker evidence so it can be counted. It is read as `l`
238
+ // everywhere, so nothing is classified differently yet.
239
+ for (const key of keys) {
240
+ this._push(this.keys, key, 'c');
241
+ this._push(this.programs, programOf(key), 'c');
242
+ }
243
+ return true;
224
244
  }
225
245
  for (const key of keys) {
226
246
  this._push(this.keys, key, obs);
@@ -238,11 +258,17 @@ export class CommandHistory {
238
258
  if (v === 'heavy' || verdict === 'heavy') verdict = 'heavy';
239
259
  else if (v === 'unknown') verdict = 'unknown';
240
260
  };
241
- if (keys.length > 1 || opaque) {
242
- const chain = this.keys.get(chainKey(keys, opaque));
243
- if (chain?.slice(-5).includes('h')) return 'heavy';
244
- if (opaque) worse('unknown');
245
- }
261
+ // A command's own record outranks the records of its parts: it is the only
262
+ // reading that covered everything the command starts, and the parts drift
263
+ // light (see `record`). The key is coarser than the command, so an old
264
+ // heavy run also holds back cheap variants that share it; that is the
265
+ // price. A middling run only keeps the command out of the light lane, where
266
+ // `lightLane` of them would start together, and `_holdsSlot` hands the slot
267
+ // back once a run measures small.
268
+ const own = measured(this.keys.get(keys.length > 1 || opaque ? chainKey(keys, opaque) : keys[0]));
269
+ if (own.includes('h')) return 'heavy';
270
+ if (own.includes('m')) worse('unknown');
271
+ if (opaque) worse('unknown');
246
272
  for (const key of keys) worse(this._classifyKey(key));
247
273
  return verdict;
248
274
  }
@@ -251,10 +277,10 @@ export class CommandHistory {
251
277
  const own = this.keys.get(key);
252
278
  const recent = measured(own?.slice(-5));
253
279
  if (recent.includes('h')) return 'heavy';
254
- if (recent.length >= 2 && recent.every((o) => o === 'l')) return 'light';
255
- if (recent.some((o) => o !== 'l')) return 'unknown';
280
+ if (recent.length >= 2 && recent.every(isLight)) return 'light';
281
+ if (recent.some((o) => !isLight(o))) return 'unknown';
256
282
  const prog = measured(this.programs.get(programOf(key))?.slice(-10));
257
- if (prog.length >= 5 && prog.every((o) => o === 'l')) return 'light';
283
+ if (prog.length >= 5 && prog.every(isLight)) return 'light';
258
284
  return 'unknown';
259
285
  }
260
286
 
@@ -278,7 +304,7 @@ export class CommandHistory {
278
304
  if (!Array.isArray(entries)) return;
279
305
  for (const e of entries) {
280
306
  if (Array.isArray(e) && typeof e[0] === 'string' && Array.isArray(e[1])) {
281
- map.set(e[0], e[1].filter((o) => ['l', 'm', 'h', '?'].includes(o)).slice(-10));
307
+ map.set(e[0], e[1].filter((o) => ['l', 'c', 'm', 'h', '?'].includes(o)).slice(-10));
282
308
  }
283
309
  }
284
310
  };