codexmonitor-orchestration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CodexMonitor Orchestration Skill

CodexMonitor编排技能

Skill by ara.so — Codex Skills collection.
CodexMonitor is a Tauri-based desktop and mobile app for orchestrating multiple Codex agents across local workspaces. It provides workspace management, thread persistence, git/GitHub integration, file browsing, prompt libraries, and a remote daemon mode for connecting iOS clients or headless setups.
ara.so提供的技能——Codex技能合集。
CodexMonitor是一款基于Tauri的桌面和移动应用,用于在本地工作区中编排多个Codex agents。它提供工作区管理、线程持久化、Git/GitHub集成、文件浏览、提示词库,以及用于连接iOS客户端或无头部署的远程守护进程模式。

What CodexMonitor Does

CodexMonitor功能介绍

  • Multi-workspace orchestration: Spawn one
    codex app-server
    per workspace, resume threads, track unread/running state
  • Thread management: Pin, rename, archive, copy threads; per-thread drafts; stop/interrupt in-flight turns
  • Worktree agents: Clone agents for isolated work under app data directory (legacy
    .codex-worktrees
    supported)
  • Git & GitHub: Diff stats, staged/unstaged files, commit log, branch management, GitHub Issues/PRs via
    gh
  • Composer: Image attachments, autocomplete for skills (
    $
    ), prompts (
    /prompts:
    ), reviews (
    /review
    ), file paths (
    @
    )
  • Remote daemon: Run Codex on another machine, connect iOS client via TCP (Tailscale support)
  • Prompt library: Global/workspace prompts with create/edit/delete/move and run in threads
  • File tree: Search, file-type icons, reveal in Finder/Explorer
  • Terminal dock: Multiple tabs for background commands (experimental)
  • 多工作区编排:为每个工作区启动一个
    codex app-server
    ,支持线程恢复,跟踪未读/运行状态
  • 线程管理:固定、重命名、归档、复制线程;支持线程级草稿;可停止/中断正在执行的任务
  • 工作树代理:在应用数据目录下克隆代理以实现隔离工作(支持旧版
    .codex-worktrees
  • Git & GitHub集成:差异统计、暂存/未暂存文件、提交日志、分支管理,通过
    gh
    工具集成GitHub Issues/PRs
  • 编辑器:支持图片附件、技能自动补全(
    $
    )、提示词自动补全(
    /prompts:
    )、代码评审(
    /review
    )、文件路径自动补全(
    @
  • 远程守护进程:在其他设备上运行Codex,通过TCP连接iOS客户端(支持Tailscale)
  • 提示词库:全局/工作区级提示词,支持创建/编辑/删除/移动,并可在线程中运行
  • 文件树:搜索功能、文件类型图标、在Finder/资源管理器中显示
  • 终端 dock:多标签页支持后台命令(实验性功能)

Installation

安装

Requirements

环境要求

  • Node.js + npm
  • Rust toolchain (stable)
  • CMake (for native dependencies, dictation/Whisper)
  • LLVM/Clang (Windows only, for bindgen)
  • Codex CLI installed and in
    PATH
  • Git CLI (for worktree operations)
  • GitHub CLI
    gh
    (optional, for GitHub integrations)
  • Node.js + npm
  • Rust工具链(稳定版)
  • CMake(用于依赖本地库,如听写/Whisper)
  • LLVM/Clang(仅Windows系统,用于bindgen)
  • Codex CLI已安装并添加至
    PATH
  • Git CLI(用于工作树操作)
  • GitHub CLI
    gh
    (可选,用于GitHub集成)

Install Dependencies

安装依赖

bash
npm install
bash
npm install

Check Environment

检查环境

bash
npm run doctor
bash
npm run doctor

Run in Development

开发环境运行

bash
npm run tauri:dev
bash
npm run tauri:dev

Build Production Bundle

构建生产包

bash
undefined
bash
undefined

macOS/Linux

macOS/Linux

npm run tauri:build
npm run tauri:build

Windows (opt-in, uses separate config)

Windows(可选,使用独立配置)

npm run tauri:build:win

Artifacts: `src-tauri/target/release/bundle/` (platform-specific subfolders)
npm run tauri:build:win

构建产物路径:`src-tauri/target/release/bundle/`(各平台对应子目录)

Workspace Management

工作区管理

Adding a Workspace

添加工作区

Workspaces persist to
workspaces.json
in app data directory.
Via UI: Sidebar → Add workspace → Select directory
Data structure (
src-tauri/src/workspaces/mod.rs
):
rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workspace {
    pub id: String,
    pub name: String,
    pub path: String,
    pub codex_home: Option<String>, // Overrides global Codex home
    pub is_remote: bool,
    pub remote_host: Option<String>,
    pub remote_token: Option<String>,
}
工作区信息将持久化到应用数据目录下的
workspaces.json
文件中。
通过UI操作:侧边栏 → 添加工作区 → 选择目录
数据结构
src-tauri/src/workspaces/mod.rs
):
rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workspace {
    pub id: String,
    pub name: String,
    pub path: String,
    pub codex_home: Option<String>, // 覆盖全局Codex主目录
    pub is_remote: bool,
    pub remote_host: Option<String>,
    pub remote_token: Option<String>,
}

Programmatic Workspace Access

程序化访问工作区

Frontend service (
src/services/tauri.ts
):
typescript
import { invoke } from '@tauri-apps/api/tauri';

// Load all workspaces
const workspaces = await invoke<Workspace[]>('workspace_list');

// Add workspace
const newWorkspace = await invoke<Workspace>('workspace_add', {
  path: '/path/to/project',
  name: 'My Project',
});

// Update workspace
await invoke('workspace_update', {
  id: 'workspace-id',
  updates: { codex_home: '/custom/codex/home' },
});

// Remove workspace
await invoke('workspace_remove', { id: 'workspace-id' });
Backend command (
src-tauri/src/lib.rs
):
rust
#[tauri::command]
async fn workspace_list(
    state: tauri::State<'_, AppState>,
) -> Result<Vec<Workspace>, String> {
    state.workspace_manager.lock().await.list_workspaces()
        .map_err(|e| e.to_string())
}
前端服务(
src/services/tauri.ts
):
typescript
import { invoke } from '@tauri-apps/api/tauri';

// 加载所有工作区
const workspaces = await invoke<Workspace[]>('workspace_list');

// 添加工作区
const newWorkspace = await invoke<Workspace>('workspace_add', {
  path: '/path/to/project',
  name: 'My Project',
});

// 更新工作区
await invoke('workspace_update', {
  id: 'workspace-id',
  updates: { codex_home: '/custom/codex/home' },
});

// 删除工作区
await invoke('workspace_remove', { id: 'workspace-id' });
后端命令(
src-tauri/src/lib.rs
):
rust
#[tauri::command]
async fn workspace_list(
    state: tauri::State<'_, AppState>,
) -> Result<Vec<Workspace>, String> {
    state.workspace_manager.lock().await.list_workspaces()
        .map_err(|e| e.to_string())
}

Thread Management

线程管理

Thread Reducer Architecture

线程Reducer架构

Thread state is managed by a reducer with slices in
src/features/threads/hooks/threadReducer/
.
Thread reducer pattern (
src/features/threads/hooks/threadReducer/index.ts
):
typescript
export type ThreadAction =
  | { type: 'SET_MESSAGES'; messages: Message[] }
  | { type: 'ADD_MESSAGE'; message: Message }
  | { type: 'UPDATE_MESSAGE'; messageId: string; updates: Partial<Message> }
  | { type: 'SET_RUNNING'; running: boolean }
  | { type: 'SET_UNREAD'; unread: number }
  | { type: 'RESET' };

export function threadReducer(state: ThreadState, action: ThreadAction): ThreadState {
  switch (action.type) {
    case 'SET_MESSAGES':
      return { ...state, messages: action.messages };
    case 'ADD_MESSAGE':
      return { ...state, messages: [...state.messages, action.message] };
    case 'UPDATE_MESSAGE':
      return {
        ...state,
        messages: state.messages.map(m =>
          m.id === action.messageId ? { ...m, ...action.updates } : m
        ),
      };
    case 'SET_RUNNING':
      return { ...state, running: action.running };
    case 'RESET':
      return initialThreadState;
    default:
      return state;
  }
}
线程状态由
src/features/threads/hooks/threadReducer/
目录下的Reducer分片管理。
线程Reducer模式
src/features/threads/hooks/threadReducer/index.ts
):
typescript
export type ThreadAction =
  | { type: 'SET_MESSAGES'; messages: Message[] }
  | { type: 'ADD_MESSAGE'; message: Message }
  | { type: 'UPDATE_MESSAGE'; messageId: string; updates: Partial<Message> }
  | { type: 'SET_RUNNING'; running: boolean }
  | { type: 'SET_UNREAD'; unread: number }
  | { type: 'RESET' };

