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
package/main/ipc.mjs CHANGED
@@ -40,15 +40,16 @@ import { readDoc, writeDoc } from './doc-file.mjs';
40
40
  import { readChange } from './code-change.mjs';
41
41
  import { findFile, readCodeFile, writeCodeFile } from './code-file.mjs';
42
42
  import { readNote, writeNote } from './rail-note.mjs';
43
- import { addClaudeAccount, addCodexAccount, outsideAgentsMode, permissionMode, readSettings, recheckClaude, recheckCodex, setProjectSetting, setWorkspaceSetting } from './settings.mjs';
43
+ import { addClaudeAccount, addCodexAccount, outsideAgentsMode, permissionMode, pointAtEngineBin, readSettings, recheckClaude, recheckCodex, searchForEngineBin, setProjectSetting, setWorkspaceSetting } from './settings.mjs';
44
44
  import { ICON_KINDS, clearProjectIcon, setProjectIcon, setProjectName } from './project-identity.mjs';
45
45
  import { ClaudeUsage } from './claude-usage.mjs';
46
46
  import { usageEngine } from '../shared/usage.mjs';
47
- import { DEFAULT_ENGINE } from '../shared/engines.mjs';
47
+ import { DEFAULT_ENGINE, isEngine } from '../shared/engines.mjs';
48
48
  import * as agents from './agents.mjs';
49
49
  import { findAgentFolders, readAgentFiles, readFolderAgents } from './agent-files.mjs';
50
- import { readSessionThreads } from './agent-sessions.mjs';
51
50
  import { readCodexThreads } from './codex-threads.mjs';
51
+ import { appSessionFolders } from './agent-homes.mjs';
52
+ import { readImportableThreads } from './importable-threads.mjs';
52
53
  import { markImported } from '../shared/agent-import.mjs';
53
54
  import { codexIdOf, importChoice, isCodexImportRow, isNotImportedRow } from '../shared/codex-import.mjs';
54
55
  import { checkProjectFolder } from '../shared/project-folder-check.mjs';
@@ -91,6 +92,9 @@ const NO_UPDATER = {
91
92
  }),
92
93
  check: async () => NO_UPDATER.state(),
93
94
  install: () => false,
95
+ // Nowhere to move to and nothing to move: a copy with no updater behind it
96
+ // is a copy run from source (shared/where-it-runs-from.mjs).
97
+ move: () => ({ moved: false, error: null }),
94
98
  };
95
99
 
96
100
  export function registerIpc({ store, supervisor, config, window, analytics = NO_ANALYTICS, docGrants = null, updater = NO_UPDATER, host, team = null }) {
@@ -259,9 +263,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
259
263
  // while the app is running and the settings screen already promises that
260
264
  // works without a restart.
261
265
  bin: () => config.claudeBin,
266
+ // The account marked In use, asked each time so a switch is read at once.
267
+ account: () => supervisor.usageAccount(),
262
268
  onChange: push,
263
269
  });
264
270
 
271
+ ipcMain.handle('zero:refresh-usage', async event => {
272
+ if (event.senderFrame?.parent || (event.sender && event.sender !== window.webContents)) throw Error('Usage refresh is only available in the main app window.');
273
+ const result = await usage.refreshNow();
274
+ return result.reading ? { ...result, reading: { engine: DEFAULT_ENGINE, ...result.reading } } : result;
275
+ });
276
+
265
277
  // Agents this run has already counted. In memory on purpose: a fresh launch
266
278
  // seeing the same agent again is a fresh fact about the app being used.
267
279
  const seenAgents = new Set();
@@ -279,11 +291,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
279
291
  // send one event per agent per tick, all day. The keys are only ever
280
292
  // compared in this process; nothing about an agent is sent with the
281
293
  // count, which carries no name, no path and no title.
