agentbox-app 0.1.7 → 0.1.12

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 (255) hide show
  1. package/README.md +220 -58
  2. package/bin/agentbox.mjs +18 -3
  3. package/briefs/message-rules.md +24 -3
  4. package/briefs/worker.md +63 -49
  5. package/briefs/writing-rules.md +90 -131
  6. package/main/account-discovery.mjs +21 -20
  7. package/main/account-tooling.mjs +11 -11
  8. package/main/agent-activity.mjs +45 -5
  9. package/main/agent-schedule.mjs +14 -14
  10. package/main/agent-sessions.mjs +94 -10
  11. package/main/agent-updates.mjs +116 -0
  12. package/main/agents.mjs +52 -46
  13. package/main/analytics.mjs +27 -0
  14. package/main/approval-prompt-server.mjs +2 -2
  15. package/main/approvals.mjs +14 -5
  16. package/main/artifact-path.mjs +11 -0
  17. package/main/claude-bin.mjs +32 -5
  18. package/main/claude-input.mjs +45 -3
  19. package/main/claude-plan.mjs +25 -13
  20. package/main/code-change.mjs +3 -3
  21. package/main/codex-account.mjs +1 -1
  22. package/main/codex-bin.mjs +18 -0
  23. package/main/codex-launch-env.mjs +31 -0
  24. package/main/codex-memory-gate.mjs +260 -0
  25. package/main/codex-models.mjs +12 -7
  26. package/main/codex-threads.mjs +46 -23
  27. package/main/codex-usage-file.mjs +7 -7
  28. package/main/codex-usage.mjs +4 -4
  29. package/main/codex-watch.mjs +12 -12
  30. package/main/config.mjs +82 -17
  31. package/main/demo.mjs +17 -14
  32. package/main/dev-window.mjs +9 -9
  33. package/main/doc-scheme.mjs +2 -0
  34. package/main/engine-setup.mjs +204 -0
  35. package/main/feedback.mjs +55 -0
  36. package/main/folder-pictures.mjs +48 -0
  37. package/main/frame-scrollbars.mjs +90 -0
  38. package/main/fresh-user.mjs +7 -4
  39. package/main/git-change-offthread.mjs +70 -0
  40. package/main/git-change-thread.mjs +11 -0
  41. package/main/git-change.mjs +9 -3
  42. package/main/img-scheme.mjs +70 -1
  43. package/main/instruction-settings.mjs +134 -60
  44. package/main/ipc.mjs +335 -42
  45. package/main/leftovers.mjs +518 -0
  46. package/main/live-replies.mjs +43 -5
  47. package/main/machine.mjs +48 -1
  48. package/main/main.mjs +138 -15
  49. package/main/memory-gate-history.mjs +254 -0
  50. package/main/memory-gate-server.mjs +461 -0
  51. package/main/memory-gate.mjs +206 -0
  52. package/main/message-priority.mjs +87 -0
  53. package/main/notify.mjs +1 -1
  54. package/main/rail-note.mjs +2 -2
  55. package/main/refresh-agent-models.mjs +21 -0
  56. package/main/repeats.mjs +4 -4
  57. package/main/row-label.mjs +115 -51
  58. package/main/runs-on.mjs +93 -0
  59. package/main/serve.mjs +77 -5
  60. package/main/settings.mjs +245 -26
  61. package/main/ship-queue.mjs +279 -0
  62. package/main/sign-in-files.mjs +39 -0
  63. package/main/source-updater.mjs +298 -0
  64. package/main/store/home.mjs +84 -7
  65. package/main/store/project.mjs +15 -19
  66. package/main/store/work-items.mjs +150 -6
  67. package/main/store.mjs +245 -13
  68. package/main/supervisor.mjs +1634 -417
  69. package/main/task-commands.mjs +8 -10
  70. package/main/task-folders.mjs +94 -16
  71. package/main/task-remote-control.mjs +3 -3
  72. package/main/task-terminals.mjs +14 -2
  73. package/main/team/index.mjs +440 -0
  74. package/main/team/memory-cloud.mjs +247 -0
  75. package/main/team/projects.mjs +162 -0
  76. package/main/team/session.mjs +225 -0
  77. package/main/team/sign-in.mjs +106 -0
  78. package/main/team/supabase-backend.mjs +231 -0
  79. package/main/team/sync.mjs +163 -0
  80. package/main/updater.mjs +15 -5
  81. package/main/write-before-reload.mjs +50 -0
  82. package/mcp/core/page.mjs +10 -12
  83. package/mcp/core/work.mjs +123 -26
  84. package/mcp/tools.mjs +13 -6
  85. package/package.json +21 -9
  86. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  87. package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
  88. package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
  89. package/renderer/dist/index.html +2 -2
  90. package/scripts/after-install.mjs +14 -0
  91. package/scripts/approval-server.sh +7 -2
  92. package/scripts/memory-gate-hook.sh +59 -0
  93. package/shared/agent-import.mjs +39 -16
  94. package/shared/agents.d.mts +26 -8
  95. package/shared/agents.mjs +100 -108
  96. package/shared/analytics-events.mjs +19 -2
  97. package/shared/answers.mjs +12 -0
  98. package/shared/bridge-map.d.mts +12 -0
  99. package/shared/bridge-map.mjs +38 -1
  100. package/shared/claude-commands.generated.mjs +2 -2
  101. package/shared/claude-models.generated.mjs +6 -6
  102. package/shared/claude-usage.mjs +6 -6
  103. package/shared/codex-import.mjs +7 -7
  104. package/shared/codex-modes.d.mts +50 -0
  105. package/shared/codex-modes.mjs +1 -1
  106. package/shared/codex-usage.mjs +7 -8
  107. package/shared/demo-world.mjs +6 -6
  108. package/shared/engines.mjs +27 -10
  109. package/shared/feedback.d.mts +14 -0
  110. package/shared/feedback.mjs +44 -0
  111. package/shared/first-run-examples.mjs +4 -4
  112. package/shared/first-run-practice.d.mts +16 -0
  113. package/shared/first-run-practice.mjs +176 -70
  114. package/shared/fresh-user-home.mjs +34 -4
  115. package/shared/instruction-defaults.json +2 -2
  116. package/shared/product-name.mjs +4 -2
  117. package/shared/project-folder-check.mjs +9 -3
  118. package/shared/rank.d.mts +1 -0
  119. package/shared/rank.mjs +22 -0
  120. package/shared/referenced-files.mjs +1 -1
  121. package/shared/runs-on.d.mts +20 -0
  122. package/shared/runs-on.mjs +79 -0
  123. package/shared/schemes.d.mts +11 -0
  124. package/shared/schemes.mjs +1 -1
  125. package/shared/shortcuts.d.mts +41 -0
  126. package/shared/shortcuts.mjs +236 -0
  127. package/shared/spawn-trouble.d.mts +2 -0
  128. package/shared/spawn-trouble.mjs +40 -19
  129. package/shared/team-rules.d.mts +17 -0
  130. package/shared/team-rules.mjs +230 -0
  131. package/shared/team-status.d.mts +11 -0
  132. package/shared/team-status.mjs +90 -0
  133. package/shared/terminal-links.d.mts +2 -0
  134. package/shared/terminal-links.mjs +55 -0
  135. package/shared/terminal-state.d.mts +1 -0
  136. package/shared/terminal-state.mjs +16 -0
  137. package/shared/thread-cards.d.mts +19 -0
  138. package/shared/thread-cards.mjs +254 -0
  139. package/shared/usage.d.mts +1 -1
  140. package/shared/usage.mjs +6 -7
  141. package/shared/work-items.mjs +258 -28
  142. package/shared/work-lines.mjs +393 -16
  143. package/worker-permissions.json +1 -1
  144. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  145. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  146. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  147. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  148. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  149. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  150. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  151. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  152. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  153. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  154. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  155. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  156. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  157. package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
  158. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  159. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  160. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  161. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  162. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  163. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  164. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  165. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  166. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  167. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  168. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  169. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  170. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  171. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  172. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  173. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  174. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  175. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  176. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  177. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  178. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  179. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  180. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  181. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  182. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  183. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  184. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  185. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  186. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  187. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  188. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  189. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  190. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  191. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  192. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  193. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  194. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  195. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  196. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  197. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  198. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  199. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  200. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  201. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  202. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  203. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  204. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  205. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  206. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  207. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  208. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  209. package/renderer/dist/emulator/data/compression/README.md +0 -11
  210. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  211. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  212. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  213. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  214. package/renderer/dist/emulator/data/cores/README.md +0 -22
  215. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  216. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  217. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  218. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  219. package/renderer/dist/emulator/data/cores/package.json +0 -65
  220. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  221. package/renderer/dist/emulator/data/emulator.css +0 -1614
  222. package/renderer/dist/emulator/data/loader.js +0 -170
  223. package/renderer/dist/emulator/data/localization/README.md +0 -63
  224. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  225. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  226. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  227. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  228. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  229. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  230. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  231. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  232. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  233. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  234. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  235. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  236. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  237. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  238. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  239. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  240. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  241. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  242. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  243. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  244. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  245. package/renderer/dist/emulator/data/src/compression.js +0 -142
  246. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  247. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  248. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  249. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  250. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  251. package/renderer/dist/emulator/data/src/storage.js +0 -125
  252. package/renderer/dist/emulator/data/version.json +0 -5
  253. package/renderer/dist/emulator/host.js +0 -299
  254. package/renderer/dist/emulator/index.html +0 -28
  255. package/renderer/dist/emulator/provenance.json +0 -6