export function threadReducer(state: ThreadState, action: ThreadAction): ThreadState {
  switch (action.type) {
    case 'SET_MESSAGES':
      return { ...state, messages: action.messages };
    case 'ADD_MESSAGE':
      return { ...state, messages: [...state.messages, action.message] };
    case 'UPDATE_MESSAGE':
      return {
        ...state,
        messages: state.messages.map(m =>
          m.id === action.messageId ? { ...m, ...action.updates } : m
        ),
      };
    case 'SET_RUNNING':
      return { ...state, running: action.running };
    case 'RESET':
      return initialThreadState;
    default:
      return state;
  }
}

Resuming a Thread

恢复线程

Frontend (
src/features/threads/hooks/useThreadResume.ts
):
typescript
import { invoke } from '@tauri-apps/api/tauri';

async function resumeThread(workspaceId: string, threadId: string) {
  const result = await invoke<{ messages: Message[] }>('thread_resume', {
    workspaceId,
    threadId,
  });
  
  dispatch({ type: 'SET_MESSAGES', messages: result.messages });
  dispatch({ type: 'SET_UNREAD', unread: 0 });
}
Backend (
src-tauri/src/codex/mod.rs
):
rust
#[tauri::command]
async fn thread_resume(
    workspace_id: String,
    thread_id: String,
    state: tauri::State<'_, AppState>,
) -> Result<serde_json::Value, String> {
    let manager = state.workspace_manager.lock().await;
    let workspace = manager.get_workspace(&workspace_id)
        .ok_or("Workspace not found")?;
    
    let server = state.codex_servers.lock().await
        .get(&workspace_id)
        .ok_or("Server not running")?;
    
    server.call("thread/resume", json!({ "thread_id": thread_id })).await
        .map_err(|e| e.to_string())
}
前端(
src/features/threads/hooks/useThreadResume.ts
):
typescript
import { invoke } from '@tauri-apps/api/tauri';

async function resumeThread(workspaceId: string, threadId: string) {
  const result = await invoke<{ messages: Message[] }>('thread_resume', {
    workspaceId,
    threadId,
  });
  
  dispatch({ type: 'SET_MESSAGES', messages: result.messages });
  dispatch({ type: 'SET_UNREAD', unread: 0 });
}
后端(
src-tauri/src/codex/mod.rs
):
rust
#[tauri::command]
async fn thread_resume(
    workspace_id: String,
    thread_id: String,
    state: tauri::State<'_, AppState>,
) -> Result<serde_json::Value, String> {
    let manager = state.workspace_manager.lock().await;
    let workspace = manager.get_workspace(&workspace_id)
        .ok_or("Workspace not found")?;
    
    let server = state.codex_servers.lock().await
        .get(&workspace_id)
        .ok_or("Server not running")?;
    
    server.call("thread/resume", json!({ "thread_id": thread_id })).await
        .map_err(|e| e.to_string())
}

