agentbox-app 0.1.12 → 0.1.13

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 (214) hide show
  1. package/main/acp.mjs +296 -0
  2. package/main/agent-homes.mjs +158 -0
  3. package/main/agent-sessions.mjs +57 -20
  4. package/main/agent-updates.mjs +9 -22
  5. package/main/analytics.mjs +30 -0
  6. package/main/claude-bin.mjs +45 -311
  7. package/main/claude-input.mjs +9 -3
  8. package/main/claude-launch-env.mjs +54 -0
  9. package/main/claude-models.mjs +39 -7
  10. package/main/claude-plan.mjs +16 -16
  11. package/main/claude-usage.mjs +82 -25
  12. package/main/codex-bin.mjs +51 -249
  13. package/main/codex-session.mjs +38 -3
  14. package/main/codex-threads.mjs +41 -8
  15. package/main/codex-watch.mjs +67 -3
  16. package/main/config.mjs +26 -4
  17. package/main/corner-tag.mjs +489 -0
  18. package/main/crash-report.mjs +26 -3
  19. package/main/engine-bin.mjs +595 -0
  20. package/main/engine-setup.mjs +4 -15
  21. package/main/feedback.mjs +11 -6
  22. package/main/harnesses/claude-stream.mjs +208 -0
  23. package/main/harnesses/claude.mjs +84 -0
  24. package/main/harnesses/codex.mjs +65 -0
  25. package/main/harnesses/common.mjs +78 -0
  26. package/main/harnesses/instructions-file.mjs +116 -0
  27. package/main/harnesses/opencode.mjs +234 -0
  28. package/main/harnesses.mjs +65 -0
  29. package/main/importable-threads.mjs +38 -0
  30. package/main/ipc.mjs +165 -31
  31. package/main/local-app-frames.mjs +82 -0
  32. package/main/main.mjs +100 -4
  33. package/main/memory-gate-history.mjs +51 -12
  34. package/main/memory-gate-server.mjs +67 -11
  35. package/main/memory-gate.mjs +49 -8
  36. package/main/memory-log.mjs +124 -0
  37. package/main/notify.mjs +47 -4
  38. package/main/opencode-server.mjs +271 -0
  39. package/main/opencode.mjs +287 -0
  40. package/main/phone-link.mjs +551 -0
  41. package/main/relay-address.mjs +28 -0
  42. package/main/relay-client.mjs +246 -0
  43. package/main/row-label.mjs +6 -6
  44. package/main/runs-on.mjs +32 -35
  45. package/main/serve.mjs +113 -9
  46. package/main/settings.mjs +148 -10
  47. package/main/shell-path.mjs +86 -0
  48. package/main/ship-queue.mjs +24 -6
  49. package/main/sign-in-files.mjs +3 -7
  50. package/main/source-updater.mjs +27 -1
  51. package/main/start-hold.mjs +73 -0
  52. package/main/store.mjs +1 -1
  53. package/main/supervisor.mjs +832 -428
  54. package/main/task-commands.mjs +13 -17
  55. package/main/task-folders-offthread.mjs +6 -1
  56. package/main/task-folders.mjs +490 -98
  57. package/main/task-terminals.mjs +37 -7
  58. package/main/updater.mjs +71 -9
  59. package/main/worktree-include.mjs +494 -0
  60. package/package.json +10 -1
  61. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  62. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  63. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  64. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  65. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  66. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  67. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  68. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  69. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  70. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  71. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  72. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  73. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  74. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  75. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  76. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  77. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  78. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  79. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  80. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  81. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  82. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  83. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  84. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  85. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  86. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  87. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  88. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  89. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  90. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  91. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  92. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  93. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  94. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  95. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  96. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  97. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  98. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  99. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  101. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  102. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  103. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  104. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  105. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  106. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  107. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  108. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  109. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  110. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  111. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  112. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  113. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  114. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  115. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  116. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  117. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  118. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  119. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  120. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  121. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  122. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  123. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  124. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  125. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  126. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  127. package/renderer/dist/corner-tag.html +13 -0
  128. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  129. package/renderer/dist/emulator/data/compression/README.md +11 -0
  130. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  131. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  132. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  133. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  134. package/renderer/dist/emulator/data/cores/README.md +22 -0
  135. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  136. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  137. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  138. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  139. package/renderer/dist/emulator/data/cores/package.json +65 -0
  140. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  141. package/renderer/dist/emulator/data/emulator.css +1614 -0
  142. package/renderer/dist/emulator/data/loader.js +170 -0
  143. package/renderer/dist/emulator/data/localization/README.md +63 -0
  144. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  145. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  146. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  147. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  148. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  149. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  150. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  151. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  152. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  153. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  154. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  155. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  156. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  157. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  158. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  159. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  160. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  161. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  162. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  163. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  164. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  165. package/renderer/dist/emulator/data/src/compression.js +142 -0
  166. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  167. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  168. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  169. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  170. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  171. package/renderer/dist/emulator/data/src/storage.js +125 -0
  172. package/renderer/dist/emulator/data/version.json +5 -0
  173. package/renderer/dist/emulator/host.js +297 -0
  174. package/renderer/dist/emulator/index.html +28 -0
  175. package/renderer/dist/emulator/provenance.json +6 -0
  176. package/renderer/dist/index.html +16 -3
  177. package/scripts/approval-server.sh +24 -5
  178. package/scripts/before-publish.mjs +12 -10
  179. package/shared/agent-import.mjs +2 -0
  180. package/shared/agents.d.mts +1 -0
  181. package/shared/bridge-map.mjs +18 -0
  182. package/shared/claude-commands.generated.mjs +6 -4
  183. package/shared/claude-models.generated.mjs +6 -4
  184. package/shared/corner-tag.d.mts +14 -0
  185. package/shared/corner-tag.mjs +141 -0
  186. package/shared/engines.mjs +20 -5
  187. package/shared/harness-definitions.mjs +103 -0
  188. package/shared/notify-rules.d.mts +25 -0
  189. package/shared/notify-rules.mjs +19 -0
  190. package/shared/provider-commands.mjs +10 -2
  191. package/shared/rank.d.mts +2 -0
  192. package/shared/rank.mjs +50 -0
  193. package/shared/relay-address.mjs +6 -0
  194. package/shared/relay-crypto.mjs +165 -0
  195. package/shared/runs-on.d.mts +0 -10
  196. package/shared/runs-on.mjs +25 -49
  197. package/shared/shortcuts.mjs +10 -4
  198. package/shared/spawn-trouble.mjs +57 -6
  199. package/shared/terminal-input.d.mts +1 -0
  200. package/shared/terminal-input.mjs +30 -0
  201. package/shared/terminal-keys.d.mts +1 -0
  202. package/shared/terminal-keys.mjs +16 -0
  203. package/shared/terminal-size.d.mts +4 -0
  204. package/shared/terminal-size.mjs +29 -0
  205. package/shared/usage.d.mts +3 -0
  206. package/shared/usage.mjs +7 -0
  207. package/shared/where-it-runs-from.d.mts +43 -0
  208. package/shared/where-it-runs-from.mjs +155 -0
  209. package/shared/work-items.mjs +25 -1
  210. package/shared/work-lines.mjs +1 -1
  211. package/shared/xterm-invalid-range.d.mts +2 -0
  212. package/shared/xterm-invalid-range.mjs +58 -0
  213. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  214. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -9,6 +9,11 @@ const LIMIT=262144;