package/main/ipc.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { openSourceFile } from './open-source-file.mjs';
2
2
  import {submitReply} from './live-replies.mjs';
3
+ import { createEngineSetup } from './engine-setup.mjs';
3
4
  // IPC: a thin, typed-by-convention surface. The renderer asks; the main
4
5
  // process derives from files and answers. Pushes go one way: "state changed,
5
6
  // refetch." No state is cached on either side of the bridge.
@@ -21,14 +22,19 @@ import {submitReply} from './live-replies.mjs';
21
22
  import {terminalSenderAllowed} from './terminal-access.mjs';
22
23
  import {TaskTerminals,terminalPlace} from './task-terminals.mjs';
23
24
  import {SETTINGS_TERMINAL} from '../shared/settings-terminal.mjs';
25
+ import {AgentUpdates,installedAgent} from './agent-updates.mjs';
26
+ import {refreshAgentModels} from './refresh-agent-models.mjs';
24
27
  import {readInstruction,writeInstruction,listVersions,readVersion} from './instruction-settings.mjs';
25
28
  import path from 'node:path';
26
29
  import os from 'node:os';
27
30
  import fsWatch from 'node:fs';
28
31
  import { listFolders } from './folders.mjs';
32
+ import { feedbackUrl, sendFeedback } from './feedback.mjs';
33
+ import { feedbackPayload } from '../shared/feedback.mjs';
29
34
  import * as approvals from './approvals.mjs';
30
35
  import { restartNeeded } from './staleness.mjs';
31
36
  import { artifactRoots, resolveArtifact } from './artifact-path.mjs';
37
+ import { folderPictures } from './folder-pictures.mjs';
32
38
  import { docUrl } from './doc-scheme.mjs';
33
39
  import { readDoc, writeDoc } from './doc-file.mjs';
34
40
  import { readChange } from './code-change.mjs';
@@ -43,6 +49,7 @@ import * as agents from './agents.mjs';
43
49
  import { findAgentFolders, readAgentFiles, readFolderAgents } from './agent-files.mjs';
44
50
  import { readSessionThreads } from './agent-sessions.mjs';
45
51
  import { readCodexThreads } from './codex-threads.mjs';
52
+ import { markImported } from '../shared/agent-import.mjs';
46
53
  import { codexIdOf, importChoice, isCodexImportRow, isNotImportedRow } from '../shared/codex-import.mjs';
47
54
  import { checkProjectFolder } from '../shared/project-folder-check.mjs';
48
55
  import { openFreshUser } from './fresh-user.mjs';
@@ -52,6 +59,7 @@ import { agentKey } from '../shared/agents.mjs';
52
59
  import { machineryPath } from './store/home.mjs';
53
60
  import { EVENT_NAMES } from '../shared/analytics-events.mjs';
54
61
  import { Name } from '../shared/product-name.mjs';
62
+ import { handedOnByReply } from '../shared/team-rules.mjs';
55
63
 