Sending a Message

发送消息

typescript
async function sendMessage(
  workspaceId: string,
  threadId: string,
  content: string,
  attachments?: { path: string; mime_type: string }[]
) {
  await invoke('thread_send_message', {
    workspaceId,
    threadId,
    message: {
      role: 'user',
      content,
      attachments,
    },
  });
}
typescript
async function sendMessage(
  workspaceId: string,
  threadId: string,
  content: string,
  attachments?: { path: string; mime_type: string }[]
) {
  await invoke('thread_send_message', {
    workspaceId,
    threadId,
    message: {
      role: 'user',
      content,
      attachments,
    },
  });
}

Thread Lifecycle Commands

线程生命周期命令

typescript
// Stop in-flight turn
await invoke('thread_interrupt', { workspaceId, threadId });

// Pin thread
await invoke('thread_pin', { workspaceId, threadId, pinned: true });

// Rename thread
await invoke('thread_rename', { workspaceId, threadId, name: 'New Name' });

// Archive thread
await invoke('thread_archive', { workspaceId, threadId });

// Copy thread (clone messages)
await invoke('thread_copy', { workspaceId, threadId });
typescript
// 中断正在执行的任务
await invoke('thread_interrupt', { workspaceId, threadId });

// 固定线程
await invoke('thread_pin', { workspaceId, threadId, pinned: true });

// 重命名线程
await invoke('thread_rename', { workspaceId, threadId, name: 'New Name' });

// 归档线程
await invoke('thread_archive', { workspaceId, threadId });

// 复制线程(克隆消息)
await invoke('thread_copy', { workspaceId, threadId });

Worktree Agents

工作树代理

Worktree agents create isolated git worktrees under
<app-data>/worktrees/<workspace-id>/
.
工作树代理会在
<app-data>/worktrees/<workspace-id>/
目录下创建独立的Git工作树。

Creating a Worktree

创建工作树

Frontend:
typescript
const worktree = await invoke<{ path: string; branch: string }>('worktree_create', {
  workspaceId: 'workspace-id',
  branch: 'feature-branch',
});

console.log(`Worktree created at ${worktree.path}`);
Backend (
src-tauri/src/shared/workspaces_core/worktree.rs
):
rust
pub async fn create_worktree(
    workspace_path: &str,
    workspace_id: &str,
    branch: &str,
    app_data_dir: &Path,
) -> Result<Worktree, WorktreeError> {
    let worktree_dir = app_data_dir.join("worktrees").join(workspace_id);
    std::fs::create_dir_all(&worktree_dir)?;
    
    let worktree_path = worktree_dir.join(branch);
    
    let output = Command::new("git")
        .args(&["worktree", "add", worktree_path.to_str().unwrap(), branch])
        .current_dir(workspace_path)
        .output()?;
    
    if !output.status.success() {
        return Err(WorktreeError::GitError(
            String::from_utf8_lossy(&output.stderr).to_string()
        ));
    }
    
    Ok(Worktree {
        path: worktree_path.to_string_lossy().to_string(),
        branch: branch.to_string(),
    })
}
前端:
typescript
const worktree = await invoke<{ path: string; branch: string }>('worktree_create', {
  workspaceId: 'workspace-id',
  branch: 'feature-branch',
});

console.log(`Worktree created at ${worktree.path}`);
后端(
src-tauri/src/shared/workspaces_core/worktree.rs
):
rust
pub async fn create_worktree(
    workspace_path: &str,
    workspace_id: &str,
    branch: &str,
    app_data_dir: &Path,
) -> Result<Worktree, WorktreeError> {
    let worktree_dir = app_data_dir.join("worktrees").join(workspace_id);
    std::fs::create_dir_all(&worktree_dir)?;
    
    let worktree_path = worktree_dir.join(branch);
    
    let output = Command::new("git")
        .args(&["worktree", "add", worktree_path.to_str().unwrap(), branch])
        .current_dir(workspace_path)
        .output()?;
    
    if !output.status.success() {
        return Err(WorktreeError::GitError(
            String::from_utf8_lossy(&output.stderr).to_string()
        ));
    }
    
    Ok(Worktree {
        path: worktree_path.to_string_lossy().to_string(),
        branch: branch.to_string(),
    })
}

Listing Worktrees

列出工作树

typescript
const worktrees = await invoke<Worktree[]>('worktree_list', {
  workspaceId: 'workspace-id',
});
typescript
const worktrees = await invoke<Worktree[]>('worktree_list', {
  workspaceId: 'workspace-id',
});

Removing a Worktree

删除工作树

typescript
await invoke('worktree_remove', {
  workspaceId: 'workspace-id',
  path: '/path/to/worktree',
});
typescript
await invoke('worktree_remove', {
  workspaceId: 'workspace-id',
  path: '/path/to/worktree',
});

Git Integration

Git集成

Git Diff Stats

Git差异统计

