agentbox-app 0.1.12 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/acp.mjs +296 -0
  9. package/main/agent-homes.mjs +158 -0
  10. package/main/agent-sessions.mjs +311 -260
  11. package/main/agent-updates.mjs +9 -22
  12. package/main/agents.mjs +179 -466
  13. package/main/analytics.mjs +42 -2
  14. package/main/answer-modes.mjs +103 -37
  15. package/main/approval-prompt-core.mjs +197 -0
  16. package/main/approval-prompt-server.mjs +4 -138
  17. package/main/approvals.mjs +101 -297
  18. package/main/artifact-path.mjs +47 -18
  19. package/main/claude-bin.mjs +45 -311
  20. package/main/claude-input.mjs +57 -7
  21. package/main/claude-launch-env.mjs +54 -0
  22. package/main/claude-models.mjs +39 -7
  23. package/main/claude-plan.mjs +64 -16
  24. package/main/claude-session-open.mjs +28 -0
  25. package/main/claude-usage-process.mjs +40 -0
  26. package/main/claude-usage.mjs +94 -59
  27. package/main/cloud/account.mjs +179 -0
  28. package/main/cloud/aws.mjs +127 -0
  29. package/main/cloud/browser-return.mjs +58 -0
  30. package/main/cloud/clients.mjs +15 -0
  31. package/main/cloud/codex-cloud.mjs +53 -0
  32. package/main/cloud/config.mjs +107 -0
  33. package/main/cloud/docs.mjs +219 -0
  34. package/main/cloud/exec.mjs +27 -0
  35. package/main/cloud/host.mjs +165 -0
  36. package/main/cloud/kit.mjs +80 -0
  37. package/main/cloud/logins.mjs +543 -0
  38. package/main/cloud/managed.mjs +115 -0
  39. package/main/cloud/marker.mjs +104 -0
  40. package/main/cloud/org.mjs +53 -0
  41. package/main/cloud/reuse.mjs +208 -0
  42. package/main/cloud/run-cli.mjs +177 -0
  43. package/main/cloud/run.mjs +1934 -0
  44. package/main/cloud/usage.mjs +76 -0
  45. package/main/cloud-permission.mjs +51 -0
  46. package/main/codex-account.mjs +78 -0
  47. package/main/codex-app-server.mjs +276 -409
  48. package/main/codex-approvals.mjs +148 -433
  49. package/main/codex-bin.mjs +51 -249
  50. package/main/codex-models.mjs +20 -2
  51. package/main/codex-session.mjs +246 -506
  52. package/main/codex-threads-worker.mjs +12 -0
  53. package/main/codex-threads.mjs +112 -11
  54. package/main/codex-usage-file.mjs +5 -3
  55. package/main/codex-usage.mjs +56 -7
  56. package/main/codex-watch.mjs +83 -5
  57. package/main/codex.mjs +95 -363
  58. package/main/config.mjs +37 -5
  59. package/main/corner-tag.mjs +810 -0
  60. package/main/crash-report.mjs +26 -3
  61. package/main/dev-window.mjs +1 -1
  62. package/main/engine-bin.mjs +545 -0
  63. package/main/engine-setup.mjs +25 -24
  64. package/main/feedback.mjs +14 -31
  65. package/main/folder-pictures.mjs +2 -2
  66. package/main/freeze-recorder-thread.mjs +86 -0
  67. package/main/freeze-recorder.mjs +146 -0
  68. package/main/fresh-user.mjs +20 -9
  69. package/main/git-change-offthread.mjs +17 -3
  70. package/main/git-change-thread.mjs +5 -2
  71. package/main/harnesses/claude-stream.mjs +208 -0
  72. package/main/harnesses/claude.mjs +105 -0
  73. package/main/harnesses/codex.mjs +72 -0
  74. package/main/harnesses/common.mjs +78 -0
  75. package/main/harnesses/instructions-file.mjs +116 -0
  76. package/main/harnesses/opencode.mjs +234 -0
  77. package/main/harnesses.mjs +65 -0
  78. package/main/importable-threads.mjs +96 -0
  79. package/main/ipc.mjs +520 -135
  80. package/main/leftovers.mjs +117 -25
  81. package/main/liquid-glass.mjs +26 -0
  82. package/main/live-replies.mjs +10 -4
  83. package/main/local-app-frames.mjs +82 -0
  84. package/main/main-went-red.mjs +330 -0
  85. package/main/main.mjs +248 -37
  86. package/main/memory-gate-history.mjs +85 -20
  87. package/main/memory-gate-server.mjs +129 -11
  88. package/main/memory-gate.mjs +62 -8
  89. package/main/memory-log.mjs +124 -0
  90. package/main/menu.mjs +4 -2
  91. package/main/message-priority.mjs +2 -2
  92. package/main/mic.mjs +38 -0
  93. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  94. package/main/new-chats-watch.mjs +110 -0
  95. package/main/new-user-window.mjs +25 -0
  96. package/main/notify.mjs +64 -5
  97. package/main/opencode-server.mjs +271 -0
  98. package/main/opencode.mjs +287 -0
  99. package/main/phone-alerts.mjs +117 -0
  100. package/main/phone-inbox.mjs +625 -0
  101. package/main/phone-link.mjs +571 -0
  102. package/main/phone-uploads.mjs +476 -0
  103. package/main/picture-shape.mjs +114 -0
  104. package/main/quit-sequence.mjs +143 -0
  105. package/main/refresh-agent-models.mjs +15 -2
  106. package/main/relay-address.mjs +28 -0
  107. package/main/relay-client.mjs +314 -0
  108. package/main/repeats.mjs +10 -4
  109. package/main/row-label.mjs +18 -47
  110. package/main/run-change.mjs +13 -0
  111. package/main/run-ledger.mjs +135 -0
  112. package/main/runs-on.mjs +32 -35
  113. package/main/save-attachments.mjs +41 -0
  114. package/main/serve.mjs +155 -9
  115. package/main/settings.mjs +364 -50
  116. package/main/shell-path.mjs +234 -0
  117. package/main/ship-conflict-brief.mjs +119 -0
  118. package/main/ship-queue.mjs +134 -20
  119. package/main/sign-in-files.mjs +3 -7
  120. package/main/source-updater.mjs +45 -11
  121. package/main/stage-attachment.mjs +68 -0
  122. package/main/stall-watch.mjs +50 -0
  123. package/main/start-hold.mjs +73 -0
  124. package/main/store/home.mjs +87 -232
  125. package/main/store/paths.mjs +11 -0
  126. package/main/store/teammates-files.mjs +99 -0
  127. package/main/store/work-items.mjs +231 -13
  128. package/main/store.mjs +227 -53
  129. package/main/supervisor.mjs +3865 -699
  130. package/main/task-commands.mjs +13 -17
  131. package/main/task-folders-offthread.mjs +6 -1
  132. package/main/task-folders.mjs +897 -105
  133. package/main/task-terminals.mjs +147 -41
  134. package/main/team/index.mjs +114 -10
  135. package/main/team/memory-cloud.mjs +183 -1
  136. package/main/team/room-hours.mjs +48 -0
  137. package/main/team/room-setup.mjs +31 -0
  138. package/main/team/rooms-memory.mjs +454 -0
  139. package/main/team/rooms-shape.mjs +42 -0
  140. package/main/team/rooms-supabase.mjs +324 -0
  141. package/main/team/rooms.mjs +575 -0
  142. package/main/team/session.mjs +96 -26
  143. package/main/team/supabase-backend.mjs +180 -0
  144. package/main/team/sync.mjs +109 -1
  145. package/main/teammates.mjs +580 -0
  146. package/main/terminal-host.mjs +143 -0
  147. package/main/terminal-hosts.mjs +261 -0
  148. package/main/transcript-search.mjs +80 -0
  149. package/main/updater.mjs +71 -9
  150. package/main/webview-guard.mjs +45 -0
  151. package/main/worktree-include.mjs +365 -0
  152. package/mcp/core/documents.mjs +3 -1
  153. package/mcp/core/repeats.mjs +88 -0
  154. package/mcp/core/teammates.mjs +222 -0
  155. package/mcp/core/work.mjs +76 -4
  156. package/mcp/stdio.mjs +24 -11
  157. package/mcp/tools.mjs +116 -8
  158. package/package.json +30 -5
  159. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
  160. package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
  161. package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
  162. package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
  163. package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
  164. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  165. package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
  166. package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
  167. package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
  168. package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
  169. package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
  170. package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
  171. package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
  172. package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
  173. package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
  174. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
  175. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  176. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  177. package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
  178. package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
  179. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
  180. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  181. package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
  182. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  183. package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
  184. package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
  185. package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
  186. package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
  187. package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
  188. package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
  189. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
  190. package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
  191. package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
  192. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
  193. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
  194. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
  195. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  196. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
  197. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  198. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
  199. package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
  200. package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
  201. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  202. package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
  203. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  204. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  205. package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
  206. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
  207. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  208. package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
  209. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
  210. package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
  211. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
  212. package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
  213. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
  214. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
  215. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
  216. package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
  217. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
  218. package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
  219. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  220. package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
  221. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
  222. package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
  223. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
  224. package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
  225. package/renderer/dist/corner-tag.html +13 -0
  226. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  227. package/renderer/dist/emulator/data/compression/README.md +11 -0
  228. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  229. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  230. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  231. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  232. package/renderer/dist/emulator/data/cores/README.md +22 -0
  233. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  234. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  235. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  236. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  237. package/renderer/dist/emulator/data/cores/package.json +65 -0
  238. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  239. package/renderer/dist/emulator/data/emulator.css +1614 -0
  240. package/renderer/dist/emulator/data/loader.js +170 -0
  241. package/renderer/dist/emulator/data/localization/README.md +63 -0
  242. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  243. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  244. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  245. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  246. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  247. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  248. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  249. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  250. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  251. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  252. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  253. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  254. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  255. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  256. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  257. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  258. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  259. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  260. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  261. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  262. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  263. package/renderer/dist/emulator/data/src/compression.js +142 -0
  264. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  265. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  266. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  267. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  268. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  269. package/renderer/dist/emulator/data/src/storage.js +125 -0
  270. package/renderer/dist/emulator/data/version.json +5 -0
  271. package/renderer/dist/emulator/host.js +297 -0
  272. package/renderer/dist/emulator/index.html +28 -0
  273. package/renderer/dist/emulator/provenance.json +6 -0
  274. package/renderer/dist/index.html +16 -3
  275. package/scripts/after-install.mjs +30 -8
  276. package/scripts/approval-server.sh +24 -5
  277. package/scripts/before-publish.mjs +12 -10
  278. package/scripts/cloud-shim.pl +57 -0
  279. package/shared/account-names.d.mts +6 -0
  280. package/shared/account-names.mjs +15 -0
  281. package/shared/agent-hours.mjs +136 -0
  282. package/shared/agent-import.mjs +62 -34
  283. package/shared/agents.d.mts +1 -0
  284. package/shared/agents.mjs +126 -522
  285. package/shared/analytics-events.mjs +12 -0
  286. package/shared/approval-card.mjs +36 -0
  287. package/shared/bridge-map.mjs +45 -1
  288. package/shared/claude-code-shape.mjs +6 -0
  289. package/shared/claude-commands.generated.mjs +6 -4
  290. package/shared/claude-models.generated.mjs +6 -4
  291. package/shared/claude-usage.d.mts +1 -0
  292. package/shared/claude-usage.mjs +42 -1
  293. package/shared/cloud-hand-back.d.mts +7 -0
  294. package/shared/cloud-hand-back.mjs +20 -0
  295. package/shared/cloud-kit.d.mts +219 -0
  296. package/shared/cloud-kit.mjs +341 -0
  297. package/shared/cloud-login-name.mjs +4 -0
  298. package/shared/cloud-start.mjs +17 -0
  299. package/shared/cloud-versions.mjs +23 -0
  300. package/shared/codex-import.mjs +2 -2
  301. package/shared/codex-modes.d.mts +22 -9
  302. package/shared/codex-modes.mjs +67 -11
  303. package/shared/codex-usage.mjs +108 -2
  304. package/shared/corner-tag.d.mts +36 -0
  305. package/shared/corner-tag.mjs +396 -0
  306. package/shared/dead-run-trace.mjs +19 -2
  307. package/shared/engines.mjs +76 -300
  308. package/shared/feedback.d.mts +7 -0
  309. package/shared/feedback.mjs +25 -0
  310. package/shared/first-run-practice.mjs +17 -24
  311. package/shared/harness-definitions.mjs +111 -0
  312. package/shared/instruction-defaults.json +2 -2
  313. package/shared/key-caps.d.mts +14 -0
  314. package/shared/key-caps.mjs +68 -0
  315. package/shared/message-edits.d.mts +7 -0
  316. package/shared/message-edits.mjs +35 -0
  317. package/shared/move-thread.d.mts +24 -0
  318. package/shared/move-thread.mjs +112 -0
  319. package/shared/named-threads.mjs +72 -0
  320. package/shared/not-shipped.d.mts +22 -0
  321. package/shared/not-shipped.mjs +89 -0
  322. package/shared/notify-rules.d.mts +31 -0
  323. package/shared/notify-rules.mjs +55 -8
  324. package/shared/phone-files.mjs +28 -0
  325. package/shared/phone-settings.mjs +118 -0
  326. package/shared/phone-snapshot.d.mts +16 -0
  327. package/shared/phone-snapshot.mjs +348 -0
  328. package/shared/picture-formats.d.mts +1 -0
  329. package/shared/picture-formats.mjs +13 -0
  330. package/shared/provider-commands.mjs +10 -2
  331. package/shared/rank.d.mts +4 -1
  332. package/shared/rank.mjs +82 -19
  333. package/shared/relay-address.mjs +6 -0
  334. package/shared/relay-crypto.mjs +165 -0
  335. package/shared/repeats.d.mts +8 -0
  336. package/shared/repeats.mjs +33 -2
  337. package/shared/room-cards.mjs +167 -0
  338. package/shared/room-links.d.mts +4 -0
  339. package/shared/room-links.mjs +21 -0
  340. package/shared/runs-on.d.mts +3 -13
  341. package/shared/runs-on.mjs +31 -52
  342. package/shared/shipped-note.d.mts +4 -0
  343. package/shared/shipped-note.mjs +6 -0
  344. package/shared/shortcuts.mjs +24 -10
  345. package/shared/spawn-trouble.d.mts +4 -0
  346. package/shared/spawn-trouble.mjs +484 -30
  347. package/shared/team-rules.mjs +9 -2
  348. package/shared/teammates.d.mts +19 -0
  349. package/shared/teammates.mjs +756 -0
  350. package/shared/terminal-focus.d.mts +1 -0
  351. package/shared/terminal-focus.mjs +13 -0
  352. package/shared/terminal-input.d.mts +1 -0
  353. package/shared/terminal-input.mjs +30 -0
  354. package/shared/terminal-keys.d.mts +1 -0
  355. package/shared/terminal-keys.mjs +16 -0
  356. package/shared/terminal-size.d.mts +4 -0
  357. package/shared/terminal-size.mjs +29 -0
  358. package/shared/thread-cards.mjs +4 -1
  359. package/shared/usage.d.mts +12 -0
  360. package/shared/usage.mjs +56 -3
  361. package/shared/websites.d.mts +15 -0
  362. package/shared/websites.mjs +61 -0
  363. package/shared/where-it-runs-from.d.mts +43 -0
  364. package/shared/where-it-runs-from.mjs +155 -0
  365. package/shared/work-items.mjs +150 -29
  366. package/shared/work-lines.mjs +93 -245
  367. package/shared/xterm-invalid-range.d.mts +2 -0
  368. package/shared/xterm-invalid-range.mjs +58 -0
  369. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  370. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -0,0 +1,143 @@