9
9
  export function terminalEnv(source=process.env){
10
10
  const env={...source,TERM:'xterm-256color',COLORTERM:'truecolor'};
11
11
  for(const key of Object.keys(env)) if(/^(ANTHROPIC_|OPENAI_API_KEY$|CLAUDE_CODE_|CLAUDECODE$|CLAUDE_PID$|CLAUDE_EFFORT$|ELECTRON_|NODE_OPTIONS$)/.test(key)) delete env[key];
12
+ // NPM'S STAND-IN EDITOR IS NOBODY'S CHOICE. `npm start` gives everything it
13
+ // runs EDITOR=vi when neither EDITOR nor VISUAL is set, and zsh reads "vi"
14
+ // there as a request for vi keys: Option+Left deleted and Option+Delete
15
+ // moved. A login shell sets the person's own EDITOR from their startup files.
16
+ if(env.EDITOR==='vi'&&!env.VISUAL) delete env.EDITOR;
12
17
  return env;
13
18
  }
14
19
  /*
@@ -68,8 +73,9 @@ export function disposeTerminalProcess(pty){
68
73
  * it is the same call the toolbar makes to decide whether to draw Stop command,
69
74
  * deliberately, so a shell she can see is busy can never be one this reaps.
70
75
  *
71
- * WHICH SHELLS NOBODY IS WATCHING: the panel asks for output every 150ms while
72
- * it is on screen and stops the instant it is hidden or she leaves the task
76
+ * WHICH SHELLS NOBODY IS WATCHING: the panel asks for output at least once a
77
+ * second while it is on screen (each ask waits up to READ_WAIT_MS for some)
78
+ * and stops the instant it is hidden or she leaves the task
73
79
  * (TaskTerminal.tsx). So `lastReadAt` is not a guess about attention, it is the
74
80
  * last time the app was actually asked what this shell had to say.
75
81
  *
@@ -77,6 +83,10 @@ export function disposeTerminalProcess(pty){
77
83
  * worth nothing, and it goes. Everything else stays until she or the app quits.
78
84
  */