Frontend:
typescript
const stats = await invoke<{
  staged: { path: string; status: string }[];
  unstaged: { path: string; status: string }[];
}>('git_diff_stats', { workspaceId: 'workspace-id' });
Backend (
src-tauri/src/shared/git_ui_core/diff.rs
):
rust
pub fn get_diff_stats(repo_path: &str) -> Result<DiffStats, GitError> {
    let repo = Repository::open(repo_path)?;
    let mut index = repo.index()?;
    
    let head_tree = repo.head()?.peel_to_tree()?;
    let diff_index_tree = repo.diff_tree_to_index(Some(&head_tree), Some(&index), None)?;
    let diff_index_workdir = repo.diff_index_to_workdir(Some(&index), None)?;
    
    let staged = collect_diff_entries(&diff_index_tree)?;
    let unstaged = collect_diff_entries(&diff_index_workdir)?;
    
    Ok(DiffStats { staged, unstaged })
}
前端:
typescript
const stats = await invoke<{
  staged: { path: string; status: string }[];
  unstaged: { path: string; status: string }[];
}>('git_diff_stats', { workspaceId: 'workspace-id' });
后端(
src-tauri/src/shared/git_ui_core/diff.rs
):
rust
pub fn get_diff_stats(repo_path: &str) -> Result<DiffStats, GitError> {
    let repo = Repository::open(repo_path)?;
    let mut index = repo.index()?;
    
    let head_tree = repo.head()?.peel_to_tree()?;
    let diff_index_tree = repo.diff_tree_to_index(Some(&head_tree), Some(&index), None)?;
    let diff_index_workdir = repo.diff_index_to_workdir(Some(&index), None)?;
    
    let staged = collect_diff_entries(&diff_index_tree)?;
    let unstaged = collect_diff_entries(&diff_index_workdir)?;
    
    Ok(DiffStats { staged, unstaged })
}

Branch Management

分支管理

typescript
// List branches
const branches = await invoke<{ name: string; current: boolean; ahead: number; behind: number }[]>(
  'git_list_branches',
  { workspaceId: 'workspace-id' }
);

// Checkout branch
await invoke('git_checkout_branch', {
  workspaceId: 'workspace-id',
  branch: 'main',
});

// Create branch
await invoke('git_create_branch', {
  workspaceId: 'workspace-id',
  branch: 'feature-new',
  fromBranch: 'main',
});
typescript
// 列出分支
const branches = await invoke<{ name: string; current: boolean; ahead: number; behind: number }[]>(
  'git_list_branches',
  { workspaceId: 'workspace-id' }
);

// 切换分支
await invoke('git_checkout_branch', {
  workspaceId: 'workspace-id',
  branch: 'main',
});

// 创建分支
await invoke('git_create_branch', {
  workspaceId: 'workspace-id',
  branch: 'feature-new',
  fromBranch: 'main',
});

GitHub Integration

GitHub集成

Requires
gh
CLI:
typescript
// List issues
const issues = await invoke<GitHubIssue[]>('github_list_issues', {
  workspaceId: 'workspace-id',
});

// List PRs
const prs = await invoke<GitHubPR[]>('github_list_prs', {
  workspaceId: 'workspace-id',
});

// Get PR diff
const diff = await invoke<string>('github_pr_diff', {
  workspaceId: 'workspace-id',
  prNumber: 42,
});

// Ask PR (send PR context to new thread)
await invoke('github_ask_pr', {
  workspaceId: 'workspace-id',
  prNumber: 42,
  question: 'What does this PR change?',
});
Backend (
src-tauri/src/shared/git_ui_core/github.rs
):
rust
pub async fn list_prs(repo_path: &str) -> Result<Vec<GitHubPR>, GitHubError> {
    let output = Command::new("gh")
        .args(&["pr", "list", "--json", "number,title,author,state"])
        .current_dir(repo_path)
        .output()
        .await?;
    
    if !output.status.success() {
        return Err(GitHubError::CliError(
            String::from_utf8_lossy(&output.stderr).to_string()
        ));
    }
    
    Ok(serde_json::from_slice(&output.stdout)?)
}
需要安装
gh
CLI:
typescript
// 列出Issues
const issues = await invoke<GitHubIssue[]>('github_list_issues', {
  workspaceId: 'workspace-id',
});

// 列出PRs
const prs = await invoke<GitHubPR[]>('github_list_prs', {
  workspaceId: 'workspace-id',
});

// 获取PR差异
const diff = await invoke<string>('github_pr_diff', {
  workspaceId: 'workspace-id',
  prNumber: 42,
});

// 针对PR提问(将PR上下文发送至新线程)
await invoke('github_ask_pr', {
  workspaceId: 'workspace-id',
  prNumber: 42,
  question: 'What does this PR change?',
});
后端(
src-tauri/src/shared/git_ui_core/github.rs
):
rust
pub async fn list_prs(repo_path: &str) -> Result<Vec<GitHubPR>, GitHubError> {
    let output = Command::new("gh")
        .args(&["pr", "list", "--json", "number,title,author,state"])
        .current_dir(repo_path)
        .output()
        .await?;
    
    if !output.status.success() {
        return Err(GitHubError::CliError(
            String::from_utf8_lossy(&output.stderr).to_string()
        ));
    }
    
    Ok(serde_json::from_slice(&output.stdout)?)
}

Remote Daemon Mode

远程守护进程模式

Remote daemon mode allows running Codex on a separate machine (e.g., desktop) and connecting from iOS or other clients.
远程守护进程模式允许在其他设备(如桌面端)运行Codex,并从iOS或其他客户端连接。

Desktop Daemon Setup

桌面端守护进程设置

Via UI:
  1. Settings → Server
  2. Set
    Remote backend token
    (shared secret)
  3. Click
    Start daemon
    in
    Mobile access daemon
Standalone Daemon CLI:
bash
undefined
通过UI操作:
  1. 设置 → 服务器
  2. 设置
    Remote backend token
    (共享密钥)
  3. Mobile access daemon
    中点击
    Start daemon
独立守护进程CLI:
bash
undefined

Build daemon binaries

构建守护进程二进制文件