294
+ //
295
+ // AND WHICH ENGINE IT WAS (w-1116fbb68a, 2026-10-08). This list is Claude
296
+ // Code processes and only ever has been; a Codex conversation has no
297
+ // process to find and is counted from main/codex-watch.mjs instead. The
298
+ // engine is what lets the two be read apart, and without it on this side
299
+ // the Codex count would look like a drop in the Claude one.
282
300
  for (const agent of live) {
283
301
  const key = agentKey(agent);
284
302
  if (!key || seenAgents.has(key)) continue;
285
303
  seenAgents.add(key);
286
- analytics.track('agent_seen');
304
+ analytics.track('agent_seen', { engine: 'claude' });
287
305
  }
288
306
  return live;
289
307
  } catch {
@@ -341,13 +359,13 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
341
359
  });
342
360
  ipcMain.handle('zero:terminal',(_event,payload={})=>{
343
361
  if(_event.senderFrame?.parent || (_event.sender && _event.sender!==window.webContents)) throw Error('Terminal is only available in the main app window.');
344
- const {product,id,action,data,cols,rows,offset}=payload;
362
+ const {product,id,action,data,cols,rows,offset,wait}=payload;
345
363
  if(typeof product!=='string'||typeof id!=='string'||!id||id.length>300)throw Error('Invalid task.');
346
364
  const key=JSON.stringify({product,id});
347
365
  if(product==='@agent-update')return agentUpdates.terminal(id,payload);
348
366
  switch(action){
349
367
  case 'open':return terminals.open(key);
350
- case 'read':return terminals.read(key,offset);
368
+ case 'read':return terminals.read(key,offset,wait);
351
369
  case 'write':terminals.write(key,data);return true;
352
370
  case 'resize':terminals.resize(key,cols,rows);return true;
353
371
  case 'close':terminals.close(key);return true;
@@ -360,6 +378,21 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
360
378
  ipcMain.handle('zero:older-items', (_e, { offset = 0, limit = 50 } = {}) =>
361
379
  store.listOlderItems({ offset: Number(offset) || 0, limit: Math.min(Number(limit) || 50, 200) }));
362
380
 
381
+ // IS ANYBODY ACTUALLY USING A SCREEN READER. Asked by the terminal pane
382
+ // before it builds an xterm, because xterm's accessibility tree is what
383
+ // `screenReaderMode` turns on and we used to turn it on for everybody.
384
+ // Electron's own words beside this property: "Rendering accessibility tree
385
+ // can significantly affect the performance of your app. It should not be
386
+ // enabled by default." It also carried xterm's 'invalid range' bug to an
387
+ // install that had no use for it (one crash report, 2026-10-08).
388
+ //
389
+ // Absent on Linux and before `ready`, where absent means no, and reading it
390
+ // may not throw: this is on the path that opens a terminal, and a terminal
391
+ // that cannot ask the question still has to open.
392
+ ipcMain.handle('zero:assistiveTech', () => {
393
+ try { return !!host.app?.accessibilitySupportEnabled; } catch { return false; }
394
+ });
395
+
363
396
  ipcMain.handle('zero:snapshot', () => {
364
397
  const now = Date.now();
365
398
  const items = store.listItems(now);
@@ -379,7 +412,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
379
412
  const reading = usageFor === DEFAULT_ENGINE ? usage.read() : supervisor.codexUsage();
380
413
  return {
381
414
  products: store.listProducts(),
382
- items,
415
+ // Each with the place she dragged it to, if she did (w-6e5b532a95).
416
+ items: supervisor.withPlaces(items),
383
417
  // THE TEAM: who is signed in, their team and its people, and the
384
418
  // title-free activity of their private work. Null on a build with no
385
419
  // team cloud, which is the single-person app.
@@ -400,6 +434,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
400
434
  // coding agent this is one entry and an empty map, and nothing anywhere
401
435
  // draws anything new.
402
436
  engines,
437
+ // WHETHER THIS MAC WAS ALREADY SIGNED INTO SOMETHING (w-e217e577e5,
438
+ // 2026-10-07), so the walk can say so once. A new user ran an agent and
439
+ // could not tell where the tokens were coming from, because the app finds
440
+ // whichever sign-in is already on the Mac and spends it in silence.
441
+ //
442
+ // It rides the snapshot rather than the settings model because the WALK
443
+ // reads it, and the settings model is only fetched while the settings
444
+ // screen is open. It costs no file read per tick: `main/runs-on.mjs`
445
+ // caches on the login files' own modification times, so this re-parses
446
+ // only after a sign-in or a profile refetch.
447
+ runsOn: supervisor.runsOnAccount(engines.workspace),
403
448
  // Whether this process is still the app on disk. It rides the snapshot
404
449
  // rather than boot-info because the staleness DEVELOPS while the app
405
450
  // runs: the case that cost her three times was a process left up for days
@@ -469,7 +514,19 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
469
514
  // it and the user's own sentence never reached the card: the row went on
470
515
  // saying only what the session was started with days before. This is the
471
516
  // only process that knows the user said it, so this is where it is written.
472
- if (out?.ok && out.key) agentSchedule.replied(out.key, out.at ?? Date.now(), Date.now(), String(text ?? ''));
517
+ //
518
+ // AND IT IS A REPLY, SO IT IS COUNTED (w-1116fbb68a, 2026-10-08). A reply
519
+ // on a task has always sent `reply_sent`; one typed into a live session
520
+ // sent nothing at all, and activation is read as three real replies to
521
+ // agents in the first day, so the people who talk to their own sessions
522
+ // rather than through a row were missing from it. `kind` says which of
523
+ // the two this was. It waits on the same delivery the mark does, and for
524
+ // the same reason: a count for words that never arrived would read as
525
+ // activation nobody had.
526
+ if (out?.ok) {
527
+ if (out.key) agentSchedule.replied(out.key, out.at ?? Date.now(), Date.now(), String(text ?? ''));
528
+ analytics.track('reply_sent', { kind: 'agent' });
529
+ }
473
530
  push();
474
531
  return out;
475
532
  } catch {
@@ -535,6 +592,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
535
592
 
536
593
  ipcMain.handle('zero:answer', async (_e, { product, id, answer, status, priority, permissionMode, model, effort, now, inReplyTo }) => {
537
594
  if (isAgentRow(id)) return { ok: false };
595
+ // A level picked from ⌘K arrives here with no words, and gives way the
596
+ // same as one picked on the summary (zero:thread-edit). A reply that
597
+ // happens to carry its row's level does not.
598
+ if (priority != null && !answer && id) supervisor.setThreadPlaces({ [id]: null });
599
+ // CLOSING A THREAD IS THE END OF WHAT IT WAS PINNED FOR (w-20323283e6).
600
+ if (id && status === 'done') supervisor.setThreadPins({ [id]: null });
538
601
  // A REPLY IN A THREAD (w-920461cbe6) only means something in a conversation
539
602
  // between people, which is the only place a thread is drawn and the only
540
603
  // record the mark crosses to a teammate in (shared/team-rules.mjs). Anywhere
@@ -553,7 +616,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
553
616
  if (choice) {
554
617
  let thread = null;
555
618
  if (choice === 'yes') {
556
- try { thread = readCodexThreads({}).threads.find((t) => t.id === codexIdOf(row)) ?? null; } catch { thread = null; }
619
+ try {
620
+ const { codexDirs } = appSessionFolders({ config, supervisor });
621
+ thread = readCodexThreads({ codexDirs }).threads.find((t) => t.id === codexIdOf(row)) ?? null;
622
+ } catch { thread = null; }
557
623
  }
558
624
  const out = store.answerCodexImport(product, id, choice, { thread });
559
625
  push();
@@ -599,6 +665,21 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
599
665
  return supervisor.status();
600
666
  });
601
667
 
668
+ // A THREAD DRAGGED TO A PLACE OF ITS OWN in the list (w-6e5b532a95): the
669
+ // places the drop worked out (`dropPlaces`, shared/rank.mjs), id to number.
670
+ ipcMain.handle('zero:set-thread-places', (_e, { places }) => {
671
+ supervisor.setThreadPlaces(places);
672
+ push();
673
+ return { ok: true };
674
+ });
675
+
676
+ // PINNED THREADS (w-20323283e6): id to true, or null to unpin.
677
+ ipcMain.handle('zero:set-thread-pins', (_e, { pins }) => {
678
+ supervisor.setThreadPins(pins);
679
+ push();
680
+ return { ok: true };
681
+ });
682
+
602
683
  // Hidden from the composer's picker and nothing else: a hidden product still
603
684
  // ranks, still spawns workers, and its work still reaches the inbox.
604
685
  ipcMain.handle('zero:set-product-hidden', (_e, { product, hidden }) => {
@@ -685,6 +766,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
685
766
  ipcMain.handle('zero:thread-edit', (_e, { product, id, patch }) => {
686
767
  try {
687
768
  store.threadEdit(product, id, patch);
769
+ // PICKING A LEVEL IS THE NEWER WORD on where a thread goes, so a place
770
+ // it was dragged to gives way and it sorts by its level again.
771
+ if (patch?.priority != null) supervisor.setThreadPlaces({ [id]: null });
688
772
  push();
689
773
  return { ok: true };
690
774
  } catch (err) {
@@ -851,6 +935,14 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
851
935
  return store.scheduleItem(product, id, runAt);
852
936
  });
853
937
 
938
+ // Snooze changes when a person sees the thread, never what an agent does.
939
+ ipcMain.handle('zero:snooze', (_e, { product, id, snoozedUntil }) => {
940
+ if (isAgentRow(id)) return { ok: false };
941
+ const item = store.snoozeItem(product, id, snoozedUntil);
942
+ push();
943
+ return item;
944
+ });
945
+
854
946
  // said about an agent instead of about a task. The row it moves stands for
855
947
  // somebody's terminal, so this writes to no ledger, sends no message and
856
948
  // stops no process: it records the moment her inbox should raise the agent
@@ -919,11 +1011,18 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
919
1011
  // an archive, so it does not collide with what done means on a work item.
920
1012
  ipcMain.handle('zero:end-repeat', (_e, { product, id }) => store.endRepeat(product, id));
921
1013
 
922
- ipcMain.handle('zero:create-product', (_e, { name, repoPath }) => {
1014
+ ipcMain.handle('zero:create-product', (_e, { name, repoPath, ofMany }) => {
923
1015
  const out = store.createProduct({ name, repoPath });
924
1016
  // (privacy page 5.1). The count is the whole payload; the path it was
925
1017
  // connected to never leaves.
926
- if (typeof repoPath === 'string' && repoPath.trim()) analytics.track('repo_connected');
1018
+ //
1019
+ // AND A PRESS THAT CONNECTS NINE FOLDERS IS ONE THING SOMEBODY DID, NOT
1020
+ // NINE (w-1116fbb68a, 2026-10-08). The import makes a project per folder,
1021
+ // so this handler fired nine times for one press and nine events read as
1022
+ // nine separate connections to anybody looking at them as activity rather
1023
+ // than as a funnel step. `ofMany` is the caller saying it will count the
1024
+ // press itself, once, with how many folders it connected.
1025
+ if (!ofMany && typeof repoPath === 'string' && repoPath.trim()) analytics.track('repo_connected');
927
1026
  return out;
928
1027
  });
929
1028
 
@@ -1063,13 +1162,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1063
1162
  // 'codex'`, land under the project whose folder they ran in exactly as the
1064
1163
  // Claude Code ones do, and the ones the user said not now to are here too, so
1065
1164
  // there is a way back to them.
1066
- const readAllThreads = () => {
1067
- let claude = [];
1068
- let codex = [];
1069
- try { ({ threads: claude } = readSessionThreads({})); } catch { claude = []; }
1070
- try { ({ threads: codex } = readCodexThreads({})); } catch { codex = []; }
1071
- return [...claude, ...codex].sort((a, b) => (b.when ?? 0) - (a.when ?? 0));
1072
- };
1165
+ // EVERY FOLDER EITHER TOOL KEEPS SESSIONS IN (main/agent-homes.mjs), not only
1166
+ // ~/.claude and ~/.codex, which is where the import used to look and nothing
1167
+ // else (w-e46f2e9cea).
1168
+ const readAllThreads = () => readImportableThreads({ config, supervisor });
1073
1169
  ipcMain.handle('zero:agent-threads', () => {
1074
1170
  const threads = readAllThreads();
1075
1171
  lastThreads = threads;
@@ -1296,15 +1392,64 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1296
1392
  }
1297
1393
  });
1298
1394
 
1395
+ /*
1396
+ * AND WHEN NEITHER BUTTON CAN FIND IT: POINT AT THE FILE YOURSELF
1397
+ * (w-ce8c1a9275). The search asks the login shell first now, which covers
1398
+ * every installer a person's own PATH knows about -- but a wrapper script
1399
+ * somewhere nobody would guess, a tool on a volume, or a machine whose login
1400
+ * shell will not start at all still used to mean a path WE ship and a release
1401
+ * THEY wait for. This is the half that ends that.
1402
+ *
1403
+ * ONE ROUND TRIP, like `zero:project-icon` below and for the same reason: the
1404
+ * panel opens in this process and the choice is applied in the same call, so
1405
+ * the only path that ever reaches `pointAtEngineBin` is one the open panel
1406
+ * itself produced. The window says WHICH coding agent it is asking about and
1407
+ * nothing else, which is the whole of what it is trusted with -- `codexBin`
1408
+ * is a path that gets spawned, and every worker on this machine has a broad
1409
+ * Bash allow (tests/where-codex-runs-from-is-not-a-file-a-worker-can-write).
1410
+ *
1411
+ * THE REFUSAL IS A SENTENCE AND NOT AN ERROR. Picking a folder, or a file
1412
+ * that cannot be run, is an ordinary mistake at a file panel; it comes back
1413
+ * as words the row prints, the way `zero:choose-folder` already does.
1414
+ */
1415
+ ipcMain.handle('zero:engine-bin', async (_e, { engine, action } = {}) => {
1416
+ if (engine !== 'claude' && engine !== 'codex') return { ok: false, error: 'Unknown coding agent.' };
1417
+ try {
1418
+ // Handing the choice back needs no panel: it is the row saying "look for
1419
+ // it again", and the search takes over from there.
1420
+ if (action === 'search') {
1421
+ const gave = searchForEngineBin(config, engine);
1422
+ if (!gave.ok) return { ok: false, refused: gave.say };
1423
+ return { ok: true, ...readSettings({ config, supervisor, store }) };
1424
+ }
1425
+ const { dialog } = await import('electron');
1426
+ const r = await dialog.showOpenDialog(window, {
1427
+ title: engine === 'codex' ? 'Where is Codex?' : 'Where is Claude Code?',
1428
+ buttonLabel: 'Use this one',
1429
+ properties: ['openFile', 'showHiddenFiles', 'treatPackageAsDirectory'],
1430
+ });
1431
+ // Cancelling is not a failure and must not draw one. `ok` with nothing
1432
+ // changed is the honest answer: they opened the panel and closed it again.
1433
+ if (r.canceled || !r.filePaths?.length) return { ok: true, canceled: true };
1434
+ const took = pointAtEngineBin(config, engine, r.filePaths[0]);
1435
+ if (!took.ok) return { ok: false, refused: took.say };
1436
+ return { ok: true, ...readSettings({ config, supervisor, store }) };
1437
+ } catch (err) {
1438
+ return { ok: false, error: String(err.message) };
1439
+ }
1440
+ });
1441
+
1299
1442
  // SETTING UP THEIR PLAN FOR THEM (w-9f6975906c). The walk asks which plan
1300
1443
  // they pay for and this installs the tool and starts its own sign-in. The
1301
1444
  // finder is the recheck, so what gets installed lands on the config and the
1302
1445
  // rest of the app runs on it without a restart.
1303
1446
  const engineSetup = createEngineSetup({
1304
1447
  find: (engine) => {
1305
- if (engine === 'claude') { const s = recheckClaude(config); return { found: s.claudeFound, path: s.claudeBin }; }
1306
- const s = recheckCodex(config);
1307
- return { found: s.found, path: s.bin };
1448
+ const finders = {
1449
+ claude: () => { const s = recheckClaude(config); return { found: s.claudeFound, path: s.claudeBin }; },
1450
+ codex: () => { const s = recheckCodex(config); return { found: s.found, path: s.bin }; },
1451
+ };
1452
+ return finders[engine]?.() ?? { found: false, path: null };
1308
1453
  },
1309
1454
  // WHICH ONE IS SIGNED IN, kept on the config for the run that reads it
1310
1455
  // (`Supervisor#_enginesFound`) and never saved: it changes outside the app.
@@ -1323,7 +1468,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1323
1468
  // A sign-in left waiting on the browser is not left running after the app.
1324
1469
  app.on('will-quit', () => { engineSetup.cancel('claude'); engineSetup.cancel('codex'); });
1325
1470
  ipcMain.handle('zero:engine-setup', async (_e, { action, engine } = {}) => {
1326
- if (engine !== 'claude' && engine !== 'codex') return { ok: false, error: 'Unknown coding agent.' };
1471
+ if (!isEngine(engine)) return { ok: false, error: 'Unknown coding agent.' };
1327
1472
  try {
1328
1473
  if (action === 'ready') return { ok: true, ...(await engineSetup.readiness(engine)) };
1329
1474
  if (action === 'start') return { ok: true, ...engineSetup.start(engine) };
@@ -1823,6 +1968,14 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1823
1968
  // to decide is when the app is allowed to disappear and come back.
1824
1969
  ipcMain.handle('zero:update-check', async () => updater.check({ manual: true }));
1825
1970
  ipcMain.handle('zero:update-install', () => ({ started: updater.install() }));
1971
+ // AND A THIRD, for the copy that cannot update itself from where it is: move
1972
+ // the app into Applications (shared/where-it-runs-from.mjs). It is a door of
1973
+ // its own rather than part of the install, because it is the opposite
1974
+ // decision — nothing has been downloaded and nothing will be until the app
1975
+ // is somewhere macOS lets it write.
1976
+ // A copy run from source answers no to it rather than throwing: there is no
1977
+ // bundle to move (main/source-updater.mjs).
1978
+ ipcMain.handle('zero:update-move', () => updater.move?.() ?? { moved: false, error: null });
1826
1979
 
1827
1980
  // Handed back so the Cmd+Y/Cmd+N chord answers through the same door the
1828
1981
  // buttons do, rather than reaching past it into approvals.answer.
@@ -0,0 +1,82 @@
1
+ // A LOCAL APP SHOWS IN THE PANE AND STAYS SIGNED IN.
2
+ //
3
+ // The pane draws an address like http://localhost:3006 in a frame, because a
4
+ // frame keeps every shortcut the app answers from inside a page (the
5
+ // before-input-event handler in main.mjs hears a frame; it would not hear a
6
+ // webview's guest). Two things a local app commonly sends broke that frame:
7
+ //
8
+ // X-Frame-Options / CSP frame-ancestors Chromium refuses to draw the page
9
+ // at all, so the pane sat blank.
10
+ // Next.js apps send both.
11
+ // a SameSite=Lax sign-in cookie the window is a file:// page, so
12
+ // the app in it is cross-site and the
13
+ // cookie is refused: sign-in loops.
14
+ //
15
+ // Both are rewritten here, below the page, for local addresses only and only
16
+ // inside the app's own window. Framing protection exists to stop another site
17
+ // drawing yours under a stranger's pointer; the app drawing her own dev server
18
+ // for her is not that. tests/a-local-app-shows-in-the-pane-and-stays-signed-in.test.mjs
19
+
20
+ const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
21
+
22
+ export function isLocalAddress(url) {
23
+ try {
24
+ const u = new URL(url);
25
+ return (u.protocol === 'http:' || u.protocol === 'https:') && LOCAL_HOSTS.has(u.hostname);
26
+ } catch { return false; }
27
+ }
28
+
29
+ const keyOf = (headers, name) => Object.keys(headers).find((k) => k.toLowerCase() === name);
30
+
31
+ function withoutFrameAncestors(policy) {
32
+ return policy.split(';').map((d) => d.trim()).filter((d) => d && !/^frame-ancestors(\s|$)/i.test(d)).join('; ');
33
+ }
34
+
35
+ // SameSite=None needs Secure, and Chromium takes Secure from http://localhost
36
+ // because it counts loopback as a secure origin.
37
+ function sentInsideTheWindow(cookie) {
38
+ const parts = cookie.split(';').map((p) => p.trim());
39
+ if (parts.some((p) => /^samesite\s*=\s*none$/i.test(p))) return cookie;
40
+ const kept = parts.filter((p) => !/^samesite\s*=/i.test(p));
41
+ if (!kept.some((p) => /^secure$/i.test(p))) kept.push('SameSite=None', 'Secure');
42
+ else kept.push('SameSite=None');
43
+ return kept.join('; ');
44
+ }
45
+
46
+ /**
47
+ * The headers a local app's response should carry inside the pane, or null
48
+ * when nothing changes. `details` is Electron's onHeadersReceived details.
49
+ */
50
+ export function localAppHeaders({ url, resourceType, responseHeaders }) {
51
+ if (!responseHeaders || resourceType === 'mainFrame' || !isLocalAddress(url)) return null;
52
+ const out = { ...responseHeaders };
53
+ let changed = false;
54
+ if (resourceType === 'subFrame') {
55
+ const xfo = keyOf(out, 'x-frame-options');
56
+ if (xfo) { delete out[xfo]; changed = true; }
57
+ const csp = keyOf(out, 'content-security-policy');
58
+ if (csp) {
59
+ const policies = out[csp].map(withoutFrameAncestors).filter(Boolean);
60
+ if (policies.join() !== out[csp].join()) {
61
+ changed = true;
62
+ if (policies.length) out[csp] = policies; else delete out[csp];
63
+ }
64
+ }
65
+ }
66
+ const cookies = keyOf(out, 'set-cookie');
67
+ if (cookies) {
68
+ const next = out[cookies].map(sentInsideTheWindow);
69
+ if (next.join('\n') !== out[cookies].join('\n')) { out[cookies] = next; changed = true; }
70
+ }
71
+ return changed ? out : null;
72
+ }
73
+
74
+ /** Wire it to the window's own session, for that window's requests only. */
75
+ export function letLocalAppsShowInThePane(contents) {
76
+ const filter = { urls: ['*://localhost/*', '*://127.0.0.1/*', '*://[::1]/*'] };
77
+ contents.session.webRequest.onHeadersReceived(filter, (details, callback) => {
78
+ const ours = details.webContentsId === contents.id;
79
+ const responseHeaders = ours ? localAppHeaders(details) : null;
80
+ callback(responseHeaders ? { responseHeaders } : {});
81
+ });
82
+ }
package/main/machine.mjs CHANGED
@@ -156,5 +156,5 @@ export function perAccountAgents(total, accounts) {
156
156
  export function machineNote({ slots, memGb, cores, known = true }, at = 0) {
157
157
  if (!known) return null;
158
158
  if (at && at <= slots) return null;
159
- return `This Mac has ${memGb} GB and ${cores} cores, so we suggest ${slots} at once. Go higher if you want to.`;
159
+ return `This computer has ${memGb} GB and ${cores} cores, so we suggest ${slots} at once. Go higher if you want to.`;
160
160
  }