79
85
  const IDLE_MS=600000;
86
+ // The longest a read waits for output before answering with none, so the panel
87
+ // still hears about a command that finished quietly and the sweep still sees
88
+ // it watching.
89
+ const READ_WAIT_MS=1000;
80
90
  const SWEEP_MS=60000;
81
91
  const MAX_TERMINALS=20;
82
92
  export class TaskTerminals {
@@ -147,7 +157,7 @@ export class TaskTerminals {
147
157
  const command=place?.command;
148
158
  const pty=this.spawn(command?.file??this.shell,command?.args??['-l'],{cwd,env:{...this.env,...command?.env},cols:80,rows:24,name:'xterm-256color'});
149
159
  let resolveExit;const exitPromise=new Promise(resolve=>{resolveExit=resolve;});
150
- const session={token:randomUUID(),pty,cwd,buffer:'',offset:0,exitCode:null,exited:false,exitPromise,lastReadAt:this.now()};this.sessions.set(key,session);
160
+ const session={token:randomUUID(),pty,cwd,buffer:'',offset:0,exitCode:null,exited:false,exitPromise,lastReadAt:this.now(),waiters:new Set()};this.sessions.set(key,session);
151
161
  /*
152
162
  * AND IT SAYS WHAT HAPPENED TO THE LAST ONE. Without this line, reopening a
153
163
  terminal the app tidied away looks exactly like the app losing her
@@ -156,17 +166,37 @@ export class TaskTerminals {
156
166
  const why=this.reaped.get(key);
157
167
  if(why!==undefined){this.reaped.delete(key);session.buffer=`\x1b[2m[${why} This is a new one.]\x1b[0m\r\n`;}
158
168
  if(place?.notice)session.buffer+=`\x1b[2m[${place.notice}]\x1b[0m\r\n`;
159
- pty.onData(data=>{session.buffer+=data;const trim=Math.max(0,session.buffer.length-LIMIT);session.buffer=session.buffer.slice(trim);session.offset+=trim;});
160
- pty.onExit(({exitCode})=>{session.exited=true;session.exitCode=exitCode;resolveExit();});
169
+ pty.onData(data=>{session.buffer+=data;const trim=Math.max(0,session.buffer.length-LIMIT);session.buffer=session.buffer.slice(trim);session.offset+=trim;this.wake(session);});
170
+ pty.onExit(({exitCode})=>{session.exited=true;session.exitCode=exitCode;resolveExit();this.wake(session);});
161
171
  return this.read(key,0);
162
172
  }
163
173
  get(key){const s=this.sessions.get(key);if(!s)throw Error('Terminal session ended. Open a new terminal to continue.');return s;}
164
174
  // EVERY READ IS THE PANEL SAYING IT IS STILL THERE. This one line is the whole
165
175
  // attention signal the sweep runs on; see the note on the class.
166
- read(key,offset=0){const s=this.get(key);if(!Number.isSafeInteger(offset)||offset<0)throw Error('Invalid terminal cursor.');s.lastReadAt=this.now();const end=s.offset+s.buffer.length;return {token:s.token,cwd:s.cwd,data:s.buffer.slice(Math.max(0,offset-s.offset)),offset:end,truncated:offset<s.offset,exited:s.exited,exitCode:s.exitCode,process:s.exited?'Exited':s.pty.process||'Shell'};}
176
+ read(key,offset=0,wait=0){
177
+ const s=this.get(key);if(!Number.isSafeInteger(offset)||offset<0)throw Error('Invalid terminal cursor.');s.lastReadAt=this.now();
178
+ const end=s.offset+s.buffer.length;
179
+ /*
180
+ * A READ MAY WAIT FOR THE SHELL'S NEXT OUTPUT, and comes back the moment
181
+ there is some. The panel used to ask every 150ms, so the echo of a key
182
+ sat in this buffer until the next ask: deleted characters stayed on her
183
+ screen after she deleted them (w-16e47d0836). `live` tells the panel this
184
+ main can wait, so it asks again at once instead of in 150ms; a main
185
+ older than its renderer (⌘R ahead of a restart) answers without it. */
186
+ if(wait>0&&!s.exited&&offset>=end)return new Promise((resolve,reject)=>{
187
+ const done=()=>{clearTimeout(timer);s.waiters.delete(done);try{if(this.sessions.get(key)!==s)throw Error('Terminal session ended. Open a new terminal to continue.');resolve({...this.read(key,offset),live:true});}catch(e){reject(e);}};
188
+ const timer=setTimeout(done,Math.min(wait,READ_WAIT_MS));
189
+ s.waiters.add(done);
190
+ });
191
+ const state={token:s.token,cwd:s.cwd,data:s.buffer.slice(Math.max(0,offset-s.offset)),offset:end,truncated:offset<s.offset,exited:s.exited,exitCode:s.exitCode,process:s.exited?'Exited':s.pty.process||'Shell'};
192
+ return wait>0?{...state,live:true}:state;
193
+ }
194
+ // Answers every waiting read. Deferred a turn so the pieces of one echo the
195
+ // pty hands over back to back go out as one answer.
196
+ wake(s){if(!s.waiters.size||s.waking)return;s.waking=true;setImmediate(()=>{s.waking=false;for(const done of [...s.waiters])done();});}
167
197
  write(key,data){const s=this.get(key);if(s.exited)throw Error('This shell has exited. Start a new session.');if(typeof data!=='string'||data.length>65536)throw Error('Terminal input is too large. Paste at most 64 KB at a time.');s.pty.write(data);}
168
198
  resize(key,cols,rows){if(!Number.isInteger(cols)||!Number.isInteger(rows)||cols<2||cols>500||rows<2||rows>300)throw Error('Invalid terminal size.');const s=this.get(key);if(!s.exited)s.pty.resize(cols,rows);}
169
- close(key){const s=this.sessions.get(key);if(s){if(!s.exited)this.disposeProcess(s.pty);this.sessions.delete(key);}}
199
+ close(key){const s=this.sessions.get(key);if(s){if(!s.exited)this.disposeProcess(s.pty);this.sessions.delete(key);for(const done of [...s.waiters])done();}}
170
200
  async shutdown(){
171
201
  const exits=[...this.sessions.values()].map(s=>s.exitPromise);
172
202
  this.dispose();
package/main/updater.mjs CHANGED
@@ -48,6 +48,7 @@
48
48
 
49
49
  import { createRequire } from 'node:module';
50
50
  import { Name } from '../shared/product-name.mjs';
51
+ import { MOVE_SAY } from '../shared/where-it-runs-from.mjs';
51
52
 
52
53
  const require = createRequire(import.meta.url);
53
54
 
@@ -68,6 +69,14 @@ export function describe(state) {
68
69
  const { phase, currentVersion, newVersion, percent, error, checkedAt, readyAt, source, changes, behind } = state;
69
70
  const installing = phase === 'installing';
70
71
  return {
72
+ // WHETHER THIS COPY CAN UPDATE ITSELF FROM WHERE IT IS, and the one press
73
+ // that fixes it when it cannot (main/where-it-runs-from.mjs). It rides the
74
+ // updater's own state because the three surfaces that would have to ask
75
+ // separately — the Settings row, the sidebar card and ⌘K — all already
76
+ // read this one object, and a second channel for it is a second answer.
77
+ canMove: !!state.canMove,
78
+ place: state.canMove ? (state.place ?? null) : null,
79
+ moveError: state.moveError ?? null,
71
80
  phase,
72
81
  currentVersion,
73
82
  newVersion: newVersion ?? null,
@@ -96,7 +105,17 @@ export function describe(state) {
96
105
  };
97
106
  }
98
107
 
99
- export function createUpdater({ app, log = console, onChanged = () => {} }) {
108
+ /**
109
+ * `autoUpdater` is a SEAM FOR THE TESTS and nothing else. The real one is
110
+ * required below; passing one in is how the failure paths get tested without a
111
+ * packaged build, a signed release and a network, which is the only reason the
112
+ * download bug (see the swallow in `check`) went seven reports unnoticed.
113
+ */
114
+ export function createUpdater({ app, log = console, onChanged = () => {}, autoUpdater: given = null, place = null }) {
115
+ // READ ONCE, at construction (main/where-it-runs-from.mjs says why the
116
+ // bundle cannot move under a running process). A build with no place passed
117
+ // in — the tests, and `main/serve.mjs` — simply never offers the move.
118
+ const where = place?.state?.() ?? { place: null, canUpdate: true, offerMove: false };
100
119
  let state = {
101
120
  phase: 'idle',
102
121
  currentVersion: app.getVersion(),
@@ -105,6 +124,9 @@ export function createUpdater({ app, log = console, onChanged = () => {} }) {
105
124
  error: null,
106
125
  checkedAt: null,
107
126
  readyAt: null,
127
+ canMove: !!where.offerMove,
128
+ place: where.place ?? null,
129
+ moveError: null,
108
130
  };
109
131
  let timer = null;
110
132
  let stopped = false;
@@ -117,12 +139,14 @@ export function createUpdater({ app, log = console, onChanged = () => {} }) {
117
139
  // Loaded lazily and inside a try, so a build that somehow shipped without the
118
140
  // dependency degrades to "no updates" instead of failing to boot at all. The
119
141
  // app not starting is a strictly worse outcome than the app not updating.
120
- let autoUpdater = null;
121
- try {
122
- ({ autoUpdater } = require('electron-updater'));
123
- } catch (e) {
124
- set({ phase: 'unsupported', error: 'This build has no updater in it.' });
125
- log.warn?.('[updater] electron-updater is not available:', e?.message ?? e);
142
+ let autoUpdater = given;
143
+ if (!autoUpdater) {
144
+ try {
145
+ ({ autoUpdater } = require('electron-updater'));
146
+ } catch (e) {
147
+ set({ phase: 'unsupported', error: 'This build has no updater in it.' });
148
+ log.warn?.('[updater] electron-updater is not available:', e?.message ?? e);
149
+ }
126
150
  }
127
151
 
128
152
  if (autoUpdater) {
@@ -184,7 +208,23 @@ export function createUpdater({ app, log = console, onChanged = () => {} }) {
184
208
  if (state.phase === 'ready') return describe(state);
185
209
  if (state.phase === 'checking' || (state.phase === 'downloading' && !manual)) return describe(state);
186
210
  try {
187
- await autoUpdater.checkForUpdates();
211
+ const found = await autoUpdater.checkForUpdates();
212
+ // THE DOWNLOAD IS A SECOND PROMISE AND NOBODY WAS HOLDING IT. With
213
+ // `autoDownload` on, electron-updater starts the download inside the
214
+ // check and hands it back here as `downloadPromise`; its own
215
+ // `downloadUpdate` dispatches the `error` event and then RE-THROWS. The
216
+ // await above resolves on the RESULT, not on the download, so a failed
217
+ // download had nowhere to land but `unhandledRejection` — and
218
+ // `main/crash-report.mjs` files one of those as a crash.
219
+ //
220
+ // That is 7 of the 14 crash reports of launch week (PostHog, 2026-10-08)
221
+ // and not one of them was a crash: five are Squirrel's read-only volume
222
+ // error from an app run out of the disk image, two are a dropped
223
+ // connection, and in every one of them the app carried on running. The
224
+ // failure is ALREADY on screen by the time we get here, because the
225
+ // `error` handler above set it, so there is nothing left to do with this
226
+ // rejection but stop it being mistaken for a crash.
227
+ found?.downloadPromise?.catch?.(() => {});
188
228
  } catch (e) {
189
229
  set({ phase: 'error', error: shortError(e), checkedAt: Date.now() });
190
230
  }
@@ -201,6 +241,18 @@ export function createUpdater({ app, log = console, onChanged = () => {} }) {
201
241
  return true;
202
242
  }
203
243
 
244
+ // THE OTHER BUTTON, for a copy that cannot update itself from where it is.
245
+ // Electron moves the bundle and relaunches, so a `true` here is the last
246
+ // thing this process does. A failure goes into the state like every other
247
+ // failure, so the row and the sidebar card both say it and both offer
248
+ // another press.
249
+ function move() {
250
+ if (!state.canMove) return { moved: false, error: null };
251
+ const got = place?.move?.() ?? { moved: false, error: null };
252
+ if (!got.moved) set({ moveError: got.error ?? null });
253
+ return got;
254
+ }
255
+
204
256
  function start() {
205
257
  if (stopped || timer) return;
206
258
  const tick = () => { void check(); };
@@ -215,7 +267,7 @@ export function createUpdater({ app, log = console, onChanged = () => {} }) {
215
267
  timer = null;
216
268
  }
217
269
 
218
- return { start, stop, check, install, state: () => describe(state) };
270
+ return { start, stop, check, install, move, state: () => describe(state) };
219
271
  }
220
272
 
221
273
  // electron-updater's errors arrive as Error objects whose `message` is
@@ -234,6 +286,16 @@ export function shortError(e) {
234
286
  // releases/download/v0.1.0/latest-mac.yml): HttpErr…" to her. The sentence
235
287
  // for exactly that failure was already on the line below it.
236
288
  const named = `${e?.code ?? ''} ${raw}`;
289
+ // SQUIRREL'S OWN PARAGRAPH, WHICH IS 240 CHARACTERS AND HAS A URL IN IT.
290
+ // "Cannot update while running on a read-only volume. The application is on
291
+ // a read-only volume. Please move the application and try again. If you'll
292
+ // need to move the application out of the Downloads directory. See
293
+ // https://github.com/Squirrel/Squirrel.Mac …" — the whole of what 23 of the
294
+ // 26 main-rejection reports carried (PostHog, 2026-10-12), and the Settings
295
+ // row showed the first 160 characters of it. The app knows where it is
296
+ // running from (shared/where-it-runs-from.mjs), so this is one sentence and
297
+ // the row beside it is the press that fixes it.
298
+ if (/read-only volume/i.test(raw)) return MOVE_SAY.cannotUpdateHere;
237
299
  if (/ERR_UPDATER_ZIP_FILE_NOT_FOUND/.test(named)) return 'That release has no app in it to install.';
238
300
  if (/ERR_UPDATER_CHANNEL_FILE_NOT_FOUND/.test(named)) return 'That release is missing its update file.';
239
301
  if (/ERR_UPDATER_NO_PUBLISHED_VERSIONS|LATEST_VERSION_NOT_FOUND/.test(named)) return 'No published version to update to yet.';