agentbox-app 0.1.6

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 (302) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +176 -0
  3. package/bin/agentbox.mjs +103 -0
  4. package/briefs/adhd-mode.md +5 -0
  5. package/briefs/message-rules.md +95 -0
  6. package/briefs/worker.md +230 -0
  7. package/briefs/writing-rules.md +336 -0
  8. package/main/account-discovery.mjs +145 -0
  9. package/main/account-tooling.mjs +261 -0
  10. package/main/agent-activity.mjs +56 -0
  11. package/main/agent-files.mjs +422 -0
  12. package/main/agent-schedule.mjs +254 -0
  13. package/main/agent-sessions.mjs +435 -0
  14. package/main/agents.mjs +1095 -0
  15. package/main/analytics.mjs +206 -0
  16. package/main/answer-modes.mjs +206 -0
  17. package/main/approval-prompt-server.mjs +160 -0
  18. package/main/approvals.mjs +487 -0
  19. package/main/artifact-path.mjs +144 -0
  20. package/main/claude-bin.mjs +311 -0
  21. package/main/claude-input.mjs +117 -0
  22. package/main/claude-models.mjs +244 -0
  23. package/main/claude-plan.mjs +148 -0
  24. package/main/claude-usage.mjs +149 -0
  25. package/main/code-change.mjs +383 -0
  26. package/main/code-file.mjs +116 -0
  27. package/main/codex-account.mjs +146 -0
  28. package/main/codex-app-server.mjs +796 -0
  29. package/main/codex-approvals.mjs +712 -0
  30. package/main/codex-bin.mjs +261 -0
  31. package/main/codex-compaction.mjs +49 -0
  32. package/main/codex-models.mjs +194 -0
  33. package/main/codex-session.mjs +797 -0
  34. package/main/codex-threads.mjs +268 -0
  35. package/main/codex-usage-file.mjs +192 -0
  36. package/main/codex-usage.mjs +149 -0
  37. package/main/codex-watch.mjs +35 -0
  38. package/main/codex.mjs +757 -0
  39. package/main/config.mjs +345 -0
  40. package/main/copy-selection.mjs +55 -0
  41. package/main/crash-report.mjs +158 -0
  42. package/main/demo.mjs +254 -0
  43. package/main/dev-window.mjs +103 -0
  44. package/main/doc-file.mjs +99 -0
  45. package/main/doc-scheme.mjs +126 -0
  46. package/main/first-run.mjs +190 -0
  47. package/main/folders.mjs +91 -0
  48. package/main/fresh-user.mjs +120 -0
  49. package/main/git-change.mjs +323 -0
  50. package/main/img-scheme.mjs +90 -0
  51. package/main/instruction-settings.mjs +205 -0
  52. package/main/ipc.mjs +1558 -0
  53. package/main/live-replies.mjs +77 -0
  54. package/main/machine.mjs +113 -0
  55. package/main/main.mjs +972 -0
  56. package/main/menu.mjs +177 -0
  57. package/main/notify.mjs +155 -0
  58. package/main/open-source-file.mjs +8 -0
  59. package/main/project-identity.mjs +149 -0
  60. package/main/provider-commands.mjs +32 -0
  61. package/main/rail-note.mjs +74 -0
  62. package/main/repeats.mjs +261 -0
  63. package/main/row-label.mjs +185 -0
  64. package/main/screen-detail.mjs +44 -0
  65. package/main/serve.mjs +262 -0
  66. package/main/settings.mjs +912 -0
  67. package/main/staleness.mjs +76 -0
  68. package/main/store/chats.mjs +188 -0
  69. package/main/store/home.mjs +383 -0
  70. package/main/store/migrate.mjs +40 -0
  71. package/main/store/paths.mjs +82 -0
  72. package/main/store/pending-writes.mjs +122 -0
  73. package/main/store/project-lock.mjs +200 -0
  74. package/main/store/project.mjs +523 -0
  75. package/main/store/queues.mjs +31 -0
  76. package/main/store/work-items.mjs +299 -0
  77. package/main/store/write-queue.mjs +14 -0
  78. package/main/store-modules.mjs +23 -0
  79. package/main/store.mjs +1269 -0
  80. package/main/supervisor.mjs +6875 -0
  81. package/main/task-commands.mjs +161 -0
  82. package/main/task-folders-offthread.mjs +84 -0
  83. package/main/task-folders-thread.mjs +13 -0
  84. package/main/task-folders.mjs +409 -0
  85. package/main/task-remote-control.mjs +65 -0
  86. package/main/task-terminals.mjs +169 -0
  87. package/main/terminal-access.mjs +6 -0
  88. package/main/updater.mjs +240 -0
  89. package/main/zoom-keys.mjs +54 -0
  90. package/main/zoom-readout.mjs +20 -0
  91. package/mcp/agentbox-mcp.sh +34 -0
  92. package/mcp/core/account.mjs +77 -0
  93. package/mcp/core/chat.mjs +61 -0
  94. package/mcp/core/documents.mjs +238 -0
  95. package/mcp/core/page.mjs +152 -0
  96. package/mcp/core/products.mjs +110 -0
  97. package/mcp/core/skills.mjs +126 -0
  98. package/mcp/core/work.mjs +377 -0
  99. package/mcp/stdio.mjs +70 -0
  100. package/mcp/tools.mjs +257 -0
  101. package/package.json +186 -0
  102. package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
  103. package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
  104. package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
  105. package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
  106. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  107. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  108. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  109. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  110. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  111. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  112. package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
  113. package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
  114. package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
  115. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  116. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  117. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  118. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  119. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  120. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  121. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  122. package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
  123. package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
  124. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  125. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  126. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  127. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  128. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  129. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  130. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  131. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  132. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  133. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  134. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  135. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  136. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  137. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  138. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  139. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  140. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  141. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  142. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  143. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  144. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  145. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  146. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  147. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  148. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  149. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  150. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  151. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  152. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  153. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  154. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  155. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  156. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  157. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  158. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  159. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  160. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  161. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  162. package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  163. package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  164. package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  165. package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  166. package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  167. package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  168. package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  169. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  170. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  171. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  172. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  173. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  174. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  175. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  176. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  177. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  178. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  179. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  180. package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
  181. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  182. package/renderer/dist/emulator/data/compression/README.md +11 -0
  183. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  184. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  185. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  186. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  187. package/renderer/dist/emulator/data/cores/README.md +22 -0
  188. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  189. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  190. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  191. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  192. package/renderer/dist/emulator/data/cores/package.json +65 -0
  193. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  194. package/renderer/dist/emulator/data/emulator.css +1614 -0
  195. package/renderer/dist/emulator/data/loader.js +170 -0
  196. package/renderer/dist/emulator/data/localization/README.md +63 -0
  197. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  198. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  199. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  200. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  201. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  202. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  203. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  204. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  205. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  206. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  207. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  208. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  209. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  210. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  211. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  212. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  213. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  214. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  215. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  216. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  217. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  218. package/renderer/dist/emulator/data/src/compression.js +142 -0
  219. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  220. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  221. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  222. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  223. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  224. package/renderer/dist/emulator/data/src/storage.js +125 -0
  225. package/renderer/dist/emulator/data/version.json +5 -0
  226. package/renderer/dist/emulator/host.js +299 -0
  227. package/renderer/dist/emulator/index.html +28 -0
  228. package/renderer/dist/emulator/provenance.json +6 -0
  229. package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
  230. package/renderer/dist/index.html +13 -0
  231. package/scripts/after-install.mjs +59 -0
  232. package/scripts/approval-server.sh +43 -0
  233. package/scripts/before-publish.mjs +133 -0
  234. package/shared/agent-import.d.mts +13 -0
  235. package/shared/agent-import.mjs +252 -0
  236. package/shared/agents.d.mts +190 -0
  237. package/shared/agents.mjs +937 -0
  238. package/shared/analytics-events.mjs +63 -0
  239. package/shared/answers.d.mts +12 -0
  240. package/shared/answers.mjs +80 -0
  241. package/shared/artifact-keys.d.mts +28 -0
  242. package/shared/artifact-keys.mjs +140 -0
  243. package/shared/bridge-map.mjs +128 -0
  244. package/shared/claude-commands.d.mts +20 -0
  245. package/shared/claude-commands.generated.mjs +149 -0
  246. package/shared/claude-commands.mjs +131 -0
  247. package/shared/claude-models.generated.d.mts +15 -0
  248. package/shared/claude-models.generated.mjs +26 -0
  249. package/shared/claude-usage.d.mts +16 -0
  250. package/shared/claude-usage.mjs +172 -0
  251. package/shared/codex-commands.d.mts +4 -0
  252. package/shared/codex-commands.mjs +17 -0
  253. package/shared/codex-import.d.mts +38 -0
  254. package/shared/codex-import.mjs +232 -0
  255. package/shared/codex-modes.mjs +159 -0
  256. package/shared/codex-usage.mjs +180 -0
  257. package/shared/contracts.mjs +412 -0
  258. package/shared/crash-scrub.mjs +182 -0
  259. package/shared/dashboard.mjs +481 -0
  260. package/shared/dead-run-trace.mjs +127 -0
  261. package/shared/demo-world.mjs +415 -0
  262. package/shared/effort-levels.d.mts +15 -0
  263. package/shared/effort-levels.mjs +76 -0
  264. package/shared/engines.d.mts +62 -0
  265. package/shared/engines.mjs +389 -0
  266. package/shared/first-run-examples.mjs +60 -0
  267. package/shared/first-run-line.mjs +149 -0
  268. package/shared/first-run-practice.d.mts +32 -0
  269. package/shared/first-run-practice.mjs +254 -0
  270. package/shared/first-run-shape.mjs +100 -0
  271. package/shared/fresh-user-home.mjs +189 -0
  272. package/shared/hook-failure.mjs +22 -0
  273. package/shared/install-id.mjs +39 -0
  274. package/shared/instruction-defaults.json +5 -0
  275. package/shared/library-folder.mjs +28 -0
  276. package/shared/notify-rules.mjs +120 -0
  277. package/shared/product-name.d.mts +6 -0
  278. package/shared/product-name.mjs +144 -0
  279. package/shared/project-folder-check.d.mts +22 -0
  280. package/shared/project-folder-check.mjs +152 -0
  281. package/shared/provider-commands.d.mts +8 -0
  282. package/shared/provider-commands.mjs +58 -0
  283. package/shared/rank.d.mts +19 -0
  284. package/shared/rank.mjs +146 -0
  285. package/shared/recovery.mjs +26 -0
  286. package/shared/referenced-files.d.mts +11 -0
  287. package/shared/referenced-files.mjs +122 -0
  288. package/shared/repeats.d.mts +45 -0
  289. package/shared/repeats.mjs +193 -0
  290. package/shared/schemes.mjs +33 -0
  291. package/shared/settings-terminal.d.mts +1 -0
  292. package/shared/settings-terminal.mjs +14 -0
  293. package/shared/side-build.mjs +57 -0
  294. package/shared/spawn-trouble.d.mts +8 -0
  295. package/shared/spawn-trouble.mjs +599 -0
  296. package/shared/terminal-state.d.mts +1 -0
  297. package/shared/terminal-state.mjs +58 -0
  298. package/shared/usage.d.mts +53 -0
  299. package/shared/usage.mjs +319 -0
  300. package/shared/work-items.mjs +561 -0
  301. package/shared/work-lines.mjs +360 -0
  302. package/worker-permissions.json +6 -0
