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
@@ -48,8 +48,8 @@ export function wantsPriority(item, product, me) {
48
48
  // AND NEVER OVER THE LEVEL THE SENDER PICKED (w-7ba439c883). A message now
49
49
  // crosses carrying how urgent the person who wrote it thought it was, and a
50
50
  // guess from here would overwrite it: the sender's line is composeItem's, so
51
- // it claims no authority the fold would defend, and a level written from here
52
- // counts for nothing anyway (an agent's tag, shared/rank.mjs itemPriority).
51
+ // it claims no authority the fold would defend, and a guess from here is not
52
+ // what the sender said.
53
53
  // A row nobody chose a level for is composeItem's 0, so a number above it is
54
54
  // exactly "somebody picked one", and `by` says it was not this Mac.
55
55
  if (set?.by && set.by !== me && Number.isFinite(item.priority) && item.priority > 0) return false;
package/main/mic.mjs ADDED
@@ -0,0 +1,38 @@
1
+ // Getting the microphone with one press, for a voice note.
2
+ //
3
+ // macOS asks on behalf of the app that opened Agentbox (Terminal, when it is
4
+ // run from a checkout), and once that app has been refused it never shows its
5
+ // Allow box again: asking just answers no. So the press clears that one
6
+ // refusal, for that one app and the microphone only, and asks again, which
7
+ // brings the Allow box back. Only ever on the press, never on its own.
8
+ import { execFile } from 'node:child_process';
9
+
10
+ export const SETTINGS_URL = 'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone';
11
+
12
+ const NAMES = {
13
+ 'com.apple.Terminal': 'Terminal',
14
+ 'com.googlecode.iterm2': 'iTerm',
15
+ 'com.microsoft.VSCode': 'VS Code',
16
+ 'dev.warp.Warp-Stable': 'Warp',
17
+ 'com.mitchellh.ghostty': 'Ghostty',
18
+ };
19
+
20
+ /** The app macOS asks about, from the bundle id macOS gave this process. */
21
+ export function micOwner(env = process.env) {
22
+ const id = env.__CFBundleIdentifier;
23
+ if (typeof id !== 'string' || !/^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/.test(id)) return { id: null, name: null };
24
+ return { id, name: NAMES[id] ?? id.split('.').at(-1) };
25
+ }
26
+
27
+ /** 'granted', or 'settings' once the Settings page has been opened instead. */
28
+ export async function getMic({ systemPreferences, openExternal, owner = micOwner(), run = execFile }) {
29
+ const status = systemPreferences.getMediaAccessStatus('microphone');
30
+ if (status === 'granted') return 'granted';
31
+ if (status === 'denied' && owner.id) {
32
+ await new Promise((resolve) => run('tccutil', ['reset', 'Microphone', owner.id], { timeout: 10_000 }, () => resolve()));
33
+ }
34
+ const granted = await systemPreferences.askForMediaAccess('microphone').catch(() => false);
35
+ if (granted) return 'granted';
36
+ openExternal(SETTINGS_URL);
37
+ return 'settings';
38
+ }
@@ -0,0 +1,110 @@
1
+ // NEW CHATS COME IN ON THEIR OWN, when somebody ticked "Keep bringing in new
2
+ // chats from Claude Code and Codex" on setup's last screen or turned it on in
3
+ // Settings (`importNew`, from the moment `importNewSince`).
4
+ //
5
+ // A chat comes in when it was started after that moment, in a folder one of
6
+ // the projects points at, and has gone quiet. Claude Code may still have it
7
+ // open: people leave it open for hours, and a reply here to an open chat
8
+ // carries on a copy (Supervisor#importedThreadFor). It is filed the way setup
9
+ // files one: waiting on you to Needs you, finished to Done. Nothing asks about
10
+ // a chat first; asking rows are what the old Codex watch was removed for.
11
+ import { productsForThread } from '../shared/codex-import.mjs';
12
+ import { isShared } from '../shared/team-rules.mjs';
13
+ import { appSessionFolders } from './agent-homes.mjs';
14
+ import { outsideAgentsMode } from './settings.mjs';
15
+ import { claudeSessionOpen } from './claude-session-open.mjs';
16
+ import { readCodexThreadsOffThread } from './codex-threads.mjs';
17
+ import { readRecentThreads } from './importable-threads.mjs';
18
+
19
+ export const NEW_CHATS_POLL_MS = 60_000;
20
+ /** How long a chat has to have been still before it comes in. */
21
+ export const QUIET_MS = 10 * 60_000;
22
+ const PASS_DEADLINE_MS = 90_000;
23
+
24
+ function idsWithRows(items) {
25
+ const ids = new Set();
26
+ for (const item of items ?? []) {
27
+ for (const label of item?.labels ?? []) {
28
+ if (typeof label !== 'string') continue;
29
+ if (label.startsWith('thread:')) ids.add(label.slice(7));
30
+ else if (label.startsWith('codex:')) ids.add(label.slice(6));
31
+ }
32
+ }
33
+ return ids;
34
+ }
35
+
36
+ /** Which of `threads` come in now, grouped by the project each lands in. */
37
+ export function newChatsToBring(threads, { products, items, since, now, isOpen = () => false, openShownLive = false }) {
38
+ const had = idsWithRows(items);
39
+ const bySlug = new Map();
40
+ for (const t of threads ?? []) {
41
+ const id = String(t?.id ?? '').trim();
42
+ if (!id || had.has(id)) continue;
43
+ if (!(Number(t.startedAt) > since)) continue;
44
+ if (now - Number(t.when ?? 0) < QUIET_MS) continue;
45
+ if (t.source === 'codex' ? t.live : openShownLive && isOpen(t)) continue;
46
+ const slug = productsForThread(t, products)[0]?.slug;
47
+ if (!slug) continue;
48
+ if (!bySlug.has(slug)) bySlug.set(slug, []);
49
+ bySlug.get(slug).push(t);
50
+ had.add(id);
51
+ }
52
+ return bySlug;
53
+ }
54
+
55
+ export async function scanNewChats({ store, config, readThreads, isOpen, now = Date.now() }) {
56
+ if (config?.importNew !== true) return { brought: 0 };
57
+ const since = Number(config.importNewSince) || now;
58
+ // Only as far back as the switch, and never further than the import card reads.
59
+ const days = Math.min(10, Math.max(1, Math.ceil((now - since) / 86400e3) + 1));
60
+ // Never a project shared with a team: teammates would read chats nobody chose to share.
61
+ const products = store.listProducts().filter((p) => !isShared(p));
62
+ const folders = products.map((p) => p.repoPath).filter(Boolean);
63
+ const threads = await readThreads({ days, since, folders, now });
64
+ // An open session already has its own row when "Show while they run" is on.
65
+ const openShownLive = outsideAgentsMode(config) !== 'off';
66
+ const picks = newChatsToBring(threads, { products, items: store.listItems(), since, now, isOpen, openShownLive });
67
+ let brought = 0;
68
+ for (const [slug, list] of picks) {
69
+ brought += store.importThreadRows(slug, list, { now, land: 'sorted' }).added;
70
+ brought += store.importCodexThreads(slug, list, { now, land: 'sorted' }).added;
71
+ }
72
+ return { brought };
73
+ }
74
+
75
+ export function startNewChatsWatch({
76
+ store, config, readThreads, isOpen,
77
+ everyMs = NEW_CHATS_POLL_MS, firstDelayMs = 30_000, deadlineMs = PASS_DEADLINE_MS, log = console.warn,
78
+ }) {
79
+ // One pass at a time, and a pass that never answers is given up on so the
80
+ // next one still runs.
81
+ let running = null;
82
+ const run = () => {
83
+ if (running) return running;
84
+ let timer = null;
85
+ const deadline = new Promise((_, reject) => {
86
+ timer = setTimeout(() => reject(new Error(`no answer in ${Math.round(deadlineMs / 1000)}s`)), deadlineMs);
87
+ });
88
+ running = Promise.race([scanNewChats({ store, config, readThreads, isOpen }), deadline])
89
+ .catch((e) => log('new chats watch:', e?.message ?? e))
90
+ .finally(() => { clearTimeout(timer); running = null; });
91
+ return running;
92
+ };
93
+ const first = setTimeout(run, firstDelayMs);
94
+ const timer = setInterval(run, everyMs);
95
+ return {
96
+ scanNow: run,
97
+ stop: () => { clearTimeout(first); clearInterval(timer); },
98
+ };
99
+ }
100
+
101
+ /** The watch as the app runs it, on the desktop (main/main.mjs) and in the browser copy (bin/agentbox.mjs). */
102
+ export function watchNewChatsFor({ store, config, supervisor, ...timing }) {
103
+ return startNewChatsWatch({
104
+ store,
105
+ config,
106
+ readThreads: (opts) => readRecentThreads({ ...opts, config, supervisor, readCodex: readCodexThreadsOffThread }),
107
+ isOpen: (t) => claudeSessionOpen(t.id, appSessionFolders({ config, supervisor }).dirs),
108
+ ...timing,
109
+ });
110
+ }
@@ -0,0 +1,25 @@
1
+ // A throwaway copy's window says so, and stands where you can see both.
2
+ //
3
+ // "Open Agentbox as a new user" (main/fresh-user.mjs) launches the app again
4
+ // inside a throwaway home, beside the one it was opened from. The offset is a
5
+ // window's shoulder rather than a tile: the point is to see there are two, with
6
+ // the real one still readable behind. A copy that cannot fit the offset is left
7
+ // centred rather than pushed half off the screen, which a 1440-wide laptop is.
8
+
9
+ /** How far down and across a second copy stands. */
10
+ export const OFFSET = 48;
11
+
12
+ /** The window options a copy adds, given whether it is a throwaway one. */
13
+ export function newUserWindow({ fresh, name, screen, width = 1440, height = 900 } = {}) {
14
+ if (!fresh) return { title: name };
15
+ const title = `${name} (new user)`;
16
+ if (!screen) return { title };
17
+ const centred = {
18
+ x: Math.round(screen.x + (screen.width - width) / 2),
19
+ y: Math.round(screen.y + (screen.height - height) / 2),
20
+ };
21
+ const x = centred.x + OFFSET;
22
+ const y = centred.y + OFFSET;
23
+ if (x + width > screen.x + screen.width || y + height > screen.y + screen.height) return { title };
24
+ return { title, x, y };
25
+ }
package/main/notify.mjs CHANGED
@@ -13,19 +13,54 @@
13
13
  // AND ONCE, NOT TWENTY TIMES. One at a time is not once. `spoke` below is the
14
14
  // fix, and shared/notify-rules.mjs carries the rule.
15
15
 
16
+ import path from 'node:path';
16
17
  import { atTheApp, banner, shouldSpeak } from '../shared/notify-rules.mjs';
17
18
  import { NAME } from '../shared/product-name.mjs';
18
19
 
20
+ // THE MARK THE BANNER WEARS, and why it is a png and not the .icns every other
21
+ // part of this repo uses.
22
+ //
23
+ // macOS draws the app's OWN bundle icon down the left of a banner, and no app
24
+ // can override that. From a source checkout the bundle being asked is the
25
+ // Electron.app in node_modules, which scripts/brand-electron.mjs renames and
26
+ // re-icons, and macOS still drew a generic grey square: "it shows a plain
27
+ // placeholder", looking at a real one, 2026-10-07.
28
+ //
29
+ // An image handed to the notification is the half we DO control; macOS attaches
30
+ // it to the banner. Measured the same day in the Electron the app runs from:
31
+ // nativeImage.createFromPath on build/icon.icns comes back empty, 0x0, so the
32
+ // icns is not a candidate however convenient it would be. The png beside it,
33
+ // drawn from the same art, reads 256x256.
34
+ //
35
+ // AN EMPTY IMAGE IS NOT A MARK. createFromPath answers a missing or unreadable
36
+ // file with an empty image rather than an error, so "it returned something" is
37
+ // the wrong question, and handing that something to macOS is how this goes
38
+ // quietly back to a grey square. Returns null when there is nothing to show,
39
+ // and the banner then looks exactly as it did before any of this.
40
+ export const MARK_FILE = path.join('build', 'notification-icon.png');
41
+
42
+ export function markFor({ root, createFromPath }) {
43
+ try {
44
+ const img = createFromPath(path.join(root, MARK_FILE));
45
+ return img && !img.isEmpty() ? img : null;
46
+ } catch { return null; }
47
+ }
48
+
19
49
  // A BURST IS ONE PIECE OF NEWS. Four workers finishing inside the same
20
50
  // supervisor tick is the ordinary case, and it arrives as four pushes over a
21
51
  // second or two. Waiting a beat before speaking turns that into one banner
22
52
  // with one sound instead of four rewrites and four chimes.
23
53
  const COALESCE_MS = 1500;
24
54
 
25
- export function createNotifier({ window, Notification, powerMonitor, coalesceMs = COALESCE_MS }) {
55
+ // `onChange` hears that what it has told you about changed: the tray counts
56
+ // all of it (main/corner-tag.mjs), so a banner never speaks for a thread the
57
+ // tray leaves out.
58
+ export function createNotifier({ window, Notification, powerMonitor, icon = null, coalesceMs = COALESCE_MS, onChange = () => {} }) {
26
59
  // Everything she has not seen since she last had the window, keyed by id so
27
60
  // a repeated push is idempotent. Insertion order is arrival order.
28
61
  const unseen = new Map();
62
+ const toldAt = new Map();
63
+ const changed = () => { try { onChange(); } catch { /* the tray catches up on its next look */ } };
29
64
  let live = null; // the one notification currently on screen, if any
30
65
  let timer = null;
31
66
  // HOW MANY TIMES SHE HAS BEEN INTERRUPTED SINCE SHE LAST HAD THE WINDOW.
@@ -53,11 +88,14 @@ export function createNotifier({ window, Notification, powerMonitor, coalesceMs
53
88
  // dropped rather than banked: showing them later, when she steps away, would
54
89
  // be telling her about work she already watched arrive.
55
90
  const seen = () => {
91
+ const had = unseen.size > 0;
56
92
  unseen.clear();
93
+ toldAt.clear();
57
94
  if (timer) { clearTimeout(timer); timer = null; }
58
95
  spoke = false;
59
96
  spokeAsk = false;
60
97
  takeDown();
98
+ if (had) changed();
61
99
  };
62
100
 
63
101
  const speak = () => {
@@ -81,6 +119,9 @@ export function createNotifier({ window, Notification, powerMonitor, coalesceMs
81
119
  title: say.title,
82
120
  body: say.body,
83
121
  silent: false,
122
+ // Omitted rather than passed empty when there is nothing to show, so a
123
+ // banner with no mark is byte for byte the banner that shipped before.
124
+ ...(icon ? { icon } : {}),
84
125
  });
85
126
  } catch { return; }
86
127
  // A banner she can act on. Clicking it brings Agentbox forward and opens the
@@ -123,33 +164,51 @@ export function createNotifier({ window, Notification, powerMonitor, coalesceMs
123
164
  // Asked before banking anything, so that a burst arriving while she is
124
165
  // at the app is dropped rather than saved up for when she leaves.
125
166
  if (atTheApp({ focused: focused(), idleMs: idleMs() })) return seen();
167
+ let banked = false;
126
168
  for (const a of arrivals) {
127
169
  if (!a || !a.id || unseen.has(a.id)) continue;
128
170
  unseen.set(a.id, a);
171
+ toldAt.set(a.id, Date.now());
172
+ banked = true;
129
173
  }
174
+ if (banked) changed();
130
175
  if (!unseen.size || timer) return;
131
176
  timer = setTimeout(speak, coalesceMs);
132
177
  },
133
178
  // The window came forward. Whatever was on screen is now behind her.
134
179
  seen,
180
+ // What it has told you about, or is about to, since you were last at the
181
+ // app, and when each arrived.
182
+ told: () => [...toldAt].map(([id, at]) => ({ id, at })),
135
183
  // Tests, and nothing else.
136
184
  _pending: () => [...unseen.values()],
137
185
  _spoken: () => ({ spoke, spokeAsk }),
138
186
  };
139
187
  }
140
188
 
141
- export function installNotifier({ app, window, Notification, powerMonitor, ipcMain }) {
189
+ // `onArrivals` hears every arrival too, with the inbox they arrived in, for
190
+ // the phone's alerts (main/phone-alerts.mjs), which make their own call.
191
+ const needsYouOf = (payload) => (Array.isArray(payload.needsYou) ? payload.needsYou : []);
192
+ export function installNotifier({ app, window, Notification, powerMonitor, ipcMain, nativeImage, onArrivals = () => {}, onChange = () => {} }) {
142
193
  if (Notification.isSupported && !Notification.isSupported()) {
143
- return { add() {}, seen() {} };
194
+ ipcMain.handle('zero:notify', (_e, payload = {}) => { try { onArrivals(Array.isArray(payload.arrivals) ? payload.arrivals : [], needsYouOf(payload)); } catch {} });
195
+ return { add() {}, seen() {}, told: () => [] };
144
196
  }
145
- const notifier = createNotifier({ window, Notification, powerMonitor });
197
+ // app.getAppPath() is the repo from source and the asar inside the bundle
198
+ // when it is packaged, and the mark sits at the same place in both.
199
+ const icon = nativeImage
200
+ ? markFor({ root: app.getAppPath(), createFromPath: (p) => nativeImage.createFromPath(p) })
201
+ : null;
202
+ const notifier = createNotifier({ window, Notification, powerMonitor, icon, onChange });
146
203
  window.on('focus', () => notifier.seen());
147
204
  // Unlocking with Agentbox already the front app never fires 'focus' (it never
148
205
  // lost it), so without this the banner that fired while the screen was
149
206
  // locked would sit in Notification Center after she is back at the row.
150
207
  try { powerMonitor.on('unlock-screen', () => { if (window.isFocused()) notifier.seen(); }); } catch {}
151
208
  ipcMain.handle('zero:notify', (_e, payload = {}) => {
152
- notifier.add(Array.isArray(payload.arrivals) ? payload.arrivals : []);
209
+ const arrivals = Array.isArray(payload.arrivals) ? payload.arrivals : [];
210
+ try { onArrivals(arrivals, needsYouOf(payload)); } catch {}
211
+ notifier.add(arrivals);
153
212
  });
154
213
  return notifier;
155
214
  }
@@ -0,0 +1,271 @@
1
+ // OPENCODE'S OWN TRANSPORT, AND WHY IT IS THE SERVER AND NOT THE CLI.
2
+ //
3
+ // Measured against a real OpenCode 1.18.35 on 2026-10-07. `opencode run` is
4
+ // the obvious road -- it is non-interactive, `--format json` prints one JSON
5
+ // object per line, and that is nearly the shape main/harnesses/claude-stream
6
+ // already reads. It is also the wrong road, and the measurement says so in one
7
+ // line. Run with `permission: { bash: "ask" }` in config, the CLI printed:
8
+ //
9
+ // ! permission requested: bash (echo probe-run-mode); auto-rejecting
10
+ //
11
+ // and handed the model "The user rejected permission to use this specific tool
12
+ // call." With no permission config at all it ran the command with no card.
13
+ // `--auto` approves everything not explicitly denied. So the CLI offers
14
+ // approve-everything or refuse-everything, and an Agentbox card is neither.
15
+ //
16
+ // The HTTP server does carry a real approval. `opencode serve` raises
17
+ // `permission.asked` on its `/event` stream with the command in
18
+ // `metadata.command`, and `POST /session/:id/permissions/:id` with
19
+ // `{ response: "once" }` releases it while `{ response: "reject" }` refuses it.
20
+ // Both measured end to end against the real binary, both answers, 200 each
21
+ // time, the tool running in the first case and coming back
22
+ // "The user rejected permission to use this specific tool call." in the second.
23
+ // That is the same card contract main/codex-approvals.mjs already fills, which
24
+ // is why this file exists and a second stream parser does not.
25
+ //
26
+ // AND IT IS WHY WE DO NOT PASS `--auto` ANYWHERE. A grep for it is a test
27
+ // (tests/opencode-asks-before-it-runs-a-command), because the flag would make
28
+ // every run quietly approve its own file writes and shell commands.
29
+ import { EventEmitter } from 'node:events';
30
+ import { Name } from '../shared/product-name.mjs';
31
+
32
+ /** How long a single HTTP call to the local server may take. */
33
+ const CALL_MS = 60_000;
34
+
35
+ /**
36
+ * A client for one `opencode serve`.
37
+ *
38
+ * `password` is the server's own basic-auth secret, which Agentbox generates
39
+ * per server and which OpenCode reads from `OPENCODE_SERVER_PASSWORD`. It is
40
+ * NOT a provider credential: OpenCode loads those itself, from its own
41
+ * `~/.local/share/opencode/auth.json`. Agentbox never has one to pass, which
42
+ * is the rule in CLAUDE.md and the reason this harness can exist at all.
43
+ */
44
+ export function openCodeClient({ baseUrl, password = null, username = 'opencode', fetchImpl = globalThis.fetch } = {}) {
45
+ if (!baseUrl) throw Error('openCodeClient needs the address of a running OpenCode server');
46
+ const root = String(baseUrl).replace(/\/+$/, '');
47
+ const authorization = password ? `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` : null;
48
+
49
+ const request = async (path, { method = 'GET', body, signal, stream = false } = {}) => {
50
+ const response = await fetchImpl(root + path, {
51
+ method,
52
+ headers: {
53
+ ...(authorization ? { authorization } : {}),
54
+ ...(body === undefined ? {} : { 'content-type': 'application/json' }),
55
+ ...(stream ? { accept: 'text/event-stream' } : {}),
56
+ },
57
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
58
+ signal: signal ?? AbortSignal.timeout(CALL_MS),
59
+ });
60
+ if (!response.ok) throw Error(`OpenCode answered ${response.status} for ${method} ${path}`);
61
+ if (stream) return response;
62
+ const text = await response.text();
63
+ if (!text) return null;
64
+ try { return JSON.parse(text); } catch { return text; }
65
+ };
66
+
67
+ return {
68
+ url: root,
69
+ request,
70
+ createSession: (body = {}) => request('/session', { method: 'POST', body }),
71
+ prompt: (sessionId, body) => request(`/session/${encodeURIComponent(sessionId)}/prompt_async`, { method: 'POST', body }),
72
+ abort: sessionId => request(`/session/${encodeURIComponent(sessionId)}/abort`, { method: 'POST', body: {} }),
73
+ // `{ response }` and not `{ action }`: the field name is off the real
74
+ // OpenAPI document the running server serves at /doc.
75
+ replyPermission: (sessionId, permissionId, response) =>
76
+ request(`/session/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(permissionId)}`, { method: 'POST', body: { response } }),
77
+ summarize: (sessionId, model) =>
78
+ request(`/session/${encodeURIComponent(sessionId)}/summarize`, { method: 'POST', body: splitModel(model) }),
79
+ runCommand: (sessionId, body) => request(`/session/${encodeURIComponent(sessionId)}/command`, { method: 'POST', body }),
80
+ providers: () => request('/config/providers'),
81
+ commands: () => request('/command'),
82
+ config: () => request('/config'),
83
+ health: () => request('/global/health'),
84
+ events: ({ signal } = {}) => eventStream(request, signal),
85
+ };
86
+ }
87
+
88
+ /**
89
+ * `provider/model` as the two fields OpenCode's API wants.
90
+ *
91
+ * Its model ids are provider-qualified and the slash is part of the id people
92
+ * see (`opencode/big-pickle`, `anthropic/claude-sonnet-4-5`), but every server
93
+ * endpoint takes `providerID` and `modelID` apart. A model with no slash is not
94
+ * guessed at: it is left for the server's own default to answer, because
95
+ * inventing a provider here is how a run dies on a model nobody chose.
96
+ */
97
+ export function splitModel(model) {
98
+ const text = typeof model === 'string' ? model.trim() : '';
99
+ const at = text.indexOf('/');
100
+ if (at <= 0 || at === text.length - 1) return {};
101
+ return { providerID: text.slice(0, at), modelID: text.slice(at + 1) };
102
+ }
103
+
104
+ /** The `/event` stream, as an async iterable of `{ type, properties }`. */
105
+ function eventStream(request, outerSignal) {
106
+ const controller = new AbortController();
107
+ outerSignal?.addEventListener('abort', () => controller.abort(), { once: true });
108
+ const opened = request('/event', { stream: true, signal: controller.signal });
109
+
110
+ return {
111
+ close: () => controller.abort(),
112
+ async *[Symbol.asyncIterator]() {
113
+ const response = await opened;
114
+ const reader = response.body.getReader();
115
+ const decoder = new TextDecoder();
116
+ let buffer = '';
117
+ try {
118
+ while (true) {
119
+ const { value, done } = await reader.read();
120
+ if (done) return;
121
+ buffer += decoder.decode(value, { stream: true });
122
+ let index;
123
+ while ((index = buffer.indexOf('\n')) >= 0) {
124
+ const line = buffer.slice(0, index).trim();
125
+ buffer = buffer.slice(index + 1);
126
+ if (!line.startsWith('data:')) continue;
127
+ const payload = line.slice(5).trim();
128
+ if (!payload) continue;
129
+ try { yield JSON.parse(payload); } catch { /* a half-written frame is not news */ }
130
+ }
131
+ }
132
+ } finally {
133
+ try { await reader.cancel(); } catch { /* already gone */ }
134
+ }
135
+ },
136
+ };
137
+ }
138
+
139
+ /** The permission events, old and new, that mean "a card is wanted". */
140
+ const ASKED = new Set(['permission.asked', 'permission.v2.asked']);
141
+ /** What the reply endpoint calls yes and no. */
142
+ const ALLOW = 'once';
143
+ const DENY = 'reject';
144
+
145
+ /**
146
+ * ONE OPENCODE RUN, SHAPED LIKE A CHILD PROCESS.
147
+ *
148
+ * The supervisor's spawn path wants something with `stderr`, `on('event')` and
149
+ * one `exit`, because that is what a Claude Code child and a Codex worker both
150
+ * look like from there (main/supervisor.mjs, `_spawnWorker`). This is the third
151
+ * one, and it owns nothing the other two own: its events are OpenCode's own
152
+ * names, passed through untranslated, exactly as the Codex adapter passes its
153
+ * notifications through rather than pretending to be Claude's JSON lines.
154
+ *
155
+ * DENY IS THE ANSWER NOBODY HAS TO CHOOSE. A card that is never answered, a
156
+ * handler that throws, a spool that has gone away, a worker killed mid-question
157
+ * -- every one of those refuses the tool call. The opposite default would mean
158
+ * a crashed inbox silently approves shell commands, which is the one failure
159
+ * here that cannot be taken back.
160
+ */
161
+ export function createOpenCodeWorker({
162
+ client,
163
+ plan = {},
164
+ cwd = null,
165
+ resumeSessionId = null,
166
+ onApproval = null,
167
+ title = null,
168
+ } = {}) {
169
+ if (!client) throw Error('createOpenCodeWorker needs a client for a running OpenCode server');
170
+
171
+ const worker = new EventEmitter();
172
+ worker.stderr = new EventEmitter();
173
+ worker.stdout = new EventEmitter();
174
+ worker.sessionId = resumeSessionId ?? null;
175
+
176
+ const say = text => {
177
+ const line = String(text ?? '').trim();
178
+ if (line) worker.stderr.emit('data', line);
179
+ };
180
+
181
+ let ended = false;
182
+ let stream = null;
183
+ const answered = new Set();
184
+
185
+ const stop = async () => {
186
+ if (ended) return;
187
+ ended = true;
188
+ try { stream?.close(); } catch { /* already closed */ }
189
+ if (worker.sessionId) { try { await client.abort(worker.sessionId); } catch { /* the run is over either way */ } }
190
+ };
191
+
192
+ /**
193
+ * One card, and the reply that releases or refuses the tool.
194
+ *
195
+ * The id is remembered BEFORE the card is raised, so a request the stream
196
+ * repeats -- which it does, `permission.asked` can arrive again for a request
197
+ * still open -- costs one card and not two.
198
+ */
199
+ const decide = async (type, properties) => {
200
+ const id = properties?.id;
201
+ const session = properties?.sessionID;
202
+ if (!id || session !== worker.sessionId || answered.has(id)) return;
203
+ answered.add(id);
204
+
205
+ let allowed = false;
206
+ try {
207
+ allowed = onApproval ? await onApproval(type, properties) === true : false;
208
+ } catch (error) {
209
+ say(`${Name} could not ask about ${String(properties?.metadata?.command ?? 'a tool call')}: ${error?.message ?? error}`);
210
+ allowed = false;
211
+ }
212
+ if (ended) allowed = false;
213
+
214
+ try {
215
+ await client.replyPermission(session, id, allowed ? ALLOW : DENY);
216
+ } catch (error) {
217
+ say(`OpenCode did not take the answer to that question: ${error?.message ?? error}`);
218
+ }
219
+ };
220
+
221
+ const run = async () => {
222
+ try {
223
+ if (!worker.sessionId) {
224
+ const session = await client.createSession({ ...(title ? { title } : {}), ...(cwd ? { directory: cwd } : {}) });
225
+ worker.sessionId = session?.id ?? null;
226
+ if (!worker.sessionId) throw Error('OpenCode did not name the session it opened');
227
+ }
228
+
229
+ // The stream is opened BEFORE the prompt. Opening it after is a race the
230
+ // fast answers win: `permission.asked` for the first tool call can beat
231
+ // our own subscription, and nothing would ever answer that card.
232
+ stream = client.events();
233
+ const reading = (async () => {
234
+ for await (const event of stream) {
235
+ if (ended) return;
236
+ const type = String(event?.type ?? '');
237
+ const properties = event?.properties ?? {};
238
+ worker.emit('event', type, properties);
239
+ if (ASKED.has(type)) { await decide(type, properties); continue; }
240
+ if (properties?.sessionID && properties.sessionID !== worker.sessionId) continue;
241
+ if (type === 'session.error') {
242
+ worker.error = properties?.error ?? 'OpenCode reported an error';
243
+ return;
244
+ }
245
+ if (type === 'session.idle') return;
246
+ }
247
+ })();
248
+
249
+ await client.prompt(worker.sessionId, {
250
+ ...splitModel(plan.model),
251
+ ...(plan.agent ? { agent: plan.agent } : {}),
252
+ parts: [{ type: 'text', text: String(plan.prompt ?? '') }],
253
+ });
254
+
255
+ await reading;
256
+ } catch (error) {
257
+ worker.error = error?.message ?? String(error);
258
+ say(worker.error);
259
+ } finally {
260
+ const already = ended;
261
+ ended = true;
262
+ try { stream?.close(); } catch { /* already closed */ }
263
+ if (!already && worker.sessionId) { try { await client.abort(worker.sessionId); } catch { /* nothing left to stop */ } }
264
+ worker.emit('exit', worker.error ? 1 : 0, null);
265
+ }
266
+ };
267
+
268
+ worker.kill = () => { void stop(); };
269
+ worker.done = run();
270
+ return worker;
271
+ }