agentbox-app 0.1.6 → 0.1.11

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 (244) hide show
  1. package/README.md +209 -55
  2. package/bin/agentbox.mjs +6 -0
  3. package/briefs/message-rules.md +13 -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 +9 -10
  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 +114 -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/artifact-path.mjs +11 -0
  16. package/main/claude-bin.mjs +31 -5
  17. package/main/claude-input.mjs +45 -3
  18. package/main/claude-plan.mjs +11 -11
  19. package/main/code-change.mjs +3 -3
  20. package/main/codex-account.mjs +1 -1
  21. package/main/codex-bin.mjs +17 -0
  22. package/main/codex-memory-gate.mjs +260 -0
  23. package/main/codex-models.mjs +12 -7
  24. package/main/codex-threads.mjs +46 -23
  25. package/main/codex-usage-file.mjs +7 -7
  26. package/main/codex-usage.mjs +4 -4
  27. package/main/codex-watch.mjs +12 -12
  28. package/main/config.mjs +68 -8
  29. package/main/dev-window.mjs +9 -9
  30. package/main/doc-scheme.mjs +2 -0
  31. package/main/engine-setup.mjs +193 -0
  32. package/main/feedback.mjs +55 -0
  33. package/main/folder-pictures.mjs +48 -0
  34. package/main/frame-scrollbars.mjs +90 -0
  35. package/main/fresh-user.mjs +6 -3
  36. package/main/git-change-offthread.mjs +70 -0
  37. package/main/git-change-thread.mjs +11 -0
  38. package/main/git-change.mjs +1 -1
  39. package/main/img-scheme.mjs +70 -1
  40. package/main/instruction-settings.mjs +92 -54
  41. package/main/ipc.mjs +316 -42
  42. package/main/leftovers.mjs +518 -0
  43. package/main/live-replies.mjs +43 -5
  44. package/main/machine.mjs +47 -0
  45. package/main/main.mjs +119 -15
  46. package/main/memory-gate-history.mjs +254 -0
  47. package/main/memory-gate-server.mjs +461 -0
  48. package/main/memory-gate.mjs +206 -0
  49. package/main/message-priority.mjs +87 -0
  50. package/main/rail-note.mjs +2 -2
  51. package/main/refresh-agent-models.mjs +20 -0
  52. package/main/repeats.mjs +4 -4
  53. package/main/row-label.mjs +114 -51
  54. package/main/serve.mjs +67 -5
  55. package/main/settings.mjs +223 -26
  56. package/main/ship-queue.mjs +279 -0
  57. package/main/sign-in-files.mjs +39 -0
  58. package/main/source-updater.mjs +298 -0
  59. package/main/store/home.mjs +47 -2
  60. package/main/store/work-items.mjs +150 -6
  61. package/main/store.mjs +235 -9
  62. package/main/supervisor.mjs +1444 -406
  63. package/main/task-commands.mjs +8 -10
  64. package/main/task-folders.mjs +74 -13
  65. package/main/task-remote-control.mjs +3 -3
  66. package/main/task-terminals.mjs +2 -1
  67. package/main/team/index.mjs +440 -0
  68. package/main/team/memory-cloud.mjs +247 -0
  69. package/main/team/projects.mjs +162 -0
  70. package/main/team/session.mjs +225 -0
  71. package/main/team/sign-in.mjs +106 -0
  72. package/main/team/supabase-backend.mjs +231 -0
  73. package/main/team/sync.mjs +163 -0
  74. package/main/updater.mjs +15 -5
  75. package/main/write-before-reload.mjs +50 -0
  76. package/mcp/core/page.mjs +9 -11
  77. package/mcp/core/work.mjs +91 -25
  78. package/mcp/tools.mjs +12 -6
  79. package/package.json +21 -9
  80. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  81. package/renderer/dist/assets/index-B-2Az952.css +32 -0
  82. package/renderer/dist/assets/index-D6qncgai.js +1168 -0
  83. package/renderer/dist/index.html +2 -2
  84. package/scripts/memory-gate-hook.sh +59 -0
  85. package/shared/agent-import.mjs +38 -15
  86. package/shared/agents.d.mts +26 -8
  87. package/shared/agents.mjs +100 -108
  88. package/shared/analytics-events.mjs +19 -2
  89. package/shared/answers.mjs +12 -0
  90. package/shared/bridge-map.d.mts +12 -0
  91. package/shared/bridge-map.mjs +37 -1
  92. package/shared/claude-commands.generated.mjs +2 -2
  93. package/shared/claude-models.generated.mjs +4 -4
  94. package/shared/claude-usage.mjs +6 -6
  95. package/shared/codex-import.mjs +7 -7
  96. package/shared/codex-modes.d.mts +50 -0
  97. package/shared/codex-usage.mjs +7 -8
  98. package/shared/demo-world.mjs +5 -5
  99. package/shared/engines.mjs +27 -10
  100. package/shared/feedback.d.mts +14 -0
  101. package/shared/feedback.mjs +44 -0
  102. package/shared/first-run-examples.mjs +4 -4
  103. package/shared/first-run-practice.d.mts +16 -0
  104. package/shared/first-run-practice.mjs +176 -70
  105. package/shared/fresh-user-home.mjs +22 -3
  106. package/shared/instruction-defaults.json +2 -2
  107. package/shared/product-name.mjs +4 -2
  108. package/shared/project-folder-check.mjs +6 -0
  109. package/shared/rank.d.mts +1 -0
  110. package/shared/rank.mjs +22 -0
  111. package/shared/referenced-files.mjs +1 -1
  112. package/shared/schemes.d.mts +11 -0
  113. package/shared/schemes.mjs +1 -1
  114. package/shared/shortcuts.d.mts +41 -0
  115. package/shared/shortcuts.mjs +236 -0
  116. package/shared/spawn-trouble.d.mts +2 -0
  117. package/shared/spawn-trouble.mjs +39 -18
  118. package/shared/team-rules.d.mts +17 -0
  119. package/shared/team-rules.mjs +230 -0
  120. package/shared/team-status.d.mts +11 -0
  121. package/shared/team-status.mjs +90 -0
  122. package/shared/terminal-links.d.mts +2 -0
  123. package/shared/terminal-links.mjs +55 -0
  124. package/shared/terminal-state.d.mts +1 -0
  125. package/shared/terminal-state.mjs +16 -0
  126. package/shared/thread-cards.d.mts +19 -0
  127. package/shared/thread-cards.mjs +254 -0
  128. package/shared/usage.d.mts +1 -1
  129. package/shared/usage.mjs +6 -7
  130. package/shared/work-items.mjs +255 -27
  131. package/shared/work-lines.mjs +393 -16
  132. package/worker-permissions.json +1 -1
  133. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  134. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  135. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  136. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  137. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  138. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  139. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  140. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  141. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  142. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  143. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  144. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  145. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  146. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  147. package/renderer/dist/assets/index-D9hCAVPl.js +0 -684
  148. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  149. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  150. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  151. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  152. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  153. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  154. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  155. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  156. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  157. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  158. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  159. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  160. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  161. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  162. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  163. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  164. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  165. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  166. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  167. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  168. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  169. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  170. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  171. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  172. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  173. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  174. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  175. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  176. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  177. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  178. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  179. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  180. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  181. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  182. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  183. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  184. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  185. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  186. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  187. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  188. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  189. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  190. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  191. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  192. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  193. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  194. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  195. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  196. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  197. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  198. package/renderer/dist/emulator/data/compression/README.md +0 -11
  199. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  200. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  201. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  202. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  203. package/renderer/dist/emulator/data/cores/README.md +0 -22
  204. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  205. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  206. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  207. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  208. package/renderer/dist/emulator/data/cores/package.json +0 -65
  209. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  210. package/renderer/dist/emulator/data/emulator.css +0 -1614
  211. package/renderer/dist/emulator/data/loader.js +0 -170
  212. package/renderer/dist/emulator/data/localization/README.md +0 -63
  213. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  214. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  215. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  216. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  217. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  218. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  219. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  220. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  221. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  222. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  223. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  224. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  225. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  226. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  227. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  228. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  229. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  230. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  231. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  232. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  233. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  234. package/renderer/dist/emulator/data/src/compression.js +0 -142
  235. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  236. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  237. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  238. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  239. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  240. package/renderer/dist/emulator/data/src/storage.js +0 -125
  241. package/renderer/dist/emulator/data/version.json +0 -5
  242. package/renderer/dist/emulator/host.js +0 -299
  243. package/renderer/dist/emulator/index.html +0 -28
  244. 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,