@@ -0,0 +1,912 @@
1
+ // The settings screen's model: everything the app lets the founder change,
2
+ // read from where it actually lives and written back to the same place.
3
+ //
4
+ // There is no settings STORE here on purpose. Every value below already has a
5
+ // home that something else owns — the supervisor's in-memory pause, its state
6
+ // file, her config, a file in the project's docs dir — and a second copy of any
7
+ // of them is a copy that can disagree with the running fleet. So this module is
8
+ // a view and a set of writes, never a cache.
9
+ //
10
+ // The four per-project settings the screen exists to surface were global arrays
11
+ // of slugs in zero.config.json. They still are, because that file is the one
12
+ // place a grant is reviewed; what changed is that she edits them as fields on
13
+ // the project they belong to instead of in a text editor.
14
+
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { saveConfig } from './config.mjs';
19
+ import { resolveClaudeBin, forgetClaudeBin, INSTALL_URL } from './claude-bin.mjs';
20
+ import { AGENT_MODES } from '../shared/agents.mjs';
21
+ import { analyticsKey, diagnosticsOn } from './analytics.mjs';
22
+ import { CLAUDE_PERMISSION_MODES } from '../shared/work-items.mjs';
23
+ import { accountSentence, engineTroubleNote } from '../shared/spawn-trouble.mjs';
24
+ import { readPlan } from './claude-plan.mjs';
25
+ import { effectiveProfiles } from './account-discovery.mjs';
26
+ import { machineSlots, machineNote, MAX_SLOTS } from './machine.mjs';
27
+ import { accountIdentity, claudeLoginCommand, duplicateAccountNote, linkAccountTooling, makeClaudeHome } from './account-tooling.mjs';
28
+ import { isEngine } from '../shared/engines.mjs';
29
+ import { codexAccount, codexLoginCommand, makeCodexHome } from './codex-account.mjs';
30
+ import { codexDefaultModel, codexModels } from './codex-models.mjs';
31
+ import { CODEX_DEFAULT_MODE, isCodexMode } from '../shared/codex-modes.mjs';
32
+ import { claudeModels } from './claude-models.mjs';
33
+ import { resolveCodexBin, forgetCodexBin, INSTALL_URL as CODEX_INSTALL_URL } from './codex-bin.mjs';
34
+
35
+ // Her Claude Code sessions in her inbox: all of them, only the ones stopped on
36
+ // a question, or none. One reader, here, so the settings screen and the
37
+ // snapshot the inbox is drawn from cannot end up believing different defaults.
38
+ //
39
+ // NOBODY'S TERMINAL IS IN THE INBOX UNLESS THEY ASK FOR IT.
40
+ //
41
+ // So the default is `off` on every install, new or old, and the fact it used to
42
+ // turn on — whether this Mac had run Agentbox before — decides nothing any more.
43
+ // Her own file has no `outsideAgents` in it, which is why the default is the
44
+ // whole of what she sees: measured 2026-08-27, her running app resolved `all`
45
+ // out of this line and filed a row for every session on the machine.
46
+ //
47
+ // SEPARATED IS NOT DELETED, and this is the half that makes it honest. An
48
+ // outside session is still on the project's rail in the sidebar (`onTheRail`,
49
+ // shared/agents.mjs), which no mode gates, and still opens in one click. It
50
+ // just does not queue up beside the work she has to answer.
51
+ //
52
+ // WHAT SHE IMPORTS IS UNAFFECTED, and it never came through here. The agents
53
+ // taken in during the first run are FILES (~/.claude/agents/*.md), and each one
54
+ // files its own work item asking for a first job (shared/agent-import.mjs).
55
+ // That is the "imported manually" side of her answer, and it is already a
56
+ // separate path from this one.
57
+ //
58
+ // A setting actually chosen still wins over this line.
59
+ export function outsideAgentsMode(config) {
60
+ if (AGENT_MODES.includes(config?.outsideAgents)) return config.outsideAgents;
61
+ return 'off';
62
+ }
63
+
64
+ /* ---------------------------- session grants ------------------------------ */
65
+ // What a session may do is a list of CLI flags, and the screen offers three
66
+ // named shapes over it. The shapes are built from her CURRENT flags rather than
67
+ // from a hardcoded list, so choosing one cannot quietly drop the model or the
68
+ // git grants she spent a day getting right (CLAUDE.md: capability grants belong
69
+ // in sessionArgs, and never in her global settings).
70
+
71
+ // EVERY WAY THE CLI CAN BE PUT IN A MODE, not just the one we happen to write.
72
+ //
73
+ // Caught in review, 2026-08-23: this used to know only the two-token form, so
74
+ // `--permission-mode=bypassPermissions` and `--dangerously-skip-permissions`
75
+ // were carried through as unknown flags and appended AFTER whatever mode the
76
+ // screen had just chosen. The CLI takes the last one, so picking Plan over a
77
+ // config holding `--dangerously-skip-permissions` produced a session running
78
+ // in bypass while the screen said Plan. That is the exact class of lie this
79
+ // whole row exists to remove.
80
+ const SKIP_FLAGS = ['--dangerously-skip-permissions'];
81
+
82
+ // NOT A MODE, BUT IT UNDERMINES ONE. Anthropic: this flag "adds the permission
83
+ // mode to the cycle without activating it", and a session where bypass is
84
+ // available does not enforce plan mode's blocks. So it is not read as a mode,
85
+ // and it is dropped the moment somebody picks one, because leaving it beside a
86
+ // deliberate choice weakens exactly the choice they just made.
87
+ const SKIP_ADJACENT = ['--allow-dangerously-skip-permissions'];
88
+
89
+ // FLAGS WE DO NOT OWN THAT TAKE A VALUE, so their value is never mistaken for
90
+ // one of ours. Caught in review, 2026-08-23: with
91
+ // `--append-system-prompt --dangerously-skip-permissions` (a perfectly legal
92
+ // pair, the prompt being that literal string) the parser read the value as a
93
+ // mode and the flag was left dangling. A parser that guesses arity is a parser
94
+ // that silently rewrites somebody's command line.
95
+ //
96
+ // It is the value-taking flags a person plausibly puts in `sessionArgs`. One we
97
+ // have not listed still costs only the old behaviour, which is that its value
98
+ // lands in `other` beside it, in order, and is passed through.
99
+ const TAKES_A_VALUE = new Set([
100
+ '--append-system-prompt', '--system-prompt', '--append-system-prompt-file',
101
+ '--system-prompt-file', '--settings', '--mcp-config', '--add-dir',
102
+ '--permission-prompt-tool', '--agent', '--agents', '--resume', '--session-id',
103
+ '--fallback-model', '--betas', '--plugin-dir', '--autocompact', '--name',
104
+ '--input-format', '--output-format', '--json-schema', '--max-budget-usd',
105
+ '--tools', '--disallowedTools', '--disallowed-tools', '--setting-sources',
106
+ '--plugin-url', '--ide', '--cwd',
107
+ ]);
108
+
109
+ // SOME OF THOSE TAKE A VALUE ONLY SOMETIMES. `--resume` on its own opens the
110
+ // session picker; `--resume <id>` resumes one. Consuming the next token blindly
111
+ // turned `--resume --permission-mode bypassPermissions` into a run where the
112
+ // bypass flag survived a deliberate Plan and won by being last, which is the
113
+ // exact mismatch this file exists to prevent. Caught in review, 2026-08-23.
114
+ //
115
+ // So the rule is the shell's own: a value is the next token ONLY if it does not
116
+ // itself look like a flag. That is right for the required ones too, and it means
117
+ // a flag missing from the set above costs nothing worse than landing in `other`
118
+ // beside its value, in order, which is what always happened.
119
+ const takesValue = (flag, next) => TAKES_A_VALUE.has(flag) && next !== undefined && !next.startsWith('--');
120
+
121
+ export function parseSessionArgs(args) {
122
+ const list = Array.isArray(args) ? args.map(String) : [];
123
+ const out = { model: null, allowedTools: [], permissionMode: null, allowsBypass: false, other: [] };
124
+ let sink = out.other;
125
+ for (let i = 0; i < list.length; i += 1) {
126
+ const arg = list[i];
127
+ // A value we do not own is carried through with its flag and never read as
128
+ // anything, whatever it looks like.
129
+ if (TAKES_A_VALUE.has(arg)) {
130
+ out.other.push(arg);
131
+ if (takesValue(arg, list[i + 1])) { out.other.push(list[i + 1]); i += 1; }
132
+ sink = out.other;
133
+ continue;
134
+ }
135
+ if (arg === '--model') { out.model = list[i + 1] ?? null; i += 1; sink = out.other; continue; }
136
+ if (arg.startsWith('--model=')) { out.model = arg.slice(8); sink = out.other; continue; }
137
+ if (arg === '--permission-mode') { out.permissionMode = list[i + 1] ?? null; i += 1; sink = out.other; continue; }
138
+ if (arg.startsWith('--permission-mode=')) { out.permissionMode = arg.slice(18); sink = out.other; continue; }
139
+ // The bypass shorthand IS a mode, so it is read as one. Nothing else in
140
+ // this file has to know the flag exists.
141
+ if (SKIP_FLAGS.includes(arg)) { out.permissionMode = 'bypassPermissions'; sink = out.other; continue; }
142
+ if (SKIP_ADJACENT.includes(arg)) { out.allowsBypass = true; sink = out.other; continue; }
143
+ // Variadic: every value up to the next flag belongs to --allowedTools.
144
+ if (arg === '--allowedTools' || arg === '--allowed-tools') { sink = out.allowedTools; continue; }
145
+ if (arg.startsWith('--allowedTools=') || arg.startsWith('--allowed-tools=')) {
146
+ out.allowedTools.push(arg.slice(arg.indexOf('=') + 1)); sink = out.other; continue;
147
+ }
148
+ if (arg.startsWith('--')) { out.other.push(arg); sink = out.other; continue; }
149
+ sink.push(arg);
150
+ }
151
+ return out;
152
+ }
153
+
154
+ // CLAUDE CODE'S OWN SIX MODES, plus "custom" for flags that are none of them.
155
+ // Custom is a real answer and never silently rounded to the nearest one:
156
+ // showing "Accept edits" over grants that are not that is how a screen starts
157
+ // lying about the fleet.
158
+ //
159
+ // She was right. This used to answer 'read', 'edit', 'auto' and 'full', and
160
+ // 'read' was not a permission mode at all: it was the absence of the flag plus
161
+ // a filtered --allowedTools list, which is a different mechanism wearing a
162
+ // mode's name.
163
+ //
164
+ // The six values below are exactly the choices `claude --permission-mode`
165
+ // prints in `claude --help` on 2.1.241, with `default` in place of the CLI's
166
+ // `manual` alias because `default` is the config value the docs and the SDK
167
+ // use. All seven spellings were handed to the real CLI and all seven started.
168
+ // ONE LIST, IN shared/, because the ledger, main and the renderer all need it
169
+ // and three copies of six strings is three chances to drift.
170
+ export const CLAUDE_MODES = CLAUDE_PERMISSION_MODES;
171
+
172
+ // The alias the CLI takes for `default`, read back as `default` so one mode
173
+ // never shows up as two.
174
+ const MODE_ALIASES = { manual: 'default' };
175
+
176
+ export function permissionMode(args) {
177
+ const parsed = parseSessionArgs(args);
178
+ // Auto is Claude Code's own classifier: every tool call is checked for risk
179
+ // and for prompt injection first, the lower-risk ones run, the rest are
180
+ // blocked.It is NOT bypassPermissions, which runs that check not at all (the
181
+ // CLI's own internal name for that one is yolo), and it is not acceptEdits
182
+ // either. Measured on 2.1.241, same command in the same folder: auto refused
183
+ // to write a file outside the workspace and logged the denial;
184
+ // bypassPermissions wrote it without a word.
185
+ const raw = parsed.permissionMode;
186
+ if (!raw) return 'custom';
187
+ const named = MODE_ALIASES[raw] ?? raw;
188
+ return CLAUDE_MODES.includes(named) ? named : 'custom';
189
+ }
190
+
191
+ export function buildSessionArgs(base, mode) {
192
+ // A mode this file does not know is a bug upstream, and the safe answer is
193
+ // to refuse rather than to quietly drop the grant that is there. Silently
194
+ // removing --permission-mode and adding nothing hands the session back to
195
+ // whatever the machine's own settings say, which is the inheritance this
196
+ // whole feature exists to stop.
197
+ if (!CLAUDE_MODES.includes(mode)) throw new Error(`unknown permission mode: ${mode}`);
198
+ const parsed = parseSessionArgs(base);
199
+ const model = parsed.model ? ['--model', parsed.model] : [];
200
+ // HER TOOL GRANTS ARE CARRIED THROUGH UNTOUCHED. The old 'read' shape used to
201
+ // strip --allowedTools down to the store tools, which meant picking a
202
+ // permission mode quietly rewrote a list that took her a day to get right
203
+ // and that a permission mode has nothing to do with. Claude Code's `plan` is
204
+ // the real read-only mode and it needs no help from us.
205
+ const tools = parsed.allowedTools;
206
+ // Every mode writes its own flag, and the flag is Claude Code's own value.
207
+ // `parsed.other` cannot contain a second way of setting the mode: the parser
208
+ // above lifts the equals form and the bypass shorthand out of it, so exactly
209
+ // one permission flag reaches the CLI and it is the one on the screen.
210
+ const grant = ['--permission-mode', mode];
211
+ return [...model, ...(tools.length ? ['--allowedTools', ...tools] : []), ...grant, ...parsed.other];
212
+ }
213
+
214
+ // THE MODEL, WRITTEN BACK INTO THE SAME LIST buildSessionArgs READS FROM.The
215
+ // screen had no control over this at all; the flag was already there and only
216
+ // a text editor could change it.
217
+ //
218
+ // null is a real answer and it means "let the CLI pick", so it REMOVES the
219
+ // flag rather than writing an empty one: `--model` with nothing after it is
220
+ // how you get a session that will not start.
221
+ //
222
+ // Everything else in the list is carried through untouched and in its original
223
+ // order, for the same reason permission changes are: her grants took a day to
224
+ // get right and choosing a model must not quietly drop one of them.
225
+ export function setModelArg(base, model) {
226
+ const list = Array.isArray(base) ? base.map(String) : [];
227
+ const out = [];
228
+ for (let i = 0; i < list.length; i += 1) {
229
+ if (list[i] === '--model') { i += 1; continue; }
230
+ // AND THE EQUALS FORM. Teaching parseSessionArgs to read `--model=x` while
231
+ // this still removed only `--model x` left a config with both, and the CLI
232
+ // and our own reader would then disagree about which model was running.
233
+ // Caught in review, 2026-08-23.
234
+ if (list[i].startsWith('--model=')) continue;
235
+ out.push(list[i]);
236
+ }
237
+ // Ahead of the rest, which is where parseSessionArgs and every existing
238
+ // config already have it.
239
+ return model ? ['--model', String(model), ...out] : out;
240
+ }
241
+
242
+ /* -------------------------------- reading -------------------------------- */
243
+
244
+ const lineCount = (text) => (text.trim() ? text.trim().split('\n').length : 0);
245
+
246
+ // Claude Code: the path, whether it is really there, and where to get it when
247
+ // it is not. One reader, here, so the screen and the config cannot disagree.
248
+ export function claudeState(config) {
249
+ const found = resolveClaudeBin(config.claudeBinConfigured ?? null);
250
+ return {
251
+ claudeBin: found.path ?? config.claudeBin,
252
+ claudeFound: found.found,
253
+ // Whether the search got an answer at all.
254
+ claudeCertain: found.certain !== false,
255
+ claudeInstallUrl: INSTALL_URL,
256
+ };
257
+ }
258
+
259
+ // LOOK AGAIN, PROPERLY. The last card of the walk will not let anybody into an
260
+ // inbox on a Mac with no Claude Code on it (hers, 2026-08-23), so the button
261
+ // that says it looked again has to have looked again. The path checks were
262
+ // never cached, but the shell answer is held for thirty seconds, and a person
263
+ // who installs Claude Code and presses this within those thirty seconds would
264
+ // be told no by a memory rather than by a search. That is exactly the promise
265
+ // `forgetClaudeBin` exists for.
266
+ export function recheckClaude(config) {
267
+ forgetClaudeBin();
268
+ const state = claudeState(config);
269
+ // The config carries the path everything else spawns from, so a search that
270
+ // has just found Claude Code updates it rather than leaving the rest of the
271
+ // app pointing at where it used to not be.
272
+ if (state.claudeFound && state.claudeBin) config.claudeBin = state.claudeBin;
273
+ return state;
274
+ }
275
+
276
+ /**
277
+ * THE SAME THREE FACTS ABOUT CODEX, AND ONE ASYMMETRY THAT IS NOT AN OVERSIGHT.
278
+ *
279
+ * `found`, `certain` and the path, read exactly as `claudeState` reads Claude
280
+ * Code's, out of the sibling finder (main/codex-bin.mjs) that already computes
281
+ * all three. Until now nothing on any screen read them: `engineSettings`
282
+ * returned early without ever saying whether Codex was here, so a Mac where the
283
+ * search missed it said nothing about Codex anywhere at all.
284
+ *
285
+ * `bin` IS THE EMPTY STRING WHEN THERE IS NONE, not the installer's path.
286
+ * `claudeState` falls back to a path because Claude Code is required and
287
+ * anything spawning it should fail with a real path in the message; Codex is
288
+ * optional and `config.codexBin` is deliberately null when it is missing
289
+ * (main/config.mjs), so inventing one here would print a path to a file that is
290
+ * not there under a row that has just said it is not there.
291
+ *
292
+ * READ AT EVERY OPEN OF THE SCREEN, never taken from boot, for the reason
293
+ * claudeState is: somebody who reads "install it, then check again" and does
294
+ * exactly that has to see it turn found without restarting the app. The search
295
+ * is stat calls plus a shell answer held for thirty seconds, and it only runs
296
+ * on a Mac that has opened the gate, so no machine pays for it that would not
297
+ * read the answer.
298
+ *
299
+ * `where` IS THE FINDER'S OWN SEAM, PASSED THROUGH RATHER THAN INVENTED HERE.
300
+ * main/codex-bin.mjs is "a pure function of its inputs so a test can put Codex
301
+ * anywhere without touching the machine it runs on", and the three states this
302
+ * screen draws are its three answers; a card whose unsure state could only be
303
+ * checked by uninstalling Codex from the machine running the suite is a card
304
+ * whose unsure state is not checked. Nothing in the app passes it.
305
+ */
306
+ export function codexState(config, where = {}) {
307
+ const found = resolveCodexBin(config.codexBinConfigured ?? null, where);
308
+ return {
309
+ // Empty rather than a path when there is nothing there, which is the same
310
+ // asymmetry `config.codexBin` already keeps (main/config.mjs): a configured
311
+ // path that does not exist is still `found: false`, and printing it under a
312
+ // row that has just said Codex is not here would be the screen arguing with
313
+ // itself.
314
+ bin: found.found ? (found.path ?? '') : '',
315
+ found: found.found,
316
+ // Whether the search got an answer at all.
317
+ certain: found.certain !== false,
318
+ url: CODEX_INSTALL_URL,
319
+ };
320
+ }
321
+
322
+ /**
323
+ * LOOK AGAIN, PROPERLY, for the second engine. The twin of `recheckClaude`
324
+ * above, and it earns its place for a sharper reason: finding Codex here is
325
+ * what puts `codexBin` on the config, which is what `engineChoices` reads, so
326
+ * a search that succeeds is also what makes the Coding agent row appear
327
+ * without a restart. */
328
+ export function recheckCodex(config) {
329
+ forgetCodexBin();
330
+ const state = codexState(config);
331
+ config.codexBin = state.found ? state.bin : null;
332
+ return state;
333
+ }
334
+
335
+ export function readSettings({ config, supervisor, store }) {
336
+ const products = store.listProducts();
337
+ const status = supervisor.status();
338
+ const sessions = [...supervisor.sessions.values()];
339
+ const workspaceArgs = Array.isArray(config.sessionArgs) ? config.sessionArgs : [];
340
+ // Read once and used twice below, so the ceiling the stepper offers and the
341
+ // sentence explaining it cannot come from two different answers.
342
+ const slotsHere = machineSlots();
343
+
344
+ let standing = '';
345
+ try { standing = supervisor.readStanding(); } catch {}
346
+ // ONE document since w-3dc46f3a67, where the writing rules and the finishing
347
+ // rules were two boxes on the same page.
348
+ let messageRules = '';
349
+ try { messageRules = supervisor.readMessageRules(); } catch {}
350
+
351
+ // THE SAME LIST THE FLEET SPAWNS ON, from the one module that decides it
352
+ // (main/account-discovery.mjs): what she configured, plus any subscription
353
+ // signed in on this machine that none of hers already covers. A page that
354
+ // derived its own list is how an account she had logged into could be
355
+ // missing from the screen while she was looking straight at it.
356
+ const profiles = effectiveProfiles(config.authProfiles, { home: config.home });
357
+ const now = Date.now();
358
+ // THE ACCOUNTS PAGE IS WHERE AN ACCOUNT-SHAPED PROBLEM BELONGS, and until it
359
+ // had no word for the only one that ever really happens. A signed-out
360
+ // subscription read here as "resting", which is what a healthy account that
361
+ // has had a bad half hour also reads as, so the page could not tell her the
362
+ // one thing she could have acted on. It says it now, and it says it without
363
+ // waiting, because a page she chose to open interrupts nobody.
364
+ const troubles = status.accountTrouble ?? {};
365
+ const accounts = (profiles.length ? profiles : ['default']).map((profile) => {
366
+ const trouble = troubles[profile] ?? null;
367
+ const resting = (supervisor._profileCooldown?.[profile] ?? 0) >= now;
368
+ // 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
372
+ // folder holds no login, and then the row reads as it always did.
373
+ const identity = accountIdentity(profile === 'default' ? null : profile);
374
+ return {
375
+ profile,
376
+ email: identity?.email ?? null,
377
+ accountUuid: identity?.accountUuid ?? null,
378
+ // WHICH ONE RUNS, when she has picked one. False on every row while she
379
+ // has not, and then every account runs as it always has.
380
+ chosen: (config.activeAccount?.claude ?? null) === profile,
381
+ // The default profile is whatever ~/.claude holds; anything else is a
382
+ // second logged-in home directory, and its path is the only name it has.
383
+ label: profile === 'default' ? 'default' : path.basename(profile),
384
+ dir: profile === 'default' ? null : profile,
385
+ cooldownUntil: supervisor._profileCooldown?.[profile] ?? 0,
386
+ live: !resting && !trouble,
387
+ state: trouble ? trouble.cause : resting ? 'resting' : 'live',
388
+ trouble: trouble ? { cause: trouble.cause, since: trouble.since, note: accountSentence(trouble.cause, profile === 'default' ? null : profile) } : null,
389
+ // SESSIONS ON THIS SUBSCRIPTION, NOT SESSIONS IN A FOLDER OF THIS NAME.
390
+ // `profile` is a folder and a CODEX worker's profile is also 'default',
391
+ // so this counted Codex threads as sessions on her Claude login -- the
392
+ // exact collision `_accountKey` exists to prevent (: a fact about one
393
+ // account applied to another), reappearing in a second place. The key is
394
+ // asked for rather than spelled here, because the supervisor owns how one
395
+ // is spelled; a session written before the second engine existed carries
396
+ // no engine and reads as Claude Code, which is what it is.
397
+ running: sessions.filter((s) => supervisor._accountKey(s.engine, s.profile) === profile).length,
398
+ // WHICH PLAN THIS ACCOUNT IS ON. Nothing in the app had ever said, on
399
+ // any screen, and the founder's own bullet off a tester's onboarding was
400
+ // the question "not on max plan?" asked out loud about somebody else's
401
+ // Mac. This page already exists to hold an account-shaped fact; the plan
402
+ // is the plainest one there is, and it decides how many sessions Agentbox
403
+ // is willing to start.
404
+ //
405
+ // Read here rather than kept, because Claude Code refreshes its own
406
+ // profile as it runs and this page is opened by hand a few times a day.
407
+ // `null` when the file cannot be read, and then no screen says anything.
408
+ plan: (() => { const p = readPlan(profile); return p.known ? { label: p.label, max: p.max } : null; })(),
409
+ };
410
+ });
411
+
412
+ const projects = products.map((product) => {
413
+ let instructions = '';
414
+ try { instructions = supervisor.readProjectInstructions(product.slug); } catch {}
415
+ const override = supervisor.projectSessionArgs(product.slug);
416
+ return {
417
+ slug: product.slug,
418
+ name: product.name,
419
+ dir: product.dir,
420
+ repoPath: product.repoPath,
421
+ // THE MARK, already a drawable url or null, resolved once by listProducts.
422
+ logo: product.logo,
423
+ // 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
427
+ // per-project pause and personal mode are gone from the app entirely.
428
+ // `permission` stays readable because `projectSessionArgs` is still
429
+ // honoured for anyone who sets one in the config by hand, and the page
430
+ // draws it only where one exists, so there is always a way back to the
431
+ // workspace default and never a strip of Claude Code jargon on a page
432
+ // she opens to change her rules.
433
+ autonomous: (config.autonomousProducts ?? []).includes(product.slug),
434
+ permission: override ? permissionMode(override) : 'workspace',
435
+ permissionArgs: override ?? null,
436
+ codexMode: config.projectCodexMode?.[product.slug] ?? 'workspace',
437
+ instructions,
438
+ running: sessions.filter((s) => s.product === product.slug).length,
439
+ };
440
+ });
441
+
442
+ return {
443
+ workspace: {
444
+ agentsRunning: !supervisor.paused,
445
+ sessionsAtOnce: config.maxConcurrentSessions,
446
+ // THE PLAN THAT SET THE NUMBER ABOVE, when a plan set it. Null in the
447
+ // ordinary case: she picked the number herself, or it is the three every
448
+ // install has always had. It is only ever filled in when somebody would
449
+ // otherwise open this page, see one, and have nothing on the screen to
450
+ // explain it.
451
+ sessionsAtOnceFromPlan: config.planSlotsFrom ?? null,
452
+ // WHAT WE SUGGEST FOR THIS MAC, AND WHY. Not a ceiling: `slotsMax` is the
453
+ // control's own range and is the same number on every machine, while
454
+ // `slotsSuggested` is what main worked out from the hardware
455
+ // (main/machine.mjs). `machineNote` is the one sentence that says so, and
456
+ // it goes quiet once somebody is at or under the suggestion.
457
+ slotsSuggested: slotsHere.slots,
458
+ slotsMax: MAX_SLOTS,
459
+ machineNote: machineNote(slotsHere, config.maxConcurrentSessions),
460
+ capacity: status.capacity,
461
+ running: sessions.length,
462
+ model: parseSessionArgs(workspaceArgs).model,
463
+ // THE SECOND CODING AGENT, and everything a screen needs to draw a choice
464
+ // between the two.
465
+ //
466
+ // `engineChoices` is the supervisor's own answer, not a look at the disk:
467
+ // it asks the capability gate before it asks whether Codex is installed,
468
+ // so a Mac with Codex on it and no `engineChoice` moment in her config
469
+ // comes back with ONE entry and this screen draws exactly what it drew
470
+ // before. A page that derived its own list would offer her a choice the
471
+ // supervisor is about to refuse.
472
+ ...engineSettings(supervisor, config),
473
+ permission: permissionMode(workspaceArgs),
474
+ permissionArgs: workspaceArgs,
475
+ // ONE SWITCH FOR EVERYTHING THAT LEAVES THE MACHINE, and it is on when
476
+ // Agentbox is installed (legal/privacy.html, section 10). Off stops the
477
+ // counts AND the crash reports; there is deliberately no partial mode and
478
+ // no second switch, because the page promises there is not.
479
+ diagnostics: diagnosticsOn(config),
480
+ // Whether this copy has anywhere to send to at all. A build from source
481
+ // has no key, so the switch can read on while nothing whatsoever leaves,
482
+ // and a screen that does not say so is a screen that is lying quietly.
483
+ diagnosticsDestination: !!analyticsKey(config),
484
+ // ADHD mode: whether the ADHD rules ride under hers (w-5737fe67cf). Off
485
+ // unless she turned it on, so only `true` means on.
486
+ adhdMode: config.adhdMode === true,
487
+ // How much of her own Claude Code goes in her inbox: all / waiting / off.
488
+ // All by default, because the machine nobody has opened this page on is
489
+ // the one with thirteen forgotten sessions on it.
490
+ outsideAgents: outsideAgentsMode(config),
491
+ accounts,
492
+ // The one thing the page could not say on 2026-08-29: that both of her
493
+ // rows had become the same subscription. Null on the ordinary machine,
494
+ // where they differ.
495
+ accountsNote: duplicateAccountNote(accounts),
496
+ storePath: config.accountRoot,
497
+ // THE HOME FOLDER ITSELF, said rather than derived. The renderer used to
498
+ // get at it by taking the last segment off `storePath`, which is not the
499
+ // home folder at all: it is `<store root>/accounts`. Two screens read
500
+ // that value as a home and both were wrong with it. The walk's folder
501
+ // screen never shortened a path to `~`, and worse, the New Project card
502
+ // proposes a parent folder judged against the home it is given, so on a
503
+ // Mac with no projects yet it proposed putting the first project's code
504
+ // INSIDE the store, and on one whose only project sits in ~/Documents it
505
+ // proposed ~/Documents, which is the guarded folder this whole row exists
506
+ // to stop us proposing.
507
+ homePath: os.homedir(),
508
+ // Looked up again on every read of this screen, not taken from boot.
509
+ // Someone who reads "install it, then open this screen again" and does
510
+ // exactly that has to see it turn found, without restarting the app.
511
+ ...claudeState(config),
512
+ // AND WHAT THIS MAC'S CLAUDE CODE CALLS ITS MODELS.
513
+ //
514
+ // In the same trip, and for the same reason the line above is looked up
515
+ // again rather than taken from boot: someone who runs `claude update` and
516
+ // reopens the app has to see the new model without waiting for a release
517
+ // of Agentbox. Read off the binary, cached against its own mtime, and
518
+ // falling back to the table committed at build time on a Mac that has no
519
+ // Claude Code on it (main/claude-models.mjs).
520
+ claudeModels: claudeModels({ bin: config.claudeBin }).models
521
+ // WITH THE LEVEL EACH ONE THINKS AT WHEN NOBODY PICKS, beside its name
522
+ // rather than as a list of its own, the same shape the Codex rows
523
+ // above already use. It is per model and it really differs.
524
+ .map((m) => ({ id: m.alias, label: m.label, model: m.id, defaultLevel: m.defaultLevel })),
525
+ standingLines: lineCount(standing),
526
+ messageRulesLines: lineCount(messageRules),
527
+ projectsWithInstructions: projects.filter((p) => p.instructions.trim()).length,
528
+ projectCount: projects.length,
529
+ },
530
+ projects,
531
+ };
532
+ }
533
+
534
+ /**
535
+ * THE SECOND CODING AGENT, AS MUCH OF IT AS A SCREEN MAY KNOW.
536
+ *
537
+ * `engineChoices` is `Supervisor#engineChoices`, which is the one place that
538
+ * asks the capability gate. One entry means no choice, and every surface reads
539
+ * it the same way: the composer draws no clause, the Settings row is not drawn,
540
+ * the byline names nothing.
541
+ *
542
+ * AND CODEX'S MODEL LIST IS ONLY READ WHEN THERE IS A CHOICE. Reading it means
543
+ * parsing the 199KB cache codex-cli keeps (main/codex-models.mjs, measured
544
+ * 2026-09-04), and on a Mac with one coding agent that is work with no reader.
545
+ * The empty answer costs those machines nothing and is also exactly what they
546
+ * should be told.
547
+ *
548
+ * IT RIDES THE SETTINGS MODEL RATHER THAN THE SNAPSHOT for the same reason, the
549
+ * other way up: the snapshot is refetched every few seconds and a picker must
550
+ * not be the reason a 199KB file is parsed while she is typing. What the INBOX
551
+ * needs -- whether there is a choice, and which engine each row runs on -- is on
552
+ * the snapshot instead, and both come from the same method.
553
+ */
554
+ /**
555
+ * ONE SENTENCE OR NOTHING, and the words are shared/spawn-trouble.mjs's, not
556
+ * this file's: every sentence the app says about a dead run lives there, once,
557
+ * so two surfaces cannot end up with two vocabularies. */
558
+ function engineTroubleFor(supervisor, engine) {
559
+ const trouble = supervisor.engineTrouble(engine);
560
+ return trouble ? engineTroubleNote(trouble.cause) : null;
561
+ }
562
+
563
+ function engineSettings(supervisor, config) {
564
+ const choices = supervisor.engineChoices();
565
+ const engine = supervisor.engineFacts().workspace;
566
+ // WHETHER THIS MAC HAS ASKED TO HEAR ABOUT CODEX AT ALL, which is a different
567
+ // question from whether there is a choice, and is the one the connection card
568
+ // is drawn on. `Supervisor#engineChoiceOpened` says why at length.
569
+ //
570
+ // AND WHETHER IT CAN ACTUALLY RUN ANYTHING, which the card had no way to say.
571
+ // `engineTrouble` is the supervisor's own book, read through the account key
572
+ // it was really written at, because the Accounts page next door indexes by a
573
+ // bare Claude folder name and could never match `codex:default`. Null on a
574
+ // healthy engine, and then the card reads exactly as it always did. WHICH
575
+ // CODEX ACCOUNT, WHICH THE CARD COULD NOT SAY UNTIL NOW.The card was built
576
+ // with a reason for that silence -- auth.json held no email and no plan --
577
+ // and that stopped being true somewhere between codex-cli versions.
578
+ // main/codex-account.mjs holds the measurement. Null when nobody is signed
579
+ // 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) {
595
+ return { engine, engineChoices: choices, codex, codexModels: [], codexModelDefault: null, codexModel: null };
596
+ }
597
+ const home = supervisor._codexHome();
598
+ return {
599
+ engine,
600
+ engineChoices: choices,
601
+ codex,
602
+ // WITH EACH MODEL'S OWN REASONING LEVELS. They differ per model, and the
603
+ // strip in the model drawer draws the picked model's list, so they travel
604
+ // beside the name rather than as a list of their own that could disagree
605
+ // with it.
606
+ codexModels: codexModels({ home }).map((m) => ({
607
+ id: m.id, label: m.label, levels: m.levels.map((l) => l.id), defaultLevel: m.defaultLevel,
608
+ })),
609
+ // What her own `codex` in a terminal runs, so the card's word and her
610
+ // terminal cannot disagree. Null is a real answer and the card says
611
+ // "Codex's own" for it rather than guessing (renderer/src/models.ts).
612
+ codexModelDefault: codexDefaultModel({ home }),
613
+ // AND WHAT SHE HAS SET FOR THE WHOLE WORKSPACE, which is the value the
614
+ // Model row draws when Codex is the coding agent. Null means she has set
615
+ // none, and then the row draws the default above and nothing is sent
616
+ // (`Supervisor#codexThreadParamsFor`).
617
+ codexModel: supervisor._codexWorkspaceModel(),
618
+ // WHAT A CODEX WORKER MAY DO, for the whole workspace. The picker reads
619
+ // this; `Supervisor#effectiveCodexMode` is what a run reads, and it layers
620
+ // the project's answer over this one.
621
+ codexMode: isCodexMode(config.codexMode) ? config.codexMode : CODEX_DEFAULT_MODE,
622
+ };
623
+ }
624
+
625
+ /* -------------------------------- writing -------------------------------- */
626
+
627
+ const withSlug = (list, slug, on) => {
628
+ const set = new Set(Array.isArray(list) ? list : []);
629
+ if (on) set.add(slug); else set.delete(slug);
630
+ return [...set];
631
+ };
632
+
633
+ // One switch on one project, and there is one switch left (w-d19d6d387c). It
634
+ // writes her config, the way the workspace settings beside it do. `personal`,
635
+ // `paused` and `driven` were the other three and all three are gone from the
636
+ // app: the first two because she could not tell what they did and would never
637
+ // have used them, the third because the self-driving it belonged to is deleted.
638
+ export function setProjectSetting({ config, supervisor }, { product, key, value }) {
639
+ if (!product) throw new Error('no project');
640
+ switch (key) {
641
+ case 'autonomous':
642
+ saveConfig(config, { autonomousProducts: withSlug(config.autonomousProducts, product, !!value) });
643
+ break;
644
+ // WHICH OF HER CLAUDE CODE AGENTS THIS PROJECT TOOK. Names only: the files
645
+ // stay where their author put them and Agentbox never copies or moves one,
646
+ // so a list here that has gone stale costs nothing and an agent she
647
+ // deletes simply stops being found.
648
+ case 'agents': {
649
+ const map = { ...(config.projectAgents ?? {}) };
650
+ const names = Array.isArray(value) ? value.filter((n) => typeof n === 'string' && n.trim()) : [];
651
+ if (!names.length) delete map[product];
652
+ else map[product] = names;
653
+ saveConfig(config, { projectAgents: Object.keys(map).length ? map : undefined });
654
+ break;
655
+ }
656
+ // Refused rather than coerced if it is not one of Claude Code's six, the
657
+ // same rule outsideAgents follows below. buildSessionArgs throws on an
658
+ // unknown mode too; this is the check at the boundary, where the message
659
+ // can say which setting was wrong.
660
+ case 'permission': {
661
+ if (value !== 'workspace' && !CLAUDE_MODES.includes(value)) {
662
+ throw new Error(`unknown permission mode: ${value}`);
663
+ }
664
+ const map = { ...(config.projectSessionArgs ?? {}) };
665
+ if (value === 'workspace') delete map[product];
666
+ else map[product] = buildSessionArgs(config.sessionArgs ?? [], value);
667
+ // An empty map is removed rather than written as `{}`: her config file is
668
+ // read by people, and a key that means nothing is a key that gets asked
669
+ // about.
670
+ saveConfig(config, { projectSessionArgs: Object.keys(map).length ? map : undefined });
671
+ break;
672
+ }
673
+ // The Codex half, stored the same way and with the same 'workspace' escape
674
+ // meaning "stop having an opinion of your own".
675
+ case 'codexMode': {
676
+ if (value !== 'workspace' && !isCodexMode(value)) throw new Error(`unknown codex mode: ${value}`);
677
+ const map = { ...(config.projectCodexMode ?? {}) };
678
+ if (value === 'workspace') delete map[product];
679
+ else map[product] = value;
680
+ saveConfig(config, { projectCodexMode: Object.keys(map).length ? map : undefined });
681
+ break;
682
+ }
683
+ default:
684
+ throw new Error(`unknown project setting: ${key}`);
685
+ }
686
+ return true;
687
+ }
688
+
689
+
690
+ /**
691
+ * EVERY CODEX LOGIN THIS MAC HOLDS, in the shape the agent card draws.
692
+ *
693
+ * `profile` is the word the fleet spawns on and `default` is her primary login
694
+ * rather than a folder called "default" -- `_codexProfileHome` owns that
695
+ * mapping and is asked rather than copied, because two spellings of it is how
696
+ * the app came to show one account's models while billing another.
697
+ *
698
+ * A folder with no readable login still gets a row: it is a thing she added and
699
+ * can remove, and a row that vanishes silently is worse than one saying it is
700
+ * not signed in.
701
+ */
702
+ function codexAccountRows(supervisor) {
703
+ const chosen = supervisor.config.activeAccount?.codex ?? null;
704
+ return supervisor._codexProfiles().map((profile) => {
705
+ const home = supervisor._codexProfileHome(profile);
706
+ const id = codexAccount(home);
707
+ return {
708
+ profile,
709
+ email: id?.email ?? null,
710
+ name: id?.name ?? null,
711
+ plan: id?.plan ?? null,
712
+ planNamed: id?.planNamed ?? false,
713
+ accountId: id?.accountId ?? null,
714
+ signedIn: !!id,
715
+ // WHICH ONE RUNS. Null when she has never picked, and then every account
716
+ // runs and the card says so rather than marking one at random.
717
+ chosen: chosen === profile,
718
+ };
719
+ });
720
+ }
721
+
722
+
723
+ /**
724
+ * MAKE ROOM FOR A SECOND CODEX LOGIN AND HAND BACK THE LINE THAT FILLS IT.
725
+ *
726
+ * The card used to print a command for her to run and the command was broken:
727
+ * codex-cli refuses a CODEX_HOME that does not exist yet.
728
+ *
729
+ * So the app does the two parts she should never have had to do -- making the
730
+ * folder and telling the fleet the login exists -- and the terminal is left
731
+ * with the one part that is genuinely hers, which is signing in to OpenAI in a
732
+ * browser. `codex login` is their OAuth flow and this app has no business
733
+ * driving it.
734
+ *
735
+ * THE PROFILE IS REGISTERED BEFORE THE LOGIN, not after, and that is deliberate.
736
+ * The row appears immediately, reading "Not signed in", so a sign-in she
737
+ * abandons leaves something she can see and remove rather than an empty folder
738
+ * nothing mentions.
739
+ */
740
+ export function addCodexAccount({ config, supervisor }) {
741
+ const made = makeCodexHome({ home: config.home });
742
+ const existing = (config.codexProfiles ?? []).map((x) => x || 'default');
743
+ const list = existing.length ? existing : ['default'];
744
+ saveConfig(config, { codexProfiles: [...list, made.profile] });
745
+ supervisor.onChange?.();
746
+ return { home: made.home, profile: made.profile, command: codexLoginCommand(made.home) };
747
+ }
748
+
749
+ /**
750
+ * THE SAME DOOR FOR CLAUDE CODE, which it did not have until 2026-09-22.
751
+ *
752
+ * The Claude card described the logins on the disk and offered no way to add
753
+ * one, on her instruction of 2026-08-30: we should not encourage multi
754
+ * accounting while this was a product we sold. She withdrew that on
755
+ * w-3498e0cad2 ("we're no longer offering this as a commercially viable
756
+ * 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
759
+ * live, on one Mac.
760
+ *
761
+ * Everything else is the Codex flow exactly. Main makes the folder and
762
+ * registers the profile, so the row appears immediately reading "Not signed
763
+ * in"; the terminal is left with the browser half, which is genuinely hers.
764
+ *
765
+ * THE TOOLING IS LINKED AT CREATION, which the Codex side has no equivalent of.
766
+ * `linkAccountTooling` otherwise runs at the first spawn, and the first thing
767
+ * that happens in this folder is not a spawn, it is her own session in the
768
+ * settings terminal. Linking here means her skills, commands and agents are
769
+ * already there when she signs in.
770
+ */
771
+ export function addClaudeAccount({ config, supervisor }) {
772
+ const made = makeClaudeHome({ home: config.home });
773
+ const existing = (config.authProfiles ?? []).map((x) => x || 'default');
774
+ const list = existing.length ? existing : ['default'];
775
+ saveConfig(config, { authProfiles: [...list, made.profile] });
776
+ // A failure here is a folder without her skills in it, which is a smaller
777
+ // thing than a sign in that does not happen: the links are retried at every
778
+ // spawn anyway.
779
+ try { linkAccountTooling(made.home, { home: config.home }); } catch { /* the spawn links it */ }
780
+ supervisor.onChange?.();
781
+ return { home: made.home, profile: made.profile, command: claudeLoginCommand(made.home) };
782
+ }
783
+
784
+ export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
785
+ switch (key) {
786
+ case 'agentsRunning':
787
+ supervisor.paused = !value;
788
+ supervisor.onChange?.();
789
+ break;
790
+ case 'sessionsAtOnce': {
791
+ // THE MACHINE DOES NOT CLAMP THIS, and for one round it did. These are
792
+ // developers using the tool as they want, so the range is the control's
793
+ // own range, the same on every Mac. What the hardware does is SUGGEST a
794
+ // starting number, one row up.
795
+ const n = Math.max(1, Math.min(MAX_SLOTS, Math.round(Number(value) || 1)));
796
+ saveConfig(config, { maxConcurrentSessions: n });
797
+ break;
798
+ }
799
+ // Written as an opt-OUT (`diagnostics: false`) so that on is what a config
800
+ // she has never touched means, and so that turning it off is a key she can
801
+ // see in her own file. `saveConfig` mutates the live config too, and
802
+ // main/analytics.mjs reads the switch at every send, so off takes effect
803
+ // from the moment she moves it and not at the next launch.
804
+ case 'diagnostics':
805
+ saveConfig(config, { diagnostics: !!value });
806
+ break;
807
+ // ADHD MODE (w-5737fe67cf). Off out of the box: the key appears in her file
808
+ // only once she has touched the switch. The supervisor reads it at every
809
+ // spawn, so it applies from the next task on.
810
+ case 'adhdMode':
811
+ saveConfig(config, { adhdMode: !!value });
812
+ break;
813
+ // WHICH ACCOUNT HER WORK RUNS ON.
814
+ //
815
+ // ONE KEY HOLDS BOTH ENGINES, because a profile word means different things
816
+ // to each of them -- a Claude entry is a CLAUDE_CONFIG_DIR and a Codex entry
817
+ // is a CODEX_HOME, and both call their first login 'default'. Keeping them
818
+ // in one object keyed by engine is what stops a Codex pick being read as a
819
+ // Claude one, which is the collision `_accountKey` exists to prevent.
820
+ //
821
+ // NULL CLEARS IT, and clearing means every account runs again, which is what
822
+ // this machine did before she ever picked. `Supervisor#_narrowToChosen` also
823
+ // ignores a name that no longer exists, so a login she removes cannot wedge
824
+ // the fleet.
825
+ case 'activeAccount': {
826
+ const engineId = value?.engine;
827
+ if (!isEngine(engineId)) throw new Error(`unknown coding agent: ${engineId}`);
828
+ const profile = typeof value?.profile === 'string' && value.profile ? value.profile : null;
829
+ saveConfig(config, { activeAccount: { ...(config.activeAccount ?? {}), [engineId]: profile } });
830
+ supervisor.onChange?.();
831
+ break;
832
+ }
833
+ // HER AGENTS, HOW MANY OF THEM INTERRUPT. Refused rather than coerced if it
834
+ // is not one of the three: a mode nothing understands is an inbox that
835
+ // silently shows her nothing, which is the failure mode this codebase cares
836
+ // about most wearing different clothes.
837
+ case 'outsideAgents': {
838
+ if (!AGENT_MODES.includes(value)) throw new Error(`unknown agent mode: ${value}`);
839
+ saveConfig(config, { outsideAgents: value });
840
+ break;
841
+ }
842
+ case 'permission':
843
+ if (!CLAUDE_MODES.includes(value)) throw new Error(`unknown permission mode: ${value}`);
844
+ saveConfig(config, { sessionArgs: buildSessionArgs(config.sessionArgs ?? [], value) });
845
+ break;
846
+ // THE SAME QUESTION FOR THE OTHER ENGINE, and it is a separate key because
847
+ // it is a separate answer: a Claude Code mode and a Codex mode are not
848
+ // translations of each other and never round-trip. shared/codex-modes.mjs
849
+ // holds the three and why there are three.
850
+ case 'codexMode':
851
+ if (!isCodexMode(value)) throw new Error(`unknown codex mode: ${value}`);
852
+ saveConfig(config, { codexMode: value });
853
+ break;
854
+ // WHICH CODING AGENT PICKS A TASK UP when the task itself says nothing.
855
+ // Refused rather than coerced if it is not one of the engines, for the same
856
+ // reason `outsideAgents` is: a word nothing understands is a fleet running
857
+ // somewhere she cannot see.
858
+ //
859
+ // THIS SETS THE DEFAULT AND IT DOES NOT OPEN THE GATE. `engineFor` still
860
+ // needs the moment in `zero.config.json`, so writing `codex` here on a Mac
861
+ // that has not opted in changes nothing about what runs -- which is also
862
+ // why the row that writes it is not drawn there.
863
+ //
864
+ // AND IT WRITES WHEN, WHICH IS WHAT MAKES THE ROLLBACK LIFECYCLE HOLD
865
+ // (2026-09-05). `engineChoice` means "Codex may be chosen FROM NOW ON", and
866
+ // a workspace default with no date on it sat outside that promise: close the
867
+ // gate, reopen it with a newer moment, and a `codex` default set weeks
868
+ // earlier immediately routed every unmarked row to Codex again without her
869
+ // saying so. `engineDefaultIsStale` in shared/engines.mjs is the check, and
870
+ // says why an ABSENT stamp still means honoured. This is the only place in
871
+ // the app that writes either key, so the pair cannot be written half way,
872
+ // and both ride in one `saveConfig` patch rather than two.
873
+ case 'engine': {
874
+ if (!isEngine(value)) throw new Error(`unknown coding agent: ${value}`);
875
+ saveConfig(config, { engine: value, engineAt: new Date().toISOString() });
876
+ break;
877
+ }
878
+ // WHAT EVERY CODEX AGENT RUNS ON, which is a different setting living in a
879
+ // different place, because `--model` is a Claude Code flag and Codex has no
880
+ // argv at all: its model is a field on `thread/start`
881
+ // (`Supervisor#codexThreadParamsFor`, which is the one place the two words
882
+ // are resolved and says the precedence in full).
883
+ //
884
+ // "CODEX'S OWN" IS THE KEY BEING ABSENT, never an empty string in her file.
885
+ // Her config is read by people, and a key that means nothing is a key that
886
+ // gets asked about -- the rule `projectSessionArgs` already follows. It is
887
+ // also what keeps a Mac nobody has opened this screen on behaving exactly as
888
+ // it did: nothing set, nothing sent, `~/.codex/config.toml` decides.
889
+ //
890
+ // NOT VALIDATED AGAINST THE MODEL LIST HERE. The screen only offers slugs
891
+ // read off this Mac, and the run refuses a word Codex does not know with a
892
+ // sentence naming this setting, so a check here would be a second copy of
893
+ // that rule that can disagree with it.
894
+ case 'codexModel': {
895
+ const model = value === null || value === undefined || String(value).trim() === ''
896
+ ? undefined
897
+ : String(value).trim();
898
+ saveConfig(config, { codexModel: model });
899
+ break;
900
+ }
901
+ // WHAT EVERY AGENT RUNS ON. One flag in one list, and the empty string and
902
+ // null both mean the CLI's own default, which is the flag being absent.
903
+ case 'model': {
904
+ const model = value === null || value === undefined || value === '' ? null : String(value);
905
+ saveConfig(config, { sessionArgs: setModelArg(config.sessionArgs ?? [], model) });
906
+ break;
907
+ }
908
+ default:
909
+ throw new Error(`unknown workspace setting: ${key}`);
910
+ }
911
+ return true;
912
+ }