1
+ // TASK TERMINALS OUTLIVE THE APP. This process holds the shells. It is started
2
+ // detached, so quitting or restarting the app leaves them running; restarting
3
+ // the computer ends them like everything else. The app reconnects when it starts.
4
+ //
5
+ // Lines of JSON over a socket only this user can reach: `{id, op, args}` in,
6
+ // `{id, value}` or `{id, error}` back. The ops are TaskTerminals' own.
7
+ // With no terminal and no app connected for a minute it ends itself.
8
+ //
9
+ // ELECTRON_RUN_AS_NODE=1 <electron> main/terminal-host.mjs <socket>
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import net from 'node:net';
13
+ import path from 'node:path';
14
+ import crypto from 'node:crypto';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { nameSlug } from '../shared/product-name.mjs';
17
+
18
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
19
+ // The app talks to any host speaking this, whichever version started it, so
20
+ // the terminals an older app left are still reachable after an update. Change
21
+ // it only with a client that still speaks the old one.
22
+ export const PROTOCOL = 1;
23
+ // A host started by older code keeps the terminals it has; new terminals open
24
+ // in a host made of the code the app is running now.
25
+ const MADE_OF = ['terminal-host.mjs', 'task-terminals.mjs', '../shared/terminal-state.mjs'];
26
+ export const HOST_ALREADY_THERE = 3;
27
+ const OPS = new Set(['open', 'read', 'write', 'resize', 'close', 'end', 'list']);
28
+
29
+ export function terminalHostStamp() {
30
+ const h = crypto.createHash('sha1');
31
+ for (const f of MADE_OF) { try { h.update(fs.readFileSync(path.join(HERE, f))); } catch { h.update(f); } }
32
+ return h.digest('hex').slice(0, 12);
33
+ }
34
+
35
+ const prefix = (uid) => `${nameSlug}-terminals-${uid}-`;
36
+
37
+ // Short enough for a socket's path (104 bytes on macOS), and this user's own.
38
+ export function terminalHostSocket({ stamp = terminalHostStamp(), dir = os.tmpdir(), uid = process.getuid?.() ?? 0 } = {}) {
39
+ return path.join(dir, `${prefix(uid)}${stamp}.sock`);
40
+ }
41
+
42
+ /** Every host socket of this user's, newest code or not. */
43
+ export function terminalHostSockets({ dir = os.tmpdir(), uid = process.getuid?.() ?? 0 } = {}) {
44
+ try {
45
+ return fs.readdirSync(dir).filter((f) => f.startsWith(prefix(uid)) && f.endsWith('.sock')).map((f) => path.join(dir, f));
46
+ } catch { return []; }
47
+ }
48
+
49
+ export function serveTerminals({ sockPath, terminals, stamp = terminalHostStamp(), idleMs = 60_000, onIdle = () => {}, log = () => {} }) {
50
+ let clients = 0;
51
+ let idleTimer = null;
52
+ const idle = () => {
53
+ clearTimeout(idleTimer);
54
+ if (clients || terminals.sessions.size) return;
55
+ idleTimer = setTimeout(() => { if (!clients && !terminals.sessions.size) onIdle(); }, idleMs);
56
+ idleTimer.unref?.();
57
+ };
58
+ const watch = setInterval(idle, idleMs);
59
+ watch.unref?.();
60
+ const answer = async ({ op, args = [] }) => {
61
+ if (op === 'hello') return { protocol: PROTOCOL, stamp, pid: process.pid };
62
+ if (!OPS.has(op)) throw new Error(`Unknown terminal action: ${op}`);
63
+ // The host cannot read the store, so it never works out a folder itself:
64
+ // a terminal it no longer has is one the app opens afresh.
65
+ if (op === 'open' && !args[1] && !terminals.sessions.has(args[0])) throw new Error('Terminal session ended. Open a new terminal to continue.');
66
+ return terminals[op](...args);
67
+ };
68
+ const server = net.createServer((sock) => {
69
+ clients += 1;
70
+ clearTimeout(idleTimer);
71
+ let buf = '';
72
+ const send = (msg) => { if (!sock.destroyed) sock.write(`${JSON.stringify(msg)}\n`); };
73
+ sock.setEncoding('utf8');
74
+ sock.on('data', (d) => {
75
+ buf += d;
76
+ let n;
77
+ while ((n = buf.indexOf('\n')) >= 0) {
78
+ const line = buf.slice(0, n);
79
+ buf = buf.slice(n + 1);
80
+ let msg;
81
+ try { msg = JSON.parse(line); } catch { continue; }
82
+ Promise.resolve().then(() => answer(msg))
83
+ .then((value) => send({ id: msg.id, value: value ?? null }), (err) => send({ id: msg.id, error: String(err?.message ?? err) }));
84
+ }
85
+ });
86
+ sock.on('close', () => { clients -= 1; idle(); });
87
+ sock.on('error', () => {});
88
+ });
89
+ server.on('error', (err) => log(`terminal host: ${err.message}`));
90
+ // Node deletes the path a server listened on as it closes, so it listens on
91
+ // a name of its own and moves it into place, and on the way out takes away
92
+ // only a socket that is still its own.
93
+ let mine = null;
94
+ const own = `${sockPath}.${process.pid}`;
95
+ const ours = () => { try { return mine !== null && fs.statSync(sockPath).ino === mine; } catch { return false; } };
96
+ const started = new Promise((resolve) => {
97
+ const probe = net.connect(sockPath);
98
+ const timer = setTimeout(() => { probe.destroy(); resolve('taken'); }, 2_000);
99
+ probe.once('connect', () => { clearTimeout(timer); probe.destroy(); resolve('taken'); });
100
+ probe.once('error', () => {
101
+ clearTimeout(timer);
102
+ try { fs.unlinkSync(own); } catch { /* not there */ }
103
+ server.listen(own, () => {
104
+ try { fs.chmodSync(own, 0o600); fs.renameSync(own, sockPath); mine = fs.statSync(sockPath).ino; } catch (err) { log(`terminal host: ${err.message}`); }
105
+ idle();
106
+ resolve('serving');
107
+ });
108
+ });
109
+ });
110
+ return {
111
+ server,
112
+ started,
113
+ close: () => started.then(() => new Promise((resolve) => {
114
+ clearTimeout(idleTimer);
115
+ clearInterval(watch);
116
+ if (ours()) { try { fs.unlinkSync(sockPath); } catch { /* gone */ } }
117
+ if (server.listening) server.close(() => resolve()); else resolve();
118
+ })),
119
+ };
120
+ }
121
+
122
+ // Started as a program by the app (main/terminal-hosts.mjs).
123
+ if (path.basename(process.argv[1] || '') === 'terminal-host.mjs') {
124
+ const sockPath = process.argv[2];
125
+ // Loaded before the socket opens, so a host that cannot make a shell never
126
+ // answers and the app keeps its terminals itself.
127
+ (await import('node:module')).createRequire(import.meta.url)('node-pty');
128
+ const { TaskTerminals } = await import('./task-terminals.mjs');
129
+ const { readMacPressureWord } = await import('./leftovers.mjs');
130
+ const terminals = new TaskTerminals({ readPressure: process.platform === 'darwin' ? readMacPressureWord : null });
131
+ const host = serveTerminals({
132
+ sockPath,
133
+ terminals,
134
+ onIdle: () => host.close().then(() => process.exit(0)),
135
+ log: (m) => process.stderr.write(`${m}\n`),
136
+ });
137
+ if (await host.started === 'taken') process.exit(HOST_ALREADY_THERE);
138
+ // One shell's stray error is that shell's, not every terminal's.
139
+ process.on('uncaughtException', (err) => process.stderr.write(`terminal host: ${err?.stack || err}\n`));
140
+ process.on('unhandledRejection', (err) => process.stderr.write(`terminal host: ${err?.stack || err}\n`));
141
+ // The app going is not a reason to end, and nor is the terminal it ran from.
142
+ process.on('SIGHUP', () => {});
143
+ }
@@ -0,0 +1,261 @@
1
+ // THE APP'S SIDE OF ./terminal-host.mjs: the same calls as TaskTerminals, each
2
+ // sent to whichever host holds that terminal. A new terminal opens in the host
3
+ // made of this app's code, started when there is none; one an older app left
4
+ // stays in the older host until it ends. If no host will start, terminals run
5
+ // inside the app as they used to, and the terminal's first line says so.
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import net from 'node:net';
9
+ import { spawn } from 'node:child_process';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { PROTOCOL, HOST_ALREADY_THERE, terminalHostStamp, terminalHostSocket, terminalHostSockets } from './terminal-host.mjs';
12
+ import { ENDED } from './task-terminals.mjs';
13
+ import { SETTINGS_TERMINAL } from '../shared/settings-terminal.mjs';
14
+ import { NAME } from '../shared/product-name.mjs';
15
+
16
+ const ENDED_SESSION = 'Terminal session ended. Open a new terminal to continue.';
17
+ // Closing a task ends its terminal this long after, so a Z that reopens it
18
+ // keeps the terminal.
19
+ export const CLOSE_GRACE_MS = 2 * 60_000;
20
+ // A host that would not start is not tried again for this long.
21
+ const RETRY_MS = 5 * 60_000;
22
+
23
+ export function launchTerminalHost(sockPath, { logFile = null } = {}) {
24
+ const out = logFile ? fs.openSync(logFile, 'a') : 'ignore';
25
+ try {
26
+ const proc = spawn(process.execPath, [fileURLToPath(new URL('./terminal-host.mjs', import.meta.url)), sockPath], {
27
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
28
+ detached: true,
29
+ stdio: ['ignore', out, out],
30
+ });
31
+ proc.on('error', () => {});
32
+ proc.unref();
33
+ return proc;
34
+ } finally {
35
+ if (typeof out === 'number') fs.closeSync(out);
36
+ }
37
+ }
38
+
39
+ /** One socket to one host, every call with a deadline that ends it. */
40
+ export function connectTerminalHost(sockPath, { connectMs = 2_000, callMs = 5_000 } = {}) {
41
+ return new Promise((resolve, reject) => {
42
+ const sock = net.connect(sockPath);
43
+ const pending = new Map();
44
+ let next = 1;
45
+ let buf = '';
46
+ const conn = {
47
+ sockPath,
48
+ closed: false,
49
+ call(op, ...args) {
50
+ if (conn.closed) return Promise.reject(new Error(ENDED_SESSION));
51
+ const id = next++;
52
+ return new Promise((res, rej) => {
53
+ const timer = setTimeout(() => { pending.delete(id); rej(new Error('The terminal did not answer.')); }, callMs);
54
+ pending.set(id, { res, rej, timer });
55
+ sock.write(`${JSON.stringify({ id, op, args })}\n`);
56
+ });
57
+ },
58
+ close() { sock.destroy(); },
59
+ };
60
+ const timer = setTimeout(() => { sock.destroy(); reject(new Error('The terminal host did not answer.')); }, connectMs);
61
+ sock.setEncoding('utf8');
62
+ sock.once('connect', () => { clearTimeout(timer); resolve(conn); });
63
+ sock.on('data', (d) => {
64
+ buf += d;
65
+ let n;
66
+ while ((n = buf.indexOf('\n')) >= 0) {
67
+ let msg = null;
68
+ try { msg = JSON.parse(buf.slice(0, n)); } catch { /* not ours */ }
69
+ buf = buf.slice(n + 1);
70
+ const p = msg && pending.get(msg.id);
71
+ if (!p) continue;
72
+ pending.delete(msg.id);
73
+ clearTimeout(p.timer);
74
+ if (msg.error !== undefined) p.rej(new Error(msg.error)); else p.res(msg.value);
75
+ }
76
+ });
77
+ sock.on('error', (err) => { clearTimeout(timer); reject(err); });
78
+ sock.on('close', () => {
79
+ conn.closed = true;
80
+ for (const p of pending.values()) { clearTimeout(p.timer); p.rej(new Error(ENDED_SESSION)); }
81
+ pending.clear();
82
+ });
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Whether a terminal's task is closed. Only a task the store says is done: one
88
+ * it cannot find may be another store's, and the sign-in terminal and an
89
+ * outside agent's belong to no task.
90
+ */
91
+ export const taskClosed = (store) => (key) => {
92
+ const { product, id } = JSON.parse(key);
93
+ if (product === SETTINGS_TERMINAL.product || String(id).startsWith('agent:')) return false;
94
+ return store.readItem?.(product, id)?.status === 'done';
95
+ };
96
+
97
+ /**
98
+ * Terminals whose task has been closed for `graceMs` on every look since. `seen`
99
+ * carries when each was first seen closed from one look to the next.
100
+ */
101
+ export function closedTerminals(list, isClosed, seen, now, graceMs = CLOSE_GRACE_MS) {
102
+ const keys = new Set(list.map((s) => s.key));
103
+ for (const key of [...seen.keys()]) if (!keys.has(key)) seen.delete(key);
104
+ const due = [];
105
+ for (const { key } of list) {
106
+ let closed = false;
107
+ try { closed = !!isClosed(key); } catch { closed = false; }
108
+ if (!closed) { seen.delete(key); continue; }
109
+ if (!seen.has(key)) seen.set(key, now);
110
+ if (now - seen.get(key) >= graceMs) due.push(key);
111
+ }
112
+ return due;
113
+ }
114
+
115
+ export class TerminalHosts {
116
+ /**
117
+ * `local` is the app's own TaskTerminals, for when no host will start.
118
+ */
119
+ constructor({
120
+ local,
121
+ stamp = terminalHostStamp(),
122
+ dir = os.tmpdir(),
123
+ uid = process.getuid?.() ?? 0,
124
+ launch = launchTerminalHost,
125
+ connect = connectTerminalHost,
126
+ logFile = null,
127
+ startMs = 8_000,
128
+ now = () => Date.now(),
129
+ log = () => {},
130
+ }) {
131
+ Object.assign(this, { local, stamp, dir, uid, launch, connect, logFile, startMs, now, log });
132
+ this.ownPath = terminalHostSocket({ stamp, dir, uid });
133
+ this.conns = new Map();
134
+ this.where = new Map();
135
+ this.closedSeen = new Map();
136
+ this.failedAt = null;
137
+ this.failure = null;
138
+ const t = local;
139
+ this.inApp = { local: true, call: async (op, ...args) => t[op](...args) };
140
+ }
141
+
142
+ async _hello(sockPath) {
143
+ const had = this.conns.get(sockPath);
144
+ if (had && !had.closed) return had;
145
+ // Every open hands over this user's environment, so never to a socket
146
+ // somebody else put at the name.
147
+ try { if (process.getuid && fs.statSync(sockPath).uid !== process.getuid()) return null; } catch { return null; }
148
+ let conn;
149
+ try { conn = await this.connect(sockPath); } catch { return null; }
150
+ try {
151
+ const hi = await conn.call('hello');
152
+ if (hi?.protocol !== PROTOCOL) { conn.close(); return null; }
153
+ } catch { conn.close(); return null; }
154
+ this.conns.set(sockPath, conn);
155
+ return conn;
156
+ }
157
+
158
+ /** The hosts an earlier run of the app left, found once. */
159
+ ready() { return (this.discovered ??= this.discover().catch(() => {})); }
160
+
161
+ /** Every host this user has, and which terminal is in which. */
162
+ async discover() {
163
+ for (const sockPath of terminalHostSockets({ dir: this.dir, uid: this.uid })) await this._hello(sockPath);
164
+ await this.list();
165
+ }
166
+
167
+ /** The host made of this app's code, started if there is none. */
168
+ async _mine() {
169
+ const there = await this._hello(this.ownPath);
170
+ if (there) return there;
171
+ if (this.failedAt !== null && this.now() - this.failedAt < RETRY_MS) throw new Error(this.failure);
172
+ let proc;
173
+ try { proc = this.launch(this.ownPath, { logFile: this.logFile }); } catch (err) { return this._failed(err.message); }
174
+ const end = Date.now() + this.startMs;
175
+ while (Date.now() < end) {
176
+ await new Promise((r) => setTimeout(r, 100));
177
+ const conn = await this._hello(this.ownPath);
178
+ if (conn) { this.failedAt = null; this.failure = null; return conn; }
179
+ if (Number.isInteger(proc?.exitCode) && proc.exitCode !== HOST_ALREADY_THERE) return this._failed(`it exited with code ${proc.exitCode}`);
180
+ }
181
+ return this._failed(`no answer in ${Math.round(this.startMs / 1000)} seconds`);
182
+ }
183
+
184
+ _failed(why) {
185
+ this.failedAt = this.now();
186
+ this.failure = why;
187
+ this.log(`terminal host: ${why}`);
188
+ throw new Error(why);
189
+ }
190
+
191
+ _at(key) {
192
+ const at = this.where.get(key);
193
+ if (!at || at.closed) throw new Error(ENDED_SESSION);
194
+ return at;
195
+ }
196
+
197
+ async open(key, place) {
198
+ await this.ready();
199
+ const at = this.where.get(key);
200
+ if (at && !at.closed && (!at.local || this.local.sessions.has(key))) {
201
+ try { return await at.call('open', key); } catch { /* ended there: a new one below */ }
202
+ }
203
+ this.where.delete(key);
204
+ let host;
205
+ try { host = await this._mine(); } catch (err) {
206
+ const notice = `This terminal ends when ${NAME} quits, because the process that keeps terminals running through a restart would not start (${err.message}).`;
207
+ const state = this.local.open(key, { ...place, notice: [place?.notice, notice].filter(Boolean).join(' ') });
208
+ this.where.set(key, this.inApp);
209
+ return state;
210
+ }
211
+ const state = await host.call('open', key, place);
212
+ this.where.set(key, host);
213
+ return state;
214
+ }
215
+
216
+ read(key, offset, wait) { return this._at(key).call('read', key, offset, wait); }
217
+ write(key, data) { return this._at(key).call('write', key, data); }
218
+ resize(key, cols, rows) { return this._at(key).call('resize', key, cols, rows); }
219
+ async close(key) {
220
+ const at = this.where.get(key);
221
+ this.where.delete(key);
222
+ if (at && !at.closed) await at.call('close', key);
223
+ }
224
+ async end(key, why) {
225
+ const at = this.where.get(key);
226
+ if (at && !at.closed) await at.call('end', key, why);
227
+ }
228
+
229
+ /** Every terminal, in every host and in the app, freshly asked. */
230
+ async list() {
231
+ const out = [];
232
+ const answered = new Set();
233
+ for (const [sockPath, conn] of this.conns) {
234
+ if (conn.closed) { this.conns.delete(sockPath); continue; }
235
+ let got = [];
236
+ try { got = await conn.call('list'); } catch { continue; }
237
+ answered.add(conn);
238
+ // An older host with nothing left ends itself once nobody is connected.
239
+ if (!got.length && sockPath !== this.ownPath) { conn.close(); this.conns.delete(sockPath); continue; }
240
+ for (const s of got) { this.where.set(s.key, conn); out.push(s); }
241
+ }
242
+ const listed = new Set(out.map((s) => s.key));
243
+ for (const [key, at] of this.where) {
244
+ if (at.closed || (answered.has(at) && !listed.has(key)) || (at.local && !this.local.sessions.has(key))) this.where.delete(key);
245
+ }
246
+ for (const s of this.local.list()) if (this.where.get(s.key) === this.inApp) out.push(s);
247
+ return out;
248
+ }
249
+
250
+ /** Whether this task has a terminal, as far as the app last knew. */
251
+ hasTerminal(id) {
252
+ for (const key of this.where.keys()) { try { if (JSON.parse(key).id === id) return true; } catch { /* not a task's */ } }
253
+ return false;
254
+ }
255
+
256
+ async endClosed(isClosed) {
257
+ const due = closedTerminals(await this.list(), isClosed, this.closedSeen, this.now());
258
+ for (const key of due) { try { await this.end(key, ENDED.closed); } catch { /* gone already */ } }
259
+ return due;
260
+ }
261
+ }
@@ -0,0 +1,80 @@
1
+ // FINDING A CLAUDE CODE TRANSCRIPT WITHOUT HOLDING THE APP MORE THAN ONCE.
2
+ //
3
+ // Claude Code keeps a session at `<projects>/<slug of its folder>/<id>.jsonl`.
4
+ // When it is not there the only way to find it is to look in every folder:
5
+ // ten thousand on a busy Mac, 1.4 s on the window's thread. So a session found
6
+ // nowhere is not looked for again for ten minutes, and a caller picks how to look:
7
+ // 'now' in this call, for an answer that decides a resume (an imported
8
+ // thread is filed under the folder it first ran in)
9
+ // 'later' in the background; this call answers null, a later one finds it
10
+ // 'never' only at the slug path: a cloud run's transcript comes home there
11
+ // and nowhere else (main/cloud/run.mjs transcriptFolderName)
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+
15
+ export const MISS_KEPT_MS = 10 * 60_000;
16
+
17
+ const found = new Map(); // `${projects}\0${id}` -> transcript path
18
+ const missed = new Map(); // `${projects}\0${id}` -> when it was last looked for and not found
19
+ const wanted = new Map(); // projects folder -> session ids to look for
20
+ let search = null;
21
+
22
+ export const slugFor = (cwd) => String(cwd || '').replace(/[^a-zA-Z0-9]/g, '-');
23
+ const keyOf = (projects, id) => `${projects}\0${id}`;
24
+
25
+ function remember(projects, id, file) {
26
+ if (file) { found.set(keyOf(projects, id), file); missed.delete(keyOf(projects, id)); return; }
27
+ missed.set(keyOf(projects, id), Date.now());
28
+ while (missed.size > 1000) missed.delete(missed.keys().next().value);
29
+ }
30
+
31
+ /** The transcript's path, or null. `look` says where else to look when it is not at the slug path. */
32
+ export function findTranscript(projects, sessionId, cwd, { look = 'later' } = {}) {
33
+ if (!projects || !sessionId) return null;
34
+ const key = keyOf(projects, sessionId);
35
+ const known = found.get(key);
36
+ if (known && fs.existsSync(known)) return known;
37
+ // Claude Code names the folder from the resolved path: /tmp is /private/tmp on a Mac.
38
+ let real = cwd;
39
+ try { real = cwd ? fs.realpathSync(cwd) : cwd; } catch { /* the folder is gone */ }
40
+ for (const at of new Set([cwd, real])) {
41
+ const direct = path.join(projects, slugFor(at), `${sessionId}.jsonl`);
42
+ if (fs.existsSync(direct)) { found.set(key, direct); return direct; }
43
+ }
44
+ if (look === 'never' || Date.now() - (missed.get(key) ?? -Infinity) < MISS_KEPT_MS) return null;
45
+ if (look === 'later') { lookEverywhere(projects, sessionId); return null; }
46
+ let file = null;
47
+ try {
48
+ for (const dir of fs.readdirSync(projects)) {
49
+ const p = path.join(projects, dir, `${sessionId}.jsonl`);
50
+ if (fs.existsSync(p)) { file = p; break; }
51
+ }
52
+ } catch { /* no projects folder */ }
53
+ remember(projects, sessionId, file);
54
+ return file;
55
+ }
56
+
57
+ function lookEverywhere(projects, sessionId) {
58
+ if (!wanted.has(projects)) wanted.set(projects, new Set());
59
+ wanted.get(projects).add(sessionId);
60
+ search ??= (async () => {
61
+ while (wanted.size) {
62
+ const [root, ids] = wanted.entries().next().value;
63
+ wanted.delete(root);
64
+ const left = new Set(ids);
65
+ let dirs = [];
66
+ try { dirs = await fs.promises.readdir(root); } catch { /* no projects folder */ }
67
+ for (const dir of dirs) {
68
+ if (!left.size) break;
69
+ for (const id of [...left]) {
70
+ const p = path.join(root, dir, `${id}.jsonl`);
71
+ try { await fs.promises.access(p); remember(root, id, p); left.delete(id); } catch { /* not here */ }
72
+ }
73
+ }
74
+ for (const id of left) remember(root, id, null);
75
+ }
76
+ })().finally(() => { search = null; });
77
+ }
78
+
79
+ /** The background look, when one is running; settled otherwise. */
80
+ export const transcriptSearch = () => search ?? Promise.resolve();
package/main/updater.mjs CHANGED
@@ -48,6 +48,7 @@
48
48
 
49
49
  import { createRequire } from 'node:module';
50
50
  import { Name } from '../shared/product-name.mjs';
51
+ import { MOVE_SAY } from '../shared/where-it-runs-from.mjs';
51
52
 
52
53
  const require = createRequire(import.meta.url);
53
54
 
@@ -68,6 +69,14 @@ export function describe(state) {
68
69
  const { phase, currentVersion, newVersion, percent, error, checkedAt, readyAt, source, changes, behind } = state;
69
70
  const installing = phase === 'installing';
70
71
  return {
72
+ // WHETHER THIS COPY CAN UPDATE ITSELF FROM WHERE IT IS, and the one press
73
+ // that fixes it when it cannot (main/where-it-runs-from.mjs). It rides the
74
+ // updater's own state because the three surfaces that would have to ask
75
+ // separately — the Settings row, the sidebar card and ⌘K — all already
76
+ // read this one object, and a second channel for it is a second answer.
77
+ canMove: !!state.canMove,
78
+ place: state.canMove ? (state.place ?? null) : null,
79
+ moveError: state.moveError ?? null,
71
80
  phase,
72
81
  currentVersion,
73
82
  newVersion: newVersion ?? null,
@@ -96,7 +105,17 @@ export function describe(state) {
96
105
  };
97
106
  }
98
107
 
99
- export function createUpdater({ app, log = console, onChanged = () => {} }) {
108
+ /**
109
+ * `autoUpdater` is a SEAM FOR THE TESTS and nothing else. The real one is
110
+ * required below; passing one in is how the failure paths get tested without a
111
+ * packaged build, a signed release and a network, which is the only reason the
112
+ * download bug (see the swallow in `check`) went seven reports unnoticed.
113
+ */
114
+ export function createUpdater({ app, log = console, onChanged = () => {}, autoUpdater: given = null, place = null }) {
115
+ // READ ONCE, at construction (main/where-it-runs-from.mjs says why the
116
+ // bundle cannot move under a running process). A build with no place passed
117
+ // in — the tests, and `main/serve.mjs` — simply never offers the move.
118
+ const where = place?.state?.() ?? { place: null, canUpdate: true, offerMove: false };
100
119
  let state = {
101
120
  phase: 'idle',
102
121
  currentVersion: app.getVersion(),
@@ -105,6 +124,9 @@ export function createUpdater({ app, log = console, onChanged = () => {} }) {
105
124
  error: null,
106
125
  checkedAt: null,
107
126
  readyAt: null,
127
+ canMove: !!where.offerMove,
128
+ place: where.place ?? null,
129
+ moveError: null,
108
130
  };
109
131
  let timer = null;
110
132
  let stopped = false;
@@ -117,12 +139,14 @@ export function createUpdater({ app, log = console, onChanged = () => {} }) {
117
139
  // Loaded lazily and inside a try, so a build that somehow shipped without the
118
140
  // dependency degrades to "no updates" instead of failing to boot at all. The
119
141
  // app not starting is a strictly worse outcome than the app not updating.
120
- let autoUpdater = null;
121
- try {
122
- ({ autoUpdater } = require('electron-updater'));
123
- } catch (e) {
124
- set({ phase: 'unsupported', error: 'This build has no updater in it.' });
125
- log.warn?.('[updater] electron-updater is not available:', e?.message ?? e);
142
+ let autoUpdater = given;
143
+ if (!autoUpdater) {
144
+ try {
145
+ ({ autoUpdater } = require('electron-updater'));
146
+ } catch (e) {
147
+ set({ phase: 'unsupported', error: 'This build has no updater in it.' });
148
+ log.warn?.('[updater] electron-updater is not available:', e?.message ?? e);
149
+ }
126
150
  }
127
151
 
128
152
  if (autoUpdater) {
@@ -184,7 +208,23 @@ export function createUpdater({ app, log = console, onChanged = () => {} }) {
184
208
  if (state.phase === 'ready') return describe(state);
185
209
  if (state.phase === 'checking' || (state.phase === 'downloading' && !manual)) return describe(state);
186
210
  try {
187
- await autoUpdater.checkForUpdates();
211
+ const found = await autoUpdater.checkForUpdates();
212
+ // THE DOWNLOAD IS A SECOND PROMISE AND NOBODY WAS HOLDING IT. With
213
+ // `autoDownload` on, electron-updater starts the download inside the
214
+ // check and hands it back here as `downloadPromise`; its own
215
+ // `downloadUpdate` dispatches the `error` event and then RE-THROWS. The
216
+ // await above resolves on the RESULT, not on the download, so a failed
217
+ // download had nowhere to land but `unhandledRejection` — and
218
+ // `main/crash-report.mjs` files one of those as a crash.
219
+ //
220
+ // That is 7 of the 14 crash reports of launch week (PostHog, 2026-10-08)
221
+ // and not one of them was a crash: five are Squirrel's read-only volume
222
+ // error from an app run out of the disk image, two are a dropped
223
+ // connection, and in every one of them the app carried on running. The
224
+ // failure is ALREADY on screen by the time we get here, because the
225
+ // `error` handler above set it, so there is nothing left to do with this
226
+ // rejection but stop it being mistaken for a crash.
227
+ found?.downloadPromise?.catch?.(() => {});
188
228
  } catch (e) {
189
229
  set({ phase: 'error', error: shortError(e), checkedAt: Date.now() });
190
230
  }
@@ -201,6 +241,18 @@ export function createUpdater({ app, log = console, onChanged = () => {} }) {
201
241
  return true;
202
242
  }
203
243
 
244
+ // THE OTHER BUTTON, for a copy that cannot update itself from where it is.
245
+ // Electron moves the bundle and relaunches, so a `true` here is the last
246
+ // thing this process does. A failure goes into the state like every other
247
+ // failure, so the row and the sidebar card both say it and both offer
248
+ // another press.
249
+ function move() {
250
+ if (!state.canMove) return { moved: false, error: null };
251
+ const got = place?.move?.() ?? { moved: false, error: null };
252
+ if (!got.moved) set({ moveError: got.error ?? null });
253
+ return got;
254
+ }
255
+
204
256
  function start() {
205
257
  if (stopped || timer) return;
206
258
  const tick = () => { void check(); };
@@ -215,7 +267,7 @@ export function createUpdater({ app, log = console, onChanged = () => {} }) {
215
267
  timer = null;
216
268
  }
217
269
 
218
- return { start, stop, check, install, state: () => describe(state) };
270
+ return { start, stop, check, install, move, state: () => describe(state) };
219
271
  }
220
272
 
221
273
  // electron-updater's errors arrive as Error objects whose `message` is
@@ -234,6 +286,16 @@ export function shortError(e) {
234
286
  // releases/download/v0.1.0/latest-mac.yml): HttpErr…" to her. The sentence
235
287
  // for exactly that failure was already on the line below it.
236
288
  const named = `${e?.code ?? ''} ${raw}`;
289
+ // SQUIRREL'S OWN PARAGRAPH, WHICH IS 240 CHARACTERS AND HAS A URL IN IT.
290
+ // "Cannot update while running on a read-only volume. The application is on
291
+ // a read-only volume. Please move the application and try again. If you'll
292
+ // need to move the application out of the Downloads directory. See
293
+ // https://github.com/Squirrel/Squirrel.Mac …" — the whole of what 23 of the
294
+ // 26 main-rejection reports carried (PostHog, 2026-10-12), and the Settings
295
+ // row showed the first 160 characters of it. The app knows where it is
296
+ // running from (shared/where-it-runs-from.mjs), so this is one sentence and
297
+ // the row beside it is the press that fixes it.
298
+ if (/read-only volume/i.test(raw)) return MOVE_SAY.cannotUpdateHere;
237
299
  if (/ERR_UPDATER_ZIP_FILE_NOT_FOUND/.test(named)) return 'That release has no app in it to install.';
238
300
  if (/ERR_UPDATER_CHANNEL_FILE_NOT_FOUND/.test(named)) return 'That release is missing its update file.';
239
301
  if (/ERR_UPDATER_NO_PUBLISHED_VERSIONS|LATEST_VERSION_NOT_FOUND/.test(named)) return 'No published version to update to yet.';
@@ -0,0 +1,45 @@
1
+ // What an embedded page may do, decided here rather than on the tag that
2
+ // asked for it. The preload is the one that matters: `preload.cjs` is the
3
+ // bridge the app's own page talks to the store through, so a page off the web
4
+ // holding it holds everything the person holds.
5
+ //
6
+ // The partition is deliberately left alone. It says WHERE a page's sign-ins
7
+ // go rather than what the page may reach, and flattening it would merge the
8
+ // website pane's store into the default one (shared/websites.mjs).
9
+ //
10
+ // Pinned by tests/a-website-in-the-pane-cannot-reach-the-app.test.mjs.
11
+
12
+ /**
13
+ * Pin one attaching webview to the floor. `webPreferences` and `params` are
14
+ * Electron's own objects from `will-attach-webview`, mutated in place, which
15
+ * is how that event is answered.
16
+ */
17
+ export function hardenAttachedWebview(webPreferences, params) {
18
+ if (webPreferences) {
19
+ // Nothing of ours reaches the page.
20
+ delete webPreferences.preload;
21
+ delete webPreferences.preloadURL;
22
+ webPreferences.nodeIntegration = false;
23
+ webPreferences.nodeIntegrationInSubFrames = false;
24
+ webPreferences.nodeIntegrationInWorker = false;
25
+ webPreferences.contextIsolation = true;
26
+ webPreferences.sandbox = true;
27
+ webPreferences.webSecurity = true;
28
+ webPreferences.allowRunningInsecureContent = false;
29
+ webPreferences.experimentalFeatures = false;
30
+ webPreferences.enableBlinkFeatures = '';
31
+
32
+ // No embedded page starts sound before a click in it. Electron's default
33
+ // lets any page start media the moment it loads.
34
+ webPreferences.autoplayPolicy = 'document-user-activation-required';
35
+ }
36
+ if (params) {
37
+ // A window the guest opens for itself would be a window with none of the
38
+ // above. Links that want one go out to the real browser instead
39
+ // (setWindowOpenHandler in main/main.mjs).
40
+ params.allowpopups = false;
41
+ params.nodeintegration = false;
42
+ params.nodeintegrationinsubframes = false;
43
+ }
44
+ return webPreferences;
45
+ }