@@ -414,23 +453,22 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
414
453
  };
415
454
  });
416
455
 
417
- // HER WORDS, INTO A LIVE AGENT OF HERS. The only thing in the app that writes
456
+ // THE USER'S WORDS, INTO ONE OF THEIR LIVE AGENTS. The only thing in the app that writes
418
457
  // anywhere outside its own store, and it writes into a socket rather than to
419
458
  // disk. The reader refuses a send to an agent frozen on a permission box, and
420
459
  // reports back whether the agent actually moved rather than assuming it did.
421
460
  ipcMain.handle('zero:agent-reply', async (_e, { pid, text }) => {
422
461
  try {
423
462
  const out = await agents.reply(Number(pid), text);
424
- // AND THE ROW REMEMBERS THAT SHE SPOKE. Only on a delivered message: a
463
+ // AND THE ROW REMEMBERS THAT THE USER SPOKE. Only on a delivered message: a
425
464
  // mark written for a send that failed would move the row into In progress
426
465
  // 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.
466
+ // about most. It does now, until the session stops. AND WHAT THE USER
467
+ // TYPED, not only that they typed. Claude Code marks a message arriving over
468
+ // its socket as machine chatter (`isMeta`), so the transcript reader drops
469
+ // it and the user's own sentence never reached the card: the row went on
470
+ // saying only what the session was started with days before. This is the
471
+ // only process that knows the user said it, so this is where it is written.
434
472
  if (out?.ok && out.key) agentSchedule.replied(out.key, out.at ?? Date.now(), Date.now(), String(text ?? ''));
435
473
  push();
436
474
  return out;
@@ -491,8 +529,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
491
529
  ipcMain.handle('zero:compact', (_e, { product, id }) => supervisor.compactItem(product, id));
492
530
  ipcMain.handle('zero:compaction-status', (_e, { product, id }) => supervisor.compactionStatus(product, id));
493
531
 
494
- ipcMain.handle('zero:answer', async (_e, { product, id, answer, status, priority, permissionMode, model, effort }) => {
532
+ // Send now: cut the running agent's current step so her waiting message is
533
+ // answered at once (supervisor.sendNow).
534
+ ipcMain.handle('zero:send-now', (_e, { product, id }) => supervisor.sendNow(product, id));
535
+
536
+ ipcMain.handle('zero:answer', async (_e, { product, id, answer, status, priority, permissionMode, model, effort, now, inReplyTo }) => {
495
537
  if (isAgentRow(id)) return { ok: false };
538
+ // A REPLY IN A THREAD (w-920461cbe6) only means something in a conversation
539
+ // between people, which is the only place a thread is drawn and the only
540
+ // record the mark crosses to a teammate in (shared/team-rules.mjs). Anywhere
541
+ // else it is dropped and the words go in as an ordinary reply.
542
+ const thread = typeof inReplyTo === 'string' && inReplyTo && productOf(product)?.team?.direct ? inReplyTo : undefined;
496
543
  // A CODEX CONVERSATION'S YES OR NO NEVER REACHES A WORKER. The row asking
497
544
  // whether to import one is answered by the app itself: yes makes it a row
498
545
  // she can read, no parks it in Closed under Not imported, and Import on a
@@ -524,11 +571,15 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
524
571
  // handing them over in one call and writing the model afterwards would be a
525
572
  // race the run usually wins, so she would pick a model and watch the old one
526
573
  // 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:
574
+ const out = await submitReply(supervisor,{product,id,answer,status,permissionMode,now:!!now},()=>store.answerItem(product, id, { answer, status, priority, permissionMode, model, effort, inReplyTo: thread }));
575
+ // A REPLY ON A ROW A TEAMMATE GAVE YOU HANDS IT BACK TO THEM, so the
576
+ // conversation moves to their inbox instead of sitting in both
577
+ // (shared/team-rules.mjs handedOnByReply). Archiving hands nothing on.
578
+ if (team && status !== 'done' && typeof answer === 'string' && answer.trim()) handOnByReply(product, id);
579
+ // THAT the user replied and THAT something finished. Never a word of either:
529
580
  // 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');
581
+ if (typeof answer === 'string' && answer.trim()) analytics.track('reply_sent', { practice: isPractice(product) });
582
+ if (status === 'done') analytics.track('task_finished', { practice: isPractice(product) });
532
583
  // HER REPLY IS THE OTHER HALF OF THE SAME WAIT, and it is the one she pays
533
584
  // on every round trip rather than once: measured 2026-08-24, median 9.1
534
585
  // seconds between pressing the key and the continuation being spawned.
@@ -555,13 +606,141 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
555
606
  return supervisor.status();
556
607
  });
557
608
 
558
- ipcMain.handle('zero:compose', (_e, { product, title, body, kind, priority, runAt, labels, engine, model, effort }) => {
609
+ // WHO IS SIGNED IN, and the project a row is in, for the team's rules.
610
+ const teamMe = () => team?.state()?.me?.id ?? null;
611
+ const productOf = (slug) => store.listProducts().find((p) => p.slug === slug) ?? null;
612
+
613
+ function handOnByReply(product, id) {
614
+ try {
615
+ const row = store.readItem(product, id);
616
+ const next = row && handedOnByReply(row, productOf(product), teamMe());
617
+ if (next) store.teamPatch(product, id, { assignee: next });
618
+ } catch (err) {
619
+ console.warn(`team: could not hand ${id} on: ${err.message}`);
620
+ }
621
+ }
622
+
623
+ // FEEDBACK, from the card the sidebar's Feedback row opens (w-1b574413db).
624
+ // main/feedback.mjs decides where it goes and whether it may; the version
625
+ // and the macOS release ride along so a report can be matched to a build.
626
+ ipcMain.handle('zero:send-feedback', async (_e, { text, files } = {}) => sendFeedback({
627
+ url: feedbackUrl(config),
628
+ payload: feedbackPayload({ text, files, app: { version: app.getVersion(), os: `${process.platform} ${os.release()}` } }),
629
+ }));
630
+
631
+ // THE TEAM'S DOORS. Each answers { ok, team } or { ok: false, error } with
632
+ // the error in words, so the window can say what went wrong where it
633
+ // happened instead of a row silently not appearing.
634
+ const teamCall = (fn) => async (_e, arg) => {
635
+ if (!team) return { ok: false, error: 'This build has no team cloud set up.' };
636
+ try {
637
+ await fn(arg ?? {});
638
+ push();
639
+ return { ok: true, team: team.state() };
640
+ } catch (err) {
641
+ return { ok: false, error: String(err?.message ?? err), team: team.state() };
642
+ }
643
+ };
644
+ // Signing in finishes in the browser, so the app comes back to the front
645
+ // when it lands rather than leaving the person to find it.
646
+ ipcMain.handle('zero:team-sign-in', teamCall(async () => {
647
+ await team.signIn();
648
+ try { if (window?.isMinimized?.()) window.restore(); window?.show?.(); app?.focus?.({ steal: true }); } catch { /* no window to raise */ }
649
+ }));
650
+ ipcMain.handle('zero:team-sign-in-cancel', teamCall(() => team.cancelSignIn()));
651
+ ipcMain.handle('zero:team-sign-in-reopen', teamCall(() => team.reopenSignIn()));
652
+ ipcMain.handle('zero:team-sign-out', teamCall(() => team.signOut()));
653
+ ipcMain.handle('zero:team-sign-in-email', teamCall(({ email, password }) => team.signInWithEmail(email, password)));
654
+ // Its own door, because it has one more thing to say: whether the account
655
+ // waits on the link in its confirmation email.
656
+ ipcMain.handle('zero:team-sign-up', async (_e, { email, password, name } = {}) => {
657
+ if (!team) return { ok: false, error: 'This build has no team cloud set up.' };
658
+ try {
659
+ const out = await team.signUp(email, password, name);
660
+ push();
661
+ return { ok: true, team: team.state(), confirm: !!out?.confirm };
662
+ } catch (err) {
663
+ return { ok: false, error: String(err?.message ?? err), team: team.state() };
664
+ }
665
+ });
666
+ ipcMain.handle('zero:team-create', teamCall(({ name }) => team.createTeam(name)));
667
+ // An invite answered yes. Nothing else joins a team.
668
+ ipcMain.handle('zero:team-accept-invite', teamCall(({ teamId }) => team.acceptInvite(teamId)));
669
+ ipcMain.handle('zero:team-invite', teamCall(({ email }) => team.invite(email)));
670
+ ipcMain.handle('zero:team-rename', teamCall(({ name }) => team.renameTeam(name)));
671
+ ipcMain.handle('zero:team-remove-member', teamCall(({ personId }) => team.removeMember(personId)));
672
+ ipcMain.handle('zero:team-leave', teamCall(() => team.leaveTeam()));
673
+ ipcMain.handle('zero:team-cancel-invite', teamCall(({ email }) => team.cancelInvite(email)));
674
+ // A line you write about yourself, so a day in meetings is not read off
675
+ // the Team page as idleness.
676
+ ipcMain.handle('zero:team-status', teamCall(({ text, hold, until }) => team.setStatus({ text, hold, until })));
677
+ ipcMain.handle('zero:team-share', teamCall(({ product, visibility, people }) => team.share(product, { visibility, people })));
678
+ ipcMain.handle('zero:team-sync', teamCall(() => team.syncNow()));
679
+ // A MESSAGE TO A PERSON (approved 2026-10-01: people get messages, never
680
+ // tasks). main/team/index.mjs keeps it between the two of them.
681
+ ipcMain.handle('zero:team-message', teamCall(({ to, body, priority }) => team.message(to, body, priority)));
682
+ // AN EDIT TO A THREAD, MADE IN PLACE from its summary: the summary's lines,
683
+ // who sees it, its priority and what it is linked to. Works with or without a
684
+ // team, since every thread has a summary.
685
+ ipcMain.handle('zero:thread-edit', (_e, { product, id, patch }) => {
686
+ try {
687
+ store.threadEdit(product, id, patch);
688
+ push();
689
+ return { ok: true };
690
+ } catch (err) {
691
+ return { ok: false, error: String(err?.message ?? err) };
692
+ }
693
+ });
694
+ // A TASK A TEAMMATE GAVE YOU, ROUTED WITH ONE KEY. To an agent: it runs on
695
+ // your Mac (you become its runner). Keep it: it stays yours and the routing
696
+ // choices go away. Hand it back: it returns to whoever gave it to you.
697
+ ipcMain.handle('zero:team-route', teamCall(({ product, id, route }) => {
698
+ const me = teamMe();
699
+ if (!me) throw new Error('sign in first');
700
+ const row = store.readItem(product, id);
701
+ if (!row) throw new Error('that task is gone');
702
+ if (row.assignee !== me) throw new Error('this task is not yours to route');
703
+ if (route === 'agent') store.teamPatch(product, id, { assignee: 'agent', runner: me });
704
+ else if (route === 'me') store.teamPatch(product, id, { assignee: me });
705
+ else if (route === 'back') {
706
+ const to = (row.people ?? []).find((p) => p !== me) || row.createdBy;
707
+ if (!to || to === me) throw new Error('there is nobody to hand it back to');
708
+ store.teamPatch(product, id, { assignee: to });
709
+ } else throw new Error(`unknown route ${route}`);
710
+ supervisor.wake();
711
+ }));
712
+
713
+ // PUT A REACTION ON ONE MESSAGE, OR TAKE YOURS BACK (w-560647d4db). Only in a
714
+ // conversation between people, because a chip is only ever drawn in one: the
715
+ // sync carries a reaction across in a message record and nowhere else
716
+ // (shared/team-rules.mjs), so one written anywhere else would sit on this Mac
717
+ // alone and look to its author like it had reached somebody.
718
+ ipcMain.handle('zero:team-react', teamCall(async ({ product, id, on, emoji, off }) => {
719
+ const me = teamMe();
720
+ if (!me) throw new Error('sign in first');
721
+ const row = store.readItem(product, id);
722
+ if (!row) throw new Error('that conversation is gone');
723
+ const here = store.listProducts().find((p) => p.slug === product);
724
+ if (!here?.team?.direct) throw new Error('reactions are for a conversation with a teammate');
725
+ store.react(product, id, { on, emoji, off });
726
+ await team.syncNow();
727
+ }));
728
+
729
+ ipcMain.handle('zero:compose', (_e, { product, title, body, kind, priority, runAt, start, labels, engine, model, effort, assignee, due, visibility, visibleTo }) => {
559
730
  const out = store.composeItem(product, {
560
731
  // HOW HARD IT THINKS rides through unjudged: the store keeps any word
561
732
  // shaped like a level, and the spawn is the gate that knows each
562
733
  // engine's real list (`Supervisor#spawnPlan` for Claude Code,
563
734
  // `codexEffortRefusal` for Codex).
564
- title, body, kind, priority, runAt, labels, effort,
735
+ title, body, kind, priority, runAt, start, labels, effort,
736
+ // WHO DOES IT, when it is a person (the team version): the teammate it
737
+ // goes to and the day it is due, and the two of you as the conversation,
738
+ // so a reply can hand it back. An agent's row carries none of these.
739
+ assignee, due, visibility,
740
+ // AND WHO MAY SEE IT, when she chose people rather than the team
741
+ // (w-41ff964775). The store keeps the list only beside 'people'.
742
+ visibleTo,
743
+ people: assignee && teamMe() ? [teamMe(), assignee] : undefined,
565
744
  // WHICH CODING AGENT SHE CHOSE, AND ONLY IF SHE COULD HAVE. `engineOffered`
566
745
  // is the supervisor's own test and the door makes no judgement of its own:
567
746
  // it answers null on any Mac where the picker is not drawn, so a renderer
@@ -576,12 +755,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
576
755
  // fields cannot be corrected separately.
577
756
  model: supervisor.modelOffered(engine, model),
578
757
  });
758
+ // A real thread ends the walk's hold on agents (`Supervisor#sentByThem`).
759
+ supervisor.sentByThem(labels);
760
+ // THAT a task was written, and whether in practice. Never its words.
761
+ analytics.track('task_written', { practice: isPractice(product) });
579
762
  // AND LOOK AT IT NOW. Composing used to write the line and stop, so the
580
763
  // task sat in the store until the next fifteen second tick: measured
581
764
  // 2026-08-24, median 7.0 seconds and up to 15.0 before the spawn was even
582
765
  // attempted. A row scheduled for later is not woken for, because it is not
583
766
  // due; the timer is what serves those.
584
- if (!runAt || runAt <= Date.now()) supervisor.wake();
767
+ // Nothing to wake for a thread added to Later: it starts when she says.
768
+ if (start !== 'later' && (!runAt || runAt <= Date.now())) supervisor.wake();
585
769
  return out;
586
770
  });
587
771
 
@@ -591,8 +775,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
591
775
  // The whole of the work is reading the readme in the folder chosen two
592
776
  // screens earlier, so this returns in milliseconds. The two second pause
593
777
  // 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);
778
+ ipcMain.handle('zero:first-run-answer', (_e, { product, id, round = 0 }) => {
779
+ return store.finishFirstRunTask(product, id, round);
596
780
  });
597
781
 
598
782
  // THE PRACTICE PROJECT.Made when the hand-off screen is pressed, filled with
@@ -606,7 +790,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
606
790
  // somebody mid-walk. They land in an empty practice inbox and the beat that
607
791
  // clears them is skipped, exactly as beat thirteen already behaves.
608
792
  try { rows = store.stagePracticeRows(made.slug); } catch { rows = { ids: [] }; }
609
- return { ...made, examples: rows.ids };
793
+ // AND THE REST OF THE PRACTICE TEAM'S WORK, so the tour and the board have
794
+ // something in every column (w-58c8f466e7). Its own try: a store that will
795
+ // not take these still leaves the walk its four rows.
796
+ let backdrop = { ids: [] };
797
+ try { backdrop = store.stagePracticeBackdrop(made.slug); } catch { backdrop = { ids: [] }; }
798
+ return { ...made, examples: rows.ids, backdrop: backdrop.ids };
610
799
  });
611
800
 
612
801
  // AND TAKING IT BACK OFF THE SCREEN. `sweepPractice` rather than the one
@@ -640,6 +829,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
640
829
  ipcMain.handle('zero:open-fresh-user', (_e, payload) => {
641
830
  return openFreshUser({
642
831
  withAgents: payload?.withAgents !== false,
832
+ withTools: payload?.withTools !== false,
643
833
  packaged: app.isPackaged,
644
834
  });
645
835
  });
@@ -869,10 +1059,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
869
1059
  // feel instant. The card read them a moment before pressing, so the last read
870
1060
  // is the right answer and it is ours rather than the renderer's.
871
1061
  let lastThreads = [];
872
- // HER CODEX CONVERSATIONS ARE ON THE SAME CARD. They carry `source:
1062
+ // THE USER'S CODEX CONVERSATIONS ARE ON THE SAME CARD. They carry `source:
873
1063
  // '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.
1064
+ // Claude Code ones do, and the ones the user said not now to are here too, so
1065
+ // there is a way back to them.
876
1066
  const readAllThreads = () => {
877
1067
  let claude = [];
878
1068
  let codex = [];
@@ -883,7 +1073,11 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
883
1073
  ipcMain.handle('zero:agent-threads', () => {
884
1074
  const threads = readAllThreads();
885
1075
  lastThreads = threads;
886
- return { threads };
1076
+ // Each one already in the inbox says so, and the card leaves it out
1077
+ // (w-db6f5e331e). The walk's folder suggestions still read all of them.
1078
+ let items = [];
1079
+ try { items = store.listItems(); } catch { items = []; }
1080
+ return { threads: markImported(threads, items) };
887
1081
  });
888
1082
 
889
1083
  // So this is `zero:import-agents` with the agent taken out of it. No ticks are
@@ -924,7 +1118,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
924
1118
  // `projectAgents` and stopped. Three occurrences of that key in the whole
925
1119
  // shipped app, all of them this file and settings.mjs, and none in the
926
1120
  // 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
1121
+ // ticks the next time somebody walked the setup. Measured on the build a tester
928
1122
  // used, the store was byte for byte identical before and after pressing it.
929
1123
  //
930
1124
  // The names are still saved, because the card still opens on what was taken
@@ -976,6 +1170,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
976
1170
  return store.answerItem(product, id, { status: 'blocked' });
977
1171
  });
978
1172
 
1173
+ // Run now, from the three-dot menu on a waiting task (supervisor.runNow).
1174
+ ipcMain.handle('zero:run-now', (_e, { product, id }) => {
1175
+ if (isAgentRow(id)) return { ok: false, reason: 'missing' };
1176
+ return supervisor.runNow(product, id);
1177
+ });
1178
+
979
1179
  // Send a stopped/blocked item back to the queue; the fresh worker's brief
980
1180
  // carries whatever the founder wrote on the item meanwhile.
981
1181
  ipcMain.handle('zero:reopen', (_e, { product, id }) => {
@@ -1096,6 +1296,45 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1096
1296
  }
1097
1297
  });
1098
1298
 
1299
+ // SETTING UP THEIR PLAN FOR THEM (w-9f6975906c). The walk asks which plan
1300
+ // they pay for and this installs the tool and starts its own sign-in. The
1301
+ // finder is the recheck, so what gets installed lands on the config and the
1302
+ // rest of the app runs on it without a restart.
1303
+ const engineSetup = createEngineSetup({
1304
+ 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 };
1308
+ },
1309
+ // WHICH ONE IS SIGNED IN, kept on the config for the run that reads it
1310
+ // (`Supervisor#_enginesFound`) and never saved: it changes outside the app.
1311
+ onSignIn: (engine, signedIn) => {
1312
+ config[`${engine}SignedIn`] = signedIn;
1313
+ },
1314
+ });
1315
+ // ASKED ONCE AT LAUNCH, and only on a Mac with both, which is the only Mac
1316
+ // where the answer moves a task (w-d5d632e503): a tester who pays for
1317
+ // ChatGPT had the Claude app's copy of Claude Code, never signed in, so
1318
+ // every task went to Claude Code and Codex was never offered.
1319
+ if (config.claudeFound && config.codexBin) {
1320
+ void engineSetup.signedInNow('claude', config.claudeBin);
1321
+ void engineSetup.signedInNow('codex', config.codexBin);
1322
+ }
1323
+ // A sign-in left waiting on the browser is not left running after the app.
1324
+ app.on('will-quit', () => { engineSetup.cancel('claude'); engineSetup.cancel('codex'); });
1325
+ ipcMain.handle('zero:engine-setup', async (_e, { action, engine } = {}) => {
1326
+ if (engine !== 'claude' && engine !== 'codex') return { ok: false, error: 'Unknown coding agent.' };
1327
+ try {
1328
+ if (action === 'ready') return { ok: true, ...(await engineSetup.readiness(engine)) };
1329
+ if (action === 'start') return { ok: true, ...engineSetup.start(engine) };
1330
+ if (action === 'again') return { ok: true, ...engineSetup.signInAgain(engine) };
1331
+ if (action === 'cancel') return { ok: true, ...engineSetup.cancel(engine) };
1332
+ return { ok: true, ...engineSetup.status(engine) };
1333
+ } catch (err) {
1334
+ return { ok: false, error: String(err.message) };
1335
+ }
1336
+ });
1337
+
1099
1338
  /* * ADD A CODEX ACCOUNT: make the folder, register the login, hand back the one line the
1100
1339
  terminal has to run.
1101
1340
  */
@@ -1166,7 +1405,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1166
1405
  const dir = productDirOf(product);
1167
1406
  if (!dir) return { ok: false, error: 'That project could not be found.' };
1168
1407
  const saved = setProjectName(dir, name);
1169
- // PUSH, because the name is on far more than the screen she typed it on:
1408
+ // PUSH, because the name is on far more than the screen it was typed on:
1170
1409
  // the rail, every row's byline, the composer's picker and ⌘K all print it,
1171
1410
  // and a rename that only redrew Settings would leave the rest of the app
1172
1411
  // calling it the old thing until something else happened to refresh.
@@ -1177,6 +1416,32 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1177
1416
  }
