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.
- package/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- 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
|
+
}
|