源码解析 · 第二十份 · 桌面指挥台

给 Codex 配一个
Tauri 指挥台

CodexMonitor 是 SwiftUI 名手 Dimillian 的 Tauri 2 桌面应用:每个工作区 spawn 一个 codex app-server,走官方 stdio JSON-RPC 协议(而非 PTY 抓屏)驱动多个 Agent thread。全文与 codex 官方源码(app-server-protocol/rollout/config/auth)逐字段交叉印证,第 14 章专述开发思路与品味。标注 文件:行号,可回源码核对。

22
官方协议方法
125
Tauri 命令
19
前端运行时依赖
双端
桌面+iOS daemon

分析对象:CodexMonitor(github.com/Dimillian/CodexMonitor),MIT,本分析基于 main 分支 2026-03-26(commit dd61b9a,PR #584 "Refactor tray thread menu")。作者 Dimillian(Thomas Ricouard)——SwiftUI 开源圈知名开发者(ACarousel、MovieSwiftUI)。 代码规模:860 文件 / 28MB。Rust 后端 92 文件 / 31,171 行(src-tauri/),TS 前端 src/ 557 个 ts/tsx 文件 / 112,428 行(连 css 计 603 文件 / 127,161 行),另有 docs/、scripts/(iOS 构建链)。运行时依赖官方 codex CLI(codex app-server 协议)。 一句话定位给 OpenAI Codex CLI 配的桌面指挥台——Tauri 2 应用,每个工作区 spawn 一个 codex app-server 子进程,通过官方 stdio JSON-RPC 协议(而非 PTY 抓屏)驱动多个 Codex agent thread;提供工作区/worktree 管理、流式对话、diff/PR 审查、用量统计、听写、iOS 远程 daemon 全套。 读者对象:已经读过本系列至少一份分析的读者(尤其 OpenAI Codex 源码分析Kanban 源码分析——本文是 Codex 分析"加深版"的姊妹篇,也是与 kanban"PTY+hooks 方案"的对照样本)。全文标注 文件:行号,可回源码核对。


Part 2

第 1 章 项目概览:给 Codex CLI 配一个指挥台

CodexMonitor 是什么? README 第一句:A Tauri app for orchestrating multiple Codex agents across local workspaces。它不是又一个编码 Agent——Agent 是 codex,它是 Agent 的指挥台

  • 多工作区:侧栏管理多个项目,每个工作区 spawn 一个 codex app-server,独立 CODEX_HOME
  • 多线程:一个工作区里多个 thread(对话会话),支持 resume/fork/archive/copy、pin/rename、每线程草稿、打断/steer 进行中的回合。
  • worktree 隔离:给 agent 建独立 git worktree(独立分支 + 独立 codex home),多 agent 并行零冲突——和 kanban 一卡一 worktree 同一精神。
  • Composer:图片附件、Queue vs Steer 双跟随模式、$ skills / /prompts: / /review / @ 文件自动补全、模型选择、context ring。
  • git/GitHub 全流程:diff 面板、stage/revert、commit log、分支、GitHub issues/PR(列表/diff/评论),"Ask PR" 把 PR 上下文送进新 agent 线程。
  • 用量仪表:home 用量快照(解析 codex 的 rollout JSONL)+ 侧栏 credits 计量表。
  • 听写:本地 Whisper(whisper-rs),hold-to-talk + 波形。
  • iOS 远程模式:桌面起 daemon(TCP JSON-RPC),iPhone 连 Tailscale 跑瘦客户端。

它和 codex / kanban 的关系(本系列的两个邻居)

codex(官方 CLI) kanban(编排看板) CodexMonitor(桌面指挥台)
谁是 Agent 自己就是 7 种 CLI Agent 只有 codex
集成方式 PTY 进程 + hooks 反向注入 官方 app-server 协议(stdio JSON-RPC)
形态 终端 CLI 浏览器看板 Tauri 桌面 + iOS
编排粒度 单会话 卡片(任务) workspace × thread
与 codex 关系 本体 codex 是 7 种之一(PTY) codex 是唯一核心,走官方插座

最有意思的对照:kanban 和 CodexMonitor 做同一件事(多 agent 编排),但走了两条截然不同的技术路线——黑盒适配(PTY 抓屏 + hooks)vs 官方协议(app-server JSON-RPC)。这是贯穿全文的主线。

数字化的项目形状

维度 数字 说明
后端 src-tauri/ 92 文件 / 31,171 行 Tauri 命令 + shared 领域核心 + daemon
前端 src/ 557 个 ts/tsx / 112,428 行(含 css 计 603 文件 / 127,161 行) React + Vite + feature-sliced,25 个 feature
Tauri 命令 generate_handler! 注册 125 条(源码共 138 处 #[tauri::command],差额是 dictation/terminal/menu 的 cfg 双实现) lib.rs invoke_handler 注册,每条命令"本地 or 转发 daemon"双轨
app-server 方法 约 22 个 codex_core.rs 调用,全部官方协议方法(无私有/实验)
依赖(前端) 19 条运行时依赖,UI 库只有 6 类 lucide / react-markdown(+remark-gfm) / prismjs / xterm / react-virtual / Sentry,另加 @pierre/diffs、vscode-material-icons 与 6 个 @tauri-apps 插件——无 Tailwind/Radix/状态库
平台 macOS 主 + Windows/iOS 托盘仅 macOS;Windows opt-in;iOS 远程 daemon 模式
许可 MIT 极宽松

Part 3

第 2 章 全景架构:Tauri 双进程 + app-server 协议 + 事件流

分层约定(AGENTS.md 硬性规则)

┌──────────────────────────────────────────────────────────────┐
│  React 前端(feature-sliced,src/features/ 25 个 feature)    │
│  编排在 hooks/* 与 bootstrap/*,事件 fanout 只在 services/     │
└───────────────┬──────────────────────────────────────────────┘
                │ Tauri IPC(注册 125 条 #[tauri::command])
┌───────────────▼──────────────────────────────────────────────┐
│  Rust 后端(src-tauri/)                                       │
│  ┌──────────────┐  ┌─────────────┐  ┌──────────────────────┐ │
│  │ lib.rs 命令层 │→ │ shared/*    │← │ bin/codex_monitor_  │ │
│  │ workspaces/  │  │ 领域核心     │  │ daemon(远程模式)    │ │
│  │ files/ ...   │  │ (source of  │  │ #[path] include 同源  │ │
│  │ 薄适配层      │  │  truth)     │  │                      │ │
│  └──────┬───────┘  └──────┬──────┘  └──────────────────────┘ │
│         ▼                 ▼                                   │
│  codex app-server 子进程(每 workspace 一个,stdio JSON-RPC)  │
└──────────────────────────────────────────────────────────────┘
  • 领域逻辑放 src-tauri/src/shared/*——app 与远程 daemon 编译进同一二进制、共用同一套 *_core 函数,避免逻辑漂移。
  • codex/workspaces/git/ 等是 Tauri 命令薄适配层;前端 IPC 只经 src/services/tauri.ts
  • daemon(src-tauri/src/bin/codex_monitor_daemon.rs,TCP 行分隔 JSON-RPC)与 app 复用 shared core,方法名/负载对齐。
  • 前端 feature-slicedsrc/features/{app,threads,workspaces,...},事件 fanout 只在 src/services/events.ts

命令注册表(src-tauri/src/lib.rs,337 行)

invoke_handler 注册 125 条命令(源码共 138 处 #[tauri::command],多出的是 dictation/terminal/menu 按 cfg 分桌面与移动两套实现)。每个 codex 命令模式统一:先查 remote_backend::is_remote_mode,是则 call_remote(转发 daemon),否则转 shared::codex_core::*_core。状态经 state::AppState manage:

  • workspaces: Mutex<HashMap<String, WorkspaceEntry>>
  • sessions: Mutex<HashMap<String, Arc<WorkspaceSession>>>——每个 workspace 一个 app-server 进程
  • app_settingscodex_login_cancelstcp_daemon

事件流(贯穿全篇的主干)

codex app-server stdout(JSON 行)
  → backend/app_server.rs 分类(请求响应 / thread/list cwd 归属 / 纯通知)
  → AppServerEvent{workspace_id, message}
  → event_sink.rs → app.emit("app-server-event")
  → 前端 services/events.ts createEventHub 懒订阅 fanout
  → useAppServerEvents(30+ 方法白名单 + approval 后缀匹配)
  → useThreadEventHandlers → 各 reducer slice dispatch
  → React UI

Part 4

第 3 章 app-server 接入:一个工作区一个 codex 进程,LSP 式握手

spawn(src-tauri/src/backend/app_server.rs,L749 spawn_workspace_session

  1. check_codex_installationcodex --version)。
  2. build_codex_command_with_bin 构造命令:可配 codexBin/codexArgs,Windows 特判 cmd/bat。
  3. current_dir(entry.path) 设工作目录,设 CODEX_HOME,参数 ["app-server"]——每个工作区一个独立 codex 进程 + 独立 codex home(互不污染,配置/会话/用量数据天然隔离)。
  4. stdio 全管道化。

生命周期(src-tauri/src/shared/workspaces_core/connect.rs

  • connect_workspace_core全局 spawn 锁:已有存活 session 直接复用(take_live_shared_session 遍历现有 session try_wait() 判活,同进程多 workspace 共享同一 codex 进程——app-server 本身支持多客户端)。
  • 删除:kill_session_by_idkill_child_process_tree
  • 重连是"惰性"的:无后台自动重启,靠前端启动/窗口聚焦时逐 workspace 调 connect_workspace(README 明示),失败的 session 从 map 清除后下次调用重生。
  • 握手:initialize(clientInfo + experimentalApi,15s 超时失败即 kill)→ initialized 通知 → 发 codex/connected 合成事件——LSP 式双步握手

stdio 读写与 JSON-RPC 编解码

  • 写侧:write_message(JSON 单行 \n 定界)。
  • 读侧两个 task:stdout 用 BufReader::lines() 逐行 serde_json::from_str;stderr 转发为 codex/stderr 事件。
  • send_request_for_workspace(L494):自增 next_id → 注册 pending: oneshotrequest_context(workspace, method)超时 300s → 响应按 id 回填。
  • send_notification / send_response(服务器请求如 approval 的应答)。

事件分类(智能路由的关键)

每条 stdout 行先判三类:

  1. id + result/error = 请求响应 → 查 request_context 还原 workspace/method,并 extract_related_thread_ids 回填 thread_workspace 映射。
  2. thread/list 响应:额外解析 cwd → resolve_workspace_for_cwd最长前缀匹配)建立 thread→workspace 归属;memory_consolidation 子代理线程加入 hidden_thread_ids 并改发 codex/backgroundThread(hide) 合成事件(内存整理子代理不污染主界面)。
  3. 纯通知:按 thread_workspace 路由 workspace;全局通知(account/*)广播到 session 下所有 workspace。

与 kanban PTY+hooks 方案的本质区别

这是本分析最重要的对照点:

kanban CodexMonitor
数据来源 PTY 字节流 + hooks 文本回调 结构化 JSON-RPC 通知
状态语义 解析 hook payload / 轮询 rollout jsonl 类型化事件(thread/status/changed、item/started/completed、delta 流)
控制方向 单向(只收) 双向(send_response 应答权限、turn/steer 打断)
可靠性 依赖各 Agent hook 实现,Codex 甚至要轮询 协议原生保证
代价 适配 7 种 Agent 的适配器矩阵 绑定 codex 官方协议(生态单一)

一句话:kanban 把 Agent 当"黑盒子"(屏幕抓取 + 文本流),CodexMonitor 把 codex 当"服务"(官方插座直连)。前者广、后者深。


Part 5

第 4 章 协议交叉印证:CodexMonitor × codex 官方 app-server 协议

本章所有 codex 侧引用均来自本仓库检出的 codex 官方源码(参考项目/codex/codex-rs/)。这是把 CodexMonitor 的代码与 codex 本体对齐的一次"对照实验"。

4.1 协议方法全集(codex 侧)

codex 侧 codex-rs/app-server-protocol/src/protocol/common.rs(4072 行)定义了完整协议面(本节所有 common.rs: 行号均指该文件):

  • 客户端请求(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 等。
  • 实验性方法#[experimental]):thread/searchenvironment/*process/spawn|killthread/increment_elicitationmemory/reset
  • 服务器→客户端请求(ServerRequestPayload,即 approval 请求,common.rs:1505–1554):item/commandExecution/requestApprovalitem/fileChange/requestApprovalitem/tool/requestUserInputitem/permissions/requestApprovalitem/tool/callattestation/generate
  • 服务器通知(common.rs:1658–1731):thread/startedthread/status/changedturn/startedturn/completeditem/starteditem/completeditem/agentMessage/deltaitem/plan/deltacommand/exec/outputDeltaaccount/updatedmodel/rerouted 等。

4.2 CodexMonitor 的覆盖面(src-tauri/src/shared/codex_core.rs,1033 行)

调用约 22 个方法,全部是官方协议方法,无一个私有/实验方法。它虽然声明 experimentalApi: true,但一个实验方法都没用(协议能力开关,见下)。

  • 审批请求用 send_response(request_id, result) 通用转发(codex_core.rs:848–856),未按官方 codex-rs/app-server/src/bespoke_event_handling.rs:1872–2018 的类型化 *ApprovalResponse 结构化处理。
  • 未覆盖:多 agent 编排、plugin/marketplace 管理(skills 只读 list)、fs/* RPC、command/exec 双模式、process/spawn、config RPC(它改走直接读写 config.toml)、thread/realtimeremoteControl

结论:CodexMonitor 不是"协议全覆盖客户端"——它覆盖了 thread/turn 生命周期、查询与账户这一个纵深,其余面刻意不碰。这是"够用就好"的品味:深度集成核心路径,外围用更简单的直接手段(config.toml 直写)。

4.3 app-server 的官方设计初衷(codex 侧 AGENTS.md)

  • AGENTS.md:104–106:把 app-server APIs 列为外部集成面的破坏性变更必搜项(CodexMonitor 这类客户端要盯版本)。
  • :270:新 API 只进 v2,v1 冻结;:273 <resource>/<method> 单数命名;:274–276 线上 camelCase;:286–287 实验面用 #[experimental] 门控;:294–296 list 方法默认 cursor/limit 分页。
  • 官方 README 定位:app-server = 给编辑器/IDE 的"插座"——让 IDE 不用解析终端 ANSI、不用 hack hooks,以 LSP 式协议直接驱动 Codex。

CodexMonitor 正是走在官方指定路径上的src-tauri/src/backend/app_server.rs:760–771 spawn 参数、build_initialize_params(:419–430)的 clientInfo{name:"codex_monitor"} + capabilities.experimentalApi:trueinitialized 通知(:1091)——与官方 codex codex-rs/app-server/src/request_processors/initialize_processor.rs:70–101 的握手契约逐项吻合

4.4 rollout JSONL 用量统计的交叉印证

  • 官方格式:~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl,行结构 RolloutLine{timestamp, ordinal, item}(codex codex-rs/protocol/src/protocol.rs:3380),item 是 8 变体枚举 RolloutItemSessionMeta | ResponseItem | InterAgentCommunication | InterAgentCommunicationMetadata | Compacted | TurnContext | WorldState | EventMsg(同文件 :3186–3199)。TokenCountEventinfo(total/last_token_usage 的 input/cached/output)+ rate_limits(同文件 :2140–2143)。
  • CodexMonitor(src-tauri/src/shared/local_usage_core.rs,840 行):spawn_blocking 解析 JSONL——session_meta/turn_context 的 cwd 过滤 workspace(:220–229)、取 model(:232–237)、token_count 差值去重计 input/cached/output(:281–380)、agent_message/agent_reasoning 计时长(:260–279)、response_item assistant 计运行数(:383–409)。
  • 选了:token 用量、缓存、model、cwd、活跃时长/次数。漏了rate_limits(credits/plan_type——官方已落盘的最有价值字段)、originator/source(CLI vs VSCode vs appServer)、子 agent 血缘、exec_command/file_change 调用计数。

品味观察:用官方已落盘的数据做本地统计(不调 API、不挂钩子),是"与官方数据格式同频"的聪明做法;但 840 行手写解析也暴露了"够用为止"——真正该做的 credits 统计反而没读。

4.5 config.toml / auth.json / rules 三处小交叉

  • [features](codex codex-rs/config/src/config_toml.rs:455codex-rs/features/src/lib.rs:656FeaturesToml):键定义在同文件 steer:1286(恒启用)、collaboration_modes:1340(恒启用)、unified_exec:856、apps:1106(需 chatgpt 认证)。personality 顶层枚举 None|Friendly|Pragmatic(codex codex-rs/protocol/src/config_types.rs:311–317)。CodexMonitor 同步的 5 个字段与官方键名完全吻合,且正确拒绝已废弃的 collab 键(CodexMonitor src-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 含 chatgpt_plan_type/email。CodexMonitor src-tauri/src/shared/account.rs:63–99 的 URL_SAFE_NO_PAD 解码→取 chatgpt_plan_type/email 完全对应,并作为 account/read 失败时的 fallback(src-tauri/src/shared/codex_core.rs:627–650account_read_core)。
  • prefix_rule 是 execpolicy 的 Starlark 内建(定义在 execpolicy/src/parser.rsconfig/src/requirements_exec_policy.rs:51–58 是它在 requirements.toml 里的镜像结构,源码注释亦如此写),decision="allow" 可让 agent 绕过审批。CodexMonitor src-tauri/src/rules.rs:97–100~/.codex/rules/default.rules 追加 prefix_rule(pattern=[...], decision="allow")——语义正确

这四组交叉印证说明一件事:Dimillian 写 CodexMonitor 时是逐字段对着 codex 官方源码和配置格式做的——协议方法名、握手参数、config 键、auth JWT、rules 语法全部对齐。这是"与上游保持契约级同步"的集成品味。


Part 6

第 5 章 thread 模型与事件驱动状态机

5.1 thread 生命周期(src-tauri/src/shared/codex_core.rs

操作 codex 方法 关键参数
新建 thread/start {cwd, approvalPolicy:"on-request"}
列出 thread/list sourceKinds 排除泛 subAgent(防内存整理泄漏)分页
恢复 thread/resume 前端选中即调,从磁盘刷新消息
fork/archive/compact thread/fork/archive/compact/start
改名 thread/name/set
发送 turn/start input items(text/image/localImage/mention),accessMode→sandboxPolicy(dangerFullAccess/readOnly/workspaceWrite)+ approvalPolicy
插话 turn/steer 带 expectedTurnId
打断 turn/interrupt

5.2 后端不维护状态机

关键设计:CodexMonitor 的 Rust 侧不维护 thread 状态机——状态是事件驱动的前端 reducerthread/status/changedturn/started/completeditem/started/completed、approval 请求(*requestApproval 后缀)经 useAppServerEvents.tsMETHODS_ROUTED_IN_USE_APP_SERVER_EVENTS 路由到 useThreadsReducer 的 slices,threadNormalize.ts 兜底 camelCase/snake_case。running/idle/needs_approval 完全由前端从事件推导。

品味观察:这是"协议即真相"的推论——既然 app-server 会推送权威事件,后端就不需要镜像状态(少一层状态同步 bug)。对比 kanban 的后端状态机(session-state-machine.ts 78 行),CodexMonitor 把状态迁移放进了前端 reducer(1179 行)。


Part 7

第 6 章 Rust shared 层:app 与 daemon 共享的领域核心

src-tauri/src/shared/ 共 17 个模块 / 31 文件 / 11,197 行——纯逻辑层,不依赖 Tauri/IPC,只依赖 types、codex::config/home、backend::app_server::WorkspaceSession、storage。app 与 daemon 编译进同一二进制、各自调用同一套 *_core 函数。

6.1 worktree 核心(workspaces_core/worktree.rs,642 行)

add_worktree_core高度泛型函数(FSpawn/FGit/FSanitize 等 9 个闭包注入),核心就是 git worktree add,三种分支情形:分支已存在 → worktree add <path> <branch>;本地无但有远程跟踪 → worktree add -b <branch> <path> <remote_ref>;否则 worktree add -b <branch> <path>

  • 根目录优先级:workspace 级 worktrees_folder > 全局 global_worktrees_folder/<parent_id> > data_dir/worktrees/<parent_id>
  • 创建后生成 WorkspaceEntry{kind: Worktree, parent_id, worktree:{branch}};可 copy_agents_md 继承父仓库 agents.md;用 marker 文件记录一次性 worktree_setup_script 是否已跑。
  • remove_worktree_core:kill session → git worktree remove --forcegit worktree prune --expire now
  • rename_worktree_coregit branch -m + git worktree move带完整回滚)。

6.2 git / GitHub(git_ui_core/*

混合实现(本分析品味亮点): - 读操作走 libgit2(git2 crate):status/diff(diff.rs StatusOptions/Status 位枚举映射 A/M/D/R/T、DiffOptions patch、10MB 图片 base64、2MB 文本上限、has_ignored_parent_directory);log(log.rs revwalk + graph_ahead_behind)。忽略文件识别才用 git check-ignore --stdin -z CLI。 - 写操作走 git CLIcommands.rs,tokio_command):commit/push/pull(--autostash--no-rebase 回退链)/fetch/stage/revert/checkout/git init。 - GitHub 全走 gh CLIgithub.rs):gh issue list --jsongh pr list --jsongh pr diff --color neverparse_pr_diff 文本解析器切分文件)、gh api /repos/.../issues/N/comments --jqgh pr checkoutgh repo create

6.3 账号与用量

  • account.rs(221 行):account/rateLimits/read + account/read,失败回退解析 $CODEX_HOME/auth.json → idToken → JWT → chatgpt_plan_type/email(第 4 章已对照官方格式)。
  • local_usage_core.rs(840 行):扫描 $CODEX_HOME/sessions/<YYYY-MM-DD>/*.jsonl(含各 workspace 独立 codex_home),聚合每日 input/cached/output tokens、agent_time_ms、agent_runs,算 last7/last30、缓存命中率、峰值日、top4 模型;MAX_ACTIVITY_GAP_MS=2min 归并 run。

6.4 多 agent 配置(agents_config_core.rs,1026 行)

读写 [features] multi_agent[agents] max_threads/max_depth(默认 6/1,钳制 1-12/1-4);agent 定义为 [agents.<name>] 表,managed agent 配置落在 $CODEX_HOME/agents/<name>.toml。CRUD 齐全(create 模板生成、update 重命名同步改文件名、delete),含路径穿越防护与配置回滚

6.5 设计品味

  • shared 独立的原因:app 与 daemon 双进程共享同一 crate 的纯逻辑;所有状态经 Mutex<HashMap> 注入、副作用(git/fs/RPC)经闭包或类型注入——天然可单测(tests.rs 437 行)。
  • RPC 复用git_rpc.rs/workspace_rpc.rs 只声明方法名常量 + DTO(to_params/from_params),实现全在 *_core——daemon 的 handler 与 app 的 client 共用同一套签名,一层薄映射即可。
  • 混合 git 策略:读用 libgit2(快、纯内存、类型安全),写用 CLI(行为保真),worktree 编排也用 CLI——务实而非教条

Part 8

第 7 章 worktree 与工作区:一等对象

7.1 工作区模型(workspaces_core

WorkspaceEntry:id/name/path/kind/parent_id/worktree/settings。kind 有普通仓库、worktree、clone 三种。持久化在 workspaces.json(app data 目录)。

  • add_workspace:本地路径或 git clone
  • add_worktree:从父仓库建 worktree(第 6.1 章)。
  • add_clone:URL clone 新工作区。
  • 每个工作区可有独立 settings(codexBin、codex_home、worktrees_folder)。

7.2 与 kanban 的 worktree 对照

kanban CodexMonitor
粒度 一卡一 worktree(任务级) 一工作区可建多个 worktree(agent 级)
身份 worktree 是卡片的"影子" worktree 是 workspace 的一种 kind(一等对象)
生命周期 卡片 trash 即删 可增删改查、改名(带回滚)、改上游
目的 并行任务的资源隔离 给 agent 独立分支 + 独立 codex home

品味观察:kanban 把 worktree 当作任务执行的副产物;CodexMonitor 把 worktree 当作可管理的实体(WorktreeEntry 可 rename/pin/archive)——它是"工作区管理产品"而非"任务看板",产品意图决定了数据模型。


Part 9

第 8 章 git / GitHub:libgit2 读 + CLI 写 + gh 集成

(核心机制已在第 6.2 章覆盖,本章补充前端面)

  • diff 面板:渲染用 @pierre/diffs + 自建 diffsWorker(WorkerPool)+ @tanstack/react-virtual 虚拟列表,主题在 design-system/diffViewerTheme。
  • stage/unstage/revertstage_git_file / unstage_git_file / revert_git_file(useGitActions)。
  • 行内评论:GitHub PR review——usePullRequestLineSelection 选行、get_github_pull_request_comments 拉取、提交为独立 review thread。
  • "Ask PR":PR 被选中即 prefillDraft PR 摘要进 composer;runPullRequestReview:确保 workspace 已连接 → 复用当前 thread 或新建 → buildPullRequestReviewPrompt(PR 元数据 + diffs ≤6 文件×40 行 + comments ≤8 + intent 指令,安全 fence)→ sendUserMessageToThread 送进新 agent thread 并 activate。

品味观察:"Ask PR"是"把 GitHub 流程交给 agent"的妙笔——agent 能读 PR 上下文、按意图干活,这正是 CodexMonitor 相对传统 git GUI 的差异化价值。


Part 10

第 9 章 远程 daemon 与 iOS:一套 core 两份形态

src-tauri/src/bin/ 共 4,902 行:daemon 入口(1966)+ daemonctl(1414)+ rpc 域处理(codex 510 / workspace 273 / rpc.rs 201 / git 196 / prompts 149 / daemon 60)+ transport(103)+ dispatcher(30)。

9.1 daemon 架构

  • 协议:TCP + 行分隔 JSON-RPC(JSONL,每行一个 JSON 对象),非 HTTP。默认 127.0.0.1:4732(daemon 内部)/ 0.0.0.0:4732(daemonctl 对外)。
  • 帧格式:请求 {"id":n,"method","params"};响应 {"id":n,"result"|"error"};事件无 id:{"method":"app-server-event","params":...}broadcast::channel(2048) 推给所有已认证连接)。
  • 认证auth 握手。token 来自 --tokenCODEX_MONITOR_DAEMON_TOKEN;未认证时非 auth 请求一律返回 unauthorized--insecure-no-auth 跳过。并发限流:每连接 Semaphore(32)

9.2 RPC 分发与复用度

dispatcher.rs 按序 try_handle:daemon → workspace → codex → git → prompts。复用度极高:daemon 入口用 #[path=] 直接把主程序模块(backend/shared/files/workspaces_core 等)include 进来,DaemonState 方法几乎 1:1 包装 xxx_core——与本地 Tauri 命令共用同一批 core。差异仅是 AppStateDaemonState + DaemonEventSinkmenu_set_accelerators 降级 no-op("remote parity")。

9.3 daemonctl 生命周期控制(1414 行里的大半)

  • statusprobe_daemon 三步状态机——ping(未认证则收到 unauthorized)→ authdaemon_info;产出 NotReachable / Running{auth_ok} / NotDaemon
  • start:先探测;在跑但身份/版本/mode 不匹配(should_restart_daemon)→ RPC daemon_shutdown 优雅停机 → 轮询 20×100ms → 仍不退则 lsof -iTCP:<port> 解析 PID → SIGTERM → SIGKILL。仅当认证通过且进程名是 codex-monitor-daemon 才允许强杀can_force_stop_daemon)。
  • 无日志文件、无 PID 文件——PID 全靠端口反查(极简主义,但依赖 lsof)。
  • command-preview:输出完整启动命令模板(token 用 <remote-backend-token> 占位符)。

9.4 为什么 iOS 要远程模式

iOS 无桌面资源:无法 spawn codex app-server 子进程、无法跑 git、无本地 workspace 文件系统——做成瘦客户端连桌面 daemon。terminal_mobile.rsdictation/stub.rs 全部命令直接返回 "Terminal/Dictation is not available on mobile builds."(终端需要本地 PTY;听写需要本地 Whisper 模型)。

9.5 Tailscale 集成

tailscale_status:扫描候选二进制(PATH → homebrew → Tailscale.app bundle)→ tailscale version 验证(looks_like_tailscale_version 防 GUI 错误输出)→ tailscale status --json。macOS 用 launchctl asuser <uid> 以 GUI 用户身份执行。suggested_remote_host 解析 DNSName/TailscaleIPs/CurrentTailnet 生成 macbook.tailnet.ts.net:4732。iOS 端直接把建议 host + token 填进 Settings > Server

9.6 品味观察

~5000 行看似多,核心逻辑全是复用的薄壳,真正新增的是三层粘合:#[path] 模块拼接、JSON 参数解析样板、daemonctl 生命周期管理。为什么值得:移动端无法本地跑 codex,若不这么做就得为 iOS 单独重写整个 backend——"一套 core + 一份协议"同时服务桌面与 iOS,行为天然一致。代价:桌面端多了一类"进程 + 端口 + 版本漂移"运维复杂度,TCP 明文 token 依赖 Tailscale 私有网络而非自身加密。


Part 11

第 10 章 前端架构:feature-sliced + 零状态库

10.1 规模与组织

src/ 共 603 个 ts/tsx/css 文件、约 127K 行;src/features/ 496 文件 / 103K 行,25 个 feature,采用 feature-sliced 的 components/hooks/utils 子目录约定:

feature 文件/行 职责
app 141 / 27,205 启动、布局、侧栏、控制器(最重)
threads 70 / 20,636 聊天线程状态机
settings 42 / 12,111 设置页
git 59 / 9,099 diff/log/branch/GitHub
workspaces 36 / 6,931 工作区管理
composer 32 / 6,270 输入框 + 自动补全

10.2 入口与路由

  • 无路由库,纯状态驱动App.tsx(64 行)用 useWindowLabel()(读 Tauri 窗口 label)分流——about 窗口懒加载 <AboutView>,否则 <MainApp />
  • main.tsx:Sentry 初始化 + app_open 计数 + 移动端手势/视口兼容。
  • MainApp.tsx(1882 行)巨型组合根:无 Context,通过 ~70 个 hook 的返回对象逐层传 props。视图切换靠 activeTab state(home|projects|codex|git|log)+ centerMode(chat/diff)+ showHome

10.3 状态管理选型(品味核心)

零外部状态库:无 redux/zustand/jotai/mobx(已 grep 验证)。纯 useReducer + useState + useRef,通过 props 显式下钻。

  • threadReducer(1179 行,6 个 slice,无第三方):threadLifecycleSlice(493 行)/ threadItemsSlice(333 行)/ threadConfigSlice / threadQueueSlice / threadSnapshotSlice / common。约 40 个 action(ensureThread/upsertItem/appendAgentDelta/appendToolOutput/appendReasoningSummary/markProcessing…)。
  • useThreads.ts(938 行,主聚合)内部用 ref 镜像 stateitemsByThreadRef 等)供事件回调同步读取——解决"闭包捕获旧状态"的经典问题。
  • 事件到 reducer 的桥:useThreadEventHandlersuseThreadTurnEvents / useThreadItemEvents / useThreadApprovalEvents → 各回调 dispatch。

为什么不用外部 store(品味判断): 1. 单 workspace 桌面应用,状态图收敛到"一个 activeWorkspace + 一堆 threads",reducer 足够; 2. 事件密集流式更新(delta 级),需要 ref 镜像 + dispatch,外部 store 收益不大; 3. feature-sliced 本身提供模块边界,避免全局 store 的耦合; 4. 代价是 MainApp 巨型 props 传递和大量 useMemo/useCallback 胶水。

10.4 组件库与样式

无 Tailwind、无 Radix、无 shadcn。依赖仅 lucide-react(图标)、react-markdown+remark-gfmprismjs@xterm/xterm@tanstack/react-virtual、Sentry。UI 全手写:CSS 变量 tokens(ds-tokens.css)+ 语义化类名,组件是普通 function component 拼出来的 ModalShell/ToastShell/PanelShell

10.5 与 kanban 前端对比

架构同构(后端进程 + Tauri/浏览器前端 + 事件流 fanout + reducer 驱动 UI),但 CodexMonitor 的状态管理明显更重——reducer 1179 行/40 actions、事件解析 hook 数千行、ref 镜像同步、多 workspace 键控状态。差异根源:kanban 卡片状态是粗粒度快照(fetch 后整体替换),CodexMonitor 要承载流式 delta 渲染(agentMessage 逐 delta append、tool 输出流、reasoning 流)+ 审批队列 + token/rate-limit 实时面板 + subagent 级联归档。但架构层反而更轻:无 store 中间件、无 selector 库、无异步 action,复杂度靠 feature-sliced 目录和巨量单测压制。


Part 12

第 11 章 前端功能特性:Composer / 听写 / diff / 用量

11.1 Composer 自动补全(完全自主实现)

useComposerAutocompleteState.ts 组装三组 triggers:$(skills+apps)、/(8 个硬编码 slash 命令 + prompts)、@(files)。数据全部走 RPC:skills→skills_list、apps→apps_list、prompts→prompts_list(均经 daemon 转发到 codex app-server);files→list_workspace_files(Rust 直接扫盘,上限 500)。触发判定/子序列打分/高亮全部手写,UI 为 ComposerSuggestionsPopover 定位在 textarea 上方,无第三方补全库。$ 选中 app 生成 app://id mention 绑定;/prompts: 支持 findNextPromptArgCursor Tab 跳转参数位。

11.2 Queue vs Steer(交互打磨的典范)

ComposerSendIntent = "default"|"queue"|"steer"resolveSendMessageOptionscanSteerCurrentTurn = isProcessing && steerEnabled && activeTurnId。steer → RPC turn_steer;queue/不支持时 → send_user_message(turn/start,消息入队由 app 侧 ComposerQueue 管理)。默认模式取设置 followUpMessageBehaviorShift+Cmd/Ctrl+Enter 发相反模式oppositeSubmitIntent)。失败区分 steer_failed 且处理 stale-steer 错误,运行时按钮文案在 Steer/Queue 间切换。

11.3 图片附件

三入口均落为本地路径数组useComposerImages 按 thread draftKey 存):picker 用 tauri open;drag-drop 用窗口级 onDragDropEvent + 本地 DOM drop(web 端 FileReader 转 data URL);paste 由 useComposerImageDrop.handlePaste 读 clipboardData。传给 codex 是 data URL 字符串数组normalizeImagesForRpc 对 remote/mobile 模式调用 Rust read_image_as_data_url 逐个转换。

11.4 听写(Whisper)

Rust 侧 whisper-rs 本地推理,real.rs 定义 ggml-tiny/base/small/medium/large-v3 从 HuggingFace 下载;download/cancel/remove/status 走 daemon RPC,进度事件推前端。waveform 是前端自绘 36 条动态 bar(DictationWaveform,订阅 level 事件)。

11.5 用量与通知

  • 用量:Home usage = local_usage_snapshot(30天)(Rust 读 codex home 数据,useLocalUsage 5 分钟轮询);侧栏 credits meter = account/rateLimits/updated AppServerEvent → RateLimitSnapshot → getUsageLabels
  • 通知:纯前端驱动,订阅 AppServerEvents(turn 完成/审批请求);系统通知用 @tauri-apps/plugin-notification;Rust 仅 send_notification_fallback——macOS debug 下用 osascript AppleScript(非 notify_rust)。声音用本地 mp3 资源 + HTMLAudio。均有最小间隔/窗口聚焦抑制。

11.6 品味观察

倾向自主实现:补全引擎、prompt 历史、rank 打分、waveform、usage view model 全手写;仅把重活交给库(@pierre/diffs、xterm、whisper-rs、tauri 插件、react-virtual)。交互打磨:stale-steer 容错、drag 位置 devicePixelRatio 归一化、代码 fence 展开、/prompts: Tab 跳参、hold-to-talk、follow-up 提示条动态说明 Steer 可用性、mobile 触屏布局分支、每条 RPC 写 DebugEntry 可追踪。


Part 13

第 12 章 配置三权分立:settings.json / workspaces.json / config.toml

文件 内容 归属
settings.json(app data 目录) AppSettings:codexBin/codexArgs、backendMode、remote backend host/token、快捷键、defaultAccessMode、uiScale/theme、global_worktrees_folder CodexMonitor 自身
workspaces.json WorkspaceEntry 列表(id/name/path/kind/worktree/settings) CodexMonitor 自身
config.toml$CODEX_HOME codex 自身:features(steer/collaboration_modes/unified_exec/apps)、personality、model codex 是 source of truth

同步策略settings_core.rs,70 行 + codex/config.rs):5 个与 config.toml 同步的字段(collaboration_modes_enabled/steer_enabled/unified_exec_enabled/experimental_apps_enabled + personality)在读写两个方向都双向同步——get_app_settings_core 从 config.toml 覆盖内存值(codex 权威);update_app_settings_core 反向写回 config.toml(通过 config_toml_core.rs 的 toml_edit)+ 写本地 storage。

品味观察:尊重"codex 的配置归 codex"——App 只同步自己 UI 暴露的那几个开关,不做全量镜像,避免两套配置打架。第 4 章已证实键名与官方完全吻合。


Part 14

第 13 章 工程化与测试:AGENTS.md 分层纪律 + 单测密度

13.1 AGENTS.md(6KB 部落知识)

  • 领域逻辑 → shared/*;命令 → 薄适配层;前端 IPC 只经 services/tauri.ts;事件 fanout 只在 services/events.ts。
  • 前端 feature-sliced:src/features/{app,threads,...},编排在 hooks/ 与 bootstrap/
  • daemon 与 app 复用 shared core,方法名/负载对齐(remote parity)。
  • docs/codebase-map.md:任务导向的文件查找("if you need X, edit Y")——这是为 agent 开发的导航文档

13.2 测试密度

  • Rust 侧:git_ui_core/tests.rs 437 行、tray.rs 6 单测、files io.rs 6 个 symlink 逃逸测试、worktree/workspaces 各有测试——纯逻辑层因闭包注入而可测
  • 前端:每个 hook 都有 *.test.ts(x)(useSettingsDefaultModels、useThreads、composer 等)。
  • 集成:main.test.tsxsmoke 等。

13.3 与 codex / kanban 的工程文化对照

  • codex:bazel monorepo、126 crate、AGENTS.md 长而全(含 app-server API 最佳实践)。
  • kanban:GritQL 静态检查 + husky + dogfood(用 kanban 开发 kanban)。
  • CodexMonitor:分层纪律写入 AGENTS.md(agent 可读可执行)、docs/codebase-map.md 导航、双平台(桌面 + daemon)共用 core 的架构测试、.codex/ 目录(项目自己用 codex 开发)。

共性:三个项目都被 agent 优先开发范式 塑造——AGENTS.md 不是文档而是"给 agent 的工程契约",代码库结构要为 agent 的可导航性服务。


Part 15

第 14 章 开发思路与品味:Dimillian 的十个选择

本章是全文重心。Dimillian(Thomas Ricouard)以 SwiftUI 开源组件闻名(ACarousel 等),却为一个"编排 AI agent 的工具"选择了 Tauri(Rust + Web 前端)。这个选择本身就透露了大量信息。以下十个选择,每个都标注了源码证据。

选择一:用官方协议直连,而非黑盒抓取

CodexMonitor 没有走"spawn codex 的 TUI 然后解析屏幕"的路(那是 kanban 对 codex 做的事),而是 codex app-server + stdio JSON-RPC。这显示了对"上游生态"的尊重:官方提供插座(app-server 协议是给 IDE 设计的),就用插座;宁可绑定 codex 生态,也不做脆弱的屏幕适配。第 4 章证实所有 22 个方法全部是官方协议方法、握手参数逐项吻合、config 键名一致、auth JWT 解析对应、rules 语法正确——这是一份"契约级对齐"的代码,背后是逐字段读官方源码的耐心。

选择二:shared-core + 薄命令 + 闭包注入

领域逻辑(git、worktree、codex、settings、usage)全部收进 shared/* 纯逻辑层,Tauri 命令层只做"本地 or 转发 daemon"的薄壳,平台副作用(git 命令、fs、spawn)一律闭包注入。这是非常 Rust 的架构观:类型化的依赖注入 + 纯函数可测。效果:app 与 daemon 双进程共用一套 core,逻辑零漂移;单测可以直接构造内存态测试核心(git_ui_core/tests.rs 437 行)。

选择三:读用 libgit2,写用 CLI,GitHub 用 gh

不教条——读操作要"快且类型安全"(libgit2 纯内存),写操作要"行为与 git 完全保真"(CLI),GitHub 直接复用 gh(不重复造 API 客户端)。每个场景选最合适的工具,而不是统一抽象。这是老练的工程判断:libgit2 的写路径坑多(配置差异、hooks),读路径稳。

选择四:worktree 是一等对象,不是任务副产物

对比 kanban(worktree 是卡片的影子,trash 即删),CodexMonitor 把 worktree 做成 WorkspaceEntry{kind: Worktree} 的完整实体:可增删改查、改名(带回滚)、改上游、继承 agents.md。产品意图决定数据模型——它是"工作区管理产品",worktree 是给 agent 的独立执行环境(独立分支 + 独立 codex home)。

选择五:前端零状态库 + feature-sliced

无 redux/zustand,纯 useReducer + ref 镜像 + 显式 props 下钻;无 Tailwind/Radix,CSS 变量 tokens + 手写组件;UI 侧只用 6 类库。"复杂度靠目录结构和纪律压制,不靠库"。流式 delta 渲染需要 ref 镜像 + dispatch(useThreads.ts 938 行),外部 store 在这里收益不大;feature-sliced 的 25 个 feature 目录提供了足够强的模块边界。

选择六:能自己写的就自己写

补全引擎(触发判定/子序列打分/高亮)、prompt 历史、rank 打分、waveform(36 条动态 bar)、usage 聚合(840 行 JSONL 解析)、frontmatter 解析、rules 去重、锁文件——全部手写,只把重活交给库(xterm、whisper-rs、@pierre/diffs、react-virtual)。依赖极简主义:前端 600 多个文件,运行时依赖只有 19 条,其中 UI 相关的仅 6 类。

选择七:macOS 优先,但不止 macOS

托盘仅 macOS(tray.rs 701 行,非 macOS 空实现)、窗口外观用 objc2-app-kit 直接调 NSAppearance、图标解析调 defaults/sips——原生平台 API 直接调,不抽象;同时 Windows opt-in(separate tauri config 避免 macOS-only 窗口效果)、iOS 远程模式。这是"主平台做到极致,其他平台务实跟进"的策略。

选择八:iOS 远程 daemon——一套 core 两份形态

iOS 无法本地跑 codex,就起一个桌面 daemon(TCP JSONL JSON-RPC)让 iPhone 连 Tailscale 用。#[path] include 复用主程序模块、daemonctl 用端口反查 PID + 版本比对 + 强杀安全闸门管理生命周期。这是"为多端形态做架构投资"——一份核心 + 一份协议,桌面/iOS 行为天然一致;代价是 daemon 进程/端口/版本漂移的运维复杂度(daemonctl 1414 行里大半在防"杀错进程")。

选择九:agent 优先开发

AGENTS.md 是"给 agent 的工程契约"(分层规则、remote parity、feature-sliced 约定);docs/codebase-map.md 是"if you need X, edit Y"的任务导航;仓库自带 .codex/ 配置——这个项目自己就是用 codex 开发的(与 kanban 的 dogfood 同构)。代码库结构为 agent 的可导航性服务,这是 2025-2026 新一代 AI 原生项目群的共同特征。

选择十:防御性编程 + 交互打磨

canonicalize + symlink 逃逸校验(6 个测试)、400KB 读取截断、严格 UTF-8、Arc::ptr_eq 防竞态、stale-steer 容错、断线 DISCONNECTED_MESSAGE——工程上的保守;同时 Shift+Cmd+Enter 反转 Queue/Steer、hold-to-talk 波形、/prompts: Tab 跳参、follow-up 提示条动态说明——产品上的进取。保守的工程 + 进取的交互,是成熟独立开发者的典型气质。

一句话总结品味

用最少的依赖、最干净的架构分层,把官方协议用到位,做成一个"给 codex 的指挥台"——工程上克制保守,产品上打磨入微,架构上为多端形态提前投资。


Part 16

第 15 章 总结:三个最独特的设计与取舍

三件事(如果只记三个设计)

1. 官方 app-server 协议直连,而非 PTY 抓屏 这是 CodexMonitor 与 kanban 最本质的分野。kanban 要兼容 7 种 Agent 所以必须黑盒适配;CodexMonitor 只服务 codex 所以走官方插座(LSP 式握手 + 类型化事件 + 双向控制)。取舍:深而不广——放弃"任意 agent 可插",换来"codex 的每一层能力都拿到"(steer 打断、审批应答、sandboxPolicy 精确传递、account/rateLimits 实时)。

2. shared-core 一套核心,桌面/iOS 两份形态 领域逻辑收进纯逻辑层(闭包注入副作用),app 与远程 daemon 用 #[path] include 共用同一批 *_core这是为 iOS 远程模式做的架构投资——不这么做就得为移动端重写整个 backend。代价是 daemon 运维复杂度(进程/端口/版本漂移)。

3. 前端零状态库 + 流式 delta 状态机 无 redux/zustand/Tailwind/Radix,纯 useReducer + ref 镜像 + 手写 CSS tokens,前端 600 多个文件、19 条运行时依赖(UI 库仅 6 类)。事件驱动状态机(app-server 事件 → hub fanout → reducer slice)承载流式渲染。取舍:MainApp 巨型 props 胶水换来了零中间件复杂度——单 workspace 桌面应用的明智选择。

与 codex / kanban 的一页对照

维度 codex(官方) kanban CodexMonitor
角色 Agent 本体 多 Agent 编排看板 codex 专用桌面指挥台
集成路径 PTY + hooks(黑盒) app-server 协议(官方插座)
状态模型 rollout 文件资产 卡片 4 列状态机 workspace × thread 事件流
worktree 一卡一 worktree worktree 为一等 workspace
前端 TUI React 看板 React feature-sliced
哲学 本地优先 CLI 浏览器控制面 桌面 + iOS 远程

适合谁 / 不适合谁

  • 适合:重度 Codex 用户(桌面端体验远胜终端)、想多 agent 并行且要 worktree 隔离的开发者、iOS + Tailscale 移动办公场景、研究"官方协议集成"范式的读者。
  • 不适合:非 codex 用户(绑定生态)、需要浏览器访问的团队(无 web 版)、轻量用户(安装 Rust toolchain + codex + gh 门槛不低)。
  • 与本系列的连接:它是 OpenAI Codex 源码分析的"加深版实战印证"(协议、rollout、config、auth 全部对得上号),也是 Kanban 源码分析的"官方协议对照组"。

🔍 源码指路(回源码核对)

想知道 看这里
app-server spawn 与握手 src-tauri/src/backend/app_server.rs(L749 spawn、:419 握手)
codex 方法调用全集 src-tauri/src/shared/codex_core.rs(1033 行)
事件分类路由 src-tauri/src/backend/app_server.rs(extract_related_thread_ids / resolve_workspace_for_cwd)
shared 领域核心 src-tauri/src/shared/(31 文件 11,197 行)
worktree 核心 src-tauri/src/shared/workspaces_core/worktree.rs(642 行)
git 混合策略 src-tauri/src/shared/git_ui_core/(libgit2 读 + CLI 写)
rollout 用量统计 src-tauri/src/shared/local_usage_core.rs(840 行)
远程 daemon src-tauri/src/bin/codex_monitor_daemon.rs + codex_monitor_daemonctl.rs
前端事件 hub src/services/events.ts(createEventHub 懒订阅)
前端状态机 src/features/threads/hooks/threadReducer/(1179 行 6 slices)
Composer 补全 src/features/composer/(useComposerAutocompleteState.ts)
codex 协议定义 参考项目/codex/codex-rs/app-server-protocol/src/protocol/common.rs
codex 握手实现 参考项目/codex/codex-rs/app-server/src/request_processors/initialize_processor.rs(:70–101)
codex 最佳实践 参考项目/codex/AGENTS.md(:104 app-server 破坏性变更面)