56
64
  // WHAT A WORKER IS TOLD WHEN HER DECISION COULD NOT BE RECORDED.
57
65
  const SPOOL_REFUSED = `${Name} could not record the founder's decision, so nothing was allowed. Finish what your grants cover and file the rest as a review.`;
@@ -85,7 +93,7 @@ const NO_UPDATER = {
85
93
  install: () => false,
86
94
  };
87
95
 
88
- export function registerIpc({ store, supervisor, config, window, analytics = NO_ANALYTICS, docGrants = null, updater = NO_UPDATER, host }) {
96
+ export function registerIpc({ store, supervisor, config, window, analytics = NO_ANALYTICS, docGrants = null, updater = NO_UPDATER, host, team = null }) {
89
97
  // The three Electron things, or a plain Node stand-in for them. There is no
90
98
  // default: a caller that forgets says so here rather than throwing eighty
91
99
  // lines further down on `ipcMain.handle` of undefined.
@@ -123,7 +131,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
123
131
  // Allow/Deny Pending Approval in the Agents menu. All three go through here
124
132
  // so all three tell the renderer what was just decided: without that, the
125
133
  // card only ever "disappeared" on the next store push and nothing on screen
126
- // acknowledged the press (the founder, 2026-08-06). The card's exit is drawn
134
+ // acknowledged the press. The card's exit is drawn
127
135
  // from this.
128
136
  //
129
137
  // THE MENU WAS NOT ONE OF THE THREE UNTIL NOW, AND THIS PARAGRAPH SAID IT
@@ -196,12 +204,19 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
196
204
  // and attaches the properties itself. This is the reason there is no
197
205
  // analytics SDK in the renderer at all (a test fails the build if one
198
206
  // appears): a browser SDK autocaptures the text of what was clicked, and in
199
- // Agentbox that text is task titles.
200
- ipcMain.handle('zero:track', (_e, { name } = {}) => {
207
+ // Agentbox that text is task titles. A NUMBER MAY RIDE WITH THE NAME, and only
208
+ // a number (2026-10-06): which setup step was reached.
209
+ ipcMain.handle('zero:track', (_e, { name, count } = {}) => {
201
210
  if (!EVENT_NAMES.includes(name)) return false;
202
- return analytics.track(name);
211
+ return analytics.track(name, Number.isFinite(count) ? { count } : undefined);
203
212
  });
204
213
 
214
+ // WHETHER A COUNT CAME FROM SETUP'S PRACTICE PROJECT, read off the project's
215
+ // own flag rather than its name, so the walkthrough and real use part.
216
+ const isPractice = (slug) => {
217
+ try { return store.listProducts().some((p) => p.slug === slug && p.practice === true); } catch { return false; }
218
+ };
219
+
205
220
  ipcMain.handle('zero:approve', (_e, { id, allow, note }) => {
206
221
  return { ok: answerApproval(id, allow, note) };
207
222
  });
@@ -304,17 +319,32 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
304
319
  const agent=id.startsWith('agent:')?liveAgents().find(a=>'agent:'+a.pid===id && (a.product??'')===product):null;
305
320
  return terminalPlace({store,supervisor,product,id,agent});
306
321
  }});
322
+ const agentUpdates=new AgentUpdates({
323
+ installation:async engine=>{if(engine==='claude')recheckClaude(config);else recheckCodex(config);return installedAgent(engine,config);},
324
+ refresh:async engine=>{if(engine==='claude')recheckClaude(config);else recheckCodex(config);await refreshAgentModels(engine,config,supervisor._codexHome());if(engine==='codex')for(const profile of supervisor._codexProfiles())supervisor._releaseIdleCodex(profile);},
325
+ });
326
+ ipcMain.handle('zero:agent-update',async(event,{engine,action}={})=>{
327
+ if(event.senderFrame?.parent||(event.sender&&event.sender!==window.webContents))throw Error('Updates are only available in the main app window.');
328
+ if(!['claude','codex'].includes(engine))throw Error('Unknown agent.');
329
+ if(action==='check')return agentUpdates.check(engine);
330
+ if(action==='recheck')return agentUpdates.check(engine,true);
331
+ if(action==='start')return agentUpdates.start(engine);
332
+ if(action==='status')return agentUpdates.status(engine);
333
+ if(action==='refresh'){await refreshAgentModels(engine,config,supervisor._codexHome());return true;}
334
+ throw Error('Unknown update action.');
335
+ });
307
336
  let terminalQuitPending=false,terminalQuitReady=false;
308
337
  app.on('before-quit',event=>{
309
- if(terminalQuitReady||(!terminalQuitPending&&!terminals.sessions.size))return;
338
+ if(terminalQuitReady||(!terminalQuitPending&&!terminals.sessions.size&&!agentUpdates.terminals.sessions.size))return;
310
339
  event.preventDefault();
311
- if(!terminalQuitPending){terminalQuitPending=true;void terminals.shutdown().finally(()=>{terminalQuitReady=true;app.quit();});}
340
+ if(!terminalQuitPending){terminalQuitPending=true;void Promise.all([terminals.shutdown(),agentUpdates.shutdown()]).finally(()=>{terminalQuitReady=true;app.quit();});}
312
341
  });
313
342
  ipcMain.handle('zero:terminal',(_event,payload={})=>{
314
343
  if(_event.senderFrame?.parent || (_event.sender && _event.sender!==window.webContents)) throw Error('Terminal is only available in the main app window.');
315
344
  const {product,id,action,data,cols,rows,offset}=payload;
316
345
  if(typeof product!=='string'||typeof id!=='string'||!id||id.length>300)throw Error('Invalid task.');
317
346
  const key=JSON.stringify({product,id});
347
+ if(product==='@agent-update')return agentUpdates.terminal(id,payload);
318
348
  switch(action){
319
349
  case 'open':return terminals.open(key);
320
350
  case 'read':return terminals.read(key,offset);
@@ -325,6 +355,11 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
325
355
  }
326
356
  });
327
357
 
358
+ // The Done and All tabs' next page of finished threads the snapshot's 8 MB
359
+ // read left out, asked for as she scrolls to their foot (main/store.mjs).
360
+ ipcMain.handle('zero:older-items', (_e, { offset = 0, limit = 50 } = {}) =>
361
+ store.listOlderItems({ offset: Number(offset) || 0, limit: Math.min(Number(limit) || 50, 200) }));
362
+
328
363
  ipcMain.handle('zero:snapshot', () => {
329
364
  const now = Date.now();
330
365
  const items = store.listItems(now);
@@ -345,6 +380,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
345
380
  return {
346
381
  products: store.listProducts(),
347
382
  items,
383
+ // THE TEAM: who is signed in, their team and its people, and the
384
+ // title-free activity of their private work. Null on a build with no
385
+ // team cloud, which is the single-person app.
386
+ team: team ? team.state() : null,
348
387
  agents: liveAgents(),
349
388
  approvals: approvals.listPending(config.storeRoot),
350
389
  supervisor: running,
@@ -361,6 +400,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
361
400
  // coding agent this is one entry and an empty map, and nothing anywhere
362
401
  // draws anything new.
363
402
  engines,
403
+ // WHICH SUBSCRIPTION ALL OF THIS IS RUNNING ON (w-e217e577e5, 2026-10-07).
404
+ // A new user watched agents work and could not tell what was powering
405
+ // them, because the app finds whichever sign-in is already on the Mac and
406
+ // spends it in silence.
407
+ //
408
+ // It rides the snapshot rather than the settings model because the SIDEBAR
409
+ // reads it, which is on screen the whole time, and the settings model is
410
+ // only fetched while that screen is open. It costs no file read per tick:
411
+ // `main/runs-on.mjs` caches on the login files' own modification times, so
412
+ // this re-parses only after a sign-in or a profile refetch.
413
+ runsOn: supervisor.runsOnAccount(engines.workspace),
364
414
  // Whether this process is still the app on disk. It rides the snapshot
365
415
  // rather than boot-info because the staleness DEVELOPS while the app
366
416
  // runs: the case that cost her three times was a process left up for days
@@ -414,23 +464,22 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
414
464
  };
415
465
  });
416
466
 
417
- // HER WORDS, INTO A LIVE AGENT OF HERS. The only thing in the app that writes
467
+ // THE USER'S WORDS, INTO ONE OF THEIR LIVE AGENTS. The only thing in the app that writes
418
468
  // anywhere outside its own store, and it writes into a socket rather than to
419
469
  // disk. The reader refuses a send to an agent frozen on a permission box, and
420
470
  // reports back whether the agent actually moved rather than assuming it did.
421
471
  ipcMain.handle('zero:agent-reply', async (_e, { pid, text }) => {
422
472
  try {
423
473
  const out = await agents.reply(Number(pid), text);
424
- // AND THE ROW REMEMBERS THAT SHE SPOKE. Only on a delivered message: a
474
+ // AND THE ROW REMEMBERS THAT THE USER SPOKE. Only on a delivered message: a
425
475
  // mark written for a send that failed would move the row into In progress
426
476
  // over words that never arrived, which is the failure this codebase cares
427
- // about most.It does now, until the session stops. AND WHAT SHE TYPED,
428
- // not only that she typed. Claude Code marks a message arriving over its
429
- // socket as machine chatter (`isMeta`), so the transcript reader drops it
430
- // and her own sentence never reached the card: she sent session-67 two
431
- // messages on 2026-08-17 and the row went on saying only what she had
432
- // started it with four days before. This is the only process that knows
433
- // she said it, so this is where it is written.
477
+ // about most. It does now, until the session stops. AND WHAT THE USER
478
+ // TYPED, not only that they typed. Claude Code marks a message arriving over
479
+ // its socket as machine chatter (`isMeta`), so the transcript reader drops
480
+ // it and the user's own sentence never reached the card: the row went on
481
+ // saying only what the session was started with days before. This is the
482
+ // only process that knows the user said it, so this is where it is written.
434
483
  if (out?.ok && out.key) agentSchedule.replied(out.key, out.at ?? Date.now(), Date.now(), String(text ?? ''));
435
484
  push();
436
485
  return out;
@@ -491,8 +540,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
491
540
  ipcMain.handle('zero:compact', (_e, { product, id }) => supervisor.compactItem(product, id));
492
541
  ipcMain.handle('zero:compaction-status', (_e, { product, id }) => supervisor.compactionStatus(product, id));
493
542
 
494
- ipcMain.handle('zero:answer', async (_e, { product, id, answer, status, priority, permissionMode, model, effort }) => {
543
+ // Send now: cut the running agent's current step so her waiting message is
544
+ // answered at once (supervisor.sendNow).
545
+ ipcMain.handle('zero:send-now', (_e, { product, id }) => supervisor.sendNow(product, id));
546
+
547
+ ipcMain.handle('zero:answer', async (_e, { product, id, answer, status, priority, permissionMode, model, effort, now, inReplyTo }) => {
495
548
  if (isAgentRow(id)) return { ok: false };
549
+ // A REPLY IN A THREAD (w-920461cbe6) only means something in a conversation
550
+ // between people, which is the only place a thread is drawn and the only
551
+ // record the mark crosses to a teammate in (shared/team-rules.mjs). Anywhere
552
+ // else it is dropped and the words go in as an ordinary reply.
553
+ const thread = typeof inReplyTo === 'string' && inReplyTo && productOf(product)?.team?.direct ? inReplyTo : undefined;
496
554
  // A CODEX CONVERSATION'S YES OR NO NEVER REACHES A WORKER. The row asking
497
555
  // whether to import one is answered by the app itself: yes makes it a row
498
556
  // she can read, no parks it in Closed under Not imported, and Import on a
@@ -524,11 +582,15 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
524
582
  // handing them over in one call and writing the model afterwards would be a
525
583
  // race the run usually wins, so she would pick a model and watch the old one
526
584
  // take the task. `answerItem` writes both, and it writes the model first.
527
- const out = await submitReply(supervisor,{product,id,answer,status,permissionMode},()=>store.answerItem(product, id, { answer, status, priority, permissionMode, model, effort }));
528
- // THAT she replied and THAT something finished. Never a word of either:
585
+ const out = await submitReply(supervisor,{product,id,answer,status,permissionMode,now:!!now},()=>store.answerItem(product, id, { answer, status, priority, permissionMode, model, effort, inReplyTo: thread }));
586
+ // A REPLY ON A ROW A TEAMMATE GAVE YOU HANDS IT BACK TO THEM, so the
587
+ // conversation moves to their inbox instead of sitting in both
588
+ // (shared/team-rules.mjs handedOnByReply). Archiving hands nothing on.
589
+ if (team && status !== 'done' && typeof answer === 'string' && answer.trim()) handOnByReply(product, id);
590
+ // THAT the user replied and THAT something finished. Never a word of either:
529
591
  // in Agentbox the answer IS the prompt (privacy page, section 4).
530
- if (typeof answer === 'string' && answer.trim()) analytics.track('reply_sent');
531
- if (status === 'done') analytics.track('task_finished');
592
+ if (typeof answer === 'string' && answer.trim()) analytics.track('reply_sent', { practice: isPractice(product) });
593
+ if (status === 'done') analytics.track('task_finished', { practice: isPractice(product) });
532
594
  // HER REPLY IS THE OTHER HALF OF THE SAME WAIT, and it is the one she pays
533
595
  // on every round trip rather than once: measured 2026-08-24, median 9.1
534
596
  // seconds between pressing the key and the continuation being spawned.
@@ -555,13 +617,141 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
555
617
  return supervisor.status();
556
618
  });
557
619
 
558
- ipcMain.handle('zero:compose', (_e, { product, title, body, kind, priority, runAt, labels, engine, model, effort }) => {
620
+ // WHO IS SIGNED IN, and the project a row is in, for the team's rules.
621
+ const teamMe = () => team?.state()?.me?.id ?? null;
622
+ const productOf = (slug) => store.listProducts().find((p) => p.slug === slug) ?? null;
623
+
624
+ function handOnByReply(product, id) {
625
+ try {
626
+ const row = store.readItem(product, id);
627
+ const next = row && handedOnByReply(row, productOf(product), teamMe());
628
+ if (next) store.teamPatch(product, id, { assignee: next });
629
+ } catch (err) {
630
+ console.warn(`team: could not hand ${id} on: ${err.message}`);
631
+ }
632
+ }
633
+
634
+ // FEEDBACK, from the card the sidebar's Feedback row opens (w-1b574413db).
635
+ // main/feedback.mjs decides where it goes and whether it may; the version
636
+ // and the macOS release ride along so a report can be matched to a build.
637
+ ipcMain.handle('zero:send-feedback', async (_e, { text, files } = {}) => sendFeedback({
638
+ url: feedbackUrl(config),
639
+ payload: feedbackPayload({ text, files, app: { version: app.getVersion(), os: `${process.platform} ${os.release()}` } }),
640
+ }));
641
+
642
+ // THE TEAM'S DOORS. Each answers { ok, team } or { ok: false, error } with
643
+ // the error in words, so the window can say what went wrong where it
644
+ // happened instead of a row silently not appearing.
645
+ const teamCall = (fn) => async (_e, arg) => {
646
+ if (!team) return { ok: false, error: 'This build has no team cloud set up.' };
647
+ try {
648
+ await fn(arg ?? {});
649
+ push();
650
+ return { ok: true, team: team.state() };
651
+ } catch (err) {
652
+ return { ok: false, error: String(err?.message ?? err), team: team.state() };
653
+ }
654
+ };
655
+ // Signing in finishes in the browser, so the app comes back to the front
656
+ // when it lands rather than leaving the person to find it.
657
+ ipcMain.handle('zero:team-sign-in', teamCall(async () => {
658
+ await team.signIn();
659
+ try { if (window?.isMinimized?.()) window.restore(); window?.show?.(); app?.focus?.({ steal: true }); } catch { /* no window to raise */ }
660
+ }));
661
+ ipcMain.handle('zero:team-sign-in-cancel', teamCall(() => team.cancelSignIn()));
662
+ ipcMain.handle('zero:team-sign-in-reopen', teamCall(() => team.reopenSignIn()));
663
+ ipcMain.handle('zero:team-sign-out', teamCall(() => team.signOut()));
664
+ ipcMain.handle('zero:team-sign-in-email', teamCall(({ email, password }) => team.signInWithEmail(email, password)));
665
+ // Its own door, because it has one more thing to say: whether the account
666
+ // waits on the link in its confirmation email.
667
+ ipcMain.handle('zero:team-sign-up', async (_e, { email, password, name } = {}) => {
668
+ if (!team) return { ok: false, error: 'This build has no team cloud set up.' };
669
+ try {
670
+ const out = await team.signUp(email, password, name);
671
+ push();
672
+ return { ok: true, team: team.state(), confirm: !!out?.confirm };
673
+ } catch (err) {
674
+ return { ok: false, error: String(err?.message ?? err), team: team.state() };
675
+ }
676
+ });
677
+ ipcMain.handle('zero:team-create', teamCall(({ name }) => team.createTeam(name)));
678
+ // An invite answered yes. Nothing else joins a team.
679
+ ipcMain.handle('zero:team-accept-invite', teamCall(({ teamId }) => team.acceptInvite(teamId)));
680
+ ipcMain.handle('zero:team-invite', teamCall(({ email }) => team.invite(email)));
681
+ ipcMain.handle('zero:team-rename', teamCall(({ name }) => team.renameTeam(name)));
682
+ ipcMain.handle('zero:team-remove-member', teamCall(({ personId }) => team.removeMember(personId)));
683
+ ipcMain.handle('zero:team-leave', teamCall(() => team.leaveTeam()));
684
+ ipcMain.handle('zero:team-cancel-invite', teamCall(({ email }) => team.cancelInvite(email)));
685
+ // A line you write about yourself, so a day in meetings is not read off
686
+ // the Team page as idleness.
687
+ ipcMain.handle('zero:team-status', teamCall(({ text, hold, until }) => team.setStatus({ text, hold, until })));
688
+ ipcMain.handle('zero:team-share', teamCall(({ product, visibility, people }) => team.share(product, { visibility, people })));
689
+ ipcMain.handle('zero:team-sync', teamCall(() => team.syncNow()));
690
+ // A MESSAGE TO A PERSON (approved 2026-10-01: people get messages, never
691
+ // tasks). main/team/index.mjs keeps it between the two of them.
692
+ ipcMain.handle('zero:team-message', teamCall(({ to, body, priority }) => team.message(to, body, priority)));
693
+ // AN EDIT TO A THREAD, MADE IN PLACE from its summary: the summary's lines,
694
+ // who sees it, its priority and what it is linked to. Works with or without a
695
+ // team, since every thread has a summary.
696
+ ipcMain.handle('zero:thread-edit', (_e, { product, id, patch }) => {
697
+ try {
698
+ store.threadEdit(product, id, patch);
699
+ push();
700
+ return { ok: true };
701
+ } catch (err) {
702
+ return { ok: false, error: String(err?.message ?? err) };
703
+ }
704
+ });
705
+ // A TASK A TEAMMATE GAVE YOU, ROUTED WITH ONE KEY. To an agent: it runs on
706
+ // your Mac (you become its runner). Keep it: it stays yours and the routing
707
+ // choices go away. Hand it back: it returns to whoever gave it to you.
708
+ ipcMain.handle('zero:team-route', teamCall(({ product, id, route }) => {
709
+ const me = teamMe();
710
+ if (!me) throw new Error('sign in first');
711
+ const row = store.readItem(product, id);
712
+ if (!row) throw new Error('that task is gone');
713
+ if (row.assignee !== me) throw new Error('this task is not yours to route');
714
+ if (route === 'agent') store.teamPatch(product, id, { assignee: 'agent', runner: me });
715
+ else if (route === 'me') store.teamPatch(product, id, { assignee: me });
716
+ else if (route === 'back') {
717
+ const to = (row.people ?? []).find((p) => p !== me) || row.createdBy;
718
+ if (!to || to === me) throw new Error('there is nobody to hand it back to');
719
+ store.teamPatch(product, id, { assignee: to });
720
+ } else throw new Error(`unknown route ${route}`);
721
+ supervisor.wake();
722
+ }));
723
+
724
+ // PUT A REACTION ON ONE MESSAGE, OR TAKE YOURS BACK (w-560647d4db). Only in a
725
+ // conversation between people, because a chip is only ever drawn in one: the
726
+ // sync carries a reaction across in a message record and nowhere else
727
+ // (shared/team-rules.mjs), so one written anywhere else would sit on this Mac
728
+ // alone and look to its author like it had reached somebody.
729
+ ipcMain.handle('zero:team-react', teamCall(async ({ product, id, on, emoji, off }) => {
730
+ const me = teamMe();
731
+ if (!me) throw new Error('sign in first');
732
+ const row = store.readItem(product, id);
733
+ if (!row) throw new Error('that conversation is gone');
734
+ const here = store.listProducts().find((p) => p.slug === product);
735
+ if (!here?.team?.direct) throw new Error('reactions are for a conversation with a teammate');
736
+ store.react(product, id, { on, emoji, off });
737
+ await team.syncNow();
738
+ }));
739
+
740
+ ipcMain.handle('zero:compose', (_e, { product, title, body, kind, priority, runAt, start, labels, engine, model, effort, assignee, due, visibility, visibleTo }) => {
559
741
  const out = store.composeItem(product, {
560
742
  // HOW HARD IT THINKS rides through unjudged: the store keeps any word
561
743
  // shaped like a level, and the spawn is the gate that knows each
562
744
  // engine's real list (`Supervisor#spawnPlan` for Claude Code,
563
745
  // `codexEffortRefusal` for Codex).
564
- title, body, kind, priority, runAt, labels, effort,
746
+ title, body, kind, priority, runAt, start, labels, effort,
747
+ // WHO DOES IT, when it is a person (the team version): the teammate it
748
+ // goes to and the day it is due, and the two of you as the conversation,
749
+ // so a reply can hand it back. An agent's row carries none of these.
750
+ assignee, due, visibility,
751
+ // AND WHO MAY SEE IT, when she chose people rather than the team
752
+ // (w-41ff964775). The store keeps the list only beside 'people'.
753
+ visibleTo,
754
+ people: assignee && teamMe() ? [teamMe(), assignee] : undefined,
565
755
  // WHICH CODING AGENT SHE CHOSE, AND ONLY IF SHE COULD HAVE. `engineOffered`
566
756
  // is the supervisor's own test and the door makes no judgement of its own:
567
757
  // it answers null on any Mac where the picker is not drawn, so a renderer
@@ -576,12 +766,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
576
766
  // fields cannot be corrected separately.
577
767
  model: supervisor.modelOffered(engine, model),
578
768
  });
769
+ // A real thread ends the walk's hold on agents (`Supervisor#sentByThem`).
770
+ supervisor.sentByThem(labels);
771
+ // THAT a task was written, and whether in practice. Never its words.
772
+ analytics.track('task_written', { practice: isPractice(product) });
579
773
  // AND LOOK AT IT NOW. Composing used to write the line and stop, so the
580
774
  // task sat in the store until the next fifteen second tick: measured
581
775
  // 2026-08-24, median 7.0 seconds and up to 15.0 before the spawn was even
582
776
  // attempted. A row scheduled for later is not woken for, because it is not
583
777
  // due; the timer is what serves those.
584
- if (!runAt || runAt <= Date.now()) supervisor.wake();
778
+ // Nothing to wake for a thread added to Later: it starts when she says.
779
+ if (start !== 'later' && (!runAt || runAt <= Date.now())) supervisor.wake();
585
780
  return out;
586
781
  });
587
782
 
@@ -591,8 +786,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
591
786
  // The whole of the work is reading the readme in the folder chosen two
592
787
  // screens earlier, so this returns in milliseconds. The two second pause
593
788
  // before it is called is in the renderer, where the row is being watched.
594
- ipcMain.handle('zero:first-run-answer', (_e, { product, id }) => {
595
- return store.finishFirstRunTask(product, id);
789
+ ipcMain.handle('zero:first-run-answer', (_e, { product, id, round = 0 }) => {
790
+ return store.finishFirstRunTask(product, id, round);
596
791
  });
597
792
 
598
793
  // THE PRACTICE PROJECT.Made when the hand-off screen is pressed, filled with
@@ -606,7 +801,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
606
801
  // somebody mid-walk. They land in an empty practice inbox and the beat that
607
802
  // clears them is skipped, exactly as beat thirteen already behaves.
608
803
  try { rows = store.stagePracticeRows(made.slug); } catch { rows = { ids: [] }; }
609
- return { ...made, examples: rows.ids };
804
+ // AND THE REST OF THE PRACTICE TEAM'S WORK, so the tour and the board have
805
+ // something in every column (w-58c8f466e7). Its own try: a store that will
806
+ // not take these still leaves the walk its four rows.
807
+ let backdrop = { ids: [] };
808
+ try { backdrop = store.stagePracticeBackdrop(made.slug); } catch { backdrop = { ids: [] }; }
809
+ return { ...made, examples: rows.ids, backdrop: backdrop.ids };
610
810
  });
611
811
 
612
812
  // AND TAKING IT BACK OFF THE SCREEN. `sweepPractice` rather than the one
@@ -640,6 +840,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
640
840
  ipcMain.handle('zero:open-fresh-user', (_e, payload) => {
641
841
  return openFreshUser({
642
842
  withAgents: payload?.withAgents !== false,
843
+ withTools: payload?.withTools !== false,
643
844
  packaged: app.isPackaged,
644
845
  });
645
846
  });
@@ -661,6 +862,14 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
661
862
  return store.scheduleItem(product, id, runAt);
662
863
  });
663
864
 
865
+ // Snooze changes when a person sees the thread, never what an agent does.
866
+ ipcMain.handle('zero:snooze', (_e, { product, id, snoozedUntil }) => {
867
+ if (isAgentRow(id)) return { ok: false };
868
+ const item = store.snoozeItem(product, id, snoozedUntil);
869
+ push();
870
+ return item;
871
+ });
872
+
664
873
  // said about an agent instead of about a task. The row it moves stands for
665
874
  // somebody's terminal, so this writes to no ledger, sends no message and
666
875
  // stops no process: it records the moment her inbox should raise the agent
@@ -869,10 +1078,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
869
1078
  // feel instant. The card read them a moment before pressing, so the last read
870
1079
  // is the right answer and it is ours rather than the renderer's.
871
1080
  let lastThreads = [];
872
- // HER CODEX CONVERSATIONS ARE ON THE SAME CARD. They carry `source:
1081
+ // THE USER'S CODEX CONVERSATIONS ARE ON THE SAME CARD. They carry `source:
873
1082
  // 'codex'`, land under the project whose folder they ran in exactly as the
874
- // Claude Code ones do, and the ones she said not now to are here too, which
875
- // is the way back she asked for.
1083
+ // Claude Code ones do, and the ones the user said not now to are here too, so
1084
+ // there is a way back to them.
876
1085
  const readAllThreads = () => {
877
1086
  let claude = [];
878
1087
  let codex = [];
@@ -883,7 +1092,11 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
883
1092
  ipcMain.handle('zero:agent-threads', () => {
884
1093
  const threads = readAllThreads();
885
1094
  lastThreads = threads;
886
- return { threads };
1095
+ // Each one already in the inbox says so, and the card leaves it out
1096
+ // (w-db6f5e331e). The walk's folder suggestions still read all of them.
1097
+ let items = [];
1098
+ try { items = store.listItems(); } catch { items = []; }
1099
+ return { threads: markImported(threads, items) };
887
1100
  });
888
1101
 
889
1102
  // So this is `zero:import-agents` with the agent taken out of it. No ticks are
@@ -924,7 +1137,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
924
1137
  // `projectAgents` and stopped. Three occurrences of that key in the whole
925
1138
  // shipped app, all of them this file and settings.mjs, and none in the
926
1139
  // renderer: the list was read back only so the card could remember its own
927
- // ticks the next time somebody walked the setup. Measured on the build David
1140
+ // ticks the next time somebody walked the setup. Measured on the build a tester
928
1141
  // used, the store was byte for byte identical before and after pressing it.
929
1142
  //
930
1143
  // The names are still saved, because the card still opens on what was taken
@@ -976,6 +1189,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
976
1189
  return store.answerItem(product, id, { status: 'blocked' });
977
1190
  });
978
1191
 
1192
+ // Run now, from the three-dot menu on a waiting task (supervisor.runNow).
1193
+ ipcMain.handle('zero:run-now', (_e, { product, id }) => {
1194
+ if (isAgentRow(id)) return { ok: false, reason: 'missing' };
1195
+ return supervisor.runNow(product, id);
1196
+ });
1197
+
979
1198
  // Send a stopped/blocked item back to the queue; the fresh worker's brief
980
1199
  // carries whatever the founder wrote on the item meanwhile.
981
1200
  ipcMain.handle('zero:reopen', (_e, { product, id }) => {
@@ -1096,6 +1315,45 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1096
1315
  }
1097
1316
  });
1098
1317
 
1318
+ // SETTING UP THEIR PLAN FOR THEM (w-9f6975906c). The walk asks which plan
1319
+ // they pay for and this installs the tool and starts its own sign-in. The
1320
+ // finder is the recheck, so what gets installed lands on the config and the
1321
+ // rest of the app runs on it without a restart.
1322
+ const engineSetup = createEngineSetup({
1323
+ find: (engine) => {
1324
+ if (engine === 'claude') { const s = recheckClaude(config); return { found: s.claudeFound, path: s.claudeBin }; }
1325
+ const s = recheckCodex(config);
1326
+ return { found: s.found, path: s.bin };
1327
+ },
1328
+ // WHICH ONE IS SIGNED IN, kept on the config for the run that reads it
1329
+ // (`Supervisor#_enginesFound`) and never saved: it changes outside the app.
1330
+ onSignIn: (engine, signedIn) => {
1331
+ config[`${engine}SignedIn`] = signedIn;
1332
+ },
1333
+ });
1334
+ // ASKED ONCE AT LAUNCH, and only on a Mac with both, which is the only Mac
1335
+ // where the answer moves a task (w-d5d632e503): a tester who pays for
1336
+ // ChatGPT had the Claude app's copy of Claude Code, never signed in, so
1337
+ // every task went to Claude Code and Codex was never offered.
1338
+ if (config.claudeFound && config.codexBin) {
1339
+ void engineSetup.signedInNow('claude', config.claudeBin);
1340
+ void engineSetup.signedInNow('codex', config.codexBin);
1341
+ }
1342
+ // A sign-in left waiting on the browser is not left running after the app.
1343
+ app.on('will-quit', () => { engineSetup.cancel('claude'); engineSetup.cancel('codex'); });
1344
+ ipcMain.handle('zero:engine-setup', async (_e, { action, engine } = {}) => {
1345
+ if (engine !== 'claude' && engine !== 'codex') return { ok: false, error: 'Unknown coding agent.' };
1346
+ try {
1347
+ if (action === 'ready') return { ok: true, ...(await engineSetup.readiness(engine)) };
1348
+ if (action === 'start') return { ok: true, ...engineSetup.start(engine) };
1349
+ if (action === 'again') return { ok: true, ...engineSetup.signInAgain(engine) };
1350
+ if (action === 'cancel') return { ok: true, ...engineSetup.cancel(engine) };
1351
+ return { ok: true, ...engineSetup.status(engine) };
1352
+ } catch (err) {
1353
+ return { ok: false, error: String(err.message) };
1354
+ }
1355
+ });
1356
+
1099
1357
  /* * ADD A CODEX ACCOUNT: make the folder, register the login, hand back the one line the
1100
1358
  terminal has to run.
1101
1359
  */
@@ -1166,7 +1424,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1166
1424
  const dir = productDirOf(product);
1167
1425
  if (!dir) return { ok: false, error: 'That project could not be found.' };
1168
1426
  const saved = setProjectName(dir, name);
1169
- // PUSH, because the name is on far more than the screen she typed it on:
1427
+ // PUSH, because the name is on far more than the screen it was typed on:
1170
1428
  // the rail, every row's byline, the composer's picker and ⌘K all print it,
1171
1429
  // and a rename that only redrew Settings would leave the rest of the app
1172
1430
  // calling it the old thing until something else happened to refresh.
@@ -1177,6 +1435,32 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1177
1435
  }
1178
1436
  });
1179
1437
 
1438
+ // ARCHIVE A PROJECT, OR BRING ONE BACK (main/store.mjs setProductArchived).
1439
+ // Its files stay on disk; it leaves every list the app draws. Pushed for the
1440
+ // same reason a rename is: the rail, the inbox and the picker all list it.
1441
+ ipcMain.handle('zero:project-archive', (_e, { product, archived } = {}) => {
1442
+ try {
1443
+ store.setProductArchived(product, archived === true);
1444
+ push();
1445
+ return { ok: true, ...readSettings({ config, supervisor, store }) };
1446
+ } catch (err) {
1447
+ return { ok: false, error: String(err.message) };
1448
+ }
1449
+ });
1450
+
1451
+ // WHO SEES A PROJECT'S THREADS (main/store.mjs setProductSeenBy,
1452
+ // w-b989839656). Pushed, because the inbox's locks and the New thread
1453
+ // card's starting chip both read it off the snapshot's projects.
1454
+ ipcMain.handle('zero:project-seen-by', (_e, { product, who, people } = {}) => {
1455
+ try {
1456
+ store.setProductSeenBy(product, { who, people });
1457
+ push();
1458
+ return { ok: true };
1459
+ } catch (err) {
1460
+ return { ok: false, error: String(err.message) };
1461
+ }
1462
+ });
1463
+
1180
1464
  // Opens the picker AND does the copy, in one round trip. Two handlers would
1181
1465
  // mean the renderer briefly holds a path into her Downloads folder and hands
1182
1466
  // it back to be trusted; this way the only path that crosses the bridge is one
@@ -1248,10 +1532,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1248
1532
  for (const file of files ?? []) {
1249
1533
  const safe = String(file.name ?? 'file').replace(/[^\w.-]+/g, '-').slice(0, 80);
1250
1534
  // A RANDOM SUFFIX, THE SAME ONE THE STAGING PATH ALREADY USES. The name
1251
- // was the millisecond plus her filename, so two files saved in the same
1535
+ // was the millisecond plus the filename, so two files saved in the same
1252
1536
  // millisecond with the same name overwrote each other and the message
1253
- // ended up with two links to one picture. She sends screenshots in pairs,
1254
- // which is exactly the shape that collides. It has never been observed
1537
+ // ended up with two links to one picture. Screenshots are often sent in
1538
+ // pairs, which is exactly the shape that collides. It has never been observed
1255
1539
  // happening; it is one line to make impossible.
1256
1540
  const rel = `attachments/${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`;
1257
1541
  const abs = path.join(dir, rel);
@@ -1265,13 +1549,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1265
1549
 
1266
1550
  // A PASTED SCREENSHOT GOES TO DISK THE MOMENT IT IS PASTED.
1267
1551
  //
1268
- // She was right and the constraint was ours. Pasted bytes used to ride in the
1269
- // draft as base64, and a draft lives in localStorage, whose quota MEASURED on
1270
- // this machine is 5,241,856 characters for the WHOLE app. Seven of the
1271
- // ninety-one screenshots she has actually pasted are over that; the largest
1272
- // is 6.08 MB.
1552
+ // The constraint was ours. Pasted bytes used to ride in the draft as base64,
1553
+ // and a draft lives in localStorage, whose quota MEASURED on this machine is
1554
+ // 5,241,856 characters for the WHOLE app. Ordinary full-screen screenshots
1555
+ // are regularly over that; 6 MB is not unusual.
1273
1556
  //
1274
- // Raising the number cannot reach her bar: even the whole quota is under 4 MB
1557
+ // Raising the number cannot reach that bar: even the whole quota is under 4 MB
1275
1558
  // of image, and Claude's own limit is 10 MB base64 per image (about 7.5 MB of
1276
1559
  // file) at up to 8000x8000. localStorage is simply the wrong place for image
1277
1560
  // bytes. So they go here instead, and the draft keeps a path — which the draft
@@ -1408,6 +1691,16 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1408
1691
  return err ? { ok: false, error: err } : { ok: true, opened: found.path };
1409
1692
  });
1410
1693
 
1694
+ // THE PICTURES IN A FOLDER A MESSAGE NAMES, so the folder line draws them
1695
+ // (FolderPreview in Focus.tsx). Only pictures, only inside this product.
1696
+ ipcMain.handle('zero:folder-pictures', (_e, { product, src } = {}) => folderPictures({
1697
+ src,
1698
+ product,
1699
+ products: store.listProducts(),
1700
+ accountRoot: config.accountRoot,
1701
+ storeRoot: config.storeRoot,
1702
+ }));
1703
+
1411
1704
  // THE DOCUMENT PANE READS AND WRITES THE FILE ITSELF. A path goes through
1412
1705
  // the very same finder the links and the chips use, so the pane opens
1413
1706
  // exactly the file a click would have opened, and then main hands back the