1178
1417
  });
1179
1418
 
1419
+ // ARCHIVE A PROJECT, OR BRING ONE BACK (main/store.mjs setProductArchived).
1420
+ // Its files stay on disk; it leaves every list the app draws. Pushed for the
1421
+ // same reason a rename is: the rail, the inbox and the picker all list it.
1422
+ ipcMain.handle('zero:project-archive', (_e, { product, archived } = {}) => {
1423
+ try {
1424
+ store.setProductArchived(product, archived === true);
1425
+ push();
1426
+ return { ok: true, ...readSettings({ config, supervisor, store }) };
1427
+ } catch (err) {
1428
+ return { ok: false, error: String(err.message) };
1429
+ }
1430
+ });
1431
+
1432
+ // WHO SEES A PROJECT'S THREADS (main/store.mjs setProductSeenBy,
1433
+ // w-b989839656). Pushed, because the inbox's locks and the New thread
1434
+ // card's starting chip both read it off the snapshot's projects.
1435
+ ipcMain.handle('zero:project-seen-by', (_e, { product, who, people } = {}) => {
1436
+ try {
1437
+ store.setProductSeenBy(product, { who, people });
1438
+ push();
1439
+ return { ok: true };
1440
+ } catch (err) {
1441
+ return { ok: false, error: String(err.message) };
1442
+ }
1443
+ });
1444
+
1180
1445
  // Opens the picker AND does the copy, in one round trip. Two handlers would
1181
1446
  // mean the renderer briefly holds a path into her Downloads folder and hands
1182
1447
  // it back to be trusted; this way the only path that crosses the bridge is one
@@ -1248,10 +1513,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1248
1513
  for (const file of files ?? []) {
1249
1514
  const safe = String(file.name ?? 'file').replace(/[^\w.-]+/g, '-').slice(0, 80);
1250
1515
  // 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
1516
+ // was the millisecond plus the filename, so two files saved in the same
1252
1517
  // 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
1518
+ // ended up with two links to one picture. Screenshots are often sent in
1519
+ // pairs, which is exactly the shape that collides. It has never been observed
1255
1520
  // happening; it is one line to make impossible.
1256
1521
  const rel = `attachments/${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`;
1257
1522
  const abs = path.join(dir, rel);
@@ -1265,13 +1530,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1265
1530
 
1266
1531
  // A PASTED SCREENSHOT GOES TO DISK THE MOMENT IT IS PASTED.
1267
1532
  //
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.
1533
+ // The constraint was ours. Pasted bytes used to ride in the draft as base64,
1534
+ // and a draft lives in localStorage, whose quota MEASURED on this machine is
1535
+ // 5,241,856 characters for the WHOLE app. Ordinary full-screen screenshots
1536
+ // are regularly over that; 6 MB is not unusual.
1273
1537
  //
1274
- // Raising the number cannot reach her bar: even the whole quota is under 4 MB
1538
+ // Raising the number cannot reach that bar: even the whole quota is under 4 MB
1275
1539
  // of image, and Claude's own limit is 10 MB base64 per image (about 7.5 MB of
1276
1540
  // file) at up to 8000x8000. localStorage is simply the wrong place for image
1277
1541
  // bytes. So they go here instead, and the draft keeps a path — which the draft
@@ -1408,6 +1672,16 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
1408
1672
  return err ? { ok: false, error: err } : { ok: true, opened: found.path };
1409
1673
  });
1410
1674
 
1675
+ // THE PICTURES IN A FOLDER A MESSAGE NAMES, so the folder line draws them
1676
+ // (FolderPreview in Focus.tsx). Only pictures, only inside this product.
1677
+ ipcMain.handle('zero:folder-pictures', (_e, { product, src } = {}) => folderPictures({
1678
+ src,
1679
+ product,
1680
+ products: store.listProducts(),
1681
+ accountRoot: config.accountRoot,
1682
+ storeRoot: config.storeRoot,
1683
+ }));
1684
+
1411
1685
  // THE DOCUMENT PANE READS AND WRITES THE FILE ITSELF. A path goes through
1412
1686
  // the very same finder the links and the chips use, so the pane opens
1413
1687
  // exactly the file a click would have opened, and then main hands back the