Subject: CodexMonitor (
github.com/Dimillian/CodexMonitor), MIT, this analysis is based on main branch 2026-03-26 (commit dd61b9a, PR #584 "Refactor tray thread menu"). Author Dimillian (Thomas Ricouard)—a well-known developer in the SwiftUI open-source circle (ACarousel, MovieSwiftUI). Code size: 860 files / 28MB. Rust backend 92 files / 31,171 lines (src-tauri/); TS frontendsrc/557 ts/tsx files / 112,428 lines (counting css, 603 files / 127,161 lines); also docs/, scripts/ (iOS build chain). Runtime depends on the officialcodexCLI (codex app-serverprotocol). One-line position: a desktop command stand for the OpenAI Codex CLI—a Tauri 2 app. Each workspace spawns acodex app-serversubprocess and drives multiple Codex agent threads through the official stdio JSON-RPC protocol (not PTY screen scraping); it ships workspace/worktree management, streaming chat, diff/PR review, usage stats, dictation, and a full iOS remote daemon. Audience: readers who have already read at least one analysis in this series (especially the OpenAI Codex source analysis and the Kanban source analysis—this lecture is a “deeper companion” of the Codex analysis, and a contrast sample against kanban’s “PTY+hooks scheme”). The whole text citesfile:lineso you can check the source.
Contents
- Project overview: a command stand for the Codex CLI
- Panorama: Tauri dual-process + app-server protocol + event stream
- app-server attach: one codex process per workspace, LSP-style handshake
- Protocol cross-check: CodexMonitor × Codex’s official app-server protocol
- Thread model and event-driven state machine
- Rust shared layer: domain core shared by app and daemon
- worktree and workspace: first-class objects
- git / GitHub: libgit2 read + CLI write + gh integration
- Remote daemon and iOS: one core, two shapes
- Frontend architecture: feature-sliced + zero state libraries
- Frontend features: Composer / dictation / diff / usage
- Config tripartite: settings.json / workspaces.json / config.toml
- Engineering and tests: AGENTS.md layered discipline + unit-test density
- Development taste: Dimillian’s ten choices
- Summary: three most distinctive designs and their trade-offs
Chapter 1 · Project overview: a command stand for the Codex CLI
What is CodexMonitor? The first sentence of the README: A Tauri app for orchestrating multiple Codex agents across local workspaces. It is not another coding agent—the agent is codex; it is the agent’s command stand:
- Multi-workspace: the sidebar manages many projects; each workspace spawns a
codex app-server, with an independentCODEX_HOME. - Multi-thread: many threads (chat sessions) inside one workspace; resume/fork/archive/copy, pin/rename, per-thread drafts, interrupt/steer an in-flight turn.
- worktree isolation: give the agent an independent git worktree (independent branch + independent codex home), many agents in parallel with zero conflict—the same spirit as kanban’s one worktree per card.
- Composer: image attachments, Queue vs Steer dual follow-up modes,
$skills //prompts://review/@file autocomplete, model selection, context ring. - git/GitHub full flow: diff panel, stage/revert, commit log, branches, GitHub issues/PR (list/diff/comments), “Ask PR” sending PR context into a new agent thread.
- Usage dashboard: home usage snapshot (parse Codex’s rollout JSONL) + sidebar credits meter.
- Dictation: local Whisper (whisper-rs), hold-to-talk + waveform.
- iOS remote mode: the desktop runs a daemon (TCP JSON-RPC); the iPhone connects over Tailscale as a thin client.
How it relates to codex / kanban (two neighbors in this series)
| codex (official CLI) | kanban (orchestration board) | CodexMonitor (desktop command stand) | |
|---|---|---|---|
| Who is the Agent | itself | 7 CLI agents | only codex |
| Integration | — | PTY process + hooks reverse injection | official app-server protocol (stdio JSON-RPC) |
| Shape | terminal CLI | browser board | Tauri desktop + iOS |
| Orchestration grain | single session | card (task) | workspace × thread |
| Relation to codex | the body | codex is one of 7 (PTY) | codex is the only core, over the official socket |
The most interesting contrast: kanban and CodexMonitor do the same job (multi-agent orchestration) but take two sharply different technical routes—black-box adaptation (PTY screen scrape + hooks) vs official protocol (app-server JSON-RPC). That is the through-line of this lecture.
Digitized project shape
| Dimension | Number | Note |
|---|---|---|
| Backend | src-tauri/ 92 files / 31,171 lines | Tauri commands + shared domain core + daemon |
| Frontend | src/ 557 ts/tsx / 112,428 lines (with css, 603 files / 127,161 lines) | React + Vite + feature-sliced, 25 features |
| Tauri commands | generate_handler! registers 125 (#[tauri::command] appears 138 times in source; the gap is dictation/terminal/menu cfg dual implementations) |
lib.rs invoke_handler registration; each command is dual-track “local or forward to daemon” |
| app-server methods | ~22 | called from codex_core.rs; all official protocol methods (no private/experimental) |
| Dependencies (frontend) | 19 runtime deps; UI libraries only 6 families | lucide / react-markdown(+remark-gfm) / prismjs / xterm / react-virtual / Sentry, plus @pierre/diffs, vscode-material-icons, and 6 @tauri-apps plugins—no Tailwind/Radix/state library |
| Platforms | macOS primary + Windows/iOS | tray macOS only; Windows opt-in; iOS remote-daemon mode |
| License | MIT | extremely permissive |
Chapter 2 · Panorama: Tauri dual-process + app-server protocol + event stream
Layering convention (AGENTS.md hard rule)
┌──────────────────────────────────────────────────────────────┐
│ React frontend (feature-sliced, src/features/ 25 features) │
│ Orchestration in hooks/* and bootstrap/*; event fanout only in services/ │
└───────────────┬──────────────────────────────────────────────┘
│ Tauri IPC (125 #[tauri::command] registered)
┌───────────────▼──────────────────────────────────────────────┐
│ Rust backend (src-tauri/) │
│ ┌──────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ lib.rs cmd │→ │ shared/* │← │ bin/codex_monitor_ │ │
│ │ workspaces/ │ │ domain core │ │ daemon (remote mode) │ │
│ │ files/ ... │ │ (source of │ │ #[path] include same │ │
│ │ thin adapter │ │ truth) │ │ │ │
│ └──────┬───────┘ └──────┬──────┘ └──────────────────────┘ │
│ ▼ ▼ │
│ codex app-server subprocess (one per workspace, stdio JSON-RPC) │
└──────────────────────────────────────────────────────────────┘
- Domain logic lives in
src-tauri/src/shared/*—app and remote daemon compile into the same binary and share the same*_corefunctions, so logic cannot drift. codex/,workspaces/,git/etc. are thin Tauri-command adapters; frontend IPC only goes throughsrc/services/tauri.ts.- The daemon (
src-tauri/src/bin/codex_monitor_daemon.rs, TCP newline-delimited JSON-RPC) reuses the shared core with the app; method names/payloads align. - Frontend feature-sliced:
src/features/{app,threads,workspaces,...}; event fanout only insrc/services/events.ts.
Command registry (src-tauri/src/lib.rs, 337 lines)
invoke_handler registers 125 commands (#[tauri::command] appears 138 times in source; the extras are dictation/terminal/menu dual desktop vs mobile implementations under cfg). Every codex command has a uniform pattern: first check remote_backend::is_remote_mode; if so, call_remote (forward to daemon); else go to shared::codex_core::*_core. State is managed via state::AppState:
workspaces: Mutex<HashMap<String, WorkspaceEntry>>sessions: Mutex<HashMap<String, Arc<WorkspaceSession>>>—one app-server process per workspaceapp_settings,codex_login_cancels,tcp_daemon, etc.
Event stream (the through-line of the whole lecture)
codex app-server stdout (JSON lines)
→ backend/app_server.rs classify (request response / thread/list cwd attribution / pure notification)
→ AppServerEvent{workspace_id, message}
→ event_sink.rs → app.emit("app-server-event")
→ frontend services/events.ts createEventHub lazy-subscribe fanout
→ useAppServerEvents (30+ method allowlist + approval suffix match)
→ useThreadEventHandlers → each reducer slice dispatch
→ React UI
Chapter 3 · app-server attach: one codex process per workspace, LSP-style handshake
spawn (src-tauri/src/backend/app_server.rs, L749 spawn_workspace_session)
check_codex_installation(codex --version).build_codex_command_with_binbuilds the command: configurablecodexBin/codexArgs; Windows special-cases cmd/bat.current_dir(entry.path)sets the working directory, setsCODEX_HOME, args["app-server"]—one independent codex process + independent codex home per workspace (no mutual pollution; config/session/usage data are naturally isolated).- stdio fully piped.
stdio read/write and JSON-RPC codec
- Write side:
write_message(JSON single line\n-delimited). - Read side two tasks: stdout uses
BufReader::lines()thenserde_json::from_strper line; stderr is forwarded ascodex/stderrevents. send_request_for_workspace(L494): incrementnext_id→ registerpending: oneshotandrequest_context(workspace, method)→ timeout 300s → response fills back by id.send_notification/send_response(answers to server requests such as approval).
Event classification (the key to intelligent routing)
Each stdout line is first judged as one of three classes:
- Has
id+result/error= request response → look uprequest_contextto restore workspace/method, andextract_related_thread_idsbackfills thethread_workspacemap. thread/listresponse: extra parse of cwd →resolve_workspace_for_cwd(longest-prefix match) establishes thread→workspace attribution;memory_consolidationsub-agent threads joinhidden_thread_idsand instead emit a syntheticcodex/backgroundThread(hide)event (memory-consolidation sub-agents do not pollute the main UI).- Pure notification: route workspace by
thread_workspace; global notifications (account/*) broadcast to all workspaces under the session.
Essential difference from kanban’s PTY+hooks scheme
This is the most important contrast point of the analysis:
| kanban | CodexMonitor | |
|---|---|---|
| Data source | PTY byte stream + hooks text callback | structured JSON-RPC notifications |
| Status semantics | parse hook payload / poll rollout jsonl | typed events (thread/status/changed, item/started/completed, delta streams) |
| Control direction | one-way (receive only) | two-way (send_response answers permissions, turn/steer interrupts) |
| Reliability | depends on each agent’s hook implementation; Codex even has to poll | guaranteed by the protocol itself |
| Cost | an adapter matrix for 7 agents | bound to Codex’s official protocol (single ecology) |
One sentence: kanban treats the agent as a “black box” (screen scrape + text stream); CodexMonitor treats codex as a “service” (official socket, direct). The former is wide; the latter is deep.
Chapter 4 · Protocol cross-check: CodexMonitor × Codex’s official app-server protocol
Every Codex-side citation in this chapter comes from the official Codex source checked out in this repository (
参考项目/codex/codex-rs/). This is a “contrast experiment” aligning CodexMonitor’s code with Codex itself.
4.1 Full protocol method surface (Codex side)
Codex-side codex-rs/app-server-protocol/src/protocol/common.rs (4072 lines) defines the full protocol surface (every common.rs: line number in this section refers to that file):
- Client requests (ClientRequestPayload, common.rs:482–1235):
initialize:482,thread/start:491,thread/resume:497,thread/fork:503,thread/archive:509,thread/list:630 (cursor/limit/sortKey/sourceKinds),turn/start:836,turn/steer:842,turn/interrupt:848,review/start:889,model/list:895,collaborationMode/list:964,skills/list:684,account/rateLimits/read:1058,account/read:1192, etc. - Experimental methods (
#[experimental]):thread/search,environment/*,process/spawn|kill,thread/increment_elicitation,memory/reset. - Server→client requests (ServerRequestPayload, i.e. approval requests, common.rs:1505–1554):
item/commandExecution/requestApproval,item/fileChange/requestApproval,item/tool/requestUserInput,item/permissions/requestApproval,item/tool/call,attestation/generate. - Server notifications (common.rs:1658–1731):
thread/started,thread/status/changed,turn/started,turn/completed,item/started,item/completed,item/agentMessage/delta,item/plan/delta,command/exec/outputDelta,account/updated,model/rerouted, etc.
4.3 Official design intent of app-server (Codex-side AGENTS.md)
AGENTS.md:104–106: lists app-server APIs as a breaking-change must-search surface for external integrations (clients like CodexMonitor must watch versions).:270: new APIs only go into v2; v1 is frozen;:273<resource>/<method>singular naming;:274–276on-the-wire camelCase;:286–287experimental surface gated by#[experimental];:294–296list methods default to cursor/limit paging.- Official README position: app-server = the “socket” for editors/IDEs—so an IDE does not have to parse terminal ANSI or hack hooks, and can drive Codex with an LSP-style protocol.
CodexMonitor is walking the officially designated path: src-tauri/src/backend/app_server.rs:760–771 spawn args, build_initialize_params (:419–430) clientInfo{name:"codex_monitor"} + capabilities.experimentalApi:true, initialized notification (:1091)—item-by-item match with official Codex codex-rs/app-server/src/request_processors/initialize_processor.rs:70–101 handshake contract.
4.4 Rollout JSONL usage-stats cross-check
- Official format:
~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl, line structureRolloutLine{timestamp, ordinal, item}(Codexcodex-rs/protocol/src/protocol.rs:3380);itemis an 8-variant enumRolloutItem:SessionMeta | ResponseItem | InterAgentCommunication | InterAgentCommunicationMetadata | Compacted | TurnContext | WorldState | EventMsg(same file :3186–3199).TokenCountEventcontainsinfo(input/cached/output of total/last_token_usage) +rate_limits(same file :2140–2143). - CodexMonitor (
src-tauri/src/shared/local_usage_core.rs, 840 lines):spawn_blockingparses JSONL—session_meta/turn_contextcwd filters the workspace (:220–229), take model (:232–237), token_count delta-dedup counts input/cached/output (:281–380),agent_message/agent_reasoningcount duration (:260–279),response_itemassistant counts runs (:383–409). - Took: token usage, cache, model, cwd, active duration/count. Missed:
rate_limits(credits/plan_type—the most valuable field already on disk officially), originator/source (CLI vs VSCode vs appServer), sub-agent lineage, exec_command/file_change call counts.
Taste observation: using already-on-disk official data for local stats (no API call, no hooks) is the smart move of “same frequency as the official data format”; but 840 lines of handwritten parse also expose “enough is enough”—the credits stats that should actually be done were not read.
4.5 Three small cross-checks: config.toml / auth.json / rules
[features](Codexcodex-rs/config/src/config_toml.rs:455→FeaturesTomlincodex-rs/features/src/lib.rs:656): keys defined in the same filesteer:1286 (always on),collaboration_modes:1340 (always on),unified_exec:856,apps:1106 (needs chatgpt auth).personalitytop-level enumNone|Friendly|Pragmatic(Codexcodex-rs/protocol/src/config_types.rs:311–317). The 5 fields CodexMonitor syncs match official key names exactly, and it correctly rejects the deprecatedcollabkey (CodexMonitorsrc-tauri/src/codex/config.rs:50–53).auth.json:{OPENAI_API_KEY, tokens:{id_token, access_token, refresh_token, account_id}, last_refresh}; idToken JWT payload containschatgpt_plan_type/email. CodexMonitorsrc-tauri/src/shared/account.rs:63–99URL_SAFE_NO_PAD decode → takechatgpt_plan_type/email corresponds exactly, and is the fallback whenaccount/readfails (account_read_coreinsrc-tauri/src/shared/codex_core.rs:627–650).prefix_ruleis an execpolicy Starlark builtin (defined inexecpolicy/src/parser.rs;config/src/requirements_exec_policy.rs:51–58is its mirrored structure inrequirements.toml, and the source comments say so),decision="allow"lets the agent skip approval. CodexMonitorsrc-tauri/src/rules.rs:97–100appendsprefix_rule(pattern=[...], decision="allow")to~/.codex/rules/default.rules—semantics correct.
These four cross-checks say one thing: when Dimillian wrote CodexMonitor he aligned field-by-field with Codex’s official source and config formats—protocol method names, handshake params, config keys, auth JWT, rules syntax, all aligned. This is the integration taste of “stay in contract-level sync with upstream.”
Chapter 5 · Thread model and event-driven state machine
5.2 The backend does not maintain a state machine
Key design: CodexMonitor’s Rust side does not maintain a thread state machine—state is an event-driven frontend reducer. thread/status/changed, turn/started/completed, item/started/completed, approval requests (*requestApproval suffix) are routed by METHODS_ROUTED_IN_USE_APP_SERVER_EVENTS in useAppServerEvents.ts into useThreadsReducer slices; threadNormalize.ts is the camelCase/snake_case fallback. running/idle/needs_approval are entirely derived on the frontend from events.
Taste observation: this is the corollary of “the protocol is the truth”—since app-server will push authoritative events, the backend does not need a mirrored state (one less layer of state-sync bugs). Contrast kanban’s backend state machine (session-state-machine.ts 78 lines): CodexMonitor put state transition into the frontend reducer (1179 lines).
Chapter 6 · Rust shared layer: domain core shared by app and daemon
src-tauri/src/shared/ has 17 modules / 31 files / 11,197 lines—a pure-logic layer, no Tauri/IPC dependency, only types, codex::config/home, backend::app_server::WorkspaceSession, storage. App and daemon compile into the same binary and each call the same *_core functions.
6.1 worktree core (workspaces_core/worktree.rs, 642 lines)
add_worktree_core is a highly generic function (9 closures injected: FSpawn/FGit/FSanitize etc.). The core is simply git worktree add, three branch cases: branch already exists → worktree add <path> <branch>; not local but has a remote tracking → worktree add -b <branch> <path> <remote_ref>; else worktree add -b <branch> <path>.
- Root-directory priority: workspace-level
worktrees_folder> globalglobal_worktrees_folder/<parent_id>>data_dir/worktrees/<parent_id>. - After create, generate
WorkspaceEntry{kind: Worktree, parent_id, worktree:{branch}}; cancopy_agents_mdinherit the parent repo’s agents.md; a marker file records whether a one-shotworktree_setup_scripthas already run. remove_worktree_core: kill session →git worktree remove --force→git worktree prune --expire now.rename_worktree_core:git branch -m+git worktree move(with full rollback).
6.2 git / GitHub (git_ui_core/*)
Hybrid implementation (a taste highlight of this analysis):
- Reads go through libgit2 (git2 crate): status/diff (diff.rs StatusOptions/Status bit-enum mapping A/M/D/R/T, DiffOptions patch, 10MB image base64, 2MB text cap, has_ignored_parent_directory); log (log.rs revwalk + graph_ahead_behind). Ignored-file recognition is the only place that uses git check-ignore --stdin -z CLI.
- Writes go through git CLI (commands.rs, tokio_command): commit/push/pull (--autostash→--no-rebase fallback chain) / fetch/stage/revert/checkout/git init.
- GitHub all through gh CLI (github.rs): gh issue list --json, gh pr list --json, gh pr diff --color never (parse_pr_diff text parser splits files), gh api /repos/.../issues/N/comments --jq, gh pr checkout, gh repo create.
6.3 Account and usage
account.rs(221 lines):account/rateLimits/read+account/read; on failure fall back to parsing$CODEX_HOME/auth.json→ idToken → JWT →chatgpt_plan_type/email (Chapter 4 already cross-checked the official format).local_usage_core.rs(840 lines): scan$CODEX_HOME/sessions/<YYYY-MM-DD>/*.jsonl(including each workspace’s independent codex_home), aggregate daily input/cached/output tokens, agent_time_ms, agent_runs, compute last7/last30, cache hit rate, peak day, top4 models;MAX_ACTIVITY_GAP_MS=2minmerges runs.
6.4 Multi-agent config (agents_config_core.rs, 1026 lines)
Read/write [features] multi_agent, [agents] max_threads/max_depth (defaults 6/1, clamped 1-12/1-4); agent definitions are [agents.<name>] tables; managed agent config lands in $CODEX_HOME/agents/<name>.toml. Full CRUD (create generates from a template, update rename also changes the filename, delete), including path-traversal protection and config rollback.
6.5 Design taste
- Why shared is independent: app and daemon dual-process share the same crate’s pure logic; all state is injected via
Mutex<HashMap>; side effects (git/fs/RPC) via closures or type injection—naturally unit-testable (tests.rs 437 lines). - RPC reuse:
git_rpc.rs/workspace_rpc.rsonly declare method-name constants + DTOs (to_params/from_params); implementation is all in*_core—the daemon’s handler and the app’s client share the same signatures; one thin map is enough. - Hybrid git strategy: read with libgit2 (fast, in-memory, type-safe), write with CLI (behavior-faithful), worktree orchestration also with CLI—pragmatic, not dogmatic.
Chapter 7 · worktree and workspace: first-class objects
7.1 Workspace model (workspaces_core)
WorkspaceEntry: id/name/path/kind/parent_id/worktree/settings. kind has ordinary repo, worktree, clone. Persisted in workspaces.json (app data directory).
- add_workspace: local path or
git clone. - add_worktree: build a worktree from the parent repo (Chapter 6.1).
- add_clone: URL clone a new workspace.
- Each workspace can have independent settings (codexBin, codex_home, worktrees_folder).
7.2 worktree contrast with kanban
| kanban | CodexMonitor | |
|---|---|---|
| Grain | one worktree per card (task-level) | one workspace can build many worktrees (agent-level) |
| Identity | worktree is the card’s “shadow” | worktree is a kind of workspace (first-class object) |
| Lifecycle | trash the card and it is deleted | CRUD, rename (with rollback), change upstream |
| Purpose | resource isolation for parallel tasks | independent branch + independent codex home for the agent |
Taste observation: kanban treats worktree as a by-product of task execution; CodexMonitor treats worktree as a manageable entity (WorktreeEntry can rename/pin/archive)—it is a “workspace-management product,” not a “task board”; product intent decided the data model.
Chapter 8 · git / GitHub: libgit2 read + CLI write + gh integration
(The core mechanism is already covered in Chapter 6.2; this chapter supplements the frontend face.)
- diff panel: render with
@pierre/diffs+ homemadediffsWorker(WorkerPool) +@tanstack/react-virtualvirtual list; theme in design-system/diffViewerTheme. - stage/unstage/revert:
stage_git_file/unstage_git_file/revert_git_file(useGitActions). - Inline comments: GitHub PR review—
usePullRequestLineSelectionselects lines,get_github_pull_request_commentsfetches, submit as an independent review thread. - “Ask PR”: when a PR is selected,
prefillDrafta PR summary into composer;runPullRequestReview: ensure the workspace is connected → reuse the current thread or create new →buildPullRequestReviewPrompt(PR metadata + diffs ≤6 files×40 lines + comments ≤8 + intent instruction, safety fence) →sendUserMessageToThreadinto a new agent thread and activate.
Taste observation: “Ask PR” is the clever stroke of “hand the GitHub flow to the agent”—the agent can read PR context and work by intent. That is CodexMonitor’s differentiated value against a traditional git GUI.
Chapter 9 · Remote daemon and iOS: one core, two shapes
src-tauri/src/bin/ is 4,902 lines in all: daemon entry (1966) + daemonctl (1414) + rpc domain handling (codex 510 / workspace 273 / rpc.rs 201 / git 196 / prompts 149 / daemon 60) + transport (103) + dispatcher (30).
9.1 Daemon architecture
- Protocol: TCP + newline-delimited JSON-RPC (JSONL, one JSON object per line), not HTTP. Default
127.0.0.1:4732(daemon internal) /0.0.0.0:4732(daemonctl external). - Frame format: request
{"id":n,"method","params"}; response{"id":n,"result"|"error"}; events have no id:{"method":"app-server-event","params":...}(broadcast::channel(2048)pushes to all authenticated connections). - Auth:
authhandshake. Token from--tokenorCODEX_MONITOR_DAEMON_TOKEN; when unauthenticated, non-auth requests all returnunauthorized;--insecure-no-authskips. Concurrency limit:Semaphore(32)per connection.
9.2 RPC dispatch and reuse
dispatcher.rs try_handle in order: daemon → workspace → codex → git → prompts. Reuse is extremely high: the daemon entry uses #[path=] to include the main-program modules (backend/shared/files/workspaces_core etc.) directly; DaemonState methods almost 1:1 wrap xxx_core—the same batch of core as local Tauri commands. The only difference is AppState → DaemonState + DaemonEventSink; menu_set_accelerators degrades to no-op (“remote parity”).
9.3 daemonctl lifecycle control (most of the 1414 lines)
- status:
probe_daemonthree-step state machine—ping(if unauthenticated, receive unauthorized) →auth→daemon_info; produceNotReachable / Running{auth_ok} / NotDaemon. - start: first probe; if running but identity/version/mode mismatch (
should_restart_daemon) → RPCdaemon_shutdowngraceful stop → poll 20×100ms → if still not gone,lsof -iTCP:<port>parse PID → SIGTERM → SIGKILL. Force-kill is allowed only when auth passed and the process name iscodex-monitor-daemon(can_force_stop_daemon). - No log file, no PID file—PID is entirely reverse-looked-up from the port (minimalism, but depends on lsof).
- command-preview: output a full start-command template (token placeholder
<remote-backend-token>).
9.4 Why iOS needs remote mode
iOS has none of the desktop resources: cannot spawn a codex app-server subprocess, cannot run git, no local workspace filesystem—so it is a thin client connecting to the desktop daemon. terminal_mobile.rs and dictation/stub.rs all commands directly return "Terminal/Dictation is not available on mobile builds." (terminal needs a local PTY; dictation needs a local Whisper model).
9.5 Tailscale integration
tailscale_status: scan candidate binaries (PATH → homebrew → Tailscale.app bundle) → tailscale version verify (looks_like_tailscale_version against GUI error output) → tailscale status --json. macOS uses launchctl asuser <uid> to run as the GUI user. suggested_remote_host parses DNSName/TailscaleIPs/CurrentTailnet to produce macbook.tailnet.ts.net:4732. The iOS side fills the suggested host + token into Settings > Server.
9.6 Taste observation
~5000 lines looks like a lot; the core logic is all a reused thin shell. What is genuinely new is three layers of glue: #[path] module splicing, JSON param-parse boilerplate, daemonctl lifecycle management. Why it is worth it: mobile cannot run codex locally; without this you would rewrite the entire backend for iOS—“one core + one protocol” serving desktop and iOS at once, behavior naturally consistent. The cost: the desktop side gains a class of “process + port + version drift” ops complexity; TCP plaintext token depends on Tailscale’s private network rather than its own encryption.
Chapter 10 · Frontend architecture: feature-sliced + zero state libraries
10.1 Scale and organization
src/ has 603 ts/tsx/css files, about 127K lines; src/features/ 496 files / 103K lines, 25 features, using feature-sliced components/hooks/utils subdirectory convention:
| feature | files/lines | Duty |
|---|---|---|
| app | 141 / 27,205 | startup, layout, sidebar, controllers (heaviest) |
| threads | 70 / 20,636 | chat-thread state machine |
| settings | 42 / 12,111 | settings page |
| git | 59 / 9,099 | diff/log/branch/GitHub |
| workspaces | 36 / 6,931 | workspace management |
| composer | 32 / 6,270 | input box + autocomplete |
10.2 Entry and routing
- No router library, purely state-driven.
App.tsx(64 lines) usesuseWindowLabel()(read Tauri window label) to split—aboutwindow lazy-loads<AboutView>, else<MainApp />. main.tsx: Sentry init +app_opencount + mobile gesture/viewport compatibility.MainApp.tsx(1882 lines) giant composition root: no Context; ~70 hooks’ return objects are passed as props layer by layer. View switching relies onactiveTabstate (home|projects|codex|git|log) +centerMode(chat/diff) +showHome.
10.3 State-management choice (the taste core)
Zero external state libraries: no redux/zustand/jotai/mobx (grep-verified). Pure useReducer + useState + useRef, explicit props drill-down.
threadReducer(1179 lines, 6 slices, no third party): threadLifecycleSlice (493 lines) / threadItemsSlice (333 lines) / threadConfigSlice / threadQueueSlice / threadSnapshotSlice / common. About 40 actions (ensureThread/upsertItem/appendAgentDelta/appendToolOutput/appendReasoningSummary/markProcessing…).useThreads.ts(938 lines, main aggregator) internally uses ref-mirrored state (itemsByThreadRefetc.) for event callbacks to read synchronously—solving the classic “closure captured stale state” problem.- Bridge from events to reducer:
useThreadEventHandlers→useThreadTurnEvents/useThreadItemEvents/useThreadApprovalEvents→ each callback dispatch.
Why no external store (taste judgment): 1. A single-workspace desktop app; the state graph converges to “one activeWorkspace + a pile of threads”; a reducer is enough; 2. Event-dense streaming updates (delta-level) need ref mirror + dispatch; an external store’s benefit is small; 3. feature-sliced itself provides module boundaries, avoiding global-store coupling; 4. The cost is MainApp’s giant props passing and a lot of useMemo/useCallback glue.
10.4 Component library and styles
No Tailwind, no Radix, no shadcn. Dependencies only lucide-react (icons), react-markdown+remark-gfm, prismjs, @xterm/xterm, @tanstack/react-virtual, Sentry. UI is all handwritten: CSS-variable tokens (ds-tokens.css) + semantic class names; components are ordinary function components assembled into ModalShell/ToastShell/PanelShell.
10.5 Frontend contrast with kanban
The architecture is isomorphic (backend process + Tauri/browser frontend + event-stream fanout + reducer-driven UI), but CodexMonitor’s state management is clearly heavier—reducer 1179 lines/40 actions, event-parse hooks thousands of lines, ref-mirror sync, multi-workspace keyed state. The root of the difference: kanban card state is a coarse-grained snapshot (fetch then wholesale replace); CodexMonitor has to carry streaming delta rendering (agentMessage append per delta, tool output stream, reasoning stream) + approval queue + token/rate-limit live panel + subagent cascade archive. But the architecture layer is actually lighter: no store middleware, no selector library, no async actions; complexity is suppressed by feature-sliced directories and a huge unit-test count.
Chapter 11 · Frontend features: Composer / dictation / diff / usage
11.1 Composer autocomplete (fully homemade)
useComposerAutocompleteState.ts assembles three trigger groups: $ (skills+apps), / (8 hardcoded slash commands + prompts), @ (files). All data goes through RPC: skills→skills_list, apps→apps_list, prompts→prompts_list (all forwarded via daemon to Codex app-server); files→list_workspace_files (Rust scans disk directly, cap 500). Trigger decision / subsequence scoring / highlight are all handwritten; UI is ComposerSuggestionsPopover positioned above the textarea; no third-party autocomplete library. Selecting $ an app generates an app://id mention binding; /prompts: supports findNextPromptArgCursor Tab-jump of argument slots.
11.2 Queue vs Steer (a model of interaction polish)
ComposerSendIntent = "default"|"queue"|"steer". resolveSendMessageOptions: canSteerCurrentTurn = isProcessing && steerEnabled && activeTurnId. steer → RPC turn_steer; queue/unsupported → send_user_message (turn/start; message enqueue is managed by app-side ComposerQueue). Default mode from setting followUpMessageBehavior; Shift+Cmd/Ctrl+Enter sends the opposite mode (oppositeSubmitIntent). Failures distinguish steer_failed and handle stale-steer errors; runtime button copy switches between Steer/Queue.
11.3 Image attachments
Three entries all land as a local-path array (useComposerImages stored by thread draftKey): picker uses tauri open; drag-drop uses window-level onDragDropEvent + local DOM drop (web side FileReader to data URL); paste by useComposerImageDrop.handlePaste reading clipboardData. What is sent to Codex is a data-URL string array: normalizeImagesForRpc in remote/mobile mode calls Rust read_image_as_data_url one by one.
11.4 Dictation (Whisper)
Rust side whisper-rs local inference; real.rs defines ggml-tiny/base/small/medium/large-v3 download from HuggingFace; download/cancel/remove/status go through daemon RPC; progress events push to the frontend. waveform is frontend-drawn 36 dynamic bars (DictationWaveform, subscribe level events).
11.5 Usage and notifications
- Usage: Home usage =
local_usage_snapshot(30 days)(Rust reads Codex home data,useLocalUsage5-minute poll); sidebar credits meter =account/rateLimits/updatedAppServerEvent → RateLimitSnapshot →getUsageLabels. - Notifications: purely frontend-driven, subscribe AppServerEvents (turn complete / approval request); system notifications use
@tauri-apps/plugin-notification; Rust onlysend_notification_fallback—on macOS debug, osascript AppleScript (not notify_rust). Sound uses local mp3 assets + HTMLAudio. All have min-interval / window-focus suppression.
11.6 Taste observation
Bias toward homemade: autocomplete engine, prompt history, rank scoring, waveform, usage view model all handwritten; only the heavy work is given to libraries (@pierre/diffs, xterm, whisper-rs, tauri plugins, react-virtual). Interaction polish: stale-steer tolerance, drag-position devicePixelRatio normalize, code-fence expand, /prompts: Tab-jump args, hold-to-talk, follow-up hint bar dynamically explaining Steer availability, mobile touch layout branch, every RPC writes a DebugEntry for tracing.
Chapter 12 · Config tripartite: settings.json / workspaces.json / config.toml
| File | Contents | Ownership |
|---|---|---|
settings.json (app data directory) |
AppSettings: codexBin/codexArgs, backendMode, remote backend host/token, shortcuts, defaultAccessMode, uiScale/theme, global_worktrees_folder | CodexMonitor itself |
workspaces.json |
WorkspaceEntry list (id/name/path/kind/worktree/settings) | CodexMonitor itself |
config.toml ($CODEX_HOME) |
Codex itself: features (steer/collaboration_modes/unified_exec/apps), personality, model | codex is the source of truth |
Sync strategy (settings_core.rs, 70 lines + codex/config.rs): 5 fields synced with config.toml (collaboration_modes_enabled/steer_enabled/unified_exec_enabled/experimental_apps_enabled + personality) are bidirectionally synced in both read and write—get_app_settings_core overwrites in-memory values from config.toml (codex is authoritative); update_app_settings_core writes back to config.toml (via toml_edit in config_toml_core.rs) + writes local storage.
Taste observation: respect “Codex’s config belongs to Codex”—the App only syncs the few switches its own UI exposes, does not full-mirror, avoiding two configs fighting. Chapter 4 already confirmed key names match official exactly.
Chapter 13 · Engineering and tests: AGENTS.md layered discipline + unit-test density
13.1 AGENTS.md (6KB of tribal knowledge)
- Domain logic → shared/*; commands → thin adapter layer; frontend IPC only through services/tauri.ts; event fanout only in services/events.ts.
- Frontend feature-sliced:
src/features/{app,threads,...}, orchestration in hooks/ and bootstrap/. - Daemon and app reuse shared core; method names/payloads align (remote parity).
docs/codebase-map.md: task-oriented file finding (“if you need X, edit Y”)—this is a navigation document written for agents.
13.2 Test density
- Rust side:
git_ui_core/tests.rs437 lines, tray.rs 6 unit tests, files io.rs 6 symlink-escape tests, worktree/workspaces each have tests—the pure-logic layer is testable because of closure injection. - Frontend: every hook has
*.test.ts(x)(useSettingsDefaultModels, useThreads, composer, etc.). - Integration:
main.test.tsx,smoke, etc.
13.3 Engineering-culture contrast with Codex / kanban
- codex: bazel monorepo, 126 crates, AGENTS.md long and complete (including app-server API best practices).
- kanban: GritQL static checks + husky + dogfood (use kanban to develop kanban).
- CodexMonitor: layered discipline written into AGENTS.md (agent-readable, agent-executable), docs/codebase-map.md navigation, dual-platform (desktop + daemon) shared-core architecture tests,
.codex/directory (the project itself is developed with Codex).
Commonality: all three projects are shaped by an agent-first development paradigm—AGENTS.md is not documentation but “an engineering contract for the agent”; the codebase structure has to serve the agent’s navigability.
Chapter 14 · Development taste: Dimillian’s ten choices
This chapter is the center of gravity of the lecture. Dimillian (Thomas Ricouard) is known for SwiftUI open-source components (ACarousel etc.), yet for a “tool that orchestrates AI agents” he chose Tauri (Rust + Web frontend). The choice itself leaks a lot of information. Each of the ten choices below is annotated with source evidence.
Choice 1: connect over the official protocol, not black-box scrape
CodexMonitor did not take the “spawn Codex’s TUI then parse the screen” path (that is what kanban does to Codex). It uses codex app-server + stdio JSON-RPC. This shows respect for the “upstream ecology”: the official side provided a socket (the app-server protocol is designed for IDEs), so use the socket; rather bind to the Codex ecology than do a fragile screen adapter. Chapter 4 confirmed all 22 methods are official protocol methods, handshake params item-by-item match, config key names consistent, auth JWT parse corresponds, rules syntax correct—this is “contract-level alignment” code, behind it the patience of reading official source field by field.
Choice 3: read with libgit2, write with CLI, GitHub with gh
Not dogmatic—reads want “fast and type-safe” (libgit2 pure memory); writes want “behavior fully faithful to git” (CLI); GitHub directly reuses gh (do not rebuild an API client). Pick the most suitable tool for each scene, rather than a unified abstraction. This is seasoned engineering judgment: libgit2’s write path has many pits (config differences, hooks); the read path is stable.
Choice 4: worktree is a first-class object, not a task by-product
Contrast kanban (worktree is the card’s shadow; trash and it is gone): CodexMonitor made worktree a complete entity of WorkspaceEntry{kind: Worktree}: CRUD, rename (with rollback), change upstream, inherit agents.md. Product intent decides the data model—it is a “workspace-management product”; worktree is an independent execution environment for the agent (independent branch + independent codex home).
Choice 5: frontend zero state libraries + feature-sliced
No redux/zustand; pure useReducer + ref mirror + explicit props drill-down; no Tailwind/Radix; CSS-variable tokens + handwritten components; UI side uses only 6 families of libraries. “Complexity is suppressed by directory structure and discipline, not by libraries.” Streaming delta rendering needs ref mirror + dispatch (useThreads.ts 938 lines); an external store’s benefit here is small; feature-sliced’s 25 feature directories provide a strong enough module boundary.
Choice 6: if you can write it yourself, write it yourself
Autocomplete engine (trigger decision / subsequence scoring / highlight), prompt history, rank scoring, waveform (36 dynamic bars), usage aggregation (840-line JSONL parse), frontmatter parse, rules dedup, lock files—all handwritten; only the heavy work is given to libraries (xterm, whisper-rs, @pierre/diffs, react-virtual). Dependency minimalism: 600+ frontend files, only 19 runtime deps, of which UI-related only 6 families.
Choice 7: macOS first, but not only macOS
Tray macOS only (tray.rs 701 lines; non-macOS empty implementation), window appearance uses objc2-app-kit calling NSAppearance directly, icon resolve calls defaults/sips—native platform APIs called directly, not abstracted; meanwhile Windows opt-in (separate tauri config to avoid macOS-only window effects), iOS remote mode. This is the strategy of “take the primary platform to the extreme; other platforms follow pragmatically.”
Choice 8: iOS remote daemon—one core, two shapes
iOS cannot run Codex locally, so start a desktop daemon (TCP JSONL JSON-RPC) and let the iPhone use Tailscale. #[path] include reuses main-program modules; daemonctl uses port reverse-lookup of PID + version compare + force-kill safety gate to manage lifecycle. This is “architectural investment for multi-end shapes”—one core + one protocol, desktop/iOS behavior naturally consistent; the cost is daemon process/port/version-drift ops complexity (most of daemonctl’s 1414 lines is preventing “killing the wrong process”).
Choice 9: agent-first development
AGENTS.md is “an engineering contract for the agent” (layering rules, remote parity, feature-sliced conventions); docs/codebase-map.md is “if you need X, edit Y” task navigation; the repo ships .codex/ config—this project itself is developed with Codex (isomorphic with kanban’s dogfood). Codebase structure serves the agent’s navigability; this is a common trait of the 2025-2026 generation of AI-native project clusters.
Choice 10: defensive programming + interaction polish
canonicalize + symlink-escape checks (6 tests), 400KB read truncation, strict UTF-8, Arc::ptr_eq against races, stale-steer tolerance, disconnect DISCONNECTED_MESSAGE—engineering conservatism; meanwhile Shift+Cmd+Enter reverses Queue/Steer, hold-to-talk waveform, /prompts: Tab-jump args, follow-up hint bar dynamically explaining—product aggressiveness. Conservative engineering + aggressive interaction is the typical temperament of a mature independent developer.
Taste in one sentence
With the fewest dependencies and the cleanest architectural layering, put the official protocol to work, and make a “command stand for Codex”—restrained and conservative in engineering, polished in product, and architecturally investing early for multi-end shapes.
Chapter 15 · Summary: three most distinctive designs and their trade-offs
Three things (if you remember only three designs)
1. Official app-server protocol direct connect, not PTY screen scrape This is the most essential split between CodexMonitor and kanban. kanban has to be compatible with 7 agents, so it must black-box-adapt; CodexMonitor only serves Codex, so it takes the official socket (LSP-style handshake + typed events + two-way control). Trade-off: deep not wide—give up “any agent can plug in,” in exchange for “every layer of Codex capability is in hand” (steer interrupt, approval response, sandboxPolicy passed precisely, account/rateLimits live).
2. shared-core one core, desktop/iOS two shapes
Domain logic collected into a pure-logic layer (closures inject side effects); app and remote daemon use #[path] include to share the same batch of *_core. This is architectural investment for iOS remote mode—without it you would rewrite the entire backend for mobile. The cost is daemon ops complexity (process/port/version drift).
3. Frontend zero state libraries + streaming-delta state machine No redux/zustand/Tailwind/Radix; pure useReducer + ref mirror + handwritten CSS tokens; 600+ frontend files, 19 runtime deps (UI libraries only 6 families). Event-driven state machine (app-server events → hub fanout → reducer slice) carries streaming render. Trade-off: MainApp’s giant props glue in exchange for zero middleware complexity—a wise choice for a single-workspace desktop app.
One-page contrast with Codex / kanban
| Dimension | Codex (official) | kanban | CodexMonitor |
|---|---|---|---|
| Role | the Agent itself | multi-agent orchestration board | Codex-dedicated desktop command stand |
| Integration path | — | PTY + hooks (black box) | app-server protocol (official socket) |
| State model | rollout file assets | card 4-column state machine | workspace × thread event stream |
| worktree | none | one worktree per card | worktree as first-class workspace |
| Frontend | TUI | React board | React feature-sliced |
| Philosophy | local-first CLI | browser control plane | desktop + iOS remote |
Who it is for / who it is not for
- For: heavy Codex users (desktop experience far better than the terminal), developers who want multi-agent parallel with worktree isolation, iOS + Tailscale mobile-office scenes, readers studying the “official-protocol integration” paradigm.
- Not for: non-Codex users (bound ecology), teams that need browser access (no web edition), light users (installing a Rust toolchain + Codex + gh is not a low bar).
- Connection to this series: it is a “deeper practical confirmation” of the OpenAI Codex source analysis (protocol, rollout, config, auth all match), and the “official-protocol contrast group” of the Kanban source analysis.
🔍 Source pointers (go check the source)
| Want to know | Look here |
|---|---|
| app-server spawn and handshake | src-tauri/src/backend/app_server.rs (L749 spawn, :419 handshake) |
| Full set of Codex method calls | src-tauri/src/shared/codex_core.rs (1033 lines) |
| Event classify-and-route | src-tauri/src/backend/app_server.rs (extract_related_thread_ids / resolve_workspace_for_cwd) |
| shared domain core | src-tauri/src/shared/ (31 files 11,197 lines) |
| worktree core | src-tauri/src/shared/workspaces_core/worktree.rs (642 lines) |
| git hybrid strategy | src-tauri/src/shared/git_ui_core/ (libgit2 read + CLI write) |
| rollout usage stats | src-tauri/src/shared/local_usage_core.rs (840 lines) |
| Remote daemon | src-tauri/src/bin/codex_monitor_daemon.rs + codex_monitor_daemonctl.rs |
| Frontend event hub | src/services/events.ts (createEventHub lazy subscribe) |
| Frontend state machine | src/features/threads/hooks/threadReducer/ (1179 lines 6 slices) |
| Composer autocomplete | src/features/composer/ (useComposerAutocompleteState.ts) |
| Codex protocol definition | 参考项目/codex/codex-rs/app-server-protocol/src/protocol/common.rs |
| Codex handshake implementation | 参考项目/codex/codex-rs/app-server/src/request_processors/initialize_processor.rs (:70–101) |
| Codex best practices | 参考项目/codex/AGENTS.md (:104 app-server breaking-change surface) |