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/settings.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
  import fs from 'node:fs';
16
16
  import os from 'node:os';
17
17
  import path from 'node:path';
18
- import { saveConfig } from './config.mjs';
18
+ import { codexChoiceToOpen, saveConfig } from './config.mjs';
19
19
  import { resolveClaudeBin, forgetClaudeBin, INSTALL_URL } from './claude-bin.mjs';
20
20
  import { AGENT_MODES } from '../shared/agents.mjs';
21
21
  import { analyticsKey, diagnosticsOn } from './analytics.mjs';
@@ -23,7 +23,9 @@ import { CLAUDE_PERMISSION_MODES } from '../shared/work-items.mjs';
23
23
  import { accountSentence, engineTroubleNote } from '../shared/spawn-trouble.mjs';
24
24
  import { readPlan } from './claude-plan.mjs';
25
25
  import { effectiveProfiles } from './account-discovery.mjs';
26
- import { machineSlots, machineNote, MAX_SLOTS } from './machine.mjs';
26
+ import { machineSlots, machineNote, autoAgents, MAX_SLOTS } from './machine.mjs';
27
+ import { NAME } from '../shared/product-name.mjs';
28
+ import { autoSlots } from './memory-gate.mjs';
27
29
  import { accountIdentity, claudeLoginCommand, duplicateAccountNote, linkAccountTooling, makeClaudeHome } from './account-tooling.mjs';
28
30
  import { isEngine } from '../shared/engines.mjs';
29
31
  import { codexAccount, codexLoginCommand, makeCodexHome } from './codex-account.mjs';
@@ -270,6 +272,10 @@ export function recheckClaude(config) {
270
272
  // has just found Claude Code updates it rather than leaving the rest of the
271
273
  // app pointing at where it used to not be.
272
274
  if (state.claudeFound && state.claudeBin) config.claudeBin = state.claudeBin;
275
+ // And whether it is here at all, which is what decides that a Mac with only
276
+ // Codex runs on Codex (`Supervisor#_enginesFound`). Only a certain answer
277
+ // moves it, so an unsure search never takes Claude Code away from anybody.
278
+ if (state.claudeFound || state.claudeCertain) config.claudeFound = state.claudeFound;
273
279
  return state;
274
280
  }
275
281
 
@@ -319,6 +325,12 @@ export function codexState(config, where = {}) {
319
325
  };
320
326
  }
321
327
 
