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
@@ -63,15 +63,38 @@ export function report(kind, err, extra = {}) {
63
63
  return write(payload);
64
64
  }
65
65
 
66
+ // THE ONE KIND WHOSE PROCESS DOES NOT SURVIVE BEING REPORTED. `onUncaught`
67
+ // re-throws, by the decision three lines below where it does it, so the main
68
+ // process stops. posthog-node's `capture` only buffers (flushAt 20,
69
+ // flushInterval 10s), and the `will-quit` flush never runs for a process that
70
+ // died, so for this kind the file on disk is the only copy that can reach
71
+ // anybody. It is therefore kept even when the live send said it took the
72
+ // report: one report counted twice is a worse number, and no report at all is
73
+ // a worse app.
74
+ const DIES_REPORTING = new Set(['main-uncaught']);
75
+
66
76
  function write(payload) {
67
77
  try {
68
78
  fs.mkdirSync(ctx.dir, { recursive: true });
69
79
  const file = path.join(ctx.dir, `crash-${payload.ts.replace(/[:.]/g, '-')}-${Math.floor(Math.random() * 1e6)}.json`);
70
80
  fs.writeFileSync(file, JSON.stringify(payload, null, 2));
71
81
  prune();
72
- // No await, no retry, no queue drain here: this runs while the process is
73
- // dying. Delivery is the transport's problem, on the next boot if it has to be.
74
- if (transport) { try { transport(payload); } catch {} }
82
+ // ON DISK FIRST, ALWAYS, because this can run while the process is dying
83
+ // and there is no time to wait on a socket. No await, no retry.
84
+ //
85
+ // AND THEN TAKEN BACK OFF IT IF IT WENT. The queue is for a report nobody
86
+ // has sent yet; a report that HAS been sent and stays in it is sent a
87
+ // second time by the next launch's `drainCrashes`, which is 6 of the 21
88
+ // crash events since the Oct 6 launch being 6 crashes counted twice, and
89
+ // three of them filed against the version that drained them rather than
90
+ // the one they happened on (PostHog, 2026-10-08).
91
+ if (transport) {
92
+ let took = false;
93
+ try { took = transport(payload) !== false; } catch { took = false; }
94
+ if (took && !DIES_REPORTING.has(payload.kind)) {
95
+ try { fs.unlinkSync(file); } catch {}
96
+ }
97
+ }
75
98
  return file;
76
99
  } catch { return null; }
77
100
  }
@@ -8,7 +8,7 @@
8
8
  // The change that hurts is one to App.tsx that adds, removes or moves a
9
9
  // hook, which is what a branch switch does to it. React Fast Refresh keeps a
10
10
  // component's state only while its hooks match, so App is rebuilt from nothing:
11
- // `snap` is null again, App.tsx draws `<div className="boot">the app</div>`, and
11
+ // `snap` is null again, App.tsx draws its boot screen (`<Booting />`), and
12
12
  // every child of App is thrown away and remade. The window drops back to the
13
13
  // loading page, and an open artifact closes and reopens. The
14
14
  // page never navigates, which is why nothing in the dev server's log looked bad