cd src-tauri cargo build --bin codex_monitor_daemon --bin codex_monitor_daemonctl
cd src-tauri cargo build --bin codex_monitor_daemon --bin codex_monitor_daemonctl

Start daemon (reads settings.json from app data dir)

启动守护进程(从应用数据目录读取settings.json)

./target/debug/codex_monitor_daemonctl start
./target/debug/codex_monitor_daemonctl start

Status

查看状态

./target/debug/codex_monitor_daemonctl status
./target/debug/codex_monitor_daemonctl status

Stop

停止守护进程

./target/debug/codex_monitor_daemonctl stop
./target/debug/codex_monitor_daemonctl stop

Override settings

覆盖配置

./target/debug/codex_monitor_daemonctl start
--listen 0.0.0.0:4732
--token $REMOTE_TOKEN
--data-dir /path/to/app/data
undefined
./target/debug/codex_monitor_daemonctl start
--listen 0.0.0.0:4732
--token $REMOTE_TOKEN
--data-dir /path/to/app/data
undefined

Daemon RPC Architecture

守护进程RPC架构

Daemon entrypoint (
src-tauri/src/bin/codex_monitor_daemon.rs
):
rust
#[tokio::main]
async fn main() -> Result<()> {
    let listener = TcpListener::bind(&args.listen).await?;
    let shared_state = Arc::new(DaemonState::new(app_data_dir, codex_path)?);
    
    loop {
        let (socket, _) = listener.accept().await?;
        let state = shared_state.clone();
        tokio::spawn(handle_connection(socket, state));
    }
}

async fn handle_connection(socket: TcpStream, state: Arc<DaemonState>) {
    let (reader, writer) = socket.into_split();
    let reader = BufReader::new(reader);
    let mut writer = BufWriter::new(writer);
    
    let mut lines = reader.lines();
    while let Some(line) = lines.next_line().await? {
        let request: JsonRpcRequest = serde_json::from_str(&line)?;
        let response = handle_rpc_request(request, &state).await;
        writer.write_all(serde_json::to_string(&response)?.as_bytes()).await?;
        writer.write_all(b"\n").await?;
        writer.flush().await?;
    }
}
RPC routing (
src-tauri/src/bin/codex_monitor_daemon/rpc.rs
):
rust
pub async fn handle_rpc_request(
    request: JsonRpcRequest,
    state: &DaemonState,
) -> JsonRpcResponse {
    match request.method.as_str() {
        "workspace/list" => workspace_list(state).await,
        "workspace/add" => workspace_add(request.params, state).await,
        "thread/list" => thread_list(request.params, state).await,
        "thread/resume" => thread_resume(request.params, state).await,
        "thread/send" => thread_send(request.params, state).await,
        "git/diff_stats" => git_diff_stats(request.params, state).await,
        _ => JsonRpcResponse::error(-32601, "Method not found"),
    }
}
守护进程入口(
src-tauri/src/bin/codex_monitor_daemon.rs
):
rust
#[tokio::main]
async fn main() -> Result<()> {
    let listener = TcpListener::bind(&args.listen).await?;
    let shared_state = Arc::new(DaemonState::new(app_data_dir, codex_path)?);
    
    loop {
        let (socket, _) = listener.accept().await?;
        let state = shared_state.clone();
        tokio::spawn(handle_connection(socket, state));
    }
}

async fn handle_connection(socket: TcpStream, state: Arc<DaemonState>) {
    let (reader, writer) = socket.into_split();
    let reader = BufReader::new(reader);
    let mut writer = BufWriter::new(writer);
    
    let mut lines = reader.lines();
    while let Some(line) = lines.next_line().await? {
        let request: JsonRpcRequest = serde_json::from_str(&line)?;
        let response = handle_rpc_request(request, &state).await;
        writer.write_all(serde_json::to_string(&response)?.as_bytes()).await?;
        writer.write_all(b"\n").await?;
        writer.flush().await?;
    }
}
RPC路由(
src-tauri/src/bin/codex_monitor_daemon/rpc.rs
):
rust
pub async fn handle_rpc_request(
    request: JsonRpcRequest,
    state: &DaemonState,
) -> JsonRpcResponse {
    match request.method.as_str() {
        "workspace/list" => workspace_list(state).await,
        "workspace/add" => workspace_add(request.params, state).await,
        "thread/list" => thread_list(request.params, state).await,
        "thread/resume" => thread_resume(request.params, state).await,
        "thread/send" => thread_send(request.params, state).await,
        "git/diff_stats" => git_diff_stats(request.params, state).await,
        _ => JsonRpcResponse::error(-32601, "Method not found"),
    }
}

iOS Client Connection (Tailscale)

iOS客户端连接(Tailscale)

Desktop (Tailscale helper):
  1. Settings → Server → Tailscale helper
  2. Click
    Detect Tailscale
    → note suggested host (e.g.,
    your-mac.your-tailnet.ts.net:4732
    )
iOS:
  1. Settings → Server
  2. Enter desktop Tailscale host and matching token
  3. Tap
    Connect & test
Frontend client (
src/services/remoteDaemon.ts
):
typescript
class RemoteDaemonClient {
  private socket: WebSocket | null = null;
  private requestId = 0;
  
  async connect(host: string, token: string): Promise<void> {
    this.socket = new WebSocket(`ws://${host}`);
    
    await new Promise((resolve, reject) => {
      this.socket!.onopen = () => {
        this.send('auth', { token }).then(resolve).catch(reject);
      };
      this.socket!.onerror = reject;
    });
  }
  
  async send(method: string, params: any): Promise<any> {
    const id = ++this.requestId;
    const request = { jsonrpc: '2.0', id, method, params };
    
    return new Promise((resolve, reject) => {
      const handler = (event: MessageEvent) => {
        const response = JSON.parse(event.data);
        if (response.id === id) {
          this.socket!.removeEventListener('message', handler);
          if (response.error) {
            reject(new Error(response.error.message));
          } else {
            resolve(response.result);
          }
        }
      };
      
      this.socket!.addEventListener('message', handler);
      this.socket!.send(JSON.stringify(request));
    });
  }
}
桌面端(Tailscale助手):
  1. 设置 → 服务器 → Tailscale助手
  2. 点击
    Detect Tailscale
    → 记录推荐的主机地址(例如:
    your-mac.your-tailnet.ts.net:4732
iOS端:
  1. 设置 → 服务器
  2. 输入桌面端Tailscale主机地址和匹配的密钥
  3. 点击
    Connect & test
前端客户端(
src/services/remoteDaemon.ts
):
typescript
class RemoteDaemonClient {
  private socket: WebSocket | null = null;
  private requestId = 0;
  
  async connect(host: string, token: string): Promise<void> {
    this.socket = new WebSocket(`ws://${host}`);
    
    await new Promise((resolve, reject) => {
      this.socket!.onopen = () => {
        this.send('auth', { token }).then(resolve).catch(reject);
      };
      this.socket!.onerror = reject;
    });
  }
  
  async send(method: string, params: any): Promise<any> {
    const id = ++this.requestId;
    const request = { jsonrpc: '2.0', id, method, params };
    
    return new Promise((resolve, reject) => {
      const handler = (event: MessageEvent) => {
        const response = JSON.parse(event.data);
        if (response.id === id) {
          this.socket!.removeEventListener('message', handler);
          if (response.error) {
            reject(new Error(response.error.message));
          } else {
            resolve(response.result);
          }
        }
      };
      
      this.socket!.addEventListener('message', handler);
      this.socket!.send(JSON.stringify(request));
    });
  }
}

Composer & Autocomplete

编辑器与自动补全

Autocomplete Triggers

自动补全触发词

  • $
    — skills
  • /prompts:
    — prompts
  • /review
    — code review
  • @
    — file paths
Frontend autocomplete hook (
src/features/composer/hooks/useAutocomplete.ts
):
typescript
export function useAutocomplete(value: string, cursorPosition: number) {
  const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
  
  useEffect(() => {
    const prefix = value.slice(0, cursorPosition);
    
    if (prefix.endsWith('$')) {
      // Fetch skills
      invoke<string[]>('autocomplete_skills', { prefix }).then(setSuggestions);
    } else if (prefix.includes('/prompts:')) {
      // Fetch prompts
      invoke<Prompt[]>('autocomplete_prompts', { prefix }).then(setSuggestions);
    } else if (prefix.endsWith('@')) {
      // Fetch file paths
      invoke<string[]>('autocomplete_files', {
        workspaceId,
        prefix,
      }).then(setSuggestions);
    } else {
      setSuggestions([]);
    }
  }, [value, cursorPosition]);
  
  return suggestions;
}
  • $
    — 技能
  • /prompts:
    — 提示词
  • /review
    — 代码评审
  • @
    — 文件路径
前端自动补全钩子(
src/features/composer/hooks/useAutocomplete.ts
):
typescript
export function useAutocomplete(value: string, cursorPosition: number) {
  const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
  
  useEffect(() => {
    const prefix = value.slice(0, cursorPosition);
    
    if (prefix.endsWith('$')) {
      // 获取技能列表
      invoke<string[]>('autocomplete_skills', { prefix }).then(setSuggestions);
    } else if (prefix.includes('/prompts:')) {
      // 获取提示词列表
      invoke<Prompt[]>('autocomplete_prompts', { prefix }).then(setSuggestions);
    } else if (prefix.endsWith('@')) {
      // 获取文件路径列表
      invoke<string[]>('autocomplete_files', {
        workspaceId,
        prefix,
      }).then(setSuggestions);
    } else {
      setSuggestions([]);
    }
  }, [value, cursorPosition]);
  
  return suggestions;
}

Follow-up Behavior

后续消息行为

Settings → Composer → Follow-up default:
  • Queue
    — queue messages if agent is running
  • Steer
    — interrupt current turn and steer
Override for single message:
  • macOS:
    Shift+Cmd+Enter
  • Windows/Linux:
    Shift+Ctrl+Enter
设置 → 编辑器 → 默认后续行为:
  • Queue
    — 若代理正在运行,则将消息加入队列
  • Steer
    — 中断当前任务并转向新请求
单条消息覆盖默认行为:
  • macOS:
    Shift+Cmd+Enter
  • Windows/Linux:
    Shift+Ctrl+Enter

Prompt Library

提示词库

Prompts load from
$CODEX_HOME/prompts
(or
~/.codex/prompts
).
提示词从
$CODEX_HOME/prompts
(或
~/.codex/prompts
)加载。

Prompt File Format

提示词文件格式

markdown
---
description: Generate unit tests for a function
args:
  - name: function_name
    description: Name of the function to test
---

Generate comprehensive unit tests for the function `{{function_name}}`, covering edge cases and error handling.
markdown
---
description: Generate unit tests for a function
args:
  - name: function_name
    description: Name of the function to test
---

Generate comprehensive unit tests for the function `{{function_name}}`, covering edge cases and error handling.

Managing Prompts

管理提示词

typescript
// List prompts
const prompts = await invoke<Prompt[]>('prompt_list', { workspaceId });

// Create prompt
await invoke('prompt_create', {
  workspaceId,
  name: 'generate-tests',
  content: '...',
  isGlobal: false, // workspace-specific
});

// Run prompt in current thread
await invoke('prompt_run', {
  workspaceId,
  threadId,
  promptId: 'generate-tests',
  args: { function_name: 'calculateTotal' },
});

// Run prompt in new thread
await invoke('prompt_run_new_thread', {
  workspaceId,
  promptId: 'generate-tests',
  args: { function_name: 'calculateTotal' },
});
typescript
// 列出提示词
const prompts = await invoke<Prompt[]>('prompt_list', { workspaceId });

// 创建提示词
await invoke('prompt_create', {
  workspaceId,
  name: 'generate-tests',
  content: '...',
  isGlobal: false, // 工作区专属
});

// 在当前线程运行提示词
await invoke('prompt_run', {
  workspaceId,
  threadId,
  promptId: 'generate-tests',
  args: { function_name: 'calculateTotal' },
});

// 在新线程运行提示词
await invoke('prompt_run_new_thread', {
  workspaceId,
  promptId: 'generate-tests',
  args: { function_name: 'calculateTotal' },
});

Configuration

配置

App Settings

应用设置

Persisted to
settings.json
in app data directory.
typescript
interface AppSettings {
  theme: 'light' | 'dark' | 'system';
  backend_mode: 'local' | 'remote';
  remote_provider?: 'tcp' | 'ws';
  remote_tcp_host?: string;
  remote_tcp_token?: string;
  codex_path?: string; // Custom Codex binary path
  default_access_mode?: 'default' | 'direct' | 'tool_only';
  ui_scale?: number;
  follow_up_behavior?: 'queue' | 'steer';
  reduced_transparency?: boolean;
}

// Get settings
const settings = await invoke<AppSettings>('get_app_settings');

// Update settings
await invoke('update_app_settings', {
  updates: { theme: 'dark', ui_scale: 1.2 },
});
设置信息持久化到应用数据目录下的
settings.json
文件中。
typescript
interface AppSettings {
  theme: 'light' | 'dark' | 'system';
  backend_mode: 'local' | 'remote';
  remote_provider?: 'tcp' | 'ws';
  remote_tcp_host?: string;
  remote_tcp_token?: string;
  codex_path?: string; // 自定义Codex二进制文件路径
  default_access_mode?: 'default' | 'direct' | 'tool_only';
  ui_scale?: number;
  follow_up_behavior?: 'queue' | 'steer';
  reduced_transparency?: boolean;
}

// 获取设置
const settings = await invoke<AppSettings>('get_app_settings');

// 更新设置
await invoke('update_app_settings', {
  updates: { theme: 'dark', ui_scale: 1.2 },
});

Codex Config

Codex配置

Feature settings sync to
$CODEX_HOME/config.toml
:
toml
[features]
collaboration_modes = true
unified_exec = true  # Background terminal
apps = false  # Experimental

[personality]
tone = "professional"
Load/save via:
typescript
const config = await invoke<CodexConfig>('get_codex_config_path');

await invoke('codex_doctor'); // Validate Codex setup
功能设置会同步到
$CODEX_HOME/config.toml
toml
[features]
collaboration_modes = true
unified_exec = true  # 后台终端
apps = false  # 实验性功能

[personality]
tone = "professional"
通过以下方式加载/保存:
typescript
const config = await invoke<CodexConfig>('get_codex_config_path');

await invoke('codex_doctor'); // 验证Codex配置

iOS Development

iOS开发

Build for Simulator

针对模拟器构建

bash
./scripts/build_run_ios.sh
bash
./scripts/build_run_ios.sh

Options: --simulator "<name>", --target aarch64-sim|x86_64-sim, --skip-build, --no-clean

可选参数: --simulator "<name>", --target aarch64-sim|x86_64-sim, --skip-build, --no-clean

undefined
undefined

Build for USB Device

针对USB设备构建

bash
undefined
bash
undefined

List devices

列出设备

./scripts/build_run_ios_device.sh --list-devices
./scripts/build_run_ios_device.sh --list-devices

Build and run

构建并运行

./scripts/build_run_ios_device.sh --device "<device name>" --team <TEAM_ID>
./scripts/build_run_ios_device.sh --device "<device name>" --team <TEAM_ID>

Options: --target aarch64, --skip-build, --bundle-id <id>

可选参数: --target aarch64, --skip-build, --bundle-id <id>

undefined
undefined

Signing Configuration

签名配置

Preferred:
src-tauri/tauri.ios.local.conf.json
(gitignored):
json
{
  "bundle": {
    "iOS": {
      "developmentTeam": "YOUR_TEAM_ID"
    }
  },
  "identifier": "com.yourcompany.codexmonitor"
}
推荐使用
src-tauri/tauri.ios.local.conf.json
(已加入git忽略):
json
{
  "bundle": {
    "iOS": {
      "developmentTeam": "YOUR_TEAM_ID"
    }
  },
  "identifier": "com.yourcompany.codexmonitor"
}

TestFlight Release

TestFlight发布

bash
undefined
bash
undefined

Copy .testflight.local.env.example to .testflight.local.env and fill values

复制.testflight.local.env.example为.testflight.local.env并填写配置

./scripts/release_testflight_ios.sh

Required env vars in `.testflight.local.env`:

```bash
IOS_TEAM_ID=YOUR_TEAM_ID
BUNDLE_ID=com.yourcompany.codexmonitor
TESTFLIGHT_BETA_GROUP="Beta Testers"
APPLE_ID=your-apple-id@example.com
APP_STORE_CONNECT_TEAM_ID=YOUR_ASC_TEAM_ID
./scripts/release_testflight_ios.sh

`.testflight.local.env`中需要配置的环境变量:

```bash
IOS_TEAM_ID=YOUR_TEAM_ID
BUNDLE_ID=com.yourcompany.codexmonitor
TESTFLIGHT_BETA_GROUP="Beta Testers"
APPLE_ID=your-apple-id@example.com
APP_STORE_CONNECT_TEAM_ID=YOUR_ASC_TEAM_ID

Terminal Dock

Terminal Dock

Experimental feature for background commands.
typescript
// Execute command in terminal tab
await invoke('terminal_exec', {
  workspaceId,
  tabId: 'tab-1',
  command: 'npm test',
});

// Create new terminal tab
await invoke('terminal_create_tab', { workspaceId, name: 'Tests' });

// Close terminal tab
await invoke('terminal_close_tab', { workspaceId, tabId: 'tab-1' });
用于后台命令的实验性功能。
typescript
// 在终端标签页执行命令
await invoke('terminal_exec', {
  workspaceId,
  tabId: 'tab-1',
  command: 'npm test',
});

// 创建新终端标签页
await invoke('terminal_create_tab', { workspaceId, name: 'Tests' });

// 关闭终端标签页
await invoke('terminal_close_tab', { workspaceId, tabId: 'tab-1' });

Troubleshooting

故障排查

Native Build Errors

本地构建错误

bash
npm run doctor
Common issues:
  • CMake not found: Install CMake
  • bindgen errors (Windows): Install LLVM/Clang
  • Rust targets missing (iOS):
    rustup target add aarch64-apple-ios aarch64-apple-ios-sim
bash
npm run doctor
常见问题:
  • 未找到CMake:安装CMake
  • bindgen错误(Windows):安装LLVM/Clang
  • 缺少Rust目标(iOS):执行
    rustup target add aarch64-apple-ios aarch64-apple-ios-sim

Codex Not Found

未找到Codex

Set custom Codex path in Settings → General → Codex binary path, or ensure
codex
is in
PATH
.
在设置 → 通用 → Codex二进制文件路径中设置自定义路径,或确保
codex
已添加至
PATH

Remote Daemon Connection Fails

远程守护进程连接失败

  1. Confirm daemon is running:
    ./target/debug/codex_monitor_daemonctl status
  2. Check token matches between desktop and client
  3. Verify host/port reachable (Tailscale: both devices online in same tailnet)
  4. Check firewall rules for listening port (default 4732)
  1. 确认守护进程正在运行:
    ./target/debug/codex_monitor_daemonctl status
  2. 检查桌面端与客户端的密钥是否匹配
  3. 验证主机/端口是否可达(Tailscale:确保两台设备在同一网络中且在线)
  4. 检查监听端口的防火墙规则(默认端口4732)

Thread Resume Shows Stale Messages

线程恢复显示陈旧消息

Threads are restored from disk via
thread/resume
. If messages are stale:
  1. Ensure workspace
    cwd
    matches thread working directory
  2. Restart workspace server: Remove and re-add workspace
线程通过
thread/resume
从磁盘恢复。若消息陈旧:
  1. 确保工作区
    cwd
    与线程工作目录一致
  2. 重启工作区服务器:删除并重新添加工作区

Worktree Creation Fails

工作树创建失败

Ensure workspace is a git repository:
bash
cd /path/to/workspace
git status
Legacy worktrees under
.codex-worktrees/
are supported but new ones use
<app-data>/worktrees/<workspace-id>/
.
确保工作区是Git仓库:
bash
cd /path/to/workspace
git status
支持
.codex-worktrees/
下的旧版工作树,但新工作树会使用
<app-data>/worktrees/<workspace-id>/
目录。

iOS Signing Issues

iOS签名问题

First-time device setup:
  1. iPhone unlocked and trusted with Mac
  2. Developer Mode enabled on iPhone (Settings → Privacy & Security → Developer Mode)
  3. Open Xcode via
    ./scripts/build_run_ios_device.sh --open-xcode
    and approve signing
首次设备设置:
  1. iPhone解锁并信任Mac
  2. 在iPhone上启用开发者模式(设置 → 隐私与安全性 → 开发者模式)
  3. 通过
    ./scripts/build_run_ios_device.sh --open-xcode
    打开Xcode并批准签名

File Structure Reference

文件结构参考

src/
  features/app/bootstrap/         App bootstrap orchestration
  features/app/orchestration/     Layout/thread/workspace orchestration
  features/threads/hooks/threadReducer/  Thread reducer slices
  features/composer/              Composer UI and autocomplete
  features/git/                   Git UI components
  features/prompts/               Prompt library UI
  services/tauri.ts               Tauri IPC wrapper
  types.ts                        Shared TypeScript types

src-tauri/
  src/lib.rs                      Tauri command registry
  src/bin/codex_monitor_daemon.rs Remote daemon entrypoint
  src/bin/codex_monitor_daemon/rpc/ Daemon RPC handlers
  src/shared/git_ui_core/         Git/GitHub core
  src/shared/workspaces_core/     Workspace/worktree core
  src/workspaces/                 Workspace adapters
  src/codex/                      Codex app-server adapters
  src/files/                      File adapters
src/
  features/app/bootstrap/         应用启动编排
  features/app/orchestration/     布局/线程/工作区编排
  features/threads/hooks/threadReducer/  线程Reducer分片
  features/composer/              编辑器UI与自动补全
  features/git/                   Git UI组件
  features/prompts/               提示词库UI
  services/tauri.ts               Tauri IPC封装
  types.ts                        共享TypeScript类型

src-tauri/
  src/lib.rs                      Tauri命令注册
  src/bin/codex_monitor_daemon.rs 远程守护进程入口
  src/bin/codex_monitor_daemon/rpc/ 守护进程RPC处理器
  src/shared/git_ui_core/         Git/GitHub核心逻辑
  src/shared/workspaces_core/     工作区/工作树核心逻辑
  src/workspaces/                 工作区适配器
  src/codex/                      Codex app-server适配器
  src/files/                      文件适配器

Resources

资源