agentbox-app 0.1.11 → 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 (239) hide show
  1. package/README.md +24 -16
  2. package/bin/agentbox.mjs +18 -9
  3. package/briefs/message-rules.md +15 -4
  4. package/main/account-tooling.mjs +2 -1
  5. package/main/acp.mjs +296 -0
  6. package/main/agent-homes.mjs +158 -0
  7. package/main/agent-sessions.mjs +57 -20
  8. package/main/agent-updates.mjs +9 -20
  9. package/main/analytics.mjs +30 -0
  10. package/main/approvals.mjs +14 -5
  11. package/main/claude-bin.mjs +45 -310
  12. package/main/claude-input.mjs +9 -3
  13. package/main/claude-launch-env.mjs +54 -0
  14. package/main/claude-models.mjs +39 -7
  15. package/main/claude-plan.mjs +14 -2
  16. package/main/claude-usage.mjs +82 -25
  17. package/main/codex-bin.mjs +51 -248
  18. package/main/codex-launch-env.mjs +31 -0
  19. package/main/codex-session.mjs +38 -3
  20. package/main/codex-threads.mjs +41 -8
  21. package/main/codex-watch.mjs +67 -3
  22. package/main/config.mjs +40 -13
  23. package/main/corner-tag.mjs +489 -0
  24. package/main/crash-report.mjs +26 -3
  25. package/main/demo.mjs +17 -14
  26. package/main/engine-bin.mjs +595 -0
  27. package/main/engine-setup.mjs +27 -27
  28. package/main/feedback.mjs +11 -6
  29. package/main/fresh-user.mjs +1 -1
  30. package/main/git-change.mjs +8 -2
  31. package/main/harnesses/claude-stream.mjs +208 -0
  32. package/main/harnesses/claude.mjs +84 -0
  33. package/main/harnesses/codex.mjs +65 -0
  34. package/main/harnesses/common.mjs +78 -0
  35. package/main/harnesses/instructions-file.mjs +116 -0
  36. package/main/harnesses/opencode.mjs +234 -0
  37. package/main/harnesses.mjs +65 -0
  38. package/main/importable-threads.mjs +38 -0
  39. package/main/instruction-settings.mjs +47 -11
  40. package/main/ipc.mjs +175 -22
  41. package/main/local-app-frames.mjs +82 -0
  42. package/main/machine.mjs +1 -1
  43. package/main/main.mjs +120 -5
  44. package/main/memory-gate-history.mjs +51 -12
  45. package/main/memory-gate-server.mjs +67 -11
  46. package/main/memory-gate.mjs +49 -8
  47. package/main/memory-log.mjs +124 -0
  48. package/main/notify.mjs +48 -5
  49. package/main/opencode-server.mjs +271 -0
  50. package/main/opencode.mjs +287 -0
  51. package/main/phone-link.mjs +551 -0
  52. package/main/refresh-agent-models.mjs +2 -1
  53. package/main/relay-address.mjs +28 -0
  54. package/main/relay-client.mjs +246 -0
  55. package/main/row-label.mjs +8 -7
  56. package/main/runs-on.mjs +90 -0
  57. package/main/serve.mjs +124 -10
  58. package/main/settings.mjs +171 -11
  59. package/main/shell-path.mjs +86 -0
  60. package/main/ship-queue.mjs +25 -7
  61. package/main/sign-in-files.mjs +3 -7
  62. package/main/source-updater.mjs +27 -1
  63. package/main/start-hold.mjs +73 -0
  64. package/main/store/home.mjs +37 -5
  65. package/main/store/project.mjs +15 -19
  66. package/main/store.mjs +10 -4
  67. package/main/supervisor.mjs +1030 -447
  68. package/main/task-commands.mjs +13 -17
  69. package/main/task-folders-offthread.mjs +6 -1
  70. package/main/task-folders.mjs +491 -82
  71. package/main/task-terminals.mjs +49 -8
  72. package/main/team/index.mjs +2 -2
  73. package/main/team/projects.mjs +1 -1
  74. package/main/updater.mjs +71 -9
  75. package/main/worktree-include.mjs +494 -0
  76. package/mcp/core/page.mjs +1 -1
  77. package/mcp/core/work.mjs +34 -3
  78. package/mcp/tools.mjs +2 -1
  79. package/package.json +10 -1
  80. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  81. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  82. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  83. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  84. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  85. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  86. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  87. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  88. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  89. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  90. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  91. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  92. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  93. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  94. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  95. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  96. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  97. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  98. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  99. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  100. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  101. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  102. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  103. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  104. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  105. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  106. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  107. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  108. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  109. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  110. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  111. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  112. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  113. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  114. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  115. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  116. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  117. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  118. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  119. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  120. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  121. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  122. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  123. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  124. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  125. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  126. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  127. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  128. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  129. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  130. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  131. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  132. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  133. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  134. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  135. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  136. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  137. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  138. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  139. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  140. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  141. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  142. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  143. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  144. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  145. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  146. package/renderer/dist/corner-tag.html +13 -0
  147. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  148. package/renderer/dist/emulator/data/compression/README.md +11 -0
  149. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  150. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  151. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  152. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  153. package/renderer/dist/emulator/data/cores/README.md +22 -0
  154. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  155. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  156. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  157. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  158. package/renderer/dist/emulator/data/cores/package.json +65 -0
  159. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  160. package/renderer/dist/emulator/data/emulator.css +1614 -0
  161. package/renderer/dist/emulator/data/loader.js +170 -0
  162. package/renderer/dist/emulator/data/localization/README.md +63 -0
  163. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  164. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  165. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  166. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  167. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  168. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  169. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  170. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  171. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  172. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  173. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  174. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  175. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  176. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  177. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  178. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  179. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  180. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  181. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  182. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  183. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  184. package/renderer/dist/emulator/data/src/compression.js +142 -0
  185. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  186. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  187. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  188. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  189. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  190. package/renderer/dist/emulator/data/src/storage.js +125 -0
  191. package/renderer/dist/emulator/data/version.json +5 -0
  192. package/renderer/dist/emulator/host.js +297 -0
  193. package/renderer/dist/emulator/index.html +28 -0
  194. package/renderer/dist/emulator/provenance.json +6 -0
  195. package/renderer/dist/index.html +16 -3
  196. package/scripts/after-install.mjs +14 -0
  197. package/scripts/approval-server.sh +31 -7
  198. package/scripts/before-publish.mjs +12 -10
  199. package/shared/agent-import.mjs +3 -1
  200. package/shared/agents.d.mts +1 -0
  201. package/shared/bridge-map.mjs +19 -0
  202. package/shared/claude-commands.generated.mjs +8 -6
  203. package/shared/claude-models.generated.mjs +10 -8
  204. package/shared/codex-modes.mjs +1 -1
  205. package/shared/corner-tag.d.mts +14 -0
  206. package/shared/corner-tag.mjs +141 -0
  207. package/shared/demo-world.mjs +1 -1
  208. package/shared/engines.mjs +20 -5
  209. package/shared/fresh-user-home.mjs +12 -1
  210. package/shared/harness-definitions.mjs +103 -0
  211. package/shared/instruction-defaults.json +1 -1
  212. package/shared/notify-rules.d.mts +25 -0
  213. package/shared/notify-rules.mjs +19 -0
  214. package/shared/project-folder-check.mjs +3 -3
  215. package/shared/provider-commands.mjs +10 -2
  216. package/shared/rank.d.mts +2 -0
  217. package/shared/rank.mjs +50 -0
  218. package/shared/relay-address.mjs +6 -0
  219. package/shared/relay-crypto.mjs +165 -0
  220. package/shared/runs-on.d.mts +10 -0
  221. package/shared/runs-on.mjs +55 -0
  222. package/shared/shortcuts.mjs +11 -5
  223. package/shared/spawn-trouble.mjs +58 -7
  224. package/shared/terminal-input.d.mts +1 -0
  225. package/shared/terminal-input.mjs +30 -0
  226. package/shared/terminal-keys.d.mts +1 -0
  227. package/shared/terminal-keys.mjs +16 -0
  228. package/shared/terminal-size.d.mts +4 -0
  229. package/shared/terminal-size.mjs +29 -0
  230. package/shared/usage.d.mts +3 -0
  231. package/shared/usage.mjs +7 -0
  232. package/shared/where-it-runs-from.d.mts +43 -0
  233. package/shared/where-it-runs-from.mjs +155 -0
  234. package/shared/work-items.mjs +28 -2
  235. package/shared/work-lines.mjs +1 -1
  236. package/shared/xterm-invalid-range.d.mts +2 -0
  237. package/shared/xterm-invalid-range.mjs +58 -0
  238. package/renderer/dist/assets/index-B-2Az952.css +0 -32
  239. package/renderer/dist/assets/index-D6qncgai.js +0 -1168