328
+ /** `codexState` under the names the walk's last card reads beside Claude's. */
329
+ export function codexFoundState(config, where = {}) {
330
+ const state = codexState(config, where);
331
+ return { codexFound: state.found, codexCertain: state.certain, codexInstallUrl: state.url };
332
+ }
333
+
322
334
  /**
323
335
  * LOOK AGAIN, PROPERLY, for the second engine. The twin of `recheckClaude`
324
336
  * above, and it earns its place for a sharper reason: finding Codex here is
@@ -329,6 +341,10 @@ export function recheckCodex(config) {
329
341
  forgetCodexBin();
330
342
  const state = codexState(config);
331
343
  config.codexBin = state.found ? state.bin : null;
344
+ // Found while the app is open, by this button or the plan setup: the choice
345
+ // opens now, as it would have at the next launch (main/config.mjs).
346
+ const opened = codexChoiceToOpen(config, state.found);
347
+ if (opened && config.appDir) saveConfig(config, { engineChoice: opened });
332
348
  return state;
333
349
  }
334
350
 
@@ -366,9 +382,9 @@ export function readSettings({ config, supervisor, store }) {
366
382
  const trouble = troubles[profile] ?? null;
367
383
  const resting = (supervisor._profileCooldown?.[profile] ?? 0) >= now;
368
384
  // WHOSE SUBSCRIPTION THIS ROW IS, not merely which folder it lives in.
369
- // The page named its rows after folders, so nothing on it could tell her
370
- // that her work account had been replaced by her personal one;
371
- // main/account-tooling.mjs has the morning that cost. Null when the
385
+ // The page named its rows after folders, so nothing on it could tell a
386
+ // person that one of their accounts had been replaced by the other;
387
+ // main/account-tooling.mjs has how that happens. Null when the
372
388
  // folder holds no login, and then the row reads as it always did.
373
389
  const identity = accountIdentity(profile === 'default' ? null : profile);
374
390
  return {
@@ -421,9 +437,8 @@ export function readSettings({ config, supervisor, store }) {
421
437
  // THE MARK, already a drawable url or null, resolved once by listProducts.
422
438
  logo: product.logo,
423
439
  // ONE SWITCH A PROJECT STILL HAS (w-d19d6d387c, 2026-09-22). It had five
424
- // controls. Hers, on a picture of them: "I still don't actually
425
- // understand what those toggles do, even after reading them... I would
426
- // lean towards just having project-level instructions." The drive, the
440
+ // controls, and what they did was not clear even after reading their
441
+ // labels, so project-level instructions replace them. The drive, the
427
442
  // per-project pause and personal mode are gone from the app entirely.
428
443
  // `permission` stays readable because `projectSessionArgs` is still
429
444
  // honoured for anyone who sets one in the config by hand, and the page
@@ -434,15 +449,40 @@ export function readSettings({ config, supervisor, store }) {
434
449
  permission: override ? permissionMode(override) : 'workspace',
435
450
  permissionArgs: override ?? null,
436
451
  codexMode: config.projectCodexMode?.[product.slug] ?? 'workspace',
452
+ // THE SUPERVISOR IS ASKED RATHER THAN THE CONFIG FILE, so the page shows
453
+ // what will actually happen: a tie naming a login that has been signed out
454
+ // is ignored when work is handed out, and reads 'any' here too.
455
+ account: supervisor._projectProfile?.(product.slug) ?? 'any',
437
456
  instructions,
438
457
  running: sessions.filter((s) => s.product === product.slug).length,
439
458
  };
440
459
  });
441
460
 
461
+ // ARCHIVED PROJECTS, only so the Projects page can bring one back. Name and
462
+ // folder, the two things that tell three projects called Daydream apart.
463
+ let archivedProjects = [];
464
+ try {
465
+ archivedProjects = store.listProducts({ includeArchived: true })
466
+ .filter((p) => p.archived === true)
467
+ .map((p) => ({ slug: p.slug, name: p.name, dir: p.dir }));
468
+ } catch {}
469
+
442
470
  return {
471
+ archivedProjects,
443
472
  workspace: {
444
473
  agentsRunning: !supervisor.paused,
445
474
  sessionsAtOnce: config.maxConcurrentSessions,
475
+ // AUTOMATIC, AND WHAT IT WORKS OUT TO (w-e5225b62ba). `agentsAuto` is who
476
+ // is deciding; `agentsTotal` is what the whole Mac is running at once,
477
+ // which is the number the page shows, because the per-account one is
478
+ // arithmetic nobody should have to do. `agentsNudge` is how many steps
479
+ // down "Something felt slow" has taken it.
480
+ agentsAuto: !!config.agentsAuto,
481
+ agentsTotal: status.capacity,
482
+ agentsNudge: Math.max(0, Math.round(Number(config.agentsNudge) || 0)),
483
+ // What Automatic would say right now, so a manual number can be compared
484
+ // with it without turning Automatic back on to find out.
485
+ agentsAutoTotal: autoAgents({ gated: !!config.memoryGate, nudge: config.agentsNudge }),
446
486
  // THE PLAN THAT SET THE NUMBER ABOVE, when a plan set it. Null in the
447
487
  // ordinary case: she picked the number herself, or it is the three every
448
488
  // install has always had. It is only ever filled in when somebody would
@@ -457,6 +497,8 @@ export function readSettings({ config, supervisor, store }) {
457
497
  slotsSuggested: slotsHere.slots,
458
498
  slotsMax: MAX_SLOTS,
459
499
  machineNote: machineNote(slotsHere, config.maxConcurrentSessions),
500
+ memoryGate: memoryGateSettings({ config, supervisor }),
501
+ leftovers: leftoverSettings({ config, supervisor }),
460
502
  capacity: status.capacity,
461
503
  running: sessions.length,
462
504
  model: parseSessionArgs(workspaceArgs).model,
@@ -509,6 +551,10 @@ export function readSettings({ config, supervisor, store }) {
509
551
  // Someone who reads "install it, then open this screen again" and does
510
552
  // exactly that has to see it turn found, without restarting the app.
511
553
  ...claudeState(config),
554
+ // AND WHETHER CODEX IS, because the app needs one of the two and not
555
+ // both: the last card of the walk only shuts the inbox when neither is
556
+ // here (renderer/src/App.tsx).
557
+ ...codexFoundState(config),
512
558
  // AND WHAT THIS MAC'S CLAUDE CODE CALLS ITS MODELS.
513
559
  //
514
560
  // In the same trip, and for the same reason the line above is looked up
@@ -577,21 +623,26 @@ function engineSettings(supervisor, config) {
577
623
  // and that stopped being true somewhere between codex-cli versions.
578
624
  // main/codex-account.mjs holds the measurement. Null when nobody is signed
579
625
  // in, and the card then reads exactly as it always did.
580
- const codex = supervisor.engineChoiceOpened()
581
- ? {
582
- ...codexState(config),
583
- trouble: engineTroubleFor(supervisor, 'codex'),
584
- account: codexAccount(supervisor._codexHome()),
585
- // EVERY CODEX LOGIN ON THE MAC, NOT JUST THE FIRST. The supervisor has
586
- // run one app-server per CODEX_HOME since a second login became real;
587
- // what never existed was a screen that could see them. One entry per
588
- // `codexProfiles` word, resolved through the supervisor's own mapping so
589
- // the card and the fleet cannot disagree about which folder a name
590
- // means.
591
- accounts: codexAccountRows(supervisor),
592
- }
593
- : null;
594
- if (choices.length < 2) {
626
+ // A Mac that runs on Codex alone has asked about it by having nothing else.
627
+ //
628
+ // AND THE PAGE IS DRAWN ON EVERY MAC NOW (w-d5d632e503). A tester who only
629
+ // uses Codex looked for it in Settings, found nothing, and had no way to
630
+ // tell "not installed" from "not offered". Not found, the page says so and
631
+ // links the install, which is the question that person had.
632
+ const codexIsHome = engine === 'codex';
633
+ const codex = {
634
+ ...codexState(config),
635
+ trouble: engineTroubleFor(supervisor, 'codex'),
636
+ account: codexAccount(supervisor._codexHome()),
637
+ // EVERY CODEX LOGIN ON THE MAC, NOT JUST THE FIRST. The supervisor has
638
+ // run one app-server per CODEX_HOME since a second login became real;
639
+ // what never existed was a screen that could see them. One entry per
640
+ // `codexProfiles` word, resolved through the supervisor's own mapping so
641
+ // the card and the fleet cannot disagree about which folder a name
642
+ // means.
643
+ accounts: codexAccountRows(supervisor),
644
+ };
645
+ if (choices.length < 2 && !codexIsHome) {
595
646
  return { engine, engineChoices: choices, codex, codexModels: [], codexModelDefault: null, codexModel: null };
596
647
  }
597
648
  const home = supervisor._codexHome();
@@ -680,6 +731,24 @@ export function setProjectSetting({ config, supervisor }, { product, key, value
680
731
  saveConfig(config, { projectCodexMode: Object.keys(map).length ? map : undefined });
681
732
  break;
682
733
  }
734
+ // WHICH CLAUDE LOGIN THIS PROJECT'S AGENTS RUN ON. 'any' is the absence of
735
+ // an opinion, like 'workspace' above, and it deletes the entry rather than
736
+ // writing a word: a config file with no `projectAccounts` is the file every
737
+ // Mac had before this existed.
738
+ //
739
+ // AND A NAME THIS MAC CANNOT HONOUR IS REFUSED AT THE DOOR rather than
740
+ // written and ignored later. The supervisor does ignore one (`_projectProfile`),
741
+ // because an account can be signed out after the fact, but a pick made
742
+ // against a list we just read has no excuse to be wrong.
743
+ case 'account': {
744
+ const known = effectiveProfiles(config.authProfiles, { home: config.home });
745
+ if (value !== 'any' && !known.includes(value)) throw new Error(`unknown Claude account: ${value}`);
746
+ const map = { ...(config.projectAccounts ?? {}) };
747
+ if (value === 'any') delete map[product];
748
+ else map[product] = value;
749
+ saveConfig(config, { projectAccounts: Object.keys(map).length ? map : undefined });
750
+ break;
751
+ }
683
752
  default:
684
753
  throw new Error(`unknown project setting: ${key}`);
685
754
  }
@@ -754,8 +823,8 @@ export function addCodexAccount({ config, supervisor }) {
754
823
  * accounting while this was a product we sold. She withdrew that on
755
824
  * w-3498e0cad2 ("we're no longer offering this as a commercially viable
756
825
  * product... the rules have changed so I think you should allow
757
- * multi-accounting"), and this is the door that decision asked for. Her reason
758
- * for wanting it is the ordinary one: a work login and a personal login, both
826
+ * multi-accounting"), and this is the door that decision asked for. The
827
+ * ordinary reason for wanting it: a work login and a personal login, both
759
828
  * live, on one Mac.
760
829
  *
761
830
  * Everything else is the Codex flow exactly. Main makes the folder and
@@ -781,19 +850,143 @@ export function addClaudeAccount({ config, supervisor }) {
781
850
  return { home: made.home, profile: made.profile, command: claudeLoginCommand(made.home) };
782
851
  }
783
852
 
853
+ /**
854
+ * The Agents page's memory rows: the switch, the number of heavy commands at
855
+ * once (null is Auto), what Auto works out to here, and one sentence about
856
+ * right now while it is on.
857
+ */
858
+ export function memoryGateSettings({ config, supervisor }) {
859
+ const on = !!config.memoryGate;
860
+ const slots = Number.isFinite(config.memoryGateSlots) ? config.memoryGateSlots : null;
861
+ // What Auto works out to on this Mac, whether or not somebody picked.
862
+ const slotsAuto = autoSlots(os.totalmem());
863
+ let now = null;
864
+ if (on) {
865
+ let s = null;
866
+ try { s = supervisor.memoryGateStatus?.() ?? null; } catch {}
867
+ if (s?.role === 'standby') now = `Another ${NAME} on this computer is coordinating.`;
868
+ else if (s) {
869
+ const heavy = (s.running ?? []).filter((r) => r.holdsSlot).length;
870
+ const waiting = (s.waiting ?? []).length;
871
+ const mem = s.pressure === 'critical' ? 'Memory is very short.' : s.pressure === 'tight' ? 'Memory is tight.' : 'Memory is fine.';
872
+ const run = heavy ? `${heavy} heavy command${heavy === 1 ? '' : 's'} running` : 'Nothing heavy running';
873
+ now = `${mem} ${run}${waiting ? `, ${waiting} waiting` : ''}.`;
874
+ }
875
+ }
876
+ return { on, slots, slotsAuto, slotsMax: MAX_SLOTS, now, reading: memoryReading({ config, supervisor }) };
877
+ }
878
+
879
+ /**
880
+ * THE FIGURES THE RUNNING PAGE LEADS WITH (w-e5225b62ba), rather than the same
881
+ * facts tacked onto the end of three grey paragraphs: "6 running now", "Holding
882
+ * 3 commands now", "Nothing running now".
883
+ *
884
+ * Every field is null when it is not known, and the page leaves out what is
885
+ * null. A figure invented to fill a slot is worse than a gap: this card's whole
886
+ * job is to be the one place on the screen that is measured.
887
+ *
888
+ * `usedPct` is the kernel's own `kern.memorystatus_level` turned round, which is
889
+ * what the gate already polls, so the bar and the word above it cannot disagree.
890
+ */
891
+ export function memoryReading({ config, supervisor }) {
892
+ const out = { heavy: null, waiting: null, pressure: null, usedPct: null, asked: null, waited: null, refused: null };
893
+ if (!config.memoryGate) return out;
894
+ let s = null;
895
+ try { s = supervisor.memoryGateStatus?.() ?? null; } catch { return out; }
896
+ if (!s || s.role === 'standby') return out;
897
+ out.heavy = (s.running ?? []).filter((r) => r.holdsSlot).length;
898
+ out.waiting = (s.waiting ?? []).length;
899
+ out.pressure = s.pressure ?? null;
900
+ if (Number.isFinite(s.freePct)) out.usedPct = Math.max(0, Math.min(100, 100 - s.freePct));
901
+ const c = s.counts ?? {};
902
+ if (Number.isFinite(c.asked)) out.asked = c.asked;
903
+ if (Number.isFinite(c.waited)) out.waited = c.waited;
904
+ if (Number.isFinite(c.refused)) out.refused = c.refused;
905
+ return out;
906
+ }
907
+
908
+ /**
909
+ * The Agents page's leftovers row: the switch, and one sentence about what
910
+ * finished agents have left running while it is on.
911
+ */
912
+ export function leftoverSettings({ config, supervisor }) {
913
+ const on = !!config.cleanupLeftovers;
914
+ let now = null;
915
+ let s = null;
916
+ try { s = supervisor.leftoverStatus?.() ?? null; } catch {}
917
+ const list = s?.leftovers ?? [];
918
+ const stoppable = list.reduce((n, l) => n + (l.programs ?? 0), 0);
919
+ const kept = list.reduce((n, l) => n + (l.kept ?? 0), 0);
920
+ const total = stoppable + kept;
921
+ // OFF, IT SAYS WHAT TURNING IT ON WOULD STOP, before anybody does: the
922
+ // consent is to these programs too, not only to future ones (agreed with
923
+ // Codex, 2026-10-05).
924
+ if (!on) {
925
+ if (total && stoppable) {
926
+ now = `Right now finished agents have left ${total} program${total === 1 ? '' : 's'} running; turning this on stops ${stoppable === total ? (total === 1 ? 'it' : 'them') : `${stoppable} of them`}, two hours from then.`;
927
+ }
928
+ return { on, now };
929
+ }
930
+ {
931
+ if (!total) now = 'Nothing left running by finished agents.';
932
+ else {
933
+ const next = list.filter((l) => l.programs && l.stopsAt).map((l) => l.stopsAt).sort((a, b) => a - b)[0];
934
+ const when = (ms) => {
935
+ const m = Math.max(1, Math.round((ms - Date.now()) / 60_000));
936
+ return m >= 60 ? `${Math.floor(m / 60)} h${m % 60 ? ` ${m % 60} m` : ''}` : `${m} m`;
937
+ };
938
+ now = `Finished agents have left ${total} program${total === 1 ? '' : 's'} running.`;
939
+ if (stoppable) now += ` ${stoppable} ${next && next > Date.now() ? `stop in ${when(next)}` : 'are stopping'}`;
940
+ if (kept) now += `${stoppable ? ';' : ''} ${kept} ${kept === 1 ? 'is' : 'are'} kept`;
941
+ now += '.';
942
+ const survivors = s?.survivors?.length ?? 0;
943
+ if (survivors) now += ` ${survivors} did not stop when asked.`;
944
+ }
945
+ }
946
+ return { on, now };
947
+ }
948
+
784
949
  export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