@@ -0,0 +1,545 @@
1
+ // Where each coding agent is installed: one registry and one search for every
2
+ // engine the app can run. A new engine is an entry in ENGINES, not a file.
3
+ //
4
+ // Never tell somebody their coding agent is missing unless we are certain.
5
+ // `found` says a runnable file is really there and decides what gets spawned.
6
+ // `certain` says we may tell them it is missing, and is meant to be hard to earn.
7
+ //
8
+ // The user's login shell is asked first, because it has their real PATH and
9
+ // profile. The fixed list is for when it cannot answer: a double-clicked
10
+ // Electron app inherits launchd's PATH (/usr/bin:/bin:/usr/sbin:/sbin), which
11
+ // has no nvm, volta or Homebrew. A shell probe that throws falls through to the
12
+ // list and does not count as an answer.
13
+ //
14
+ // A path somebody set (`claudeBin`, `codexBin`) wins and is never searched
15
+ // around; that is what makes "point at it yourself" work (main/ipc.mjs,
16
+ // `zero:engine-bin`).
17
+
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import { execFile, execFileSync } from 'node:child_process';
22
+
23
+ /** Every package manager's default bin directory, in order, shared by all engines. */
24
+ export function sharedBinDirs(home = os.homedir()) {
25
+ return [
26
+ '/opt/homebrew/bin', // Homebrew, Apple Silicon
27
+ '/usr/local/bin', // Homebrew on Intel, and npm -g
28
+ path.join(home, '.bun/bin'), // bun
29
+ path.join(home, '.volta/bin'), // volta
30
+ path.join(home, '.asdf/shims'), // asdf, through its shims
31
+ path.join(home, '.npm-global/bin'), // the two npm prefixes people
32
+ path.join(home, '.npm-packages/bin'), // actually write in a guide
33
+ path.join(home, '.yarn/bin'), // yarn global
34
+ path.join(home, 'Library/pnpm'), // pnpm's own bin on macOS
35
+ path.join(home, '.local/share/pnpm'), // pnpm's own bin on Linux
36
+ path.join(home, '.deno/bin'), // deno
37
+ path.join(home, 'bin'), // a hand-rolled ~/bin on PATH
38
+ '/opt/local/bin', // MacPorts
39
+ '/usr/bin', // packaged by something system-wide
40
+ ];
41
+ }
42
+
43
+ /**
44
+ * One bin directory per installed Node. nvm, fnm, n and asdf keep one per
45
+ * version, and a global npm install under them lands only there.
46
+ */
47
+ export function versionedBinPaths(bin, home = os.homedir(), readdir = defaultReaddir) {
48
+ const under = (dir, tail) => readdir(path.join(home, dir)).map((v) => path.join(home, dir, v, tail));
49
+ return [
50
+ ...under('.nvm/versions/node', `bin/${bin}`),
51
+ ...under('.local/share/fnm/node-versions', `installation/bin/${bin}`),
52
+ ...under('Library/Application Support/fnm/node-versions', `installation/bin/${bin}`),
53
+ ...under('n/versions/node', `bin/${bin}`),
54
+ ...under('.asdf/installs/nodejs', `bin/${bin}`),
55
+ ];
56
+ }
57
+
58
+ function defaultReaddir(dir) {
59
+ try { return fs.readdirSync(dir); } catch { return []; }
60
+ }
61
+
62
+ /**
63
+ * A symlink to nothing, which `existsSync` cannot tell from no file. Both
64
+ * engines' installers point a stable link at a versioned file and swap it on
65
+ * update, so this is a tool mid-upgrade, not a missing one.
66
+ */
67
+ function defaultDangles(p) {
68
+ try { return !!fs.lstatSync(p) && !fs.existsSync(p); } catch { return false; }
69
+ }
70
+
71
+ /** Newest version folder first, by number and not by spelling. */
72
+ function newestFirst(a, b) {
73
+ const x = a.split('.').map(Number), y = b.split('.').map(Number);
74
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
75
+ if ((x[i] ?? 0) !== (y[i] ?? 0)) return (y[i] ?? 0) - (x[i] ?? 0);
76
+ }
77
+ return 0;
78
+ }
79
+
80
+ /**
81
+ * What a new engine has to declare:
82
+ *
83
+ * bin the program name, which is also its key here
84
+ * name what it is called on a screen
85
+ * installUrl where to get it, when we are sure it is not here
86
+ * ownPaths where the tool's own installer puts it, tried first
87
+ * appCopies a copy carried inside a desktop app, tried last of all
88
+ * evidence files it leaves behind; never spawned, and only ever stop us
89
+ * saying it is missing
90
+ */
91
+ function engine({ bin, name, installUrl, ownPaths, appCopies = () => [], evidence }) {
92
+ return {
93
+ bin,
94
+ name,
95
+ installUrl,
96
+ ownPaths,
97
+ appCopies,
98
+ evidence,
99
+ // The first that exists wins.
100
+ candidates(home = os.homedir(), readdir = defaultReaddir) {
101
+ return [
102
+ ...ownPaths(home),
103
+ ...sharedBinDirs(home).map((dir) => path.join(dir, bin)),
104
+ ...versionedBinPaths(bin, home, readdir),
105
+ ];
106
+ },
107
+ };
108
+ }
109
+
110
+ export const ENGINES = {
111
+ /**
112
+ * Claude Code is required: the app will not open an inbox without it.
113
+ *
114
+ * The Claude desktop app keeps its own copy, one folder per version, and it
115
+ * shares the CLI's sign-in, so someone who only uses Claude Code inside that
116
+ * app has nothing else. It is tried last because it updates with the app and
117
+ * lags a real install.
118
+ */
119
+ claude: engine({
120
+ bin: 'claude',
121
+ name: 'Claude Code',
122
+ installUrl: 'https://code.claude.com/docs/en/setup',
123
+ ownPaths: (home) => [
124
+ path.join(home, '.local/bin/claude'), // the installer
125
+ path.join(home, '.claude/local/claude'), // claude migrate-installer
126
+ ],
127
+ appCopies: (home, readdir = defaultReaddir) => {
128
+ const dir = path.join(home, 'Library/Application Support/Claude/claude-code');
129
+ return readdir(dir)
130
+ .filter((v) => /^\d+(\.\d+)*$/.test(v))
131
+ .sort(newestFirst)
132
+ .map((v) => path.join(dir, v, 'claude.app/Contents/MacOS/claude'));
133
+ },
134
+ // Any one of these means Claude Code has run on this machine.
135
+ evidence: (home) => [
136
+ path.join(home, '.claude.json'), // its own config file
137
+ path.join(home, '.claude/settings.json'),
138
+ path.join(home, '.claude/projects'), // one folder per project it has opened
139
+ path.join(home, '.claude/statsig'),
140
+ path.join(home, '.claude/.credentials.json'), // it has been logged in
141
+ path.join(home, '.claude/history.jsonl'),
142
+ path.join(home, '.local/share/claude'), // the installer's version store
143
+ ],
144
+ }),
145
+
146
+ /**
147
+ * Codex is optional, so its absence never produces a warning; it is just
148
+ * left out of the map `shared/engines.mjs` reads.
149
+ *
150
+ * Homebrew installs Codex as a cask whose bin entry is a symlink into the
151
+ * Caskroom, so it dangles mid-upgrade. The documented install.sh links
152
+ * `~/.local/bin/codex` into `$CODEX_HOME/packages/standalone`, and each
153
+ * release holds the binary at both `bin/codex` and `codex`, so both are
154
+ * listed. ~/.codex keeps its live state in sqlite files whose names carry a
155
+ * changing schema number, so the directory itself is evidence, last.
156
+ */
157
+ codex: engine({
158
+ bin: 'codex',
159
+ name: 'Codex',
160
+ installUrl: 'https://learn.chatgpt.com/docs/codex/cli',
161
+ ownPaths: (home) => [
162
+ path.join(home, '.local/bin/codex'), // the shell installer's link
163
+ path.join(home, '.codex/packages/standalone/current/bin/codex'),
164
+ path.join(home, '.codex/packages/standalone/current/codex'),
165
+ ],
166
+ // The ChatGPT app's own copy, which shares the CLI's sign-in, and the Codex
167
+ // desktop app's (reported, not verified here).
168
+ appCopies: (home) => [
169
+ 'ChatGPT.app/Contents/Resources/codex-cli/bin/codex',
170
+ 'Codex.app/Contents/Resources/codex',
171
+ ].flatMap((p) => [path.join('/Applications', p), path.join(home, 'Applications', p)]),
172
+ // Both homes when CODEX_HOME is set: an inherited CODEX_HOME does not prove
173
+ // ~/.codex is unused.
174
+ evidence: (home, env = process.env) => {
175
+ const homes = [env?.CODEX_HOME, path.join(home, '.codex')].filter(Boolean);
176
+ const out = [];
177
+ for (const dir of new Set(homes)) {
178
+ out.push(
179
+ path.join(dir, 'auth.json'), // it has been logged in
180
+ path.join(dir, 'config.toml'),
181
+ path.join(dir, 'sessions'), // one folder tree of past runs
182
+ path.join(dir, 'history.jsonl'),
183
+ path.join(dir, 'packages/standalone/releases'), // the installer's version store
184
+ dir, // and the directory itself, last
185
+ );
186
+ }
187
+ return out;
188
+ },
189
+ }),
190
+ };
191
+
192
+ export function engineKeys() {
193
+ return Object.keys(ENGINES);
194
+ }
195
+
196
+ function engineFor(key) {
197
+ const found = ENGINES[key];
198
+ if (!found) throw new Error(`no coding agent is declared as ${JSON.stringify(key)}`);
199
+ return found;
200
+ }
201
+
202
+ // Their $SHELL first, then zsh and bash, because a $SHELL that will not start
203
+ // says nothing about the tool.
204
+ export function shellProbes(env = process.env) {
205
+ const preferred = env.SHELL || '/bin/zsh';
206
+ const out = [[preferred, '-lic', 4000], [preferred, '-lc', 2500]];
207
+ for (const other of ['/bin/zsh', '/bin/bash']) {
208
+ if (other !== preferred) out.push([other, '-lic', 2500]);
209
+ }
210
+ return out;
211
+ }
212
+
213
+ /**
214
+ * `bin` is interpolated into a command their login shell runs, so only a plain
215
+ * program name is accepted. A config value reaching it would otherwise be shell
216
+ * injection. An allowlist rather than quoting, because a later edit can undo
217
+ * quoting without anyone noticing.
218
+ */
219
+ const PROGRAM_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
220
+
221
+ /**
222
+ * Ask their login shell until one answers. Only an absolute path to a file that
223
+ * exists counts, because `command -v` also prints functions and aliases.
224
+ * `answered` is true only when at least one probe ran and every probe returned
225
+ * a real exit status. Login and interactive (`-lic`) because version managers
226
+ * add themselves to PATH only in `.zprofile` and `.zshrc`.
227
+ */
228
+ export function askShell({ env = process.env, exists = fs.existsSync, run = defaultRun, bin = 'claude' } = {}) {
229
+ const asking = shellQuestion({ env, exists, bin });
230
+ for (let step = asking.next(); ; ) {
231
+ if (step.done) return step.value;
232
+ let reply;
233
+ try { reply = { out: run(...step.value) }; } catch (err) { reply = { err }; }
234
+ step = asking.next(reply);
235
+ }
236
+ }
237
+
238
+ /** `askShell` without holding the window: each probe is awaited. */
239
+ export async function askShellLater({ env = process.env, exists = fs.existsSync, run = shellRunnerLater(process.env), bin = 'claude' } = {}) {
240
+ const asking = shellQuestion({ env, exists, bin });
241
+ for (let step = asking.next(); ; ) {
242
+ if (step.done) return step.value;
243
+ let reply;
244
+ try { reply = { out: await run(...step.value) }; } catch (err) { reply = { err }; }
245
+ step = asking.next(reply);
246
+ }
247
+ }
248
+
249
+ // The probes and what their replies mean, written once for both askers: it
250
+ // yields each probe to run and is handed back `{ out }` or `{ err }`.
251
+ function* shellQuestion({ env, exists, bin }) {
252
+ if (typeof bin !== 'string' || !PROGRAM_NAME.test(bin)) {
253
+ throw new Error(`askShell looks up a program name, and ${JSON.stringify(bin)} is not one`);
254
+ }
255
+ let said = false;
256
+ let inconclusive = false;
257
+ for (const [shell, flags, timeout] of shellProbes(env)) {
258
+ const { out, err } = yield [shell, [flags, `command -v ${bin}`], timeout];
259
+ if (err) {
260
+ // A non-zero exit means not on PATH. A shell that would not start or
261
+ // timed out has told us nothing, and must not count as "not installed".
262
+ if (exitedSayingNo(err)) said = true; else inconclusive = true;
263
+ continue;
264
+ }
265
+ const line = String(out).split('\n').map((s) => s.trim()).filter(Boolean).pop();
266
+ if (line && line.startsWith('/') && exists(line)) return { path: line, answered: true };
267
+ said = true;
268
+ }
269
+ return { path: null, answered: said && !inconclusive };
270
+ }
271
+
272
+ // A timeout and a missing shell also throw here, and neither is a fact about
273
+ // the tool.
274
+ function exitedSayingNo(e) {
275
+ if (!e || typeof e !== 'object') return false;
276
+ if (e.signal || e.code === 'ETIMEDOUT' || e.code === 'ENOENT') return false;
277
+ return typeof e.status === 'number';
278
+ }
279
+
280
+ /**
281
+ * A SHELL THAT NEVER REACHED THE QUESTION HAS NOT ANSWERED IT (2026-10-15).
282
+ *
283
+ * `[ -z "$TMUX" ] && exec tmux` in .zshrc replaces the shell before `command
284
+ * -v` runs, and tmux with no terminal says "open terminal failed: not a
285
+ * terminal" and exits 1, which is exactly how `command -v` spells "not on
286
+ * PATH". Measured with the real /bin/zsh: a claude that .zshrc put on PATH came
287
+ * back `{ answered: true }`, so the setup screen was entitled to call it
288
+ * missing. `exec fish` was the same with exit 0 and nothing printed.
289
+ *
290
+ * So the runner says a word of its own first and only hands back what comes
291
+ * after it. Without that word the shell never got to the question, and what is
292
+ * thrown carries no exit status, which `exitedSayingNo` reads as no answer.
293
+ * The question itself is unchanged, so a `run` handed in by a test sees the
294
+ * same `command -v x` it always did.
295
+ */
296
+ const REACHED = 'agentbox-shell-reached';
297
+ const notReached = () => Object.assign(new Error('the shell profile ended the shell before the question ran'), { code: 'ENOTREACHED' });
298
+ function afterReached(stdout) {
299
+ const lines = String(stdout ?? '').split('\n');
300
+ const at = lines.map((l) => l.trim()).lastIndexOf(REACHED);
301
+ return at < 0 ? null : lines.slice(at + 1).join('\n');
302
+ }
303
+
304
+ export function shellRunner(env = process.env) {
305
+ return (shell, [flags, command], timeout = 4000) => {
306
+ let out;
307
+ try {
308
+ out = execFileSync(shell, [flags, `echo ${REACHED}; ${command}`], {
309
+ encoding: 'utf8',
310
+ // stdin ignored on purpose: an interactive shell handed a live terminal can
311
+ // sit waiting for input, and this runs on the boot path.
312
+ stdio: ['ignore', 'pipe', 'ignore'],
313
+ timeout,
314
+ // Not SIGTERM: a shell's exit hook can wait forever on it, and this
315
+ // spawn blocks the whole app until the shell has exited (2026-10-16).
316
+ killSignal: 'SIGKILL',
317
+ env,
318
+ });
319
+ } catch (e) {
320
+ if (typeof e?.status === 'number' && !e.signal && afterReached(e.stdout) === null) throw notReached();
321
+ throw e;
322
+ }
323
+ const answer = afterReached(out);
324
+ if (answer === null) throw notReached();
325
+ return answer;
326
+ };
327
+ }
328
+
329
+ function defaultRun(shell, args, timeout = 4000) {
330
+ return shellRunner(process.env)(shell, args, timeout);
331
+ }
332
+
333
+ /**
334
+ * `shellRunner` as a promise, so the window keeps running while the shell
335
+ * starts. Its errors are reshaped into what execFileSync throws, which is what
336
+ * `exitedSayingNo` reads: a timeout carries ETIMEDOUT, a plain non-zero exit a
337
+ * numeric `status`.
338
+ */
339
+ export function shellRunnerLater(env = process.env) {
340
+ return (shell, [flags, command], timeout = 4000) => new Promise((resolve, reject) => {
341
+ const child = execFile(shell, [flags, `echo ${REACHED}; ${command}`], {
342
+ encoding: 'utf8', timeout, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024, env,
343
+ }, (err, stdout) => {
344
+ if (!err) {
345
+ const answer = afterReached(stdout);
346
+ return answer === null ? reject(notReached()) : resolve(answer);
347
+ }
348
+ if (typeof err.code === 'number' && !err.signal) {
349
+ if (afterReached(stdout) === null) return reject(notReached());
350
+ return reject(Object.assign(err, { status: err.code, code: undefined }));
351
+ }
352
+ if (err.killed && err.code !== 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') err.code = 'ETIMEDOUT';
353
+ reject(err);
354
+ });
355
+ // The 'ignore' the waiting runner gives stdin: nothing will ever be typed.
356
+ child.stdin?.end();
357
+ });
358
+ }
359
+
360
+ // A lookup that returns a bare path or null (the old shape) counts as answered.
361
+ function shellSaid(result) {
362
+ if (typeof result === 'string') return { path: result, answered: true };
363
+ if (!result || typeof result !== 'object') return { path: null, answered: true };
364
+ return { path: result.path ?? null, answered: result.answered !== false };
365
+ }
366
+
367
+ /**
368
+ * One login shell asked once for every engine. Starting the shell is the cost,
369
+ * not the question, and `loadConfig` runs before the window opens, so asking
370
+ * per engine delayed every launch. A path is taken only when it is absolute,
371
+ * exists, and is named after an engine, since lines come back in any order.
372
+ * This never records an absence; only the per-engine probe may settle one.
373
+ */
374
+ export function askShellForAll(bins, { env = process.env, exists = fs.existsSync, run = defaultRun } = {}) {
375
+ for (const bin of bins) {
376
+ if (typeof bin !== 'string' || !PROGRAM_NAME.test(bin)) {
377
+ throw new Error(`askShellForAll looks up program names, and ${JSON.stringify(bin)} is not one`);
378
+ }
379
+ }
380
+ const command = bins.map((b) => `command -v ${b}`).join('; ');
381
+ for (const [shell, flags, timeout] of shellProbes(env)) {
382
+ let out = '';
383
+ try { out = run(shell, [flags, command], timeout); } catch { continue; }
384
+ const found = {};
385
+ for (const line of String(out).split('\n').map((s) => s.trim()).filter(Boolean)) {
386
+ if (!line.startsWith('/') || !exists(line)) continue;
387
+ const bin = bins.find((b) => path.basename(line) === b);
388
+ if (bin && !found[bin]) found[bin] = line;
389
+ }
390
+ return found;
391
+ }
392
+ return {};
393
+ }
394
+
395
+ /**
396
+ * Seed every engine's cached shell answer from one probe. If the shell fails,
397
+ * each engine falls back to its own probe.
398
+ */
399
+ export function warmShellAnswers({ now = Date.now(), ...how } = {}) {
400
+ let found = {};
401
+ try { found = askShellForAll(engineKeys().map((k) => ENGINES[k].bin), how); } catch { return; }
402
+ for (const key of engineKeys()) {
403
+ const where = found[ENGINES[key].bin];
404
+ if (where) held.set(key, { answer: { path: where, answered: true }, at: now });
405
+ }
406
+ }
407
+
408
+ // A lookup that throws is read as no answer, so the list still gets its turn.
409
+ function askedShell(lookup) {
410
+ try { return shellSaid(lookup()); } catch { return { path: null, answered: false }; }
411
+ }
412
+
413
+ /**
414
+ * The first trace the tool left behind, or null. Never spawned; it only stops
415
+ * the app saying the tool is missing. A dangling candidate counts too.
416
+ */
417
+ export function installEvidence(key, {
418
+ home = os.homedir(), env = process.env, exists = fs.existsSync, dangles = defaultDangles,
419
+ } = {}) {
420
+ const spec = engineFor(key);
421
+ for (const p of spec.evidence(home, env)) if (exists(p)) return p;
422
+ for (const p of spec.candidates(home, () => [])) if (dangles(p)) return p;
423
+ return null;
424
+ }
425
+
426
+ /**
427
+ * The whole search, with every machine fact injectable for tests. Returns
428
+ * { path, found, certain, from, searched, evidence }. A configured path that
429
+ * does not exist is still returned as the path, so a spawn fails naming the
430
+ * path they chose, with found false and certain true.
431
+ */
432
+ export function findEngine(key, {
433
+ configured = null,
434
+ home = os.homedir(),
435
+ env = process.env,
436
+ exists = fs.existsSync,
437
+ shellLookup = null,
438
+ readdir = defaultReaddir,
439
+ dangles = defaultDangles,
440
+ } = {}) {
441
+ const spec = engineFor(key);
442
+ if (configured) {
443
+ return {
444
+ path: configured, found: exists(configured), certain: true, from: 'settings',
445
+ searched: [], evidence: null,
446
+ };
447
+ }
448
+
449
+ // The default probe uses the real environment and disk, not the injected
450
+ // `exists`: a test faking only `exists` is asking about the list.
451
+ const lookup = shellLookup ?? (() => askShell({ bin: spec.bin }));
452
+ const fromShell = askedShell(lookup);
453
+ const searched = spec.candidates(home, readdir);
454
+ if (fromShell.path) {
455
+ return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
456
+ }
457
+
458
+ for (const candidate of searched) {
459
+ if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'disk', searched, evidence: null };
460
+ }
461
+
462
+ for (const candidate of spec.appCopies(home, readdir)) {
463
+ if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
464
+ }
465
+
466
+ // Any trace of the tool means we looked in the wrong places, so we may not
467
+ // say it is missing.
468
+ const evidence = installEvidence(key, { home, env, exists, dangles });
469
+ return {
470
+ path: null,
471
+ found: false,
472
+ certain: fromShell.answered && !evidence,
473
+ from: null,
474
+ searched,
475
+ evidence,
476
+ };
477
+ }
478
+
479
+ // Only the shell answer is held, per engine. The path checks are cheap stat
480
+ // calls, and holding them would keep saying "not found" after someone
481
+ // installs. The shell is up to three subprocesses and blocks the window while
482
+ // it runs, so it is waited for only when nothing is held: at launch, and after
483
+ // Check again. An answer older than thirty seconds is still handed back at
484
+ // once, and the shell is asked again in the background for the next read.
485
+
486
+ const SHELL_TTL = 30_000;
487
+ const held = new Map();
488
+ const asking = new Map();
489
+
490
+ export function resolveEngine(key, configured = null, { now = Date.now(), shellLookup = null, shellLookupLater = null, ...where } = {}) {
491
+ const spec = engineFor(key);
492
+ const remembered = () => {
493
+ const last = held.get(key);
494
+ if (!last) {
495
+ const answer = (shellLookup ?? (() => askShell({ bin: spec.bin })))();
496
+ held.set(key, { answer, at: now });
497
+ return answer;
498
+ }
499
+ // `age >= 0` too, because `warmShellAnswers` stamps real clock time and a
500
+ // caller with a synthetic `now` would otherwise read it as fresh forever.
501
+ const age = now - last.at;
502
+ const later = shellLookupLater ?? (() => askShellLater({ bin: spec.bin }));
503
+ if (!(age >= 0 && age < SHELL_TTL)) askAgain(key, later, now);
504
+ // A path held from an earlier answer may have been uninstalled since.
505
+ const gone = typeof last.answer?.path === 'string' && !(where.exists ?? fs.existsSync)(last.answer.path);
506
+ if (gone) { askAgain(key, later, now); return { path: null, answered: false }; }
507
+ return last.answer;
508
+ };
509
+ return findEngine(key, { ...where, configured, shellLookup: remembered });
510
+ }
511
+
512
+ /**
513
+ * Ask the shell again off the window's thread and settle once the answer is
514
+ * held, for a caller that promised a fresh look (Check again, setup, the
515
+ * update check). Never rejects.
516
+ */
517
+ export function lookAgain(key, { now = Date.now(), shellLookupLater = null } = {}) {
518
+ const spec = engineFor(key);
519
+ return askAgain(key, shellLookupLater ?? (() => askShellLater({ bin: spec.bin })), now);
520
+ }
521
+
522
+ function askAgain(key, lookup, now) {
523
+ if (asking.has(key)) return asking.get(key);
524
+ const turn = new Promise((resolve) => resolve(lookup())).then(
525
+ (answer) => {
526
+ if (asking.get(key) !== turn) return;
527
+ // A shell that timed out under load says nothing, and must not replace
528
+ // an answer that did say something.
529
+ const said = shellSaid(answer);
530
+ const had = held.get(key);
531
+ if (said.path || said.answered || !had) held.set(key, { answer, at: now });
532
+ else held.set(key, { ...had, at: now });
533
+ },
534
+ () => { /* the held answer stands, and the next read asks again */ },
535
+ ).finally(() => { if (asking.get(key) === turn) asking.delete(key); });
536
+ asking.set(key, turn);
537
+ return turn;
538
+ }
539
+
540
+ export function forgetEngine(key) {
541
+ engineFor(key); // throws for an undeclared engine
542
+ held.delete(key);
543
+ // An answer already on its way was asked for before this, so it is dropped.
544
+ asking.delete(key);
545
+ }