@@ -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
  /*
@@ -32,6 +37,17 @@ export function terminalPlace({store,supervisor,product,id,agent}){
32
37
  return {cwd,notice};
33
38
  }
34
39
  export function terminalFolder(args){return terminalPlace(args).cwd;}
40
+ // The shell a person actually has. `$SHELL` when that file exists, then
41
+ // zsh, bash, and sh. A machine with no zsh used to spawn `/bin/zsh` and
42
+ // the terminal never came up.
43
+ export function loginShell(env=process.env,exists=fs.existsSync){
44
+ const preferred=env.SHELL;
45
+ if(preferred&&exists(preferred))return preferred;
46
+ for(const candidate of ['/bin/zsh','/bin/bash','/bin/sh']){
47
+ if(exists(candidate))return candidate;
48
+ }
49
+ return preferred||'/bin/sh';
50
+ }
35
51
  // Enumerate only descendants of our own shell, including background process
36
52
  // groups. Kill children before shell; closing a pane never calls this.
37
53
  export function disposeTerminalProcess(pty){
@@ -57,8 +73,9 @@ export function disposeTerminalProcess(pty){
57
73
  * it is the same call the toolbar makes to decide whether to draw Stop command,
58
74
  * deliberately, so a shell she can see is busy can never be one this reaps.
59
75
  *
60
- * WHICH SHELLS NOBODY IS WATCHING: the panel asks for output every 150ms while
61
- * 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
62
79
  * (TaskTerminal.tsx). So `lastReadAt` is not a guess about attention, it is the
63
80
  * last time the app was actually asked what this shell had to say.
64
81
  *
@@ -66,10 +83,14 @@ export function disposeTerminalProcess(pty){
66
83
  * worth nothing, and it goes. Everything else stays until she or the app quits.
67
84
  */
68
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;
69
90
  const SWEEP_MS=60000;
70
91
  const MAX_TERMINALS=20;
71
92
  export class TaskTerminals {
72
- constructor({spawn,resolve,disposeProcess=disposeTerminalProcess,env=terminalEnv(),shell=process.env.SHELL||'/bin/zsh',idleMs=IDLE_MS,sweepMs=SWEEP_MS,now=()=>Date.now()}={}){
93
+ constructor({spawn,resolve,disposeProcess=disposeTerminalProcess,env=terminalEnv(),shell=loginShell(),idleMs=IDLE_MS,sweepMs=SWEEP_MS,now=()=>Date.now()}={}){
73
94
  this.spawn=spawn??((...args)=>require('node-pty').spawn(...args));this.resolve=resolve;this.disposeProcess=disposeProcess;this.env=env;this.shell=shell;this.sessions=new Map();
74
95
  this.idleMs=idleMs;this.now=now;
75
96
  // Keys whose shell this class ended on its own, and WHY. The next open on
@@ -136,7 +157,7 @@ export class TaskTerminals {
136
157
  const command=place?.command;
137
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'});
138
159
  let resolveExit;const exitPromise=new Promise(resolve=>{resolveExit=resolve;});
139
- 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);
140
161
  /*
141
162
  * AND IT SAYS WHAT HAPPENED TO THE LAST ONE. Without this line, reopening a
142
163
  terminal the app tidied away looks exactly like the app losing her
@@ -145,17 +166,37 @@ export class TaskTerminals {
145
166
  const why=this.reaped.get(key);
146
167
  if(why!==undefined){this.reaped.delete(key);session.buffer=`\x1b[2m[${why} This is a new one.]\x1b[0m\r\n`;}
147
168
  if(place?.notice)session.buffer+=`\x1b[2m[${place.notice}]\x1b[0m\r\n`;
148
- pty.onData(data=>{session.buffer+=data;const trim=Math.max(0,session.buffer.length-LIMIT);session.buffer=session.buffer.slice(trim);session.offset+=trim;});
149
- 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);});
150
171
  return this.read(key,0);
151
172
  }
152
173
  get(key){const s=this.sessions.get(key);if(!s)throw Error('Terminal session ended. Open a new terminal to continue.');return s;}
153
174
  // EVERY READ IS THE PANEL SAYING IT IS STILL THERE. This one line is the whole
154
175
  // attention signal the sweep runs on; see the note on the class.
155
- 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();});}
156
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);}
157
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);}
158
- 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();}}
159
200
  async shutdown(){
160
201
  const exits=[...this.sessions.values()].map(s=>s.exitPromise);
161
202
  this.dispose();
@@ -133,7 +133,7 @@ export function createTeamService({
133
133
  : [[], [], state.me];
134
134
  set({ lastSyncAt: Date.now(), error: null, cards, people, me: me ?? state.me });
135
135
  if (report.joined.length || report.pulled) log(`team: joined ${report.joined.length}, pulled ${report.pulled}, pushed ${report.pushed}`);
136
- if (report.notJoined) log(`team: left ${report.notJoined} shared project(s) unjoined, this Mac already holds the most it takes on by itself`);
136
+ if (report.notJoined) log(`team: left ${report.notJoined} shared project(s) unjoined, this computer already holds the most it takes on by itself`);
137
137
  return report;
138
138
  } catch (err) {
139
139
  set({ error: String(err?.message ?? err) });
@@ -320,7 +320,7 @@ export function createTeamService({
320
320
  // already in a team does not move: that is a choice nobody offers here.
321
321
  async acceptInvite(teamId) {
322
322
  if (!backend || !state.me) throw new Error('sign in first');
323
- if (state.team) throw new Error(`this Mac is already in ${state.team.name}`);
323
+ if (state.team) throw new Error(`this computer is already in ${state.team.name}`);
324
324
  if (!state.invites.some((i) => i.teamId === teamId)) throw new Error('that invite is no longer waiting');
325
325
  await backend.acceptInvite(teamId);
326
326
  kept().setTeam(state.me.id, teamId);
@@ -100,7 +100,7 @@ export function markShared(dir, { teamId, visibility = 'team', people = [], shar
100
100
  if (!project) throw new Error(`no project.json in ${dir}`);
101
101
  // Sharing it would put every line of it in the cloud, which is the one thing
102
102
  // this project exists to make hard. A thread in it is shared one at a time.
103
- if (project[PERSONAL_FLAG] === true) throw new Error(`${PERSONAL_NAME} stays on this Mac. Share a thread in it instead.`);
103
+ if (project[PERSONAL_FLAG] === true) throw new Error(`${PERSONAL_NAME} stays on this computer. Share a thread in it instead.`);
104
104
  const projectId = project.team?.projectId || crypto.randomUUID();
105
105
  project.team = {
106
106
  projectId, teamId, visibility: visibility === 'people' ? 'people' : 'team', people: [...new Set(people)],
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.';