785
950
  switch (key) {
786
951
  case 'agentsRunning':
787
952
  supervisor.paused = !value;
788
953
  supervisor.onChange?.();
789
954
  break;
955
+ // WHO DECIDES THE NUMBER (w-e5225b62ba). Turning Automatic on does not
956
+ // throw away the number somebody set: it stays in the file, unread, so
957
+ // turning Automatic off again gives them their own number back rather than
958
+ // a fresh guess.
959
+ case 'agentsAuto':
960
+ saveConfig(config, { agentsAuto: !!value });
961
+ supervisor.wake?.();
962
+ supervisor.onChange?.();
963
+ break;
964
+ // SOMETHING FELT SLOW. The one lever a person has that no reading can
965
+ // replace: they are sitting in front of the Mac and the app is not. It only
966
+ // ever subtracts, and main/machine.mjs refuses to take it below one.
967
+ case 'agentsFeltSlow': {
968
+ const now = Math.max(0, Math.round(Number(config.agentsNudge) || 0));
969
+ saveConfig(config, { agentsNudge: value === 'reset' ? 0 : now + 1 });
970
+ supervisor.wake?.();
971
+ supervisor.onChange?.();
972
+ break;
973
+ }
790
974
  case 'sessionsAtOnce': {
791
975
  // THE MACHINE DOES NOT CLAMP THIS, and for one round it did. These are
792
976
  // developers using the tool as they want, so the range is the control's
793
977
  // own range, the same on every Mac. What the hardware does is SUGGEST a
794
978
  // starting number, one row up.
795
979
  const n = Math.max(1, Math.min(MAX_SLOTS, Math.round(Number(value) || 1)));
796
- saveConfig(config, { maxConcurrentSessions: n });
980
+ // Setting a number by hand IS taking the decision back, so the page never
981
+ // shows a stepper that the supervisor is quietly ignoring.
982
+ saveConfig(config, { maxConcurrentSessions: n, agentsAuto: false });
983
+ // An explicit choice replaces the startup plan suggestion for BOTH
984
+ // engines. Leaving this marker set makes Codex ignore the new number
985
+ // until loadConfig runs again at restart. It is derived, not persisted.
986
+ config.planSlotsFrom = null;
987
+ // Fill newly available slots through the usual queue and its guards.
988
+ // Lowering the cap leaves existing work running and gates new starts.
989
+ supervisor.wake?.();
797
990
  break;
798
991
  }
799
992
  // Written as an opt-OUT (`diagnostics: false`) so that on is what a config
@@ -801,6 +994,32 @@ export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
801
994
  // see in her own file. `saveConfig` mutates the live config too, and
802
995
  // main/analytics.mjs reads the switch at every send, so off takes effect
803
996
  // from the moment she moves it and not at the next launch.
997
+ // HOLD HEAVY WORK WHEN MEMORY IS SHORT (w-3958c3753d). Takes effect now:
998
+ // the coordinator starts or stops this moment, and workers spawned from
999
+ // here on carry the check or do not. A worker already running keeps the
1000
+ // hooks it was started with; with the switch off its hook finds no socket
1001
+ // and lets every command straight through.
1002
+ case 'memoryGate':
1003
+ saveConfig(config, { memoryGate: !!value });
1004
+ supervisor.setMemoryGate?.(!!value)?.catch?.((e) => console.warn('zero: memory gate:', e.message));
1005
+ supervisor.onChange?.();
1006
+ break;
1007
+ // STOP WHAT FINISHED AGENTS LEAVE RUNNING (main/leftovers.mjs). Turning it
1008
+ // on records the moment, which is the earliest the clock may start from.
1009
+ case 'cleanupLeftovers':
1010
+ saveConfig(config, value ? { cleanupLeftovers: true, cleanupLeftoversSince: Date.now() } : { cleanupLeftovers: false });
1011
+ supervisor.setLeftoverCleanup?.(!!value)?.catch?.((e) => console.warn('zero: leftovers:', e.message));
1012
+ supervisor.onChange?.();
1013
+ break;
1014
+ case 'memoryGateSlots': {
1015
+ const n = value === null || value === undefined || value === 'auto'
1016
+ ? null
1017
+ : Math.max(1, Math.min(MAX_SLOTS, Math.round(Number(value) || 1)));
1018
+ saveConfig(config, { memoryGateSlots: n });
1019
+ if (config.memoryGate) supervisor.setMemoryGate?.(true)?.catch?.(() => {});
1020
+ supervisor.onChange?.();
1021
+ break;
1022
+ }
804
1023
  case 'diagnostics':
805
1024
  saveConfig(config, { diagnostics: !!value });
806